2022年的JYM都一致的认为2023年会更好,等来的却是"前端已死,后端当立",这到底是有人故意而为之传播焦虑,还是互联网的彻底崩盘。我相信ChatGPT会给出最好的答案。
进入主题~
今天来聊一聊Express的源码:
express()
var express = require("express");
var app = express();
源码位置在/lib/express.js,调用express函数,实际上是调用的函数createApplication, 我们来分析分析到底发生了什么!
function createApplication() {
(1) var app = function(req, res, next) {
app.handle(req, res, next);
};
(2) mixin(app, EventEmitter.prototype, false);
(3) mixin(app, proto, false);
(4) app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
(5)app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
(6)app.init();
return app;
}
通过表达式(1)定义了app函数,借用merge-descriptors库(2)将NodeJS.EventEmitter原型上的方法merge到app的prototype:
proto:(application)(3)定义了express中所有的方法,也就是说proto里面有什么方法 app里面就有什么方法,同理app也是通过对应的库继承proto的方法。
当我们app.listen()监听(2)服务器启动的时候本质上是调用application中的listen函数,其内部又通过http.createServer创建服务返回listen监听函数
app.listen = function listen() {
var server = http.createServer(function(req,res,next) {
...
});
return server.listen.apply(server, arguments);
};
然后通过Object.create(4,5)将app作为request和response的属性值
最后初始化(6)默认配置defaultConfiguration
先打断一下 ~
先看看app.init都做了些什么(摘取部分代码逐一解释)
app.init
初始化默认配置
var env = process.env.NODE_ENV || 'development';
// 默认配置
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);
express默认开启x-powered-by打开开发工具并检查响应头,你会注意到它可以明确告诉客户端当前服务器是由Express开发的,这是非常危险的!!! , 当攻击者深入的理解当前框架 ,并且知道相关漏洞,会有很严重的后果,因此我们需要设置app.disable('x-powered-by')
queryParser用来解析传入的query参数,贴一段代码,清晰易懂:
exports.compileQueryParser = function compileQueryParser(val) {
var fn;
if (typeof val === 'function') {
return val;
}
switch (val) {
case true:
fn = querystring.parse;
break;
case false:
fn = newObject;
break;
case 'extended':
fn = parseExtendedQueryString;
break;
case 'simple':
fn = querystring.parse;
break;
default:
throw new TypeError('unknown value for query parser function: ' + val);
}
return fn;
}
function parseExtendedQueryString(str) {
return qs.parse(str, {
allowPrototypes: true
});
}
trust proxy:详细见文档 , 这个很难理解, 附上两篇 stack overflow的答疑:
NodeJS, Express, why should I use app.enable('trust proxy');
Using app.set to set trust proxy
监听子应用
this.on('mount', function onmount(parent) {
if (this.settings[trustProxyDefaultSymbol] === true
&& typeof parent.settings['trust proxy fn'] === 'function') {
delete this.settings['trust proxy'];
delete this.settings['trust proxy fn'];
}
setPrototypeOf(this.request, parent.request)
setPrototypeOf(this.response, parent.response)
setPrototypeOf(this.engines, parent.engines)
setPrototypeOf(this.settings, parent.settings)
});
主要是继承父应用的配置
其他配置
this.locals = Object.create(null);
this.mountpath = '/';
this.locals.settings = this.settings;
this.set('view', View);
this.set('views', resolve('views'));
this.set('jsonp callback name', 'callback');
if (env === 'production') {
this.enable('view cache');
}
主要是保存基础配置settings, 该配置将会在renderAPI中用到,其次是设置项目中视图目录或目录数组,常见的在开发中常见的配置如下:
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
jsonp callback name, 很明显指定默认的 JSONP 回调名称。如下:
// ?callback=foo
res.jsonp({ user: 'tobi' })
// => foo({ "user": "tobi" })
app.set('jsonp callback name', 'cb')
// ?cb=foo
res.status(500).jsonp({ error: 'message' })
// => foo({ "error": "message" })
这就是执行express函数的整体过程
按照编码习惯,加下来看app.use发生了什么
app.use
注册中间件
var cors = require("cors")
app.use(cors())
根据传参执行对应的代码逻辑 除了上面的传入整个函数作为中间件, 还可以通过(paths,cb)形式匹配路由。
app.use = function use(fn) {
var offset = 0;
var path = '/';
if (typeof fn !== 'function') {
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
var fns = flatten(slice.call(arguments, offset)); //数组扁平
...
// app.use -> router.use()
fns.forEach(function (fn) {
if (!fn || !fn.handle || !fn.set) {
return router.use(path, fn);
}
fn.mountpath = path;
fn.parent = this;
(7) router.use(path, function mounted_app(req, res, next) {
var orig = req.app;
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
});
});
fn.emit('mount', this);
}, this);
return this;
};
如果参数形式是路由,则取出第一个参数,此时偏移量为1,数组扁平化得到截取path之外的fn数组,循环遍历对当前fns, 发现内部调用的是router.use()
了解真相才是真正的自由: app.use()内部实际调用的是router.use()
代码(7)还是存在疑惑,fn.handle到底指什么呢?我们在router.use最后一节末尾详细说明~
router.use
// app.use 省略代码
this.lazyrouter();//主路由
var router = this._router;
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));
}
};
这里有一个问题,为什么不在初始化的函数中直接生成一个默认路由呢?
官方是这样解释的: 如果尚未添加基本路由器,则延迟添加它。我们无法在defaultConfiguration中添加基本路由器,因为它读取的应用程序设置可能在运行后设置。
原因在于设置路由的相关参数需要调用app.set方法,这个方法明显需要有app实例,如果在获取app实例的时候就初始化了一个路由,这个路由的参数就没办法配置了。因此,在获取app实例后,必须先对路由参数进行配置,然后再调用对应的app.use等方法。
需要注意的是该过程中其实是利用了单例模式,保证整个过程中获取router实例的唯一性。
Router
var router = function(options) {
var opts = options || {};
function router(req, res, next) {
router.handle(req, res, next);
}
setPrototypeOf(router, proto)
...
return router;
};
实例化Router,主要做的就是将proto作为函数的原型对象,因此可以访问proto的任何方法,即router.handle是proto.handle。
示例代码:
var router = express.Router();
router.use("/video",require('./video'))
//video.js
router
.get("/videolist", verifyToken(), videoController.videolist)
.get("/detail/:videoId", verifyToken(), videoController.video)
下面的讲解, 按照上面的代码依次展开。
回到router.use(proto.use)。
router.use类似于 app.use()。
router.use源码中传参的判断逻辑和app.use是一样的:
...
if (typeof fn !== 'function') {
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
...
与之不同的是对中间件通过Layer“包裹”,添加到堆栈中,主要源码如下:
...
var layer = new Layer(path, { //路径 , 路由配置 , fn
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
this.stack.push(layer); //也就是中间件的回调函数都放到了 this.stack 数组中
...
我们来分析分析Layer到底做了什么,以及对stack的运用。
Layer
判断Layer是否实例化的形式调用 , 对use中的参数(fn,中间件的回调函数)保存在Layer实例中,通过path-to-regexp库对path进行解析,最后是对特殊路由进行检验
其实Layer就是对中间件进行了一层包装
Layer函数原型上的方法有很多,在这里不一一列举,整个执行流程走通后,在回过头看
router.*
接着, 是router.get等函数的解析:
当你阅读源码route.js的时候发现未对get、post等method进行单独定义,仅仅有all方法,使用了 methods库:
最后在index.js中将methods.concat('all')挂载到proto上,调用route方法
到这里逻辑非常容易混乱,那么到底
router.get("/videolist", verifyToken(), videoController.videolist) 发生了什么?
通过forEach,将所有的method添加到proto,每次调用router.* 时,实际上是通过proto.route(router.route)获取Route实例,参数是传入的路径,返回的是当前路径所有对应的路由,每次注册路由, 都会创建特殊的Layer(第三个参数方便在后续中间件的执行,见下节),并且将当前路由添加到堆栈中,route[method]其实是调用Route实例中methods方法(内部也是对methods进行遍历) ,最终通过apply 将当前路由中的所有中间件作为参数调用get( * ) 方法,上面提到过Route实例中也有单独的中间件调用栈stack,因此stack存放的就是一个路由中所有的中间件。
Router.stack vs Route.stack
Router和Route的stack是有差别的,这个差别主要体现在存放的layer(layer是用来封装中间件的一个数据结构)不太一样,
Router.stack中存放的中间件包括但不限于路由中间件,而只有路由中间件的执行才会依赖与请求method,因此Router.stack里的layer没有method属性,而是将其动态添加(layer的定义中没有method字段)到了Route.stack的layer中;layer.route字段也是动态添加的,可以通过该字段来判断中间件是否是路由中间件。
最后在回到app.use,上面的疑问(7)fn.handle,翻遍了源码,都没有找到对中间件定义的handle函数,应该是针对的第三方express 的应用,就是指有 handle 的应用,每次都把属性复制过来,就是怕第三方的把属性给改了,之后让框架崩溃
到这里其实内部的整体实现逻辑已经介绍的完了,回到最前面app.handle
app.handle
在初始化express时,app.handle并没有调用。
app.listen创建服务器完成之后调用app.handle,获取所有的stack(中间件/路由)。
// 部分核心代码
proto.handle = function handle(req, res, out) {
var self = this;
var idx = 0;
var stack = self.stack;
...
next();
function next(err) {
var layerError = err === 'route'
? null
: err;
if (idx >= stack.length) {
return;
}
var path = getPathname(req);
// find next matching layer
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;
}
}
// no match
if (match !== true) {
return done(layerError);
}
// this should be done for the layer
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
return next(layerError || err);
}
if (route) {
return layer.handle_request(req, res, next);
}
trim_prefix(layer, layerError, layerPath, path);
});
}
function trim_prefix(layer, layerError, layerPath, path) {
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
};
执行app.handle才正式处理中间件执行逻辑,其利用「闭包的特性」缓存 idx 来记录当前遍历的状态 , 该方法内部又实现了 next 方法来匹配当前需要执行的中间件,主动调用next函数,通过while循环取出取出对应的路由,调用layer.handle_request调用fn即当前中间件,从遍历的代码可以明白中间件注册的顺序是非常重要的。
Layer.prototype.handle_request = function handle(req, res, next) { //本质上是调用fn
var fn = this.handle; //当前的中间件
if (fn.length > 3) {
// not a standard request handler
return next();
}
try {
(8) fn(req, res, next);//当前的中间件 传递三个参数
// 当我们调用 ↓
app.use((req,res,next) => {
next() //如果调用next实际上是调用的lrouter/index.js/ 176:next(err) 函数 因为此时idx 已经++了 所以会匹配下一个中间件
})
} catch (err) {
next(err);
}
};
上面代码中的 next 其实是handle函数中next函数,内部通过判断函数的形参个数过滤掉非错误处理中间件,app.use开始对下一个中间件进行入栈操作,一直重复下去,直到 idx >= stack.length
handle 方法初始化执行了一次 next 方法,但是该方法每次调用最多只能匹配一个中间件,所以在执行 handle_request 方法时,会将 next 方法透传给中间件(8),这样开发者就可以通过手动调用 next 方法的方式来执行接下来的中间件。