[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)
...
```