序:
因为公司 Node
方面业务都是基于一个小型框架写的,这个框架是公司之前的一位同事根据 Express
的中间件思想写的一个小型 Socket
框架,阅读其源码之后,对 Express
的中间件思想有了更深入的了解,接下来就手写一个 Express
框架,以作为学习的产出。
在阅读了同事的代码与 Express
源码之后,发现其实 Express
的核心就是 中间件的思想,其次是封装了更丰富的 API
供我们使用,废话不多说,让我们来一步一步实现一个可用的 Express
。
本文的目的在于验证学习的收获,大概细致划分如下:
- 服务器监听的原理
- 路由的解析与匹配
- 中间件的定义与使用
- 核心
next()
方法 - 错误处理中间件定义与使用
- 内置
API
的封装
正文:
在手写框架之前,我们有必要去回顾一下 Express
的简单使用,从而对照它给我们提供的 API
去实现其相应的功能:
新建一个 app.js
文件,添加如下代码:
// 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 端口:
这里的乱码是因为:服务器不知道你要怎样去解析输出,所以我们需要指定响应头:
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
方法,但是我们的中间件和路由的 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
就完成了。
总结:
- 中间件的核心是
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