Express4.x之中间件与路由详解及源码分析
- Application.use()
- Application.router()
- express核心源码模拟
一、express.use()
1.1app.use([path,] callback [, callback ...])
通过语法结构可以看到Application.use()参数分别有以下几种情形:
app.use(function(){...}); //给全局添加一个中间件 app.use(path,function(){...}); //给指定路由path添加一个中间件 app.use(function(){...}, function(){...}, ...); //给全局添加n个中间件 app.use(path,function(){...},function(){...}, ...); //给指定路由path添加n个中间件
关于path最简单也是最常用的就是字符串类型(例:‘/abcd’);除了字符串Express还提供了模板和正则格式(例:‘<span>/abc?d</span>
‘, ‘<span>/ab+cd</span>
‘, ‘<span>/ab\*cd</span>
‘, ‘<span>/a(bc)?d</span>
‘, ‘<span>/\/abc|\/xyz/</span>
‘);除了单个的字符串和模板还可以将多个path作为一个数组的元素,然后将这个数组作为use的path,这样就可以同时给多个路由添加中间件,详细内容可以参考官方文档:https://www.expressjs.com.cn/4x/api.html#path-examples。
关于callbakc多个或单个中间件程序这已经再语法结构中直观的体现出来了,这里重点来看看回调函数的参数:
app.use(function(req,res,next){...}); //必须提供的参数 app.use(function(err,req,res,next){...}); //错误中间件需要在最前面添加一个错误参数
关于中间件的简单应用:
let express = require(‘./express‘); let app = express(); app.use(‘/‘,function(req,res,next){ console.log("我是一个全局中间件"); next(); //每个中间件的最末尾必须调用next }); app.use(‘/‘,function(err,req,res,next){ console.log("我是一个全局错误中间,当发生错误是调用") console.error(err.stack); res.status(500).send(‘服务出错误了!‘); //由于这个错误处理直接响应了客户端,可以不再调用next,当然后面还需要处理一些业务的话也是可以调用next的 });
1.2简单的模拟Express源码实现Appliction.use()以及各个请求方法的响应注册方法(这里个源码模拟路由概念还比较模糊,所以使用请求方法的响应注册API,而没有使用路由描述):
//文件结构 express index.js //源码模拟实现 let http = require("http"); let url = require(‘url‘); function createApplication(){ //app是一个监听函数 let app = (req,res) =>{ //取出每一个层 //1.获取请求的方法 let m = req.method.toLowerCase(); let {pathname} = url.parse(req.url,true); //通过next方法进行迭代 let index = 0; function next(err){ //如果routes迭代完成还没有找到,说明路径不存在 if(index === app.routes.length) return res.end(`Cannot ${m} ${pathname}`); let {method, path, handler} = app.routes[index++];//每次调用next就应该取下一个layer if(err){ if(handler.length === 4){ handler(err,req,res,next); }else{ next(err); } }else{ if(method === ‘middle‘){ //处理中间件 if(path === ‘/‘ || path === pathname || pathname.startsWith(path+‘/‘)){ handler(req,res,next); }else{ next();//如果这个中间件没有匹配到,继续通过next迭代路由容器routes } }else{ //处理路由 if( (method === m || method ===‘all‘) && (path === pathname || path === ‘*‘)){ //匹配请求方法和请求路径(接口) handler(req,res);//匹配成功后执行的Callback }else{ next(); } } } } next(); } app.routes = [];//路由容器 app.use = function(path,handler){ if(typeof handler !== ‘function‘){ handler = path; path = ‘/‘; } let layer = { method:‘middle‘, //method是middle就表示它是一个中间件 path, handler } app.routes.push(layer); } app.all = function(path,handler){ let layer = { method:‘all‘, path, handler } app.routes.push(layer); } console.log(http.METHODS); http.METHODS.forEach(method =>{ method = method.toLocaleLowerCase(); app[method] = function (path,handler){//批量生成各个请求方法的路由注册方法 let layer = { method, path, handler } app.routes.push(layer); } }); //內置中间件,给req扩展path、qury属性 app.use(function(req,res,next){ let {pathname,query} = url.parse(req.url,true); let hostname = req.headers[‘host‘].split(‘:‘)[0]; req.path = pathname; req.query = query; req.hostname = hostname; next(); }); //通过app.listen调用http.createServer()挂在app(),启动express服务 app.listen = function(){ let server = http.createServer(app); server.listen(...arguments); } return app; } module.exports = createApplication;
测试模拟实现的Express:
let express = require(‘./express‘); let app = express(); app.use(‘/‘,function(req,res,next){ console.log("我是一个全局中间件"); next(); }); app.use(‘/user‘,function(req,res,next){ console.log("我是user接口的中间件"); next(); }); app.get(‘/name‘,function(req,res){ console.log(req.path,req.query,req.hostname); res.end(‘zfpx‘); }); app.post(‘/name‘,function(req,res){ res.end(‘post name‘); }); app.all("*",function(req,res){ res.end(‘all‘); }); app.use(function(err,req,res,next){ console.log(err); next(); }); app.listen(12306);
在windows系统下测试请求:
关于源码的构建详细内容可以参考这个视频教程:app.use()模拟构建视频教程,前面就已经说明过这个模式实现仅仅是从表面的业务逻辑,虽然有一点底层的雏形,但与源码还是相差甚远,这一部分也仅仅只是想帮助理解Express采用最简单的方式表现出来。
1.3如果你看过上面的源码或自己也实现过,就会发现Express关于中间件的添加方式除了app.use()还有app.all()及app.METHOD()。在模拟源码中我并未就use和all的差异做处理,都是采用了请求路径绝对等于path,这种方式是all的特性,use的path实际表示为请求路径的开头:
app.use(path,callback):path表示请求路径的开头部分。
app.all(path,callback):paht表示完全等于请求路径。
app.METHOD(path,callback):并不是真的有METHOD这个方法,而是指HTTP请求方法,实际上表示的是app.get()、app.post()、app.put()等方法,而有时候我们会将这些方法说成用来注册路由,这是因为路由注册的确使用这些方法,但同时这些方法也是可以用作中间的添加,这在前一篇博客中的功能解析中就有说明(Express4.x之API:express),详细见过后面的路由解析就会更加明了。
二、express.router()
2.1在实例化一个Application时会实例化一个express.router()实例并被添加到app._router属性上,实际上这个app使用的use、all、METHOD时都是在底层调用了该Router实例上对应的方法,比如看下面这些示例:
let express = require("express"); let app = express(); app._router.use(function(req,res,next){ console.log("--app.router--"); next(); }); app._router.post("/csJSON",function(req,res,next){ res.writeHead(200); res.write(JSON.stringify(req.body)); res.end(); }); app.listen(12306);
上面示例中的app._router.use、app._router.post分别同等与app.use、app.post,这里到这里也就说明了上一篇博客中的路由与Application的关系Express4.x之API:express。
2.2Express中的Router除了为Express.Application提供路由功能以外,Express也将它作为一个独立的路由工具分离了出来,也就是说Router自身可以独立作为一个应用,如果我们在实际应用中有相关业务有类似Express.Application的路由需求,可以直接实例化一个Router使用,应用的方式如下:
let express = require(‘/express‘); let router = express.Router(); //这部分可以详细参考官方文档有详细的介绍
2.3由于这篇博客主要是分析Express的中间件及路由的底层逻辑,所以就不在这里详细介绍某个模块的应用,如果有时间我再写一篇关于Router模块的应用,这里我直接上一份模拟Express路由的代码以供参考:
文件结构:
express //根路径 index.js //express主入口文件 application.js //express应用构造模块 router //路由路径 index.js //路由主入口文件 layer.js //构造层的模块 route.js //子路由模块
Express路由系统的逻辑结构图:
模拟代码(express核心源码模拟):
//express主入口文件 let Application = require(‘./application.js‘); function createApplication(){ return new Application(); } module.exports = createApplication;
index.js //express主入口文件
//用来创建应用app let http = require(‘http‘); let url = require(‘url‘); //导入路由系统模块 let Router = require(‘./router‘); const methods = http.METHODS; //Application ---- express的应用系统 function Application(){ //创建一个內置的路由系统 this._router = new Router(); } //app.get ---- 实现路由注册业务 // Application.prototype.get = function(path,...handlers){ // this._router.get(path,‘use‘,handlers); // } methods.forEach(method => { method = method.toLocaleLowerCase(); Application.prototype[method] = function(path,...handlers){ this._router[method](path,handlers); } }); //app.use ---- 实现中间件注册业务 //这里模拟处理三种参数模式: // -- 1个回调函数:callback // -- 多个回调函数:[callback,] callback [,callback...] // -- 指定路由的中间件:[path,] callback [,callback...] // -- 注意源码中可以处理这三种参数形式还可以处理上面数据的数组形式,以及其他Application(直接将其他app上的中间件添加到当前应用上) Application.prototype.use = function(fn){ let path = ‘/‘; let fns = []; let arg = [].slice.call(arguments); if(typeof fn !== ‘function‘ && arg.length >= 2){ if(typeof arg[0] !== ‘string‘){ fns = arg; }else{ path = arg[0]; fns = arg.slice(1); } }else{ fns = arg; } this._router.use(path,‘use‘,fns); } Application.prototype.all = function(fn){ let path = ‘/‘; let fns = []; let arg = [].slice.call(arguments); if(typeof fn !== ‘function‘ && arg.length >= 2){ if(typeof arg[0] !== ‘string‘){ fns = arg; }else{ path = arg[0]; fns = arg.slice(1); } }else{ fns = arg; } this._router.use(path,‘all‘,fns); } //将http的listen方法封装到Application的原型上 Application.prototype.listen = function(){ let server = http.createServer((req,res)=>{ //done 用于当路由无任何可匹配项时调用的处理函数 function done(){ res.end(`Cannot ${req.url} ${req.method}`); } this._router.handle(req,res,done); //调用路由系统的handle方法处理请求 }); server.listen(...arguments); }; module.exports = Application;
application.js //express应用构造模块
//express路由系统 const Layer = require(‘./layer.js‘); const Route = require(‘./route.js‘); const http = require(‘http‘); const methods = http.METHODS; const url = require(‘url‘); //路由对象构造函数 function Router(){ this.stack = []; } //router.route ---- 用于创建子路由对象route与主路由上层(layer)的关系 //并将主路由上的层缓存到路由对象的stack容器中,该层建立路径与子路由处理请求的关系 Router.prototype.route = function(path){ let route = new Route(); let layer = new Layer(path,route.dispatch.bind(route)); this.stack.push(layer); return route; } //router.get ---- 实现路由注册 //实际上这个方法调用router.route方法分别创建一个主路由系统层、一个子路由系统,并建立两者之间的关系,详细见Router.prototype.route //然后获取子路由系统对象,并将回调函数和请求方法注册在这个子路由系统上 // Router.prototype.get = function(path,handlers){ // let route = this.route(path); // route.get(handlers); // } methods.forEach(method =>{ method = method.toLocaleLowerCase(); //注意下面这个方法会出现内存泄漏问题,有待改进 Router.prototype[method] = function(path, handlers){ let route = this.route(path); route[method](handlers); } }); //router.use ---- 实现中间件注册(按照路由开头的路径匹配,即相对路由匹配) Router.prototype.use = function(path,routerType,fns){ let router = this; fns.forEach(function(fn){ let layer = new Layer(path,fn); layer.middle = true; //标记这个层为相对路由中间件 layer.routerType = routerType; router.stack.push(layer); }); } //调用路由处理请求 Router.prototype.handle = function(req,res,out){ let {pathname} = url.parse(req.url); let index = 0; let next = () => { if(index >= this.stack.length) return out(); let layer = this.stack[index++]; if(layer.middle && (layer.path === ‘/‘ || pathname === layer.path || pathname.startsWith(layer.path + ‘/‘))){ //处理中间件 if(layer.routerType === ‘use‘){ layer.handle_request(req,res,next); }else if(layer.routerType === ‘all‘ && layer.path === pathname){ layer.handle_request(req,res,next); }else{ next(); } }else if(layer.match(pathname)){ //处理响应--更准确的说是处理具体请求方法上的中间件或响应 layer.handle_request(req,res,next); }else{ next(); } } next(); } module.exports = Router;
index.js //路由主入口文件
//Layer的构造函数 function Layer(path,handler){ this.path = path; //当前层的路径 this.handler = handler; //当前层的回调函数 } //判断请求方法与当前层的方法是否一致 Layer.prototype.match = function(pathname){ return this.path === pathname; } //调用当前层的回调函数handler Layer.prototype.handle_request = function(req,res,next){ this.handler(req,res,next); } module.exports = Layer;
layer.js //构造层的模块
//Layer的构造函数 function Layer(path,handler){ this.path = path; //当前层的路径 this.handler = handler; //当前层的回调函数 } //判断请求方法与当前层的方法是否一致 Layer.prototype.match = function(pathname){ return this.path === pathname; } //调用当前层的回调函数handler Layer.prototype.handle_request = function(req,res,next){ this.handler(req,res,next); } module.exports = Layer;
route.js //子路由模块
测试代码:
let express = require(‘./express‘); let app = express(); app.use(‘/name‘,function(req,res,next){ console.log(‘use1‘); next(); }); app.use(function(req,res,next){ console.log(‘use2-1‘); next(); },function(req,res,next){ console.log(‘use2-2‘); next(); }); app.all(‘/name‘,function(req,res,next){ console.log(‘all-1‘); next(); }); app.all(‘/name/app‘,function(req,res,next){ console.log(‘all-2‘); next(); }); app.get(‘/name/app‘,function(req,res){ res.end(req.url); }); // console.log(app._router.stack); app.listen(12306);
测试结果: