[TOC]
// ## 导读
// 我有一个问题和不太成熟的想法,不知道该不该提!
// 掘金既然支持目录TOC,为什么不能把目录放在一个显眼的地方,比如左边?一大片空白不用非要放在右下角和其它面板抢,emmm...
// - express概要
// - 创建一个服务器以及分发路由
// - 简单实现1
// - 脉络
// - 实现路由
// - .all方法和实现
// - 应用
// - 实现
// - 在app下添加.all方法(用来存储路由信息对象)
// - 对遍历路由信息对象时的规则判断做出调整
// - 中间件
// - 概要
// - 中间件和路由的异同
// - 1.都会包装成路由信息对象
// - 2.匹配条数的不同
// - 3.匹配路径上的不同
// - next与错误中间件
// - 实现
// - 在app下添加.use方法
// - 改造request方法
// - params
// - params常用属性
// - params与动态路由
// - 实现
// - 动态路由与动态路由属性的实现
// - params其它属性的实现
// - .param方法
// - api一览
// - 注意事项
// - 应用场景
// - 和中间件的区别
// - 实现
// - 添加param方法
// - 修改request
## express概要
express是一个node模块,它是对node中`http`模块的二次封装。
express相较于原生http模块,为我们提供了作为一个服务器最重要的功能:`路由`。
路由功能能帮助我们**根据不同的路径不同的请求方法来返回不同的内容**。
除此之外express还支持 `中间件` 以及其他类似于 `req.params` 这些小功能。
## 创建一个服务器以及分发路由
```
let express = require('express');
let app = express();
//针对不同的路由进行不同的返回
app.get('/eg1',function(req,res){
res.end('hello');
});
app.post('/eg1',function(req,res){
res.end('world');
});
app.listen(8080,function(){
console.log(`server started at 8080`);
});
```
可以发现,引入express后会返回一个**函数**,我们称之为express。
express这个函数运行后又会返回一个对象,这个对象就是包装后的http的**server对象**。
这个对象下有很多方法,这些方法就是express框架为我们提供的新东东了。
上面用到了`.get`方法和`.post`方法,get和post方法能帮助我们对路由进行分发。
什么是路由分发呢?其实就是在原生`request`回调中依据`请求方法`和`请求路径`的不同来返回不同的响应内容。
就像在上面的示例中我们通过`.get`和`.post`方法对路径为`/eg1`的请求**各**绑定了一个回调函数,
但这个两个回调函数不会同时被调用,因为请求方法只能是一种(get或则post或则其它)。
如果请求方法是**get**请求路径是`/eg1`则会返回`.get`中所放置的回调
```
<<< 输出
hello
```
否则若请求路径不变,请求方法是**post**则会返回`.post`方法中放置的回调
```
<<< 输出
world
```
### 简单实现1
#### 脉络
我们首先要有一个函数,这个函数运行时会返回一个app对象
```
function createApplication(){
let app = function(req,res){};
return app;
}
```
这个app对象下还有一些方法`.get`,`.post`,`.listen`。
```
app.get = function(){}
app.post = function(){}
app.listen = function(){}
```
其中`app.listen`其实就是原生http中的`server.listen`。
`app`就是原生中的`request`回调。
```
app.listen = function(){
let server = http.createServer(app);
server.listen.apply(server,arguments); //事件回调中,不管怎样this始终指向绑定对象,这里既是server,原生httpServer中也是如此
}
```
#### 实现路由
我们再来想想`app.get`这些方法到底做了什么。
其实无非定义了一些路由规则,对匹配上这些规则的路由进行一些针对性的处理(执行回调)。
上面一句话做了两件事,匹配规则 和 执行回调。
这两件事执行的时机是什么时候呢?是服务器启动的时候吗?不是。
**是当接收到客户端请求的时候**。
这意味着什么?
当服务器启动的时候,其实这些代码已经执行了,它们根本不会管请求是个什么鬼,只要服务器启动,代码就执行。
所以我们需要将**规则**和**回调**先**存起来**。(类似于发布订阅模式)
```
app.routes = [];
app.get = function(path,handler){
app.routes.push({
method:'get'
,path
,handler
})
}
```
上面我们定义了一个`routes`数组,用来存放每一条规则和规则所对应的回调以及请求方式,即`路由信息对象`。
但有一个地方需要我们优化。不同的请求方法所要做的事情都是相同的(只有method这个参数不同),我们不可能每增加一个就重复的写一次,请求的方法是有非常多的,这样的话代码会很冗余。
```
//http.METHODS能列出所有的请求方法
>>>
console.log(http.METHODS.length);
<<<
33
```
So,为了简化我们的代码我们可以遍历`http.METHODS`来创建函数
```
http.METHODS.forEach(method){
let method = method.toLowerCase();
app[method] = function(path,handler){
app.routes.push({
method
,path
,handler
})
}
}
```
然后我们会在请求的响应回调中用到这些路由信息对象。而**响应回调在哪呢?**
上面我们已经说过其实`app`这个`函数对象`就是原生的`request`回调。
接下来我们只需要等待请求来临然后执行这个app回调,**遍历每一个路由信息对象进行匹配,匹配上了则执行对应的回调函数。**
```
let app = function(req,res){
for(let i=0;i<app.routes.length;++i){
let route = app.routes[i];
let {pathname} = url.parse(req.url);
if(route.method==req.method&&route.path==pathname){
route.handler(req,res);
}
}
}
```
## .all方法和实现
### 应用
`.all`也是一个路由方法,
```
app.all('/eg1',function(req,res){})
```
和普通的`.get`,`.post`这些和请求方法直接绑定的路由分发不同,.all方法只要路径匹配得上**各种请求方法**去请求这个路由都会得到响应。
还有一种更暴力的使用方式
```
app.all('*',function(req,res){})
```
这样能匹配所有方法所有路劲,**all!**
**通常它的使用场景是对那些没有匹配上的请求做出兼容处理。**
### 实现
#### 在app下添加.all方法(用来存储路由信息对象)
和一般的请求方法是一样的,只是需要一个标识用以和普通方法区分开。
这里是在method取了一个`all`关键之作为method的值。
```
app.all = function(method,handler){
app.routs.push({
method:'all'
,path
,handler
})
}
```
#### 对遍历路由信息对象时的规则判断做出调整
另外还需要在`request`回调中对规则的匹配判断做出一些调整
```
if((route.method==req.method||route.method=='all')&&(route.path==pathname||route.path=='*')){
route.handler(req,res);
}
```
## 中间件
### 概要
中间件是什么鬼呢?中间件嘛,顾名思义中间的件。。。emmm,我们直接说说它的作用吧!
中间件主要是**在请求和真正响应之间**再加上一层处理,
处理什么呢?比如说权限验证、数据加工神马的。
这里所谓的**真正响应**,你可以把它当做`.get`这些路由方法所要执行的那些个回调。
```
app.use('/eg2',function(req,res,next){
//do something
next();
})
```
### 中间件和路由的异同
#### 1.都会包装成路由信息对象
服务器启动时,中间件也会像路由那样被存储为一个一个`路由信息对象`。
#### 2.匹配条数的不同
`路由`只要匹配上了一条就会立马返回数据并结束响应,不会再匹配第二条。
而`中间件`只是一个临时中转站,对数据进行过滤或则加工后会继续往下匹配。
**So,中间件一般放在文件的上方,路由放在下方。**
#### 3.匹配路径上的不同
中间件进行路径匹配时,只要**开头**匹配的上就能执行对应的回调。
这里所谓的开头意思是:
假若中间件要匹配的路径是`/eg2`,
那么只要url.path是以`/eg2`开头,像`/eg2`,`/eg2/a`,`/eg2/a/b`即可。(/eg2a这种不行,且必须以/eg2开头,a/eg2则不行)
而路由匹配路径时必须是完全匹配,也就是说规则若是`/eg2`则只有`/eg2`匹配的上。这里的完全匹配其实是针对路径的 **/的数量** 来说的,因为`动态路由`中匹配的值不是定死的。
除此之外,中间件可以不写路径,当不写路径时express系统会为其默认填上`/`,即全部匹配。
### next与错误中间件
中间件的回调相较于路由多了一个参数`next`,next是一个函数。
这个函数能让中间件的回调执行完后继续向下匹配,如果**没有写next也没有在中间件中结束响应**,那么请求会一直处于`pending`状态。
next还可以进行传参,如果传了惨,表示程序运行出错,将匹配`错误中间件`进行处理**且只会交由错误中间件处理**。
错误中间件相较于普通中间件在回调函数中又多了一个参数`err`,用以接收中间件`next()`传递过来的错误信息。
```
app.use('/eg2',function(req,res,next){
//something wrong
next('something wrong!');
})
app.use('/eg2',function(err,req,res,next){
console.log('i catch u'+err);
next(err); //pass to another ErrorMiddle
});
// 错误中间接收了错误信息后仍然允许接着向下传递
app.use('/eg2',function(err,req,res,next){
res.end(err);
});
```
其实错误中间件处理完成后也能匹配路由
```
app.use('/eg2',function(req,res,next){
//something wrong
next('something wrong!');
})
app.use('/eg2',function(err,req,res,next){
console.log('i catch u'+err);
next(err); //pass to another ErrorMiddle
});
app.get('/eg2',function(req,res){
//do someting
})
```
### 实现
#### 在app下添加.use方法
像路由方法一样,其实就是用来存储路由信息对象
```
app.use = function(path,handler){
if(typeof handler != 'function'){ //说明只有一个参数,没有path只有handler
handler = path;
path = "/"
}
app.routes.push({
method:'middle' //需要一个标识来区分中间件
,path
,handler
});
};
```
#### 改造request方法
```
let app = function(req,res){
const {pathname} = url.parse(req.url, true);
let i = 0;
function next(err){
if(index>=app.routes.length){ //说明路由信息对象遍历完了仍没匹配上,给出提示
return res.end(`Cannot ${req.method} ${pathname}`);
}
let route = app.routes[i++];
if(err){ //是匹配错误处理中间件
//先判断是不是中间件
if(route.method == 'middle'){
//如果是中间件再看路径是否匹配
if(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname){
//再看是否是错误处理中间件
if(route.handler.length==4){
route.handler(err,req,res,next);
}else{
next(err);
}
}else{
next(err);
}
}else{
next(err); //将err向后传递直到找到错误处理中间件
}
}else{ //匹配路由和普通中间件
if(route.method == 'middle'){ //说明是中间件
if(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname){
route.handler(req,res,next);
}else{ //此条路由没有匹配上,继续向下匹配
next();
}
}else{ //说明是路由
if((route.method==req.method||route.method=='all')&&(route.path==pathname||route.path=='*')){
//说明匹配上了
route.handler(req,res);
}else{
next();
}
}
}
}
next();
}
```
我们可以把对错误中间件的判断封装成一个函数
```
function checkErrorMiddleware(route){
if(route.method == 'middle'&&(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname)&&route.handler.length==4){
return true;
}else{
next(err);
}
}
```
## params
### params常用属性
express为我们在`request`回调中的req对象参数下封装了一些常用的属性
```
app.get('/eg3',function(req,res){
console.log(req.hostname);
console.log(req.query);
console.log(req.path);
})
```
### params与动态路由
```
app.get('/article/:artid',function(req,res){
console.log(req.artid);
})
>>>
/article/8
<<<
8
```
### 实现
#### 动态路由与动态路由属性的实现
首先因为路由规则所对应的路径我们看得懂,但机器看不懂。
So我们需要在存储路由信息对象时,对路由的规则进行**正则提炼**,将其转换成正则的规则。
```
...
app[method] = function(path,handler){
let paramsNames = [];
path = path.replace(/:([^\/]+)/g,function(/*/:aaa ,aaa*/){
paramsNames.push(arguments[1]); //aaa
return '([^\/]+)'; // /user/:aaa/:bbb 被提炼成 /user/([^\/]+)/([^\/]+)
});
layer.reg_path = new RegExp(path);
layer.paramsNames = paramsNames;
}
app.routes.push(layer);
}
```
我们拿到了一个`paramsNames`包含所有路径的分块,并将每个分块的值作为了一个新的param的名称,
我们还拿到了一个`reg_path`,它能帮助我们对请求的路径进行分块匹配,匹配上的每一个子项就是我们新param的值。
对`request`路由匹配部分做出修改
```
if(route.paramsNames){
let matchers = pathname.match(req.reg_path);
if(matchers){
let params = {};
for(let i=0;i<route.paramsNames.length;++i){
params[route.paramsNames[i]] = matchers[i+1]; //marchers从第二项开始才是匹配上的子项
}
req.params = params;
}
route.handler(req,res);
}
```
#### params其它属性的实现
这里是内置中间件,即在框架内部,它会在第一时间被注册为路由信息对象。
实现很简单,就是利用`url`模块对`req.url`进行解析
```
app.use(function(req,res,next){
const urlObj = url.parse(req.url,true);
req.query = urlObj.query;
req.path = urlObj.pathname;
req.hostname = req.headers['host'].split(':')[0];
next();
});
```
## .param方法
### api一览
```
app.param('userid',function(req,res,next,id){
req.user = getUser(id);
next();
});
```
next和中间件那个不是一个意思,这个next执行的话会执行被匹配上的那条动态路由所对应的回调
id为请求时userid这个路径位置的实际值,比如
```
访问路径为:http://localhost/ahhh/9
动态路由规则为:/username/userid
userid即为9
```
### 注意事项
**必须配合动态路由!!**
param和其它方法最大的一点不同在于,**它能对路径进行截取匹配**。
什么意思呢,
上面我们讲过,路由方法路径匹配时必须**完全匹配**,而中间件路径匹配时需要**开头一样**。
而`param`方法**无需开头一样,也无需完全匹配**,它只需要路径中某一个**分块**(即用`/`分隔开的每个路径分块)和方法的规则对上即可。
### 应用场景
当不同的路由中包含相同路径分块且使用了相同的操作时,我们就可以对这部分代码进行提取优化。
比如每个路由中都需要根据id获取用户信息
```
app.get('/username/:userid/:name',function(req,res){}
app.get('/userage/:userid/:age',function(req,res){}
app.param('userid',function(req,res,next,id){
req.user = getUser(id);
next();
});
```
### 和中间件的区别
相较于中间件它更像是一个**真正的钩子**,它不存在放置的先后问题。
如果是中间件,一般来说它必须放在文件的上方,而param方法不是。
导致这样结果的本质原因在于,**中间件类似于一个路由**,它会在请求来临时加入的路由匹配队列中参与匹配。而**param并不会包装成一个路由信息对象**也就不会参与到队列中进行匹配,
它的触发时机是在**它所对应的那些动态路由被匹配上时**才会触发。
### 实现
#### 添加param方法
在app下添加了一个param方法,并且创建了一个`paramHandlers`对象来存储这个方法所对应的回调。
```
app.paramHandlers = {};
app.param = function(name,handler){
app.paramHandlers[name] = handler; //userid
};
```
#### 修改request
修改`request`回调中 动态路由被匹配上时的部分
当动态路由被匹配上时,通过它的动态路由参数来遍历`paramHandlers`,看是否设置了对应的`param回调`,
```
if(route.paramsNames){
let matchers = pathname.match(route.reg_path);
if(matchers){
let params = {};
for(let i=0;i<route.paramsNames.length;++i){
params[route.paramsNames[i]] = matchers[i+1];
}
req.params = params;
for(let j=0;j<route.paramsNames.length;++j){
let name = route.paramsNames[j];
let handler = app.paramHandlers[name];
if(handler){
//回调触发更改在了这里
//第三个参数为next,这里把route.handler放在了这里,是让param先执行再执行该条路由
return handler(req,res,()=>route.handler(req,res),req.params[name]);
}else{
return route.handler(req,res);
}
}
}else{
next();
}
}
```
---
## 源码
```
let http = require('http');
let url = require('url');
function createApplication() {
//app其实就是真正的请求监听函数
let app = function (req, res) {
const {pathname} = url.parse(req.url, true);
let index = 0;
function next(err){
if(index>=app.routes.length){
return res.end(`Cannot ${req.method} ${pathname}`);
}
let route = app.routes[index++];
if(err){
//先判断是不是中间件
if(route.method == 'middle'){
//如果是中间件再看路径是否匹配
if(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname){
//再看是否是错误处理中间件
if(route.handler.length==4){
route.handler(err,req,res,next);
}else{
next(err);
}
}else{
next(err);
}
}else{
next(err); //将err向后传递直到找到错误处理中间件
}
}else{
if(route.method == 'middle'){ //中间件
//只要请求路径是以此中间件的路径开头即可
if(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname){
route.handler(req,res,next);
}else{
next();
}
}else{ //路由
if(route.paramsNames){
let matchers = pathname.match(route.reg_path);
if(matchers){
let params = {};
for(let i=0;i<route.paramsNames.length;++i){
params[route.paramsNames[i]] = matchers[i+1];
}
req.params = params;
for(let j=0;j<route.paramsNames.length;++j){
let name = route.paramsNames[j];
let handler = app.paramHandlers[name];
if(handler){ //如果存在paramHandlers 先执行paramHandler再执行路由的回调
return handler(req,res,()=>route.handler(req,res),req.params[name]);
}else{
return route.handler(req,res);
}
}
}else{
next();
}
}else{
if ((route.method == req.method.toLowerCase() || route.method == 'all') && (route.path == pathname || route.path == '*')) {
return route.handler(req, res);
}else{
next();
}
}
}
}
}
next();
};
app.listen = function () { //这个参数不一定
let server = http.createServer(app);
//server.listen作为代理,将可变参数透传给它
server.listen.apply(server, arguments);
};
app.paramHandlers = {};
app.param = function(name,handler){
app.paramHandlers[name] = handler; //userid
};
//此数组用来保存路由规则
app.routes = [];
// console.log(http.METHODS);
http.METHODS.forEach(function (method) {
method = method.toLowerCase();
app[method] = function (path, handler) {
//向数组里放置路由对象
const layer = {method, path, handler};
if(path.includes(':')){
let paramsNames = [];
//1.把原来的路径转成正则表达式
//2.提取出变量名
path = path.replace(/:([^\/]+)/g,function(){ //:name,name
paramsNames.push(arguments[1]);
return '([^\/]+)';
});
// /user/ahhh/12
// /user/([^\/]+)/([^\/]+)
layer.reg_path = new RegExp(path);
layer.paramsNames = paramsNames;
}
app.routes.push(layer);
};
});
//all方法可以匹配所有HTTP请求方法
app.all = function (path, handler) {
app.routes.push({
method: 'all'
, path
, handler
});
};
//添加一个中间件
app.use = function(path,handler){
if(typeof handler != 'function'){ //说明只有一个参数,没有path只有handler
handler = path;
path = "/"
}
app.routes.push({
method:'middle' //需要一个标识来区分中间件
,path
,handler
});
};
//系统内置中间件,用来为请求和响应对象添加一些方法和属性
app.use(function(req,res,next){
const urlObj = url.parse(req.url,true);
req.query = urlObj.query;
req.path = urlObj.pathname;
req.hostname = req.headers['host'].split(':')[0];
next();
});
return app;
}
module.exports = createApplication;
```