-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Node.js系列专题之Express源码解读
介绍
Node.js的发展历史
2009年,Node.js诞生
2010年,Express和Socket.io诞生
2011年,Node.js正式商用(LinkedIn、Uber)、Npm诞生
2012年,Node.js趋于成熟,商用框架Hapi诞生
2013年,Node.js在Web框架领域百花齐放,Koa诞生;蚂蚁金服内部推出Chair框架(Egg.js前身)
2016年,Egg.js开源
定义
官方对Express的描述:Fast, unopinionated, minimalist web framework for Node.js
我们可以从中看出,Express主要有2个特点:
- 快速极简:通过扩展 Node 的功能来提高开发效率
- 高度包容:框架不会对开发者过多的限制,可以自由发挥想象的空间
Demo
const express = require('express')
const app = express() // 实际执行 createcreateApplication
const port = 3000
app.use(function(req, res, next) {
// ...
next()
})
app.use('/test', function(req, res, next) {
// ...
next()
})
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})Demo非常简单,主要步骤如下:
1.引入express模块
2.调用express(),创建一个应用实例
3.通过app.use绑定两个中间件
4.通过app.get为根目录注册一个Get路由
5.通过app.listen将服务运行在3000端口,并监听请求
源码机构
Express源码合计约3500行。
- lib/
- middleware/
- init.js
- query.js
- router/
- index.js
- layer.js
- route.js
- application.js
- express.js
- request.js
- response.js
- utils.js
- view.js
- index.js
- middleware目录下包含两个默认的中间件
- router目录下主要是路由功能的源码【核心】
- request/response主要是简化和增强了http模块原始的req/res对象
- application.js跟应用实例相关,掌握整个程序的流程控制【核心】
- express.js里面暴露了createApplication这个工厂函数,用于创建应用实例
- utils/view则分别是express框架所用到的工具函数和模板引擎代码
- index.js仅仅是将express.js的导出再此导出
核心实现
下面主要从程序运行角度,将express的源码分为两个阶段:初始化流程与请求处理
初始化流程
创建应用实例
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false); // 赋予eventEmitter能力
mixin(app, proto, false); // 挂载基本方法,例如get/post/listen/init/use/handle等
// expose the prototype that will get set on requests
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
app.init(); // 初始化
return app;
}Demo中调用的express()即执行的createApplication函数;
-
在createApplication函数执行过程中,app这个函数被挂载了许多方法和属性。
-
然后调用app.init()方法进行初始化。
-
最后返回app函数。(为什么返回的是函数而不是Object?)
挂载中间件(一)
app.use = function (fn) {
// 通过offset来处理函数重载,忽略细节
var fns = flatten(slice.call(arguments, offset));
this.lazyrouter() // 初始化router,讲路由的时候再说
fns.forEach(fn => {
// 实际上app.use调用的router的中间件绑定
this.router.use(path, function mounted_app(req, res, next) {
fn.handle(req, res, function(err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
})
})
fn.emit('mount', this);
})
}绑定中间件:
- 通过对第一个实参的类型进行判断进而达到函数重载的效果
- 若为string类型,则第一个参数为 path(默认’/’);offset设置为1
- 若为function类型,则参数都为中间件;offset设置为0
- 遍历中间件数组,依次调用router.use方法进行绑定(app.use实际执行router.use)
创建Router
// 单例模式
app.lazyrouter = function lazyrouter() {
if (!this._router) {
// 初始化一个router实例,全局唯一
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
});
// router挂载基本中间件
this._router.use(query(this.get('query parser fn')));
this._router.use(middleware.init(this));
}
};通过单例模式初始化了一个Router实例,然后挂载基本中间件。
Router对象内部结构:
function Router(options) {
var opts = options || {};
function router(req, res, next) {
router.handle(req, res, next);
}
// mixin Router class functions
setPrototypeOf(router, proto)
router.params = {};
router._params = [];
router.caseSensitive = opts.caseSensitive;
router.mergeParams = opts.mergeParams;
router.strict = opts.strict;
router.stack = [];
return router;
};Router构造函数:
-
与createApplication类似,返回的也是函数而非object
-
内部维护了stack数组
挂载中间件(二)
app.use内部实际执行的router.use
// router.use(fn1, fn2); // 所有路径和方法(除options)都会执行
// router.use('/test', fn3); // 针对/test才会执行
// 伪代码
router.use = function(callbacks) {
//
for(let i = 0; i < callbacks.length; i++) {
const layer = new Layer(path, {
// 配置
}, callbacks[i]);
layer.route = undefined;
this.stack.push(layer); // 将所有中间件维护在一起
}
}1.与app.use类似,通过对参数的判断进行函数重载
2.对每个中间件包装成Layer对象,Layer对象的route属性设置为undefined;
3.将layer对象维护在router的stack数组中
这里的layer对象内部机构如下:
function layer(path, options, fn) {
// 省略部分代码
this.handle = fn;
this.path = undefined;
}Layer构造函数: 接受3个参数,第一个是路径,默认为undefined;最后一个是处理函数
注册路由
- methods为HTTP方法数组,源码灵活的为app绑定注册路由的方法
- 注册路由实际还是调用的router内部的方法即router.route
- 执行route[method]方法,为route绑定处理函数
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
route[method].apply(route, slice.call(arguments, 1)); // 绑定处理函数
return this;
}初始化route:
1.创建指定path的route对象
2.创建指定path的layer对象,并且该layer绑定的函数为route.dispatch
3.并将layer推入router的stack数组
app.use绑定中间件时创建layer时,将layer.route设置为undefined,注册路由时却将layer.route设置为route。
由此可知,Router的layer有2种类型,一种非路由中间件(未指定route),一种则为路由中间件(指定route)。
router.route = function route(path) {
let route = new Route(path);
let layer = new Layer(path, {
// 配置
}, route.dispatch.bind(route)) // layer上绑定route的dispatch方法【后续会说作用】
layer.route = route; // layer只与此route相关
this.stack.push(layer); // 将一个route下的所有路由函数放一起
return route;
}Route对象内部结构:
- 必须指定path
- 拥有stack数组
- 还有一个methods对象
function Route(path) {
// 忽略部分代码
this.path = path;
this.stack = []; // route也有stack,用来存储处理函数,即对应 http路由的 处理函数
// route handlers for various http methods
this.methods = {};
}注册路由处理函数
Route为指定方法绑定处理函数:
- 同时利用了methods数组,遍历绑定
- 用layer包装处理函数,使用stack统一管理
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];
if (typeof handle !== 'function') {
var type = toString.call(handle);
var msg = 'Route.' + method + '() requires a callback function but got a ' + type
throw new Error(msg);
}
var layer = Layer('/', {}, handle);
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);
}
return this;
};
});总结
经过初始化阶段后,各个模块关系图如下:
- Router下面有多个Layer(可以理解为中间件),中间件分2大类:
- 通过app.use或者router.use绑定的非路由中间件;中间件的功能由开发者自定义;
- (以get为例)通过app. get或者router.get绑定的路由中间件;中间件是由框架指定Route.dispatch方法;
- Route下面也有多个Layer(也可以理解为中间件),即注册路由时的回调函数;例如:app.get(‘/test’, fn); fn就是回调函数
请求处理
端口监听
Express将创建server和监听端口合并成一个app.listen函数
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};并且,创建server时 将app函数本身作为requestListener,那么每次端口监听到请求时就会执行app函数;
这样就进入express的第二个阶段,请求处理阶段
请求处理
app.handle内部实际调用的router.handle
app.handle = function handle(req, res, callback) {
var router = this._router;
// final handler
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
});
// no routes
if (!router) {
debug('no routes defined on app');
done();
return;
}
router.handle(req, res, done);
};router.handle主要做两件事情:1. 根据传进来的参数req,做一些处理,如获取ur;2.遍历layer,这个是通过next函数来实现。
next函数:按索引遍历Router.stack,找到匹配路径的中间件,然后执行layer.handle_request(req, res, next)
handle_request函数其实就是执行创建layer对象时传递的第三个参数(handle)。
-
路由中间件的layer绑定的handle为route.dispatch
-
非路由中间件的layer绑定的handle为自定义函数
layer.prototype.handle_request = function(req, res, next) {
var fn = this.handle;
if (fn.length > 3) return next(); // 如果参数不对,跳过本处理函数
try {
fn(req, res, next); // 执行处理函数
} catch(err) {
next(err);
}
}再来看route.dispatch: dispatch的功能就是,遍历route的stack数组,若layer的method和本次req的method匹配就执行。
// dispatch req, res into this route
Route.prototype.dispatch = function(req, res, next) {
if (stack.length === 0) return;
// ...不必要细节
function next(err) {
var layer = stack[idx++];
// method就是http方法
if (layer.method && layer.method !== method) { // 方法不匹配,跳过本处理函数
return next(err);
}
layer.handle_request(req, res, next); // 执行处理函数
}
next()
}总结
请求处理流程如图:
- app监听端口,有request时,交给router处理。
- router处理步骤:
- 遍历其layer数组,找到匹配的路径的layer,根据不同类型layer做出不同的操作
- 路由中间件:执行匹配的layer然后将控制权移交Route,遍历Route的layer数组,执行完与request.method方法匹配的所有layer;最后控制权交还给Router
- 非路由中间件:执行匹配的layer
- 遍历其layer数组,找到匹配的路径的layer,根据不同类型layer做出不同的操作
优/缺点
优点
- 易上手: express对web开发相关的模块进行了适度的封装,屏蔽了大量复杂繁琐的技术细节,让开发者只需要专注于业务逻辑的开发,极大的降低了入门和学习的成本。
- 高性能: express仅在web应用相关的Node.js模块上进行了适度的封装和扩展,较大程度避免了过度封装导致的性能损耗。
- 扩展性强:**基于中间件的开发模式,使得express应用的扩展、模块拆分非常简单,既灵活,扩展性又强
缺点
- 基于callback组合业务逻辑,业务逻辑复杂时嵌套过多,回调地狱蛋疼;
- 没有统一的错误处理机制,error-first贯穿整个应用
- Express团队维护频率低(推出了Koa)
总结
作为Node.js平台第一个Web应用框架,也是多数JavaScripter接触的第一个服务端框架,Express已经完成了它的历史使命。





