🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] ## Pre-Notify 阅读本文前可以先参考一下我之前那篇简单版的express实现的文章。 > []() 相较于之前那版,此次我们将实现Express所有核心功能。 (づ ̄ 3 ̄)づ Let's Go! ## 框架目录 ``` express/ | | | - application.js #app对象 | | - html.js #模板引擎 | | - route/ | | - index.js #路由系统(router)入口 | | - route.js #路由对象 | | - layer.js #router/route层 | | - middle/ | | - init.js #内置中间件 | ·- express.js #框架入口 ``` ## express.js 和 application.js 之前我们说过,将express引入到项目后会返回一个函数,当这个函数运行后会返回一个`app`对象,这个app对象是原生http的超集。 我们先来实现这个生产app对象的函数 ``` // express.js cont Application = require('./application.js'); function createApplication(){ return new Application; } module.exports = createApplication; ``` 我们在`express.js`中引入了一个`application.js` 文件,这个application文件导出的即是我们的`app对象`。 ## app对象之http服务器 `app对象` 的主要作用是用来启动一个`http服务器`,通过`.listen`方法。 ``` const http = require('http'); function Application(){ } Application.prototype.listen = function(){ let server = http.createServer(function(req,res){ //作出响应 }) server.listen.apply(server,arguments); } ``` ## app对象之路由功能 `app对象`的另外一个重要作用,也就是`Express`框架的主要作用是实现路由功能。 路由功能,即让服务器针对客户端不同的请求路径和请求方法做出不同的回应。 而要实现这个功能我们需要做两件事情:`注册路由` 和 `路由分发` >[warning] app对象中其实只包含对外的接口,真正实现部分是委托给路由系统(router.js)来处理的。 ### 注册路由 当一个请求来临时,我们可以依据它的请求方式和请求路径来决定服务器**是否给予响应以及怎么响应。** 而我们怎么让服务器知道哪些请求该给予响应以及怎样响应呢? 这就是注册路由所要做的事情了。 在服务器启动时,我们需要对服务器想要给予回应的请求做上记录,**先存起来**,这样在请求来临的时候服务器就能对照这些记录分别作出响应。 >[warning]注意 每一条记录都对应一条请求,记录中一般都包含着这条请求的请求路径和请求方式。但一条请求不一定只对应一条记录(中间件、all方法什么的)。 #### 接口实现 我们通过在 app对象 上挂载`.get`、`.post`这一类的方法来实现路由的注册。 其中`.get`方法能匹配请求方式为**get**的请求,`.post`方法能匹配请求方式为**post**的请求。 请求方式一共有33种,每一种都对应一个app下的方法,emmm...我们不可能写33遍吧?So我们需要利用一个`methods包`来帮助我们减少代码的冗余。 ``` const methods = require('methods'); // 这个包是http.METHODS的封装,区别在于原生的方法名全文大写,后者全为小写。 methods.forEach(method){ Application.prototype[method] = function(){ //记录路由信息到路由系统(router) this._router[method].apply(this._router,slice.call(arguments)); return this; } } //以上代码是以下的简写 Application.prototype.get = fn Application.prototype.post = fn ... ``` 因为`app`这个对象只是作为一个`对外接口层`,为了保证此接口层一目明了,我们把实际累死累活的工作外包给其它专门的模块来处理,比如说注册路由,我们交给了`router`这个模块来处理。 ### 分发路由 当请求来临时我们就需要依据记录的路由信息来作出对应的响应了,这个过程我们称之为`分发路由/dispatch` 上面是广义的分发路由的含义,但其实分发路由其实包括两个过程,`匹配路由` 和 `分发路由`。 当一个请求来临时,我们需要知道我们所记录的路由信息中是否囊括这条请求。如果没有囊括,一般来说服务器会对客户端作出一个提示性的回应,这就是 **路由的匹配**。 如果囊括,则会执行被匹配上的路由信息中所存储的回调(即上面所述服务器应该怎样给予客户端响应)。这就是**路由的分发**。 #### 接口实现 ``` Application.prototype.listen = function(){ let self = this; let server = http.createServer(function(req,res){ function done(){ //没有匹配上路由时的回调 res.end(`Cannot ${req.method} ${req.url}`); } //将路由匹配的具体处理交给路由系统的handle方法 //handle方法中会对匹配上的路由再进行路由分发 self._router.handle(req,res,done); }) server.listen.apply(server,arguments); } ``` ### 路由懒加载 服务器启动时并不一定会加载路由系统,只有当我们调用路由注册方法注册路由时才会加载router。 ``` Application.prototype.lazyrouter = function(){ if(!this._router){ this._router = new Router(); } }; ``` ``` ... self.lazyrouter(); self._router.handle(req,res,done); ... ``` ## router路由系统 ``` function Router(){ function router(req,res,next){ //为支持子路由容器功能做准备 router.handle(req,res,next); } Object.setPrototypeOf(router,proto); router.stack = []; router.paramCallbacks = {}; router.use(init); // 加载内置中间件 return router; } let proto = Object.create(null); proto.route = function(path){} //添加一层路由 methods.forEach(function(method){ //33个注册路由的普通方法:get、post }) proto.use = function(path,handler){} //注册中间件 proto.param = function(name,handler){} //注册param回调 proto.handle = function(req,res,out){} //路由匹配函数 包括路由分发 proto.process_params = function(){} //处理param回调的handle ``` ### router、route和layer 当我们调用app.get('/user',cb1,cb2...)方法的时候,其实我们调用的是router的get方法。 在这个方法里我们将`路由信息`抽象成了一个对象——route,并往router里的`stack`存放了一层,同时我们也在route里开辟了一个`stack`,我们也往route里的stack里存放了一层。 ``` methods.forEach(function(method){ proto[method] = function(path){ let route = this.route(path); //注册路由,往Router里添加一层 route[method].apply(route,slice.call(arguments,1)); //向Route里添加一层 return this; } }); ``` route是routerの`stack/栈`中所存放的一层路由,实际中我们是将route又包装成了一个`layer`对象存储在routerのstack中。 ``` proto.route = function(path){ let route = new Route(path) ,layer = new Layer(path,route.dispatch.bind(route)); layer.route = route; this.stack.push(layer); return route; }; ``` 上面我们说到`route`其实也拥有自己的`stack`,这个stack里存放的是也是一层层`layer`对象。 ``` methods.forEach(function(method){ Route.prototype[method] = function(){ let handlers = slice.call(arguments); this.methods[method] = true; for(let i=0;i<handlers.length;++i){ let layer = new Layer('/',handlers[i]); layer.method = method; this.stack.push(layer); } return this; } }); ``` 综上,我们可以发现layer是一个相对的感念,我们可以将router中每一层称作一个`layer`,也可以将route里的每一层称作一个`layer`。 这两个layer的不同之处在于前者在layer的handler属性下挂载是`route.dispatch`这个方法,而后者挂载的是我们使用注册路由方法时所注册的那些个**回调**。 也就是说**route里的每一层存放的才是我们真正的响应函数**,而router里每一层存放的`handle`是帮助我们路由匹配成功后对路由进行分发的。简而言之,后者存储的`dispatch方法`会调用前者所存储的每个**响应回调**。 ### handle和dispatch #### 遍历二维数据 `router.proto.handle方法` 主要对请求进行路由匹配 ``` proto.handle = function(req,res,out){ let index = 0,self = this; let {pathname} = url.parse(req.url,true); function next(err){ if(index>=self.stack.length){ return out; //路由信息清单(stack)遍历完仍没匹配上 } let layer = self.stack[index++]; if(layer.match(pathname)){ if(layer.route){ //说明匹配的是路由 if(layer.route.handler_method(req.method)){ layer.handle_request(req,res,next); }else{ next(err); //路径匹配但方法名不匹配 } }else{ //说明匹配的是中间件 } }else{ next(err); //路径不匹配 } } next(); } ``` `route.prototype.dispatch` 方法,当路由匹配上时,会执行当初该条路由注册时所注册的回调函数(请求方法想匹配的回调函数)。 ``` Route.prototype.dispatch = function(req,res,out){ let idx = 0,self = this; function next(err){ if(err){ //如果一旦在路由函数中出错了,则会跳过当前路由的所有方法匹配 return out(err); } if(idx >= self.stack.length){ return out(); //此out接收的是Router里的next,即跳转到下一条路由进行匹配 } let layer = self.stack[idx++]; if(layer.method == req.method.toLowerCase()){ layer.handle_request(req,res,next); }else{ next(); } } next(); }; ``` >[warning] 上面两个方法中的layer的层级是不一样的,通过它俩的配合使用达到了了对二维数据的遍历。 #### handle_method快速匹配 我们从上面的源码中可以发现,其实我们对一个路由的请求方法进行了两次匹配,为什么要这样做呢? 第一次我们是在对route进行匹配,route里有一个属性`methods`,这个属性告诉我们这个route下存放的响应回调都是针对哪些请求方法的。这样,我们就只用通过查看这个`methods`属性就能知道route中存放的响应回调支持哪些类型的请求方式,当请求方法不匹配时我们就能直接跳过当前路由而不需要再次进入route对routeのstack的每一层进行匹配,So达到了一种**快速匹配**的效果,这就是`handle_method`方法的作用。 ``` Route.prototype.handle_method = function(method){ method =method.toLowerCase(); return this.methods[method]; }; ``` ### app.route方法 用法示例: ``` app .route('/user') .get(function(req,res){ res.end('get'); }) .post(function(req,res){ res.end('post'); }) .put(function(req,res){ res.end('put'); }) .delete(function(req,res){ res.end('delete'); }) ``` 要实现上面这个方法,很简单,只需将route这一路由层作为`.route`方法的返回值即可 ``` Application.prototype.route = function(path){ this.lazyrouter(); return this._router.route(path); }; ``` 这样我们通过`.route`后再使用`.get`、`.post`等,就是直接调用的`route`里的`.get`和`.post`方法,**是往route里添加层**。 ## 路由容器 ``` const user = express.Router(); //router user.use(function(req,res,next){ console.log('我是一个中间件') next(); }); //在子路径里的路径是相对于父路径的 //下面是指/user/2 user.get('/2',function(req,res,next){ res.end('2'); }); //use表示使用中间件,只需要匹配前缀即可 app.use('/user',user); ``` Router就是一个路由容器,而上面这个栗子其实是在Router.stack里的一层route里添加了一个新的`router`。 这是什么意思呢? 当原本的Router.stack里的一层route被匹配时,若这个route存储的是一个新的路由容器,那么会对这个新的路由容器里的路由再次进行匹配。并且这个新的路由容器里的路由的路径都是相当于它们的父级的。 像上面的栗子,假若请求路径为`/user/1`,那么子路由系统的中间件会被匹配上但.get路由不会被匹配上,如果请求路径为`/user/2`,那么都会匹配上。 ### express.Router 修改express.js文件 ``` //添加 const Router = require('./router); createApplication.Router = Router; ``` So这个路由容器就是我们原本的`router`,不过是一个全新的实例。 ### Router和router 修改 `Router function` ``` function Router(){ function router(req,res,next){ //为支持子路由容器功能做准备 router.handle(req,res,next); } Object.setPrototypeOf(router,proto); router.stack = []; router.paramCallbacks = {}; router.use(init); // 加载内置中间件 return router; } ``` 我们将原本的Router构造函数改造成了一般的函数,不论我们使用`new`方式(调用.get(path,fn)来注册路由) 还是`Router()`方式(调用.get(path,router))我们都会得到`router`这个函数。 **注意**,`router`函数将会在路由匹配方法(router.prototype.handle)路由匹配时被执行,它执行时会再次调用`router.handle`,即递归调用遍历这个新的router容器内的路由。 ``` ... let layer = self.stack[idx++]; if(layer.method == req.method.toLowerCase()){ layer.handle_request(req,res,next); ... ``` 这时有一个问题,子路由的路径是省略了父路由的路径的,为了能容路径匹配正确,我们需要将传递给递归的那个`router.handle`的`req`的`url`稍作修改 ``` ... if(layer.match(pathname)){ if(!layer.route){ //这一层是中间件层 /usr/2 removed = layer.path; // /user req.url = req.url.slice(removed.length); // /2 //从这里以后开始获取url会是不准确的,若在回调中用到了req.url需要注意 if(err){ layer.handle_error(err,req,res,next) ... ```