手写Express核心原理,再也不怕面试官问我Express原理

开发 前端
我们可以知道,express得到的是一个方法,然后方法执行后得到了app。而app实际上也是一个函数,至于为什么会是函数,我们下面会揭秘。
本文转载自微信公众号「前端阳光」,作者事业有成的张啦啦 。转载本文请联系前端阳光公众号。

一、首先安装express

二、创建example.js文件

       创建myExpress.js文件

       实现app.get()方法

       实现post等其他方法

       实现app.all方法

       中间件app.use的实现

       什么是错误中间件?

       学习总结

一、首先安装express

  1. npm install express 

安装express是为了示范。

已经把代码放到github:https://github.com/Sunny-lucking/HowToBuildMyExpress 。可以顺手给个star吗?谢谢大佬们。

二、创建example.js文件

  1. // example.js 
  2. const express = require('express'
  3. const app = express() 
  4. const port = 3000 
  5.  
  6. app.get('/', (req, res) => { 
  7.   res.send('Hello World!'
  8. }) 
  9.  
  10. app.listen(port, () => { 
  11.   console.log(`Example app listening at http://localhost:${port}`) 
  12. }) 

如代码所示,执行node example.js就运行起了一个服务器。

如下图所示,现在我们决定创建一个属于我们的express文件,引入的express改成引入我们手写的express。

好了,现在开始实现我们的express吧!

创建myExpress.js文件

  1. const express = require('express'
  2. const app = express() 

由 这两句代码,我们可以知道,express得到的是一个方法,然后方法执行后得到了app。而app实际上也是一个函数,至于为什么会是函数,我们下面会揭秘。

我们可以初步实现express如下:

  1. // myExpress.js 
  2. function createApplication() { 
  3.     let app = function (req,res) { 
  4.  
  5.     } 
  6.     return app; 
  7.  
  8. module.exports = createApplication; 

在上面代码中,发现app有listen方法。

因此我们可以进一步给app添加listen方法:

  1. // myExpress.js 
  2. function createApplication() { 
  3.     let app = function (req,res) { 
  4.  
  5.     } 
  6.     app.listen = function () { 
  7.  
  8.     } 
  9.     return app; 
  10.  
  11. module.exports = createApplication; 

app.listen实现的是创建一个服务器,并且将服务器绑定到某个端口运行起来。

因此可以这样完善listen方法。

  1. // myExpress.js 
  2. let http = require('http'); 
  3. function createApplication() { 
  4.     let app = function (req,res) { 
  5.         res.end('hahha'); 
  6.     } 
  7.     app.listen = function () { 
  8.         let server = http.createServer(app) 
  9.         server.listen(...arguments); 
  10.  
  11.     } 
  12.     return app; 
  13.  
  14. module.exports = createApplication; 

这里可能会有同学有所疑问,为什么 http.createServer(app)这里要传入app。

其实我们不传入app,也就是说,让app不是一个方法,也是可以的。

我们可以改成这样。

  1. // myExpress.js 
  2. let http = require('http'); 
  3. function createApplication() { 
  4.     let app = {}; 
  5.  
  6.     app.listen = function () { 
  7.         let server = http.createServer(function (req, res) { 
  8.             res.end('hahha'
  9.         }) 
  10.         server.listen(...arguments); 
  11.  
  12.     } 
  13.     return app; 
  14.  
  15. module.exports = createApplication; 

如代码所示,我们将app改成一个对象,也是没有问题的。

实现app.get()方法

app.get方法接受两个参数,路径和回调函数。

  1. // myExpress.js 
  2. let http = require('http'); 
  3. function createApplication() { 
  4.     let app = {}; 
  5.     app.routes = [] 
  6.     app.get = function (path, handler) { 
  7.         let layer = { 
  8.             method: 'get'
  9.             path, 
  10.             handler 
  11.         } 
  12.         app.routes.push(layer) 
  13.     } 
  14.     app.listen = function () { 
  15.         let server = http.createServer(function (req, res) { 
  16.              
  17.             res.end('hahha'
  18.         }) 
  19.         server.listen(...arguments); 
  20.     } 
  21.     return app; 
  22.  
  23. module.exports = createApplication; 

如上面代码所示,给app添加了route对象,然后get方法执行的时候,将接收到的两个参数:路径和方法,包装成一个对象push到routes里了。

可想而知,当我们在浏览器输入路径的时候,肯定会执行http.createServer里的回调函数。

所以,我们需要在这里 获得浏览器的请求路径。解析得到路径。

然后遍历循环routes,寻找对应的路由,执行回调方法。如下面代码所示。

  1. // myExpress.js 
  2. let http = require('http'); 
  3. const url  = require('url'); 
  4. function createApplication() { 
  5.     let app = {}; 
  6.     app.routes = [] 
  7.     app.get = function (path, handler) { 
  8.         let layer = { 
  9.             method: 'get'
  10.             path, 
  11.             handler 
  12.         } 
  13.         app.routes.push(layer) 
  14.     } 
  15.     app.listen = function () { 
  16.         let server = http.createServer(function (req, res) { 
  17.             // 取出layer  
  18.             // 1. 获取请求的方法 
  19.             let m = req.method.toLocaleLowerCase(); 
  20.             let { pathname } = url.parse(req.url, true); 
  21.              
  22.             // 2.找到对应的路由,执行回调方法 
  23.             for (let i = 0 ; i< app.routes.length; i++){ 
  24.                 let {method,path,handler} = app.routes[i] 
  25.                 if (method === m && path === pathname ) { 
  26.                     handler(req,res); 
  27.                 } 
  28.             } 
  29.             res.end('hahha'
  30.         }) 
  31.         server.listen(...arguments); 
  32.     } 
  33.     return app; 
  34.  
  35. module.exports = createApplication; 

运行一下代码。

可见运行成功:

实现post等其他方法。

很简单,我们可以直接复制app.get方法,然后将method的值改成post就好了。

  1. // myExpress.js 
  2. let http = require('http'); 
  3. const url  = require('url'); 
  4. function createApplication() { 
  5.     。。。 
  6.     app.get = function (path, handler) { 
  7.         let layer = { 
  8.             method: 'get'
  9.             path, 
  10.             handler 
  11.         } 
  12.         app.routes.push(layer) 
  13.     } 
  14.     app.post = function (path, handler) { 
  15.         let layer = { 
  16.             method: 'post'
  17.             path, 
  18.             handler 
  19.         } 
  20.         app.routes.push(layer) 
  21.     } 
  22.     。。。 
  23.     return app; 
  24.  
  25. module.exports = createApplication; 

这样是可以实现,但是除了post和get,还有其他方法啊,难道每一个我们都要这样写嘛?,当然不是,有个很简单的方法。

// myExpress.js

  1. function createApplication() { 
  2.     ...  
  3.     http.METHODS.forEach(method => { 
  4.         method = method.toLocaleLowerCase() 
  5.         app[method] = function (path, handler) { 
  6.             let layer = { 
  7.                 method, 
  8.                 path, 
  9.                 handler 
  10.             } 
  11.             app.routes.push(layer) 
  12.         } 
  13.     }); 
  14.     ... 
  15.  
  16. module.exports = createApplication; 

如代码所示,http.METHODS是一个方法数组。如下面所示的数组:

  1. ["GET","POST","DELETE","PUT"]。 

遍历方法数组,就可以实现所有方法了。

测试跑了一下,确实成功。

实现app.all方法

all表示的是匹配所有的方法,

app.all('/user')表示匹配所有路径是/user的路由

app.all('*')表示匹配任何路径 任何方法 的 路由

实现all方法也非常简单,如下代码所示:

  1. app.all = function (path, handler){ 
  2.         let layer = { 
  3.             method: "all"
  4.             path, 
  5.             handler 
  6.         } 
  7.         app.routes.push(layer) 
  8.     } 

然后只需要续改下路由器匹配的逻辑,如下代码所示,只需要修改下判断。

  1. app.listen = function () { 
  2.     let server = http.createServer(function (req, res) { 
  3.         // 取出layer  
  4.         // 1. 获取请求的方法 
  5.         let m = req.method.toLocaleLowerCase(); 
  6.         let { pathname } = url.parse(req.url, true); 
  7.  
  8.         // 2.找到对应的路由,执行回调方法 
  9.         for (let i = 0 ; i< app.routes.length; i++){ 
  10.             let {method,path,handler} = app.routes[i] 
  11.             if ((method === m || method === 'all') && (path === pathname || path === "*")) { 
  12.                 handler(req,res); 
  13.             } 
  14.         } 
  15.         console.log(app.routes); 
  16.         res.end('hahha'
  17.     }) 
  18.     server.listen(...arguments); 

可见成功。

中间件app.use的实现

这个方法的实现,跟其他方法差不多,如代码所示。

  1. app.use = function (path, handler) { 
  2.     let layer = { 
  3.         method: "middle"
  4.         path, 
  5.         handler 
  6.     } 
  7.     app.routes.push(layer) 

但问题来了,使用中间件的时候,我们会使用next方法,来让程序继续往下执行,那它是怎么执行的。

  1. app.use(function (req, res, next) { 
  2.   console.log('Time:'Date.now()); 
  3.   next(); 
  4. }); 

所以我们必须实现next这个方法。

其实可以猜想,next应该就是一个疯狂调用自己的方法。也就是递归。

而且每递归一次,就把被push到routes里的handler拿出来执行。

实际上,不管是app.use还说app.all还是app.get。其实都是把layer放进routes里,然后再统一遍历routes来判断该不该执行layer里的handler方法。可以看下next方法的实现。

  1. function next() { 
  2.     // 已经迭代完整个数组,还是没有找到匹配的路径 
  3.     if (index === app.routes.length) return res.end('Cannot find '
  4.     let { method, path, handler } = app.routes[index++] // 每次调用next就去下一个layer 
  5.     if (method === 'middle') { // 处理中间件 
  6.         if (path === '/' || path === pathname || pathname.starWidth(path + '/')) { 
  7.             handler(req, res, next
  8.         } else { // 继续遍历 
  9.             next(); 
  10.         } 
  11.     } else { // 处理路由 
  12.         if ((method === m || method === 'all') && (path === pathname || path === "*")) { 
  13.             handler(req, res); 
  14.         } else { 
  15.             next(); 
  16.         } 
  17.     } 

可以看到是递归方法的遍历routes数组。

而且我们可以发现,如果是使用中间件的话,那么只要path是“/”或者前缀匹配,这个中间件就会执行。由于handler会用到参数req和res。所以这个next方法要在 listen里面定义。

如下代码所示:

  1. // myExpress.js 
  2. let http = require('http'); 
  3. const url = require('url'); 
  4. function createApplication() { 
  5.     let app = {}; 
  6.     app.routes = []; 
  7.     let index = 0; 
  8.  
  9.     app.use = function (path, handler) { 
  10.         let layer = { 
  11.             method: "middle"
  12.             path, 
  13.             handler 
  14.         } 
  15.         app.routes.push(layer) 
  16.     } 
  17.     app.all = function (path, handler) { 
  18.         let layer = { 
  19.             method: "all"
  20.             path, 
  21.             handler 
  22.         } 
  23.         app.routes.push(layer) 
  24.     } 
  25.     http.METHODS.forEach(method => { 
  26.         method = method.toLocaleLowerCase() 
  27.         app[method] = function (path, handler) { 
  28.             let layer = { 
  29.                 method, 
  30.                 path, 
  31.                 handler 
  32.             } 
  33.             app.routes.push(layer) 
  34.         } 
  35.     }); 
  36.     app.listen = function () { 
  37.         let server = http.createServer(function (req, res) { 
  38.             // 取出layer  
  39.             // 1. 获取请求的方法 
  40.             let m = req.method.toLocaleLowerCase(); 
  41.             let { pathname } = url.parse(req.url, true); 
  42.  
  43.             // 2.找到对应的路由,执行回调方法 
  44.             function next() { 
  45.                 // 已经迭代完整个数组,还是没有找到匹配的路径 
  46.                 if (index === app.routes.length) return res.end('Cannot find '
  47.                 let { method, path, handler } = app.routes[index++] // 每次调用next就去下一个layer 
  48.                 if (method === 'middle') { // 处理中间件 
  49.                     if (path === '/' || path === pathname || pathname.starWidth(path + '/')) { 
  50.                         handler(req, res, next
  51.                     } else { // 继续遍历 
  52.                         next(); 
  53.                     } 
  54.                 } else { // 处理路由 
  55.                     if ((method === m || method === 'all') && (path === pathname || path === "*")) { 
  56.                         handler(req, res); 
  57.                     } else { 
  58.                         next(); 
  59.                     } 
  60.                 } 
  61.             } 
  62.  
  63.             next() 
  64.             res.end('hahha'
  65.         }) 
  66.         server.listen(...arguments); 
  67.     } 
  68.     return app; 
  69.  
  70. module.exports = createApplication; 

当我们请求路径就会发现中间件确实执行成功。

不过,这里的中间价实现还不够完美。

因为,我们使用中间件的时候,是可以不用传递路由的。例如:

  1. app.use((req,res) => { 
  2.   console.log("我是没有路由的中间价"); 
  3. }) 

这也是可以使用的,那该怎么实现呢,其实非常简单,判断一下有没有传递路径就好了,没有的话,就给个默认路径“/”,实现代码如下:

  1. app.use = function (path, handler) { 
  2.     if(typeof path !== "string") { // 第一个参数不是字符串,说明不是路径,而是方法 
  3.         handler = path; 
  4.         path = "/" 
  5.     } 
  6.     let layer = { 
  7.         method: "middle"
  8.         path, 
  9.         handler 
  10.     } 
  11.     app.routes.push(layer) 

看,是不是很巧妙,很容易。

我们试着访问路径“/middle”

咦?第一个中间件没有执行,为什么呢?

对了,使用中间件的时候,最后要执行next(),才能交给下一个中间件或者路由执行。

当我们请求“/middle”路径的时候,可以看到确实请求成功,中间件也成功执行。说明我们的逻辑没有问题。

实际上,中间件已经完成了,但是别忘了,还有个错误中间件?

什么是错误中间件?

错误处理中间件函数的定义方式与其他中间件函数基本相同,差别在于错误处理函数有四个自变量而不是三个,专门具有特征符 (err, req, res, next):

  1. app.use(function(err, req, res, next) { 
  2.   console.error(err.stack); 
  3.   res.status(500).send('Something broke!'); 
  4. }); 

当我们的在执行next()方法的时候,如果抛出了错误,是会直接寻找错误中间件执行的,而不会去执行其他的中间件或者路由。

举个例子:

如图所示,当第一个中间件往next传递参数的时候,表示执行出现了错误。然后就会跳过其他陆游和中间件和路由,直接执行错误中间件。当然,执行完错误中间件,就会继续执行后面的中间件。

例如:

如图所示,错误中间件的后面那个是会执行的。

那原理该怎么实现呢?

很简单,直接看代码解释,只需在next里多加一层判断即可:

  1. function next(err) { 
  2.     // 已经迭代完整个数组,还是没有找到匹配的路径 
  3.     if (index === app.routes.length) return res.end('Cannot find '
  4.     let { method, path, handler } = app.routes[index++] // 每次调用next就去下一个layer 
  5.     if( err ){ // 如果有错误,应该寻找中间件执行。 
  6.         if(handler.length === 4) { //找到错误中间件 
  7.             handler(err,req,res,next
  8.         }else { // 继续徐州 
  9.             next(err)  
  10.         } 
  11.     }else { 
  12.         if (method === 'middle') { // 处理中间件 
  13.             if (path === '/' || path === pathname || pathname.starWidth(path + '/')) { 
  14.                 handler(req, res, next
  15.             } else { // 继续遍历 
  16.                 next(); 
  17.             } 
  18.         } else { // 处理路由 
  19.             if ((method === m || method === 'all') && (path === pathname || path === "*")) { 
  20.                 handler(req, res); 
  21.             } else { 
  22.                 next(); 
  23.             } 
  24.         } 
  25.     } 

看代码可见在next里判断err有没有值,就可以判断需不需要查找错误中间件来执行了。

如图所示,请求/middle路径,成功执行。

到此,express框架的实现就大功告成了。

学习总结

通过这次express手写原理的实现,更加深入地了解了express的使用,发现:

  • 中间件和路由都是push进一个routes数组里的。
  • 当执行中间件的时候,会传递next,使得下一个中间件或者路由得以执行。
  • 当执行到路由的时候就不会传递next,也使得routes的遍历提前结束。
  • 当执行完错误中间件后,后面的中间件或者路由还是会执行的。

 

责任编辑:武晓燕 来源: 前端阳光
相关推荐

2020-11-24 07:48:32

React

2020-10-20 09:12:57

axios核心原理

2022-08-27 13:49:36

ES7promiseresolve

2020-10-23 09:26:57

React-Redux

2021-05-08 07:53:33

面试线程池系统

2022-04-01 07:52:42

JavaScript防抖节流

2022-10-31 11:10:49

Javavolatile变量

2023-11-28 17:49:51

watch​computed​性能

2020-10-15 12:52:46

SpringbootJava编程语言

2021-04-22 07:49:51

Vue3Vue2.xVue3.x

2020-12-09 10:29:53

SSH加密数据安全

2024-03-05 10:33:39

AOPSpring编程

2024-08-22 10:39:50

@Async注解代理

2020-12-03 08:14:45

Axios核心Promise

2020-11-02 09:35:04

ReactHook

2021-12-02 08:19:06

MVCC面试数据库

2023-12-27 18:16:39

MVCC隔离级别幻读

2024-08-29 16:30:27

2024-08-12 17:36:54

2024-02-29 16:49:20

volatileJava并发编程
点赞
收藏

51CTO技术栈公众号