前言
關於Node JS 的後端框架,不管是Express
,Koa
, 甚至Eggjs
(Eggjs 是基於Koa 底層封裝的框架),都是基於NodeJS 的http
模組進行處理的,其最重要的是方法是
const server = http.createServer((rep, res) => {
res.end('hello world')
})
server.listen(prot, () => {
console.log('Server Started. Port: '+ prot)
})
複製程式碼
無論那個框架的中介軟體, 路由等的處理,都是從這裡是一個入口,對伺服器的資源的任何訪問都會先進入http.createServer
方法的回撥函式,也就是下面的方法
(rep, res) => {
res.end('hello world')
}
複製程式碼
之前我們已經分析過了Eggjs 框架,但是沒有分析Nodejs實現後端框架實現的底層原理,因為Eggjs 是基於Koa 實現的一個上層框架, 我們這次來通過Express來分析下Nodejs 實現後端框架的底層原理。
原始碼結構
我們先從express clone 一份原始碼,其對應的lib 資料夾就是express
框架的整個原始碼
其最重要的幾個檔案是:
- express.js (專案的入口檔案,暴露除了很多物件,其中最重要的是一個
createApplication
方法)(重點) - application.js (最核心的一個檔案,但是是對上面
createApplication
方法,返回的app 物件去掛載很多方法)(重點) - request.js 和response.js 兩個檔案,主要是對
http.createServer
方法中的rep 和 res 進行相應的封裝處理 - utils.js 只是封裝了一些幫助方法
- View.js 模版引擎的相關的方法
- router 資料夾,是express實現的關鍵,也就是路由的處理,我們的任何一個請求,其實對應的就是一個路由, 然後返回相應的資源(重點)
- middleware, 是定義中介軟體的資料夾,不過其中只有兩個很簡單的內建中介軟體, 因為Express的很多中介軟體都是第三方的庫
我們下面根據啟動服務* 和 ** 訪問服務 兩個流程來分析express
, 會針對上面標註為(重點)的相應的檔案,進行詳細的分析.
啟動服務
express.js
我們先從怎麼使用開始,作為入口,下面是一個簡單的express的demo.
const express = require('./lib/express')
const app = module.exports = express()
app.get('/', (req, res) => {
res.end('hello world')
})
if (!module.parent) {
app.listen(3000);
console.log('Express started on port 3000')
}
複製程式碼
上面一段簡單的程式碼,我們就已經搭建好了一個後端服務,當我們用瀏覽器開啟http://localhost:3000/
時,就會顯示hello world.
,下面我們就來看看是怎麼實現的.
const app = module.exports = express()
可以看出express()
應該是express.js 檔案裡面暴露出來的一個方法, 其對應的指令碼是:
exports = module.exports = createApplication;
createApplication
方法如下:
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);// 合併prototype
mixin(app, proto, false);// 合併proto
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
app.init();
return app;
}
複製程式碼
這個方法,返回一個app 物件, 這個物件相當於繼承與EventEmitter.prototype
和一個proto
物件原型, 然後執行了app.init()
方法,這個方法主要是做一些初始化工作並清空cache
, engines
, settings
, 並且去初始化一些配置,比如說:
this.enable('x-powered-by');
this.set('etag', 'weak');
this.set('env', env);
this.set('query parser', 'extended');
this.set('subdomain offset', 2);
this.set('trust proxy', false);
複製程式碼
this.set
設定的值是儲存在settings中的
, 比如我們我們可以this.settings['x-powered-by']
可以在應用中任何的地方去呼叫.所以這裡有一個擴充套件出一個應用:
const express = require('./lib/express');
const app = module.exports = express();
app.set('config', {
url: 'http://localhost:8080',
userInfo: {
name: 'ivan fan',
age: 18
}
})
app.get('/', (req, res) => {
const config = app.get('config')
console.log(config)
res.end('hello world')
})
if (!module.parent) {
app.listen(3000);
console.log('Express started on port 3000');
}
複製程式碼
上面我們通過app.set
去設定一個config
的值,我們在其他的地方可以通過app.get
去獲取這個值,這樣看起來感覺沒有什麼用途,因為我們可以直接定義一個變數就可以,沒必要通過app.set
, 但是如果我們的應用很大的時候,我們將專案拆分成了很多單獨的檔案,我們只是共享了app
物件,但是在多個js檔案中可能需要公用一個全域性的配置,我們可以建立一個config.json檔案,在不同的頁面都去import 進來,但是如果如果我們在app.js
中將這個配置注入到app
中其他的地方,只要通過app.get
就可以達到共享的作用。
總結:
- express.js 只是暴露除了一個
createApplication
方法, 並且返回了一個app
物件 - 給app物件的原型做了相應的處理
- 給app 進行初始化設定
application.js
上面我們已經分析了express.js
檔案,知道其返回了一個app
物件,但是我們至今位置沒有看到哪裡定義了listen
和get
方法。
我們在上面分析發現,執行了mixin(app, proto, false);
方法,這個是在app 原型上去新增了另外一個原型,而proto
指向的就是application.js
檔案, 下面我們就來具體分析這個檔案。
listen
首先我們找到listen 方法,其程式碼如下:
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
複製程式碼
這個就是我們在一開始說的,所有的Nodejs 後端框架都是基於http 這個模組的實現的,所以這個裡我們就已經實現了一個後端的服務。
get
在我們的demo 中,我們有呼叫一個app.get
方法,其程式碼如下:
app.get('/', (req, res) => {
const config = app.get('config')
console.log(config)
res.end('hello world')
})
複製程式碼
但是我們找遍了整個application.js
檔案,都沒有找到這個方法在哪裡實現的, get
只是http
請求眾多方法的其中一個, http方法,還有'post','put','delete'等一些列方法,為了簡潔,express 引用了第三方庫methods, 這個庫幾乎涵蓋了http 請求的常見方法,所以通過迴圈去給app
掛載不同的方法(Koa 也是這樣處理)
methods.forEach(function(method){
app[method] = function(path){
if (method === 'get' && arguments.length === 1) {
// app.get(setting)
return this.set(path);
}
this.lazyrouter();
var route = this._router.route(path);
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});
複製程式碼
首先this.lazyrouter();
方法是去給app
物件掛載一個_router
的路由(Router)屬性,
然後我們在看下route
方法:
proto.route = function route(path) {
var route = new Route(path);
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
};
複製程式碼
從上面的程式碼可知,var route = this._router.route(path);
, route
也就是this.stack 中的layer
中的reoute
, 所以最後回撥函式是掛載在stack 的layer 上面的。
route[method].apply(route, slice.call(arguments, 1));
將app.get
的回撥函式掛載在route
屬性上面,其程式碼如下(刪除異常處理程式碼):
methods.forEach(function(method){
Route.prototype[method] = function(){
var handles = flatten(slice.call(arguments));
for (var i = 0; i < handles.length; i++) {
var handle = handles[i];
var layer = Layer('/', {}, handle);
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);
}
return this;
};
});
複製程式碼
我們先不具體分析程式碼邏輯, 我們可以根據上面的圖片,分析下app物件下的一個資料結構:
- 在app 上面掛載一個
_router
屬性, (router/index.js) - 在
_router
下面有一個stack
的屬性,其是一個陣列 stack
陣列中,儲存的都是一個Layer
型別的物件Layer
物件中又掛載了一個route
(Route)的物件route
物件儲存了path
(path:/abc),methods
, 同樣也有一個stack
的屬性,也是一個陣列, 同樣裡面儲存的也是一個Layer
物件Layer
裡面掛載了一個重要的屬性handle
, 其實從現在的分析看,這個handle
就是我們app.get
方法的第二個回撥函式引數.
上面我們已經分析了express
啟動的過程,下面我們來分析訪問服務express
處理的過程,也就是我們訪問http://localhost:3000/
時,express
到底做了些什麼.
訪問服務
從一開始,我們就知道,對伺服器的方法,首先都會進入http.createServer
的回撥函式,而且express
是通過listen
方法,執行這個方法的
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
複製程式碼
其中this
就是app
例項,也就是在express.js
檔案中定義的,如下:
var app = function(req, res, next) {
app.handle(req, res, next);
};
複製程式碼
在這個方法中,會呼叫handle
方法,下面我們來分析下這個方法
handle
handle
的程式碼如下:
app.handle = function handle(req, res, callback) {
var router = this._router;
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
});
if (!router) {
debug('no routes defined on app');
done();
return;
}
router.handle(req, res, done);
};
複製程式碼
然後會執行router.handle(req, res, done);
, 在上面我們已經得知, this._router
指向的是router/index.js
這個資料夾的物件,下面我們進入到這個handle
方法中,
這個方法很長,但是其實就是根據我們訪問的路徑來查詢對應的Layer
, 其關鍵程式碼是:
while (match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;
...
}
複製程式碼
通過matchLayer(layer, path);
去匹配layer.
找到Layer
後,然後去執行layer.handle_request(req, res, next);
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
if (fn.length > 3) {
// not a standard request handler
return next();
}
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};
複製程式碼
var fn = this.handle;
這個fn
其實指向的就是app.get
裡面的第二引數,也就是回撥函式,
app.get('/', (req, res) => {
res.end('hello world')
})
複製程式碼
然後就相當於請求完成了。
總結
上面我們已經分析了,Express 在啟動的整個過程,主要是進行資料的一些載入處理和路由的處理,而且也分析了我們在請求Server時的整個過程。
後續我會繼續分析use 的用法,並且針對express.static 原始碼來分析Express 中介軟體的處理和總結中介軟體的使用方式,以及express.static
對快取的處理(Etag
, Last-Modified
)