自己曾深入的学习了一下express的源码,然后,自己根据源码,复写了一套相对简单点的express框架。 express的源码我认为从大的功能点上看,可以分为两部分:对静态资源的处理和对非静态资源的处理。在上一篇中,用几十行代码写了一个最最简单而又非常重要的微型框架,把这两部分进行涵盖。传送门:实现express之一:通过几十行代码实现一个最简单的框架
一、框架入口逻辑
看任何框架的源码,都跟我们吃一个皮皮虾
一样,需要知道从哪里入口。(哈哈,纯粹是想吃了)。同样,要实现一个express,在上面的有基础原理武装基础上,就要实现入口逻辑。
/**
* @file 使用自己写的express 创建node服务
*/
const express = require('./express/express');
const ejs = require('ejs');
const app = express();
app.use(express.static('public'));
app.use((req, res, next) => {
console.log('所有的请求都会走这里');
next();
});
app.get('/', (req, res) => {
ejs.renderFile('./views/info.ejs', {
title: 'info页面',
description: 'info描述描述。。。'
},
(err, data) => {
if (err) {
console.log('render page error:', err);
return;
}
res.end(data);
});
});
app.listen(8000, () => {
console.log('server started on port 8000');
})
重点来了, 通过上面的代码,我们可以简单分析出以下逻辑:
- express本身是个函数
- 函数有返回值(app变量),只是现在这个app是对象还是函数不好说
- express函数有静态方法(static)
1.1 express init逻辑
进入逻辑看一下:
const mixin = require('merge-descriptors');
const EventEmitter = require('events').EventEmitter;
const proto = require('./application');
exports = module.exports = createApplication;
function createApplication() {
let app = function(req, res, next) {
// 执行路由请求时会执行
app.handle(req, res, next);
};
// 将 EventEmitter.prototype混合给app, 那么app也就有这些方法了,都属于app的静态方法
mixin(app, EventEmitter.prototype, false);
// 把自己定义的方法,都也赋值给app方法,单纯为了让主入口代码更简单
mixin(app, proto, false);
app.init();
return app;
}
exports.static = require('./static_service');
上面的代码中, application.js是处理express整体逻辑的,static_service是处理静态文件逻辑的。那么,我们先进入application.js,因为这里有整体逻辑,比如app.use就是这里面的。
// 首先执行 app.init(),而后又执行app.defaultConfiguration,初始化express的内部中间件
app.defaultConfiguration = function() {
const env = process.env.NODE_ENV || 'development';
this.set('env', env);
// 这里是query中间件的初始化处理,后面会调用
this.set('query parser', 'extended');
this.set('trust proxy', false);
};
// 再次,初始化methods的监听方法,post/get/put/delete/....
methods.forEach(function(method) {
app[method] = function(path) {
if (method === 'get' && arguments.length === 1) {
// ex: app.get('title')
return this.set(path);
}
// express中是把path和对应的callback放到了router中
// router专门来管理所有与请求相关的逻辑
// 首先,如果没有实例化过基础Router的话,先进行实例化
this._router = this._router || this.lazyrouter();
// get/post/...并没有调用this._router.use,,
// 而是调用的 this._router.route()
// 这两种是有区别的
let route = this._router.route(path);
// 把cb传到route[method]中进行保存
route[method].apply(route, slice.call(arguments, 1));
}
});
// 第三点是创建app.use
app.use = function(fn) {
let offset = 0; // 默认slice到哪个位置
let path = '/'; // 默认path是根路由
// 如果是app.use('aa/bb', function() {]})的情况
// 或者是 app.use([fn1, fn2]) 类似的情况
if (typeof fn !== 'function') {
let arg = fn;
while(Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
// 将多层的回调,处理成扁平化的一层数组
let fns = flatten(slice.call(arguments, offset));
if (fns.length === 0) {
throw new TypeError('app.use() requires a middleware function')
}
// 设置router
const router = this._router = this.lazyrouter();
// 把use中的回调函数进行遍历
fns.forEach(function(fnItem) {
if (!fnItem || !fnItem.handle || !fnItem.set) {
// 现在的使用情况会进这个逻辑,去调用了router的use方法, 创建layer实例等操作
return router.use(path, fnItem);
}
});
return this;
}
二、 app.use 逻辑
app.use(express.static('public')) 做了什么?
当执行上面这句,那就说明要执行app.use了,在执行app.use函数中,this._router
如果没有,则执行 lazyrouter
app.lazyrouter = function() {
if(!this._router) {
const router = new Router({
caseSensitive: this.enabled('case sensitive routing'), // 大小写敏感,默认不敏感
strict: this.enabled('strict routing') // 激活严格路由,默认不启用,/uu和/uu/ 都可以正常访问
});
// express中默认的两个中间件: query 和 init,这里对应的是上面设置的
// query -> 把query进行parse
router.use(query(this.get('query parser fn')));
// TODO 实现middleware.init
// router.use(middleware.init(this));
return router;
}
return this._router;
}
query(this.get('query parser fn'))
这里的this.get('query parser fn'),就是通过上面的 this.set('query parser', 'extended');
处理的
app.set = function(setting, value) {
if (arguments.length === 1) {
return this.settings[setting];
}
debug('set "%s" to %o', setting, value);
this.settings[setting] = value;
switch (setting) {
case 'query parser':
this.set('query parser fn', compileQueryParser(value));
break;
default:
break;
}
}
compileQueryParser(value)
其实做的是,是使用node自带的querystring来解析query,还是用npm 模块 qs
解析query
既然现在看到了实现 router.use(query(this.get('query parser fn')));
, 那么就仔细看看app.use
执行的逻辑吧。
很显然,关键点在于 new Router(...).use
, 这里面做的是创建layer,并且把layer存储到this.stack中。以后在执行的时候,会从this.stack中把一个个layer拿出来执行
// 这里的proto就是 Router函数
// router/index.js route 入口
const proto = module.exports = function(options) {
let opts = options || {};
let router = (req, res, next) => {
router.handle();
}
router.params = {};
router._params = {};
// 是否对大小写敏感
router.caseSensitive = opts.caseSensitive;
router.mergeParams = opts.mergeParams;
// 是否启用严格模式
router.strict = opts.strict;
router.stack = [];
// 将proto方法整个复制到router方法的__proto__上
setPrototypeOf(router, proto);
// 也就是说外边用new Router(...) 返回的是这个router函数。
// 然后这个router函数的__proto__上带着proto的所有静态方法
return router;
}
proto.use = function(fn) {
let offset = 0;
let path = '/';
if (typeof fn !== 'function') {
offset = 1;
path = fn;
}
// 比如 use(fn1, fn2, fn3)、 use(fn1,[fn2, fn3])等等情况
let callbacks = flatten(slice.call(arguments, offset));
// 其实照最简单实现来说,把回调函数存到stack中就行,但是单独放到了一个Layer构造函数中,就说明还有其它工作要做
for(let i = 0; i < callbacks.length; i++) {
let fn = callbacks[i];
if (typeof fn !== 'function') {
throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn));
}
debug('use %o %s', path, fn.name || '<anonymous>');
const layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
// 这里注意一下,通过use加载的中间件,layer.route会赋值为undefined,后面会有不一致的地方进行对应,
layer.route = undefined;
this.stack.push(layer);
}
return this;
}
再看一下Layer中做了什么
function Layer(path, options, fn) {
}
// 错误处理
Layer.prototype.handle_error = function(err, req, res, next) {
// ...
}
// 正常处理回调函数
Layer.prototype.handle_request = function(req, res, next) {
// ...
}
// 路径匹配函数
Layer.prototype.match = function(path) {
// ...
}
这一条线现在先讲到这。再继续将肯定大家就晕的一塌糊涂了。所以需要把这个阶段画图来展示:
希望通过上面的代码和详细的路径图能让大家更容易理解一些。
三、 Method请求逻辑
如果上面的逻辑感觉有些明白了,那么咱们可以继续了。
3.1 method的方法定义逻辑
为了保持讲解的简单和一致, method的方法只用 app.get()来代替,其实有很多的方法,具体可参见 npm methods
设置好应用级的中间件后,那么就应该设置路由级的中间件了
app.get('/', (req, res) => {
// ....
})
app.post('/login', (req, res) => {
// ...
})
为什么我会称之为路由级的中间件呢? 原因在于,我们其实是可以在Method请求的回调中加入next的,比如:
app.get('/', (req, res, next) => {
// ....
next();
})
当然,由于我们一般不会在路由请求处理中加入next,大家知道有这么一回事就行。这不作为讨论点。但是,我们需要好好看看这个路由级中间件在express中是如何实现的。
其实,在这篇刚开始的时候,我给大家提示了一下,现在详细拿出来说。代码就是这段:
// 再次,初始化methods的监听方法,post/get/put/delete/....
methods.forEach(function(method) {
app[method] = function(path) {
if (method === 'get' && arguments.length === 1) {
// ex: app.get('title')
return this.set(path);
}
// express中是把path和对应的callback放到了router中
// router专门来管理所有与请求相关的逻辑
// 首先,如果没有实例化过基础Router的话,先进行实例化
this._router = this._router || this.lazyrouter();
// get/post/...并没有调用this._router.use,,
// 而是调用的 this._router.route()
// 这两种是有区别的
let route = this._router.route(path);
// 把cb传到route[method]中进行保存
route[method].apply(route, slice.call(arguments, 1));
}
});
methods是一个npm包,有兴趣的可以看下,里面有所有的请求方式。 其实上面做的就是把这些所有的请求方式挂载上。 所以我们可以直接在业务代码中直接使用app.get()
, app.post()
。
因为我们已经有this._router
了,所以可以直接使用 this._router.route(path)
, 这里逻辑跟app.use()类似,相同点在于,都是创建了一个layer实例,并且把这个layer实例push进this.stack
。但是区别更加重要。 区别点
在于:
- 专门又实例化了一个Route,这个Route中,也存储了一套stack(调用栈)和Method方法处理
- layer实例中的route变量赋值了。 在app.use()中,赋值的是undefined,这里有很多的不同,后面详细说。
- 并没有把开发者的cb放入layer中,而是放在了
route[method].apply(route, slice.call(arguments, 1));
中。
下面详细看下代码:
let route = this._router.route(path);
proto.route = function(path) {
// 在其中实例化了一个Route
let route = new Route(path);
// 注意这个layer的调用和app.use的不同,记住当执行这个layer中回调的时候,执行的是route.dispatch
const layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
// 这里把route进行了赋值,而使用app.use时,是赋值为undefined
// 这里也就是说把route实例保存到layer中,然后把layer存储到this.stack中
layer.route = route;
this.stack.push(layer);
return route;
}
看看 new Route
做了什么
function Route(path) {
this.path = path;
// route中维护着自己的stack,这里面存储的其实是get、post请求的callback
this.stack = [];
this.methods = {};
}
// route实例中主要有stack、methods,后面在执行的时候再说
回到 route[method].apply(route, slice.call(arguments, 1));
这句。
其实在new Route()
的时候,就已经先进行了对Method的处理。
methods.forEach(function(method) {
Route.prototype[method] = function() {
// 开发者写的回调
const handles = flatten(slice.call(arguments));
for(let i = 0; i < handles.length; i++) {
let handle = handles[i];
if (typeof handle !== 'function') {
let type = toString.call(handle);
let msg = "Route." + method + '() requires a callback function but got a ' + type;
throw new Error(msg);
}
// Layer内部还是会实例化
var layer = Layer('/', {}, handle);
// 加一个静态属性,为了区分这个layer属于get还是post。。。
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);
}
return this;
}
});
通过这段代码,我相信大家可能脑子有些乱,怎么又对methods进行了遍历?
走到这里,我们有必要把 app.use()
和 app.get()
内部的结构做一个详尽的解析了。
3.2 app.use() 和 app.get() 内部回调的存储方式解析
这里有两个执行栈(stack),一是app.use(),执行route.use, 和app.get()执行route.route时,都会new Layer()
并且push到 this.stack
, 这里没有什么区别。但区别
之处在于:
- app.use()执行后产生的layer,把layer.route 赋值为undefined, 而 app.get()后产生的layer,会把new Route()的实例赋值给layer.route。 (layer.route = route 这里还记得么?在上面的代码中有)
- app.use()会把内置中间件或者开发者的中间件直接作为layer的第三个参数传入,作为layer执行时的回调。而app.get() 并没有直接把开发者的中间件传入, 而是用的
new Layer(path, {...}, route.dispatch.bind(route))
dispatch方法后面再详细说,但它的作用是执行route中自己维护的stack - app.get()中使用
route[method].apply(route, slice.call(arguments, 1));
, 把开发者的callback存到Route自己的stack中。
还是再继续画图表示一下:
我想,大家大概能看出其中的道道
了。
为什么要分开搞,那就是为了让流程上更清楚, 各人负责各人的逻辑。 如果都糅杂到一起,相信随着项目的增大,可维护性会越来越低。
四、app.use() 和app.get() 中callback的执行
根据前三步, 开发者要监听的应用级别和中间件和路由级别中间件中的callback都保存到应该保存的地方了, 下一步就仔细来看当一个请求真正打过来的时候,是如何执行的。
4.1 next是什么
相信使用过express的同学都知道有个next
的存在。 很多人感觉又重要,又缥缈。 重要在于应用级中间件中如果不调用next(),那么程序就卡死了。 缥缈是因为不清楚next到底做了什么。
今天,我来带着大家揭开next神秘的面纱吧。
我们知道如果要有http服务,那就需要创建一个http服务器,在express中,这个细节被隐藏到了listen
函数中。
app.listen = function() {
let server = http.createServer(this);
return server.listen.apply(server, arguments);
}
所以,当一个请求来了以后,会首先执行:
// express.js
let app = function(req, res, next) {
// 执行路由请求时会执行
app.handle(req, res, next);
};
// application.js
app.handle = function(req, res, next) {
const router = this._router;
// req 的处理中作为最后的兜底处理器处理错误
let done = next || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
});
if (!router) {
done();
return;
}
// next函数的出处在这里!!
router.handle(req, res, done);
}
router.handle(req, res, done)
是next函数实现的精华所在。 原理是在执行this.stack
中的layer完成后,如果存在next()方法,则从this.stack中再拿出一个layer继续执行, 一直执行到this.stack为空,或者在app.get() 实现中直接res.end
等不再需要继续执行的情况。
proto.handle = function(req, res, out) {
debugger;
let self = this;
let idx = 0;
let stack = this.stack;
let parentUrl = req.baseUrl || '';
// 一个请求结束后触发,无论是执行栈为空了,还是执行过程中出现问题了
let done = restore(out, req, 'baseUrl', 'next', 'params');
req.next = next;
req.baseUrl = parentUrl;
req.originalUrl = req.originalUrl || req.url;
next();
function next(err) {
let layerError = err === 'route'
? null
: err;
if (layerError === 'router') {
setImmediate(done, null);
}
// 栈中已经没有Layer了
if (idx >= stack.length) {
setImmediate(done, layerError);
return;
}
let path = getPathname(req);
if (path === null || path === undefined) {
return done(layerError);
}
let layer;
let match;
let route;
while(match !== true && idx < stack.length) {
layer = stack[idx++];
match = matchLayer(layer, path);
// 拿到route,仅在app.get()的情况下
route = layer.route;
// 如果不匹配当前path
if (match !== true) {
continue;
}
// 在非路由中间件的情况下,route为undefined
if (!route) {
continue;
}
if (layerError) {
match = false;
continue;
}
}
if (match !== true) {
return done(layerError);
}
if (route) {
req.route = route;
}
req.params = layer.params;
let layerPath = layer.path;
self.process_params(layer, {}, req, res, (err) => {
if (err) {
return next(err);
}
if (route) {
// app.get(), app.set() ....
return layer.handle_request(req, res, next);
}
trim_prefix(layer, layerError, layerPath, path);
});
}
// TODO 这里的逻辑需要重新看
function trim_prefix(layer, layerError, layerPath, path) {
// TODO 需要处理
if (layerPath.length !== 0) {
// TODO
}
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
}
最重要的是会把next函数传给各个callback中,比如说,app.use(function(req, res, next){})
这个next就是上面的next函数。 所以, 当callback执行完成后,调用 next(), 则就会取下一个layer,执行。
app.get() 在执行时具体怎么处理
其实这个问题我们在上面也有说,基本原理,无非就是走到application级别的layer时, 因为之前有把route实例赋值给layer,也就是 layer.route = route
(为了加深印象,这句出现多次了), 然后代码层面就会去执行route中的逻辑。 因为route实例中存在自己的stack, 那么执行route中的stack也就理所当然了。
// router/route.js
Route.prototype.dispatch = function(req, res, done) {
let idx = 0;
let stack = this.stack;
if (stack.length === 0) {
return done();
}
let method = req.method.toLowerCase();
if (method === 'head' && !this.methods['head']) {
method = 'get';
}
// 把实例赋值到req.route
req.route = this;
// 开始执行栈中内容
next();
function next(err) {
// 命中退出route的信号
if (err && err === 'route') {
return done();
}
// 命中退出router的信号
if (err && err === 'router') {
return done(err);
}
// 注意这个layer是route实例中的,并不是application级别的
let layer = stack[idx++];
if (!layer) {
return done(err);
}
if (layer.method && layer.method !== method) {
// 如果匹配不上,则去栈中找下一项执行
return next();
}
if (err) {
// 如果上面兜不住
layer.handle_error(err, req, res, next);
} else {
// 而且这里面的next是这段代码中的next,不要跟application级别的混了
layer.handle_request(req, res, next);
}
}
}
// layer.js
Layer.prototype.handle_request = function(req, res, next) {
const fn = this.handle;
// 如果length >3, 则不是标准的处理函数
if (fn.length > 3) {
return next();
}
try {
// 执行app.get(path, (req, res) => {}) 中的callback
// 所以在这里也可以看到,其实是可以将next传出去的。
fn(req, res, next);
} catch (error) {
next(error);
}
}
通过手敲这4000多字,希望能给大家一些启发,对express有更深层的认识。
当然,并不是说express的实现方式就一定是最好的,koa也是一个非常好的框架,只是在项目中没有怎么用,也就暂时没有对其源码进行深入的研究。 koa中就不需要next也能够做到,大家有兴趣可以再去koa中搜寻一下。
大家觉得好别忘了点个赞哈,多多的赞,更能激发我不断分享,哈哈。
下一篇,我将分析 express中的静态路由,仔细分析一下 app.use(express.static('public'))
这么一句简单的代码是怎么做到加载静态资源的。传送门:实现express之三: app.use(express.static('public'))的奥秘