最近需要用到 Express,将 Express 学习了一下。Express 提供了丰富的功能,但是目前,我基本只会用到 Express 的路由和模板功能,所以我只是将 Express 的路由机制了解了一下。Express 的思想并不复杂,但是由于 Express 支持的功能多了,代码也就复杂了。我将 Express 的路由机制简写了下,方便以后自己进一步学习,同时,其他同学也多了份参考。
一、支持 app.use(path, fn)
测试代码如下:
// test/test-use.js
const express = require('../lib/express');
const app = express();
app.set('case sensitive routing', true);
app.use(fn1);
app.use('/a', fn2);
app.listen(3000);
function fn1(req, res, next) {
console.log('fn1 exec \n');
next();
}
function fn2(req, res, next) {
console.log('fn2 exec \n');
console.log('app: ', app, '\n')
console.log('app._router: ', app._router, '\n');
next();
}
对于上面的测试代码,我们预期的结果是:对于 fn1,任何请求进来,fn1 都会执行,对于 fn2,只有请求路径包含 /a(包含指请求路径为 /a、/a/、/a/b 等等,注意:/ab 并不包含 /a ),fn2 才会执行。我们能想到,可以声明一个空数组,每当执行一次 app.use,就把传入 use 的函数,加入到数组中,当请求到来时,依次执行数组中的函数即可,这样实现,则 app.use(fn) 是符合预期的。但是 app.use(path, fn) 是不符合预期的,我们稍微改造下前面的实现,就能让 app.use(path, fn) 也符合预期,我们还是先申明一个数组,每当执行一次 app.use 时,就把传入 app.use 的路径和函数,挂在一个 Layer 对象上,然后把这个对象放入数组,当请求到来时,我们遍历数组中的对象,如果请求路径与 Layer 对象上挂的路径匹配,我们则执行挂在 Layer 上的方法。这样实现 app.use(path, fn) 就是符合预期的了,下面代码就是这个思路的实现:
代码目录如下:
lib/express.js 文件:
// lib/express.js
const proto = require('./application');
exports = module.exports = createApplication;
function createApplication() {
// 执行 express(),返回此函数,同时,此函数也是请求到来时的回调函数
const app = function(req, res) {
app.handle(req, res);
}
Object.assign(app, proto);
app.init();
return app;
}
lib/application.js 文件:
// lib/application.js
/**
* 此文件用来给 app 添加各种属性和函数
*/
const Router = require('./router');
const http = require('http');
const slice = Array.prototype.slice;
const app = exports = module.exports = {};
app.init = function init() {
this.settings = {};
};
app.listen = function listen() {
const server = http.createServer(this);
return server.listen.apply(server, arguments);
};
app.use = function use(fn) {
let offset = 0;
let path = '/';
// 兼容 app.use(path, fn)
if(typeof fn !== 'function') {
offset = 1;
path = fn;
}
const fns = slice.call(arguments, offset);
this.lazyrouter();
const router = this._router;
fns.forEach(function(fn) {
router.use(path, fn);
});
return this;
};
app.lazyrouter = function lazyrouter() {
if(!this._router) {
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
});
}
};
app.enabled = function enabled(setting) {
return Boolean(this.set(setting));
};
app.set = function set(setting, val) {
if(arguments.length === 1) {
return this.settings[setting];
}
this.settings[setting] = val;
return this;
}
app.handle = function handle(req, res) {
const router = this._router;
// 兜底函数,当所有中间件遍历完时,还没有执行 res.end,则由这个函数兜底,返回响应
const done = function() {
res.end(`Cannot ${req.method} ${req.url}`);
};
router.handle(req, res, done);
}
lib/router/index.js 文件:
// lib/router/index.js
const url = require('url')
const Layer = require('./layer');
const slice = Array.prototype.slice;
const proto = module.exports = function(options) {
const opts = options || {};
function router() {
}
Object.setPrototypeOf(router, proto);
router.caseSensitive = opts.caseSensitive;
router.stack = [];
return router;
};
proto.use = function use(fn) {
let offset = 0;
let path = '/';
if(typeof fn !== 'function') {
offset = 1;
path = fn;
}
const callbacks = slice.call(arguments, offset);
for(let i = 0; i < callbacks.length; i++) {
const fn = callbacks[i];
const layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
this.stack.push(layer);
}
return this;
};
proto.handle = function(req, res, out) {
const self = this;
let idx = 0;
const stack = self.stack;
next();
function next(err) {
if(idx >= stack.length) {
out(err);
return;
}
const { pathname } = url.parse(req.url, true);
let layer;
let match;
let route;
while(match !== true && idx < stack.length) {
layer = stack[idx++];
match = layer.match(pathname);
route = layer.route;
if(match !== true) {
continue;
}
if(!route) {
continue
}
}
if(match !== true) {
return out(err);
}
trim_prefix(layer, err);
}
function trim_prefix(layer, err) {
if(err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
};
lib/layer.js 文件:
// lib/layer.js
const pathRegexp = require('path-to-regexp');
module.exports = Layer;
function Layer(path, options, fn) {
if(!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
const opts = options || {};
this.handle = fn;
this.regexp = pathRegexp(path, opts);
this.regexp.fast_slash = path === '/' && opts.end === false
}
Layer.prototype.match = function match(path) {
let match;
if(this.regexp.fast_slash) {
return true;
}
match = this.regexp.exec(path);
if(!match) {
return false;
}
return true;
};
Layer.prototype.handle_error = function handle_error(error, req, res, next) {
const fn = this.handle;
if(fn.length !== 4) {
return next(error);
}
try {
fn(error, req, res, next);
} catch(err) {
next(err);
}
};
Layer.prototype.handle_request = function handle(req, res, next) {
const fn = this.handle;
if(fn.length > 3) {
return next();
}
try {
fn(req, res, next);
} catch(err) {
next(err);
}
}
node_modules/path-to-regexp/index.js 文件:
// node_modules/path-to-regexp/index.js
module.exports = pathtoRegexp;
function pathtoRegexp(path, options) {
options = options || {};
var strict = options.strict;
var end = options.end !== false;
var flags = options.sensitive ? '' : 'i';
path = ('^' + path + (strict ? '' : path[path.length - 1] === '/' ? '?' : '/?'))
.replace(/(\/)/g, '\\$1');
path += (end ? '$' : (path[path.length - 1] === '/' ? '' : '(?=\\/|$)'));
return new RegExp(path, flags);
};
当执行完 test/test-use.js 之后,会生成如下的数据结构:
从上图可以看出,app 有一个 _router 属性,_router 属性有一个 stack 属性,stack 是一个数组,我们调用 app.use 时,会生成 Layer 实例,并将 layer 放入到 stack 中,其中 layer 上有个 handle 和 regexp 属性,handle 指向的就是传入 app.use 的函数,regexp 依据传入 app.use 的路径生成。
当请求到来时,会依次遍历 stack 中的 layer,如果请求路径与 layer 上的路径匹配,则执行 layer.handle,如果不匹配,则遍历下一个 layer。
我们可以发现,app.use 的思想还是比较简单,就是常规思路。
二、支持 app.get(path, fn1, fn2)
测试代码如下:
// test/test-get.js
const express = require('../lib/express');
const app = express();
app.set('case sensitive routing', true);
app.set('strict routing', true);
app.use(fn1);
app.get('/a', fn2, fn3);
app.listen(3000);
function fn1(req, res, next) {
console.log('fn1 exec \n');
next();
}
function fn2(req, res, next) {
console.log('fn2 exec \n');
next();
}
function fn3(req, res, next) {
console.log('fn3 exec \n');
console.log('app: ', app, '\n')
console.log('app._router: ', app._router, '\n');
console.log('app._router.stack[1]: ', app._router.stack[1], '\n');
next();
}
对于上面的测试代码,我们预期的结果是,对于任何请求都执行 fn1,当请求路径全等于 /a,且请求方法为 get 时,执行 fn2 和 fn3,前面的代码对于中间件是否执行,只比较了路径,不能支持 app.get。我们可以做如下改动,使得,改动后的代码支持 app.get。首先,执行 app.use,生成一个 Layer 对象,将传入 app.use 的路径和方法挂在 layer 对象上,并将 layer 对象放入 app._router.stack 中;执行 app.get,生成一个 Layer 对象,再生成一个 Route 对象,将请求方法和传入 app.get 的函数挂在 route 对象上,将 route 以及遍历挂在 route 的函数的方法,挂在 layer 对象上。
当请求到来时,遍历 app._router.stack,如果 layer 对象上的 route 属性不为真,则只要路径匹配,就执行挂在 layer 上的方法,如果 layer 对象上的 route 属性为真,首先看请求路径是否与挂在 layer 上的路径匹配,如果匹配,在看下请求方法是否和挂在 route 上的方法匹配,如果匹配,则执行挂在 layer 上的遍历 route.stack 的函数。
改动后的代码目录如下:
代码变动:
lib/application.js 文件变动:
// lib/application.js
// add
const methods = require('methods');
app.lazyrouter = function lazyrouter() {
if(!this._router) {
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
// add
strict: this.enabled('strict routing')
});
}
};
// add
methods.forEach(function(method) {
app[method] = function(path) {
if(method === ' get' && arguments.length === 1) {
return this.set(path);
}
this.lazyrouter();
const route = this._router.route(path);
route[method].apply(route, slice.call(arguments, 1));
return this;
}
});
lib/router/index.js 文件变动:
// lib/router/index.js
// add
const Route = require('./route');
const slice = Array.prototype.slice;
const proto = module.exports = function(options) {
const opts = options || {};
function router() {
}
Object.setPrototypeOf(router, proto);
router.caseSensitive = opts.caseSensitive;
// add
router.strict = opts.strict;
router.stack = [];
return router;
};
proto.handle = function(req, res, out) {
const self = this;
let idx = 0;
const stack = self.stack;
next();
function next(err) {
if(idx >= stack.length) {
out(err);
return;
}
const { pathname } = url.parse(req.url, true);
let layer;
let match;
let route;
while(match !== true && idx < stack.length) {
layer = stack[idx++];
match = layer.match(pathname);
route = layer.route;
if(match !== true) {
continue;
}
if(!route) {
continue
}
// add
const method = req.method;
const has_method = route._handles_method(method);
if(!has_method) {
match = false;
continue;
}
}
if(match !== true) {
return out(err);
}
trim_prefix(layer, err);
}
function trim_prefix(layer, err) {
if(err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
};
// add
proto.route = function route(path) {
const route = new Route(path);
const 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;
}
新增 lib/router/route.js 文件
// lib/router/route.js
const Layer = require('./layer');
const methods = require('methods');
const slice = Array.prototype.slice;
module.exports = Route;
function Route(path) {
this.path = path;
this.stack = [];
this.methods = {};
}
methods.forEach(function(method) {
Route.prototype[method] = function() {
const handles = slice.call(arguments);
for(let i = 0; i < handles.length; i++) {
const layer = Layer('/', {}, handles[i]);
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);
}
return this;
};
});
Route.prototype._handles_method = function _handles_method(method) {
const name = method.toLowerCase();
return Boolean(this.methods[name]);
};
Route.prototype.dispatch = function dispatch(req, res, done) {
let idx = 0;
const stack = this.stack;
if(stack.length === 0) {
return done;
}
next();
function next(err) {
const layer = stack[idx++];
if(err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
};
新增 node_modules/methods/index.js 文件
// node_modules/methods/index.js
module.exports = ['get'];
执行 /test/test-get.js 文件,我们可以得到如下的数据结构:
我们可以看到通过 app.get 生成的 Layer 对象,layer.route 是一个 Route 对象,route 对象上记录了方法,用于请求到来时,方法比较,route.stack 中有 2 个 Layer 对象,这两个 layer 对象上的 handle 属性分别是 fn2,fn3,其实我感觉,route.stack 中直接存 fn2、fn3 也是可以的。
可以看出,app.get 只是在 app.use 的数据结构上,加了点变动,有点像二位数组的遍历,思想也是较为简单。
三、支持 app.use(path, router)
了解了 app.use(path, fn) 及 app.get(path, fn1, fn2) 的实现思路后,看下代码就知道 app.use(path, router) 实现的功能及实现思路,下面是支持 app.use(path, router) 的代码改动,改动很小。
代码变动:
lib/router/index.js 文件变动:
// lib/router/index.js
const proto = module.exports = function(options) {
const opts = options || {};
// change
function router(req, res, next) {
router.handle(req, res, next);
}
Object.setPrototypeOf(router, proto);
router.caseSensitive = opts.caseSensitive;
router.strict = opts.strict;
router.stack = [];
return router;
};
proto.handle = function(req, res, out) {
const self = this;
let idx = 0;
const stack = self.stack;
// add
let slashAdded = false;
let removed = '';
next();
function next(err) {
// add
if (slashAdded) {
req.url = '';
slashAdded = false;
}
if (removed.length > 0) {
req.url = removed + req.url;
removed = '';
}
if(idx >= stack.length) {
out(err);
return;
}
const { pathname } = url.parse(req.url, true);
let layer;
let match;
let route;
while(match !== true && idx < stack.length) {
layer = stack[idx++];
match = layer.match(pathname);
route = layer.route;
if(match !== true) {
continue;
}
if(!route) {
continue
}
const method = req.method;
const has_method = route._handles_method(method);
if(!has_method) {
match = false;
continue;
}
}
if(match !== true) {
return out(err);
}
trim_prefix(layer, err);
}
function trim_prefix(layer, err) {
// add
removed = layer.path;
req.url = req.url.slice(removed.length);
if (req.url == '') {
req.url = '/';
slashAdded = true;
}
if(err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
};
// add
methods.forEach(function(method) {
proto[method] = function(path) {
const route = this.route(path);
route[method].apply(route, slice.call(arguments, 1));
return;
}
})
lib/layer.js 文件变动:
// lib/layer.js
function Layer(path, options, fn) {
if(!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
const opts = options || {};
// add
this.path = path;
this.handle = fn;
this.regexp = pathRegexp(path, opts);
this.regexp.fast_slash = path === '/' && opts.end === false
}
以上,就是我简写的 Express 路由机制,路由机制中的参数处理,我没有写,因为我似乎用不到。可以看出,Express 思路是较为简单。感觉 Express 的目标实现比较全的功能,比如模板、缓存,但是如果用了服务端渲染、将静态文件托管 CDN,其实这两个功能用的就比较少了,但是 Express 用于学习服务端还是不错的。
四、总结
略。。。
更新日志:
2021-02-18:发布