打开 Express4 的源码 ,目录结构很清晰,阅读时建议对照源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 . lib ├── middleware -------------- 中间件 ├── init.js ------------ 增强 req 对象和 res 对象 └── query.js ------------ 添加 req.query ├── router ------------------ 路由 ├── index.js ------------ Router 构造器 ├── layer.js ------------ 中间件和路由的抽象 └── route.js ------------ 实现 router.route ├── application.js ---------- app 对象拓展 ├── express.js -------------- 项目入口 ├── request.js --------------- req 对象拓展 ├── response.js ------------- res 对象拓展 ├── utils.js ---------------- 工具集 └── view.js ----------------- 模板引擎
底层原理 Express 框架建立在 node.js 内置的 http 模块上,http 模块生成服务器的原始代码如下。
1 2 3 4 5 6 7 8 var http = require ("http" );var app = http.createServer(function (request, response ) { response.writeHead(200 , { "Content-Type" : "text/plain" }); response.end("Hello world!" ); }); app.listen(3000 , "localhost" );
Express 代理了http.createServer(requestHandler)
中的requestHandler
,并使用注册后的中间件和路由,匹配响应传来的用户请求。
代码梳理 下面是一个简单的 Express 使用案例。
1 2 3 4 5 6 7 8 9 10 11 var express = require ("express" );var app = express();app.get("/" , function (req, res ) { res.send("Hello world!" ); }); app.listen(3000 );
third step app.listen 方法在 application.js
中被定义。
1 2 3 4 5 app.listen = function listen ( ) { var server = http.createServer(this ); return server.listen.apply(server, arguments ); };
启动一个服务,并指定监听端口,这里的关键是http.createServer(this)
中的this
(也就是app
),即http.createServer(requestHandler)
中的requestHandler
,this
应该是一个函数(request, response) => {}
,下面介绍 express 如何改造这个函数。
first step express 方法就是 express.js
中的 createApplication 方法,该方法返回了一个函数,即requestHandler
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var proto = require ("./application" );var req = require ("./request" );var res = require ("./response" );function createApplication ( ) { var app = function (req, res, next ) { app.handle(req, res, next); }; mixin(app, EventEmitter.prototype, false ); mixin(app, proto, false ); 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; }
1 2 3 4 5 6 7 8 app.init = function init ( ) { this .cache = {}; this .engines = {}; this .settings = {}; this .defaultConfiguration(); };
second step app.get 方法在 application.js
中被定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 var methods = require ("methods" );var Router = require ("./router" );var middleware = require ("./middleware/init" );var query = require ("./middleware/query" );methods.forEach(function (method ) { app[method] = function (path ) { if (method === "get" && arguments .length === 1 ) { return this .set(path); } this .lazyrouter(); var route = this ._router.route(path); route[method].apply(route, slice.call(arguments , 1 )); return this ; }; }); app.lazyrouter = function lazyrouter ( ) { if (!this ._router) { this ._router = new Router({ caseSensitive : this .enabled("case sensitive routing" ), strict : this .enabled("strict routing" ) }); this ._router.use(query(this .get("query parser fn" ))); this ._router.use(middleware.init(this )); } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var Layer = require ("./layer" );proto.use = function use (fn ) { for (var i = 0 ; i < callbacks.length; i++) { var fn = callbacks[i]; var layer = new Layer( path, { sensitive : this .caseSensitive, strict : false , end : false }, fn ); layer.route = undefined ; this .stack.push(layer); } return this ; };
一个 Layer 对象包含一个路径和回调,它会把路径正则表达式化,在响应请求时用来匹配路径。在app.lazyrouter
方法里添加了一个中间件middleware.init(this)
,重新设置了 req 和 res 的原型,增加了许多方法,详见request.js
和response.js
。
1 2 var req = Object .create(http.IncomingMessage.prototype);
1 2 var res = Object .create(http.ServerResponse.prototype);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 exports .init = function (app ) { return function expressInit (req, res, next ) { if (app.enabled("x-powered-by" )) res.setHeader("X-Powered-By" , "Express" ); req.res = res; res.req = req; req.next = next; setPrototypeOf(req, app.request); setPrototypeOf(res, app.response); res.locals = res.locals || Object .create(null ); next(); }; };
响应请求阶段 app.handle
实质上是调用了自身 router 的 handle,router.handle
遍历 router 维护的 stack 数组,找到匹配路径的 layer 对象。对于中间件 layer(layer.route
为 undefined
),匹配成功后就执行中间件函数;对于路由 layer(layer.route
不是 undefined
),匹配成功后还需要匹配 http method 才能执行路由函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 proto.handle = function handle (req, res, out ) { var self = this ; var idx = 0 ; var protohost = getProtohost(req.url) || "" ; var removed = "" ; var slashAdded = false ; var paramcalled = {}; var options = []; var stack = self.stack; var parentParams = req.params; var parentUrl = req.baseUrl || "" ; var done = restore(out, req, "baseUrl" , "next" , "params" ); req.next = next; if (req.method === "OPTIONS" ) { done = wrap(done, function (old, err ) { if (err || options.length === 0 ) return old(err); sendOptionsResponse(res, options, old); }); } req.baseUrl = parentUrl; req.originalUrl = req.originalUrl || req.url; next(); function next (err ) { } };
next 函数内部有个 while 循环,每次循环都会从 stack 中拿出一个 layer,这个 layer 中包含了路由和中间件信息,然后就会用 layer 和请求的 path 进行匹配,如果匹配成功就会执行 layer.handle_request,调用中间件函数。但如果匹配失败,就会循环下一个 layer 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 function next (err ) { var path = getPathname(req); var layer; var match; var route; while (match !== true && idx < stack.length) { layer = stack[idx++]; match = matchLayer(layer, path); route = layer.route; if (match !== true ) { continue ; } if (!route) { continue ; } var method = req.method; var has_method = route._handles_method(method); if (!has_method && method === "OPTIONS" ) { appendMethods(options, route._options()); } if (!has_method && method !== "HEAD" ) { match = false ; continue ; } } if (err) { layer.handle_error(err, req, res, next); } else { layer.handle_request(req, res, next); } }
路由中间件 当请求来时,会执行 handle,循环 stack,发现 path 相同且 method 相同,则调用对应 callback。当路由不断增多,stack 数组会不断增大,匹配效率必然下降,为了提高效率,express 引入了 route ,也就是路由中间件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var route = app .route("/list" ) .get((req, res ) => { res.end("hello get" ); }) .post((req, res ) => { res.end("hello post" ); }) .put((req, res ) => { res.end("hello put" ); }) .delete((req, res ) => { res.end("hello delete" ); }); app.use("/" , routers);
app.route
在application.js
中被定义,内部调用了 router 的 route 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var Route = require ("./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; };
当 layer 的路径匹配成功,就会交给 route 来匹配 method,route 存放了所有注册的方法名,可以快速匹配是否有注册该方法,如果有则执行回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 Route.prototype.dispatch = function dispatch (req, res, done ) { var idx = 0 ; var stack = this .stack; if (stack.length === 0 ) { return done(); } var method = req.method.toLowerCase(); if (method === "head" && !this .methods["head" ]) { method = "get" ; } req.route = this ; next(); function next (err ) { if (err && err === "route" ) { return done(); } if (err && err === "router" ) { return done(err); } var layer = stack[idx++]; if (!layer) { return done(err); } if (layer.method && layer.method !== method) { return next(err); } if (err) { layer.handle_error(err, req, res, next); } else { layer.handle_request(req, res, next); } } };
next(err)
将控制权传递到错误处理中间件,当调用next(err)
时,实质是调用layer.handle_error
,如果 fn 的参数不足 4 个,认为不是一个标准的错误处理中间件,则继续调用next(err)
,直到参数达到 4 个,执行错误处理中间件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Layer.prototype.handle_request = function handle (req, res, next ) { var fn = this .handle; if (fn.length > 3 ) { return next(); } try { fn(req, res, next); } catch (err) { next(err); } }; Layer.prototype.handle_error = function handle_error (error, req, res, next ) { var fn = this .handle; if (fn.length !== 4 ) { return next(error); } try { fn(error, req, res, next); } catch (err) { next(err); } };
模板渲染 下面是使用模板引擎的写法。
1 2 3 4 5 6 7 app.set("views" , "./views" ); app.set("view engine" , "pug" ); app.get("/" , function (req, res ) { res.render("home" , { title : "Hey" , message : "Hello there!" }); });
res.render
实际调用的是app.render
,内部创建 View 实例,并调用view.render(options, callback)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 res.render = function render (view, options, callback ) { var app = this .req.app; var opts = options || {}; var done = callback; done = done || function (err, str ) { if (err) return req.next(err); self.send(str); }; app.render(view, opts, done); };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 app.render = function render (name, options, callback ) { var cache = this .cache; var done = callback; var engines = this .engines; var opts = options; var renderOptions = {}; var view; if (!view) { var View = this .get("view" ); view = new View(name, { defaultEngine : this .get("view engine" ), root : this .get("views" ), engines : engines }); if (renderOptions.cache) { cache[name] = view; } } tryRender(view, renderOptions, done); }; function tryRender (view, options, callback ) { try { view.render(options, callback); } catch (err) { callback(err); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function View (name, options ) { var opts = options || {}; this .ext = extname(name); if (!opts.engines[this .ext]) { var mod = this .ext.substr(1 ); var fn = require (mod).__express; if (typeof fn !== "function" ) { throw new Error ('Module "' + mod + '" does not provide a view engine.' ); } opts.engines[this .ext] = fn; } this .engine = opts.engines[this .ext]; this .path = this .lookup(fileName); } View.prototype.render = function render (options, callback ) { this .engine(this .path, options, callback); };