🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[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; ```