由于公司 Node
方面业务都是基于一个小型框架写的,这个框架是公司以前的一位同事根据 Express
的中间件思想写的一个小型 Socket
框架,阅读其源码以后,对 Express
的中间件思想有了更深刻的了解,接下来就手写一个 Express
框架 ,以做为学习的产出 。javascript
在阅读了同事的代码与 Express
源码以后,发现其实 Express
的核心就是中间件的思想,其次是封装了更丰富的 API
供咱们使用,废话很少说,让咱们来一步一步实现一个可用的 Express
。html
本文的目的在于验证学习的收获,大概细致划分以下:java
next()
方法API
的封装在手写框架以前,咱们有必要去回顾一下 Express
的简单使用,从而对照它给咱们提供的 API
去实现其相应的功能:node
新建一个 app.js
文件,添加以下代码:express
// app.js let express = require('express'); let app = express(); app.listen(3000, function () { console.log('listen 3000 port ...') })
如今,在命令行中执行:数组
node app.js
能够看到,程序已经在咱们的后台跑起来了。浏览器
当咱们为其添加一个路由:服务器
let express = require('Express'); let app = express(); app.get('/hello', function (req, res) { res.setHeader('Content-Type', 'text/html; charset=utf-8') res.end('我是新添加的路由,只有 get 方法才能够访问到我 ~') }) app.listen(3000, function () { console.log('listen 3000 port ...') })
再次重启:在命令行中执行启动命令:(每次修改代码都须要从新执行脚本)并访问浏览器本地 3000 端口:app
这里的乱码是由于:服务器不知道你要怎样去解析输出,因此咱们须要指定响应头:框架
let express = require('Express'); let app = express(); app.get('/hello', function (req, res) { res.setHeader('Content-Type', 'text/html; charset=utf-8') // 指定 utf-8 res.end('我是新添加的路由,只有 get 方法才能够访问到我 ~') }) app.post('/hi', function (req, res) { res.end('我是新添加的路由,只有 post 方法才能够访问到我 ~') }) app.listen(3000, function () { console.log('listen 3000 port ...') })
咱们先来实现上面的功能:
新建一个 MyExpress.js
,定义一个入口函数:
let http = require('http'); function createApplication () { // 定义入口函数,初始化操做 let app = function (req, res) { } // 定义监听方法 app.listen = function () { // 经过 http 模块建立一个服务器实例,该实例的参数是一个函数,该函数有两个参数,分别是 req 请求对象和 res 响应对象 let server = http.createServer(app); // 将参数列表传入,为实例监听配置项 server.listen(...arguments); } // 返回该函数 return app } module.exports = createApplication;
如今,咱们代码中的 app.listen()
其实就已经实现了,能够将引入的 express
替换为咱们写的 MyExpress
作验证:
let express = require('Express'); // 替换为 let express = require('./MyExpress');
接下来,
咱们先看看 routes
中的原理图
根据上图,路由数组中存在多个 layer
层,每一个 layer
中包含了三个属性, method
、path
、handler
分别对应请求的方式、请求的路径、执行的回调函数,代码以下:
const http = require('http') function createApp () { let app = function (req, res) { }; app.routes = []; // 定义路由数组 let methods = http.METHODS; // 获取全部请求方法,好比常见的 GET/POST/DELETE/PUT ... methods.forEach(method => { method = method.toLocaleLowerCase() // 小写转换 app[method] = function (path, handler) { let layer = { method, path, handler, } // 将每个请求保存到路由数组中 app.routes.push(layer) } }) // 定义监听的方法 app.listen = function () { let server = http.createServer(app); server.listen(...arguments) } return app; } module.exports = createApp
到这里,仔细思考下,当脚本启动时,咱们把全部的路由都保存到了 routes
,打印 routes
,能够看到:
是否是和咱们上面图中的如出一辙 ~
此时,咱们访问对应的路径,发现浏览器一直转圈圈,这是由于咱们只是完成了存的操做,把全部的 layer
层存到了 routes
中。
那么咱们该如何才能够作的当访问的时候,调用对应的 handle
函数呢?
思路:当咱们访问路径时,也就是获取到请求对象 req
时,咱们须要遍历所存入的 layer
与访问的 method
、path
进行匹配,匹配成功,则执行对应的 handler
函数
代码以下:
const url = require('url') ...... let app = function (req, res) { let reqMethod = req.method.toLocaleLowerCase() // 获取请求方法 let pathName = url.parse(req.url, true).pathname // 获取请求路径 console.log(app.routes); app.routes.forEach(layer => { let { method, path, handler } = layer; if (method === reqMethod && path === pathName) { handler(req, res) } }); }; ......
至此,路由的定义与解析也基本完成。
接下来,就是重点了,中间件思想。
中间件的定义其实与路由的定义差很少,也是存在 routes
中,可是,必须放到全部路由的 layer
以前,原理以下图:
其中,middle1
、middle2
、middle3
都是中间件,middle3 放在最后面,通常做为错误处理中间件,而且,每次访问服务器的时候,全部的请求先要通过 middle1
、middle2
作处理。
在中间件中,有一个 next
方法,其实 next
方法就是使 layer
的 index
标志向后移一位,并进行匹配,匹配成功执行回调,匹配失败则继续向后匹配,有点像 回调队列。
next()
方法接下来咱们实现一个 next
方法:
由于只有中间件的回调中才具备 next
方法,可是咱们的中间件和路由的 layer
层都是存在 routes
中的,因此首先要判断 layer
中的 method
是否为 middle
初次以外,还要判断,中间件的路由是否相匹配,由于有些中间件是针对某个路由的。
let reqMethod = req.method.toLocaleLowerCase() let pathName = url.parse(req.url, true).pathname let index = 0; function next () { // 中间件处理 if (method === 'middle') { // 检测 path 是否匹配 if (path === '/' || pathName === path || pathName.startsWith(path + '/')) { handler(req, res, next) // 执行中间件回调 } else { next() } // 路由处理 } else { // 检测 method 与 path 是否匹配 if (method === reqMethod && path === pathName) { handler(req, res) // 执行路由回调 } else { next() } } } next() // 这里必需要调用一次 next ,意义在于初始化的时候,取到第一个 layer,
若是遍历完 routes
,都没有匹配的 layer
,该怎么办呢?因此要在 next
方法最早判断是否边已经遍历完:
function next () { // 判断是否遍历完 if (app.routes.length === index) { return res.end(`Cannot ${reqMethod} ${pathName}`) } let { method, path, handler } = app.routes[index++]; // 中间件处理 if (method === 'middle') { if (path === '/' || pathName === path || pathName.startsWith(path + '/')) { handler(req, res, next) } else { next() } } else { // 路由处理 if (method === reqMethod && path === pathName) { handler(req, res) } else { next() } } } next()
这样,一个 next 方法功能基本完成了。
如上面图中所示,错误处理中间件放在最后,就像一个流水线工厂,错误处理就是最后一道工序,但并非全部的产品都须要跑最后一道工序,就像:只有不合格的产品,才会进入最后一道工序,并被贴上不合格的标签,以及不合格的缘由。
咱们先看看 Express
中的错误是怎么被处理的:
// 中间件1 app.use(function (req, res, next) { res.setHeader('Content-Type', 'text/html; charset=utf-8') console.log('middle1') next('这是错误') }) // 中间件2 app.use(function (req, res, next) { console.log('middle2') next() }) // 中间件3(错误处理) app.use(function (err, req, res, next) { if (err) { res.end(err) } next() })
如上图所示:有三个中间件,当 next
方法中抛出错误时,会把错误当作参数传入 next
方法,而后,next
指向的下一个方法就是错误处理的回调函数,也就是说:next
方法中的参被当作了错误处理中间件的 handler
函数的参数传入。代码以下:
function next (err) { // 判断是否遍历完成 if (app.routes.length === index) { return res.end(`Cannot ${reqMethod} ${pathName}`) } let { method, path, handler } = app.routes[index++]; if (err) { console.log(handler.length) // 判断是否有 4 个参数:由于错误中间件与普通中间件最直观的区别就是参数数量不一样 if (handler.length === 4) { // 错误处理回调 handler(err, req, res, next) } else { // 一直向下传递 next(err) } } else { // 中间件处理 if (method === 'middle') { if (path === '/' || pathName === path || pathName.startsWith(path + '/')) { handler(req, res, next) } else { next() } } else { // 路由处理 if (method === reqMethod && path === pathName) { handler(req, res) } else { next() } } } }
麻雀虽小五脏俱全,至此,一个简单的 Express
就完成了。你能够根据本身的兴趣来封装本身的 API
了 ...
next
方法。next
方法只负责维护 routes
数组和取出 layer
,根据条件去决定是否执行回调。const http = require('http') const url = require('url') function createApp () { let app = function (req, res) { let reqMethod = req.method.toLocaleLowerCase() let pathName = url.parse(req.url, true).pathname let index = 0; function next (err) { if (app.routes.length === index) { return res.end(`Cannot ${reqMethod} ${pathName}`) } let { method, path, handler } = app.routes[index++]; if (err) { console.log(handler.length) if (handler.length === 4) { console.log(1) handler(err, req, res, next) } else { next(err) } } else { if (method === 'middle') { if (path === '/' || pathName === path || pathName.startsWith(path + '/')) { handler(req, res, next) } else { next() } } else { if (method === reqMethod && path === pathName) { handler(req, res) } else { next() } } } } next() }; let methods = http.METHODS; app.routes = []; methods.forEach(method => { method = method.toLocaleLowerCase() app[method] = function (path, handler) { let layer = { method, path, handler, } app.routes.push(layer) } }) app.use = function (path, handler) { if (typeof path === 'function') { handler = path; path = '/'; } let layer = { method: 'middle', handler, path } app.routes.push(layer) } app.listen = function () { let server = http.createServer(app); server.listen(...arguments) } return app; } module.exports = createApp