本笔记通过模拟实现 Express 的一些功能学习 Express 的实现原理。
初始化应用
mkdir my-express
cd ./my-express
npm init -y
npm i express
添加 app.js
:
// app.js
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.end('get /')
})
app.get('/about', (req, res) => {
res.end('get /about')
})
app.listen(3000, () => {
console.log('server is running on http://localhost:3000')
})
源码结构
从 app.js
代码上看,首先会 require('express')
加载 Express 的 npm 包,它会去 node_modules/express/package.json
中寻找 main
字段。
main
字段指定模块的入口文件,如果没有 main
字段,则默认加载 npm 包根目录下的 index.js
。
node_modules/express/index.js
中只是导入了另一个模块:
'use strict';
module.exports = require('./lib/express');
node_modules/express/lib/express.js
主要导出了一个 createApplication
方法
// 只截取了片段
// 主要导出了一个 `createApplication` 方法
exports = module.exports = createApplication;
// 创建并返回 app
function createApplication() {
// app 本身是一个函数
var app = function(req, res, next) {
app.handle(req, res, next);
};
// 通过 mixin 方法扩展其他成员
mixin(app, EventEmitter.prototype, false);
// proto --> require('./application')
mixin(app, proto, false);
// 扩展 request
app.request = Object.create(req, {
app: {
configurable: true, enumerable: true, writable: true, value: app }
})
// 扩展 response
app.response = Object.create(res, {
app: {
configurable: true, enumerable: true, writable: true, value: app }
})
app.init();
return app;
}
createApplication
方法创建并返回一个 app 函数,内部通过 mixin
方法扩展了很多其他成员。
成员中最核心的东西在于 node_modules/express/lib/application.js
,内部创建并返回了一个 app 对象,然后向这个对象上添加了很多成员,如 use
、router
、listen
等:
// 只截取了片段
var app = exports = module.exports = {
};
app.use = function use(fn) {
...}
app.route = function route(path) {
...}
app.listen = function listen() {
...}
mixin
方法就是将 application.js
返回的 app
对象的成员混入到当前 createApplication
方法创建的 app
对象中。
其他还有:
- 在 node 的 http 模块之上扩展了
request
和response
对象:lib/request.js
和lib/response.js
- 内置了一些工具函数:
lib/utils.js
- 对模板引擎的相关设置处理:
lib/view.js
- 对路由的相关处理:
lib/router
- 内部的中间件:
lib/middleware
接下来模仿 Express 的目录结构实现一些内容。
快速体验
在项目下创建 express
目录,实现模拟的 express,在目录下添加文件。
// express\index.js
module.exports = require('./lib/express')
// express\lib\express.js
const http = require('http')
const url = require('url')
// 路由表
const routes = [
// { path: '', method: '', handler: () => { } }
]
function createApplication() {
return {
get(path, handler) {
// 收集路由
routes.push({
path,
method: 'get',
handler,
})
},
listen(...args) {
const server = http.createServer((req, res) => {
/* 接收到请求的时候处理路由 */
const {
pathname } = url.parse(req.url)
const method = req.method.toLowerCase()
const route = routes.find(route => route.path === pathname && route.method === method)
if (route) {
return route.handler(req, res)
}
res.end('404 Not Found.')
})
server.listen(...args)
}
}
}
module.exports = createApplication
抽取创建 App 的模块
为了方便扩展 app,现在将内部的方法提取到一个构造函数中,通过 new
的方式创建实例,而将这些方法定义为 App 的实例方法。
// express\lib\application.js
const http = require('http')
const url = require('url')
function App() {
// 路由表
this.routes = []
}
App.prototype.get = function (path, handler) {
// 收集路由
this.routes.push({
path,
method: 'get',
handler,
})
}
App.prototype.listen = function (...args) {
const server = http.createServer((req, res) => {
/* 接收到请求的时候处理路由 */
const {
pathname } = url.parse(req.url)
const method = req.method.toLowerCase()
const route = this.routes.find(route => route.path === pathname && route.method === method)
if (route) {
return route.handler(req, res)
}
res.end('404 Not Found.')
})
server.listen(...args)
}
module.exports = App
// express\lib\express.js
const App = require('./application')
function createApplication() {
return new App()
}
module.exports = createApplication
提取路由模块
源码:
node_modules\express\lib\router\index.js
为了更方便的在 app 中对路由进行开发和管理,建议将路由相关处理封装到单独的模块中。
同样的,创建一个 Router 构造函数,所有和路由相关的都通过 Router 的路由实例进行。
// router\index.js
const url = require('url')
function Router() {
// 路由记录栈
this.stack = []
}
Router.prototype.get = function (path, handler) {
this.stack.push({
path,
method: 'get',
handler,
})
}
Router.prototype.handle = function (req, res) {
const {
pathname } = url.parse(req.url)
const method = req.method.toLowerCase()
const route = this.stack.find(route => route.path === pathname && route.method === method)
if (route) {
return route.handler(req, res)
}
res.end('404 Not Found.')
}
module.exports = Router
路由表
routes
改为 express 中的命名stack
。
// express\lib\application.js
const http = require('http')
const Router = require('./router')
function App() {
// 路由表
this._router = new Router()
}
App.prototype.get = function (path, handler) {
this._router.get(path, handler)
}
App.prototype.listen = function (...args) {
const server = http.createServer((req, res) => {
this._router.handle(req, res)
})
server.listen(...args)
}
module.exports = App
处理不同的请求方法
路由示例
// app.js
app.get('/', (req, res) => {
res.end('get /')
})
app.get('/about', (req, res) => {
res.end('get /about')
})
app.post('/about', (req, res) => {
res.end('post /about')
})
app.patch('/about', (req, res) => {
res.end('patch /about')
})
app.delete('/about', (req, res) => {
res.end('delete /about')
})
Express 源码
每个 method 的请求处理逻辑是一样的,只是 method 名不一样,可以遍历常用请求方法,批量的添加处理逻辑。
express 内部使用 methods 包,它导出了 HTTP 常用的请求方法。
它只有一个 js 文件(node_modules\methods\index.js
):
'use strict';
// 导入 nodejs 原生模块 http
var http = require('http');
// 优先 http 中支持的 methods,如果没有则导出手动整理的 methods
module.exports = getCurrentNodeMethods() || getBasicNodeMethods();
// 获取 nodejs 原生模块 http 的 METHODS 属性
// 它标识 node 环境中支持的 HTTP 请求方法
// 将它们遍历小写处理并返回
function getCurrentNodeMethods() {
return http.METHODS && http.METHODS.map(function lowerCaseMethod(method) {
return method.toLowerCase();
});
}
// 手动整理 methods
function getBasicNodeMethods() {
return [
'get',
'post',
'put',
'head',
'delete',
'options',
'trace',
'copy',
'lock',
'mkcol',
'move',
'purge',
'propfind',
'proppatch',
'unlock',
'report',
'mkactivity',
'checkout',
'merge',
'm-search',
'notify',
'subscribe',
'unsubscribe',
'patch',
'search',
'connect'
];
}
模拟实现
可以直接使用 methods 模块:
// express\lib\application.js
const http = require('http')
const Router = require('./router')
const methods = require('methods')
function App() {
// 路由表
this._router = new Router()
}
// 为每个请求方法添加处理函数
methods.forEach(method => {
App.prototype[method] = function (path, handler) {
this._router[method](path, handler)
}
})
App.prototype.listen = function (...args) {
const server = http.createServer((req, res) => {
this._router.handle(req, res)
})
server.listen(...args)
}
module.exports = App
// router\index.js
const url = require('url')
const methods = require('methods')
function Router() {
// 路由记录栈
this.stack = []
}
// 为每个请求方法添加处理函数
methods.forEach(method => {
Router.prototype[method] = function (path, handler) {
this.stack.push({
path,
method,
handler,
})
}
})
Router.prototype.handle = function (req, res) {
const {
pathname } = url.parse(req.url)
const method = req.method.toLowerCase()
const route = this.stack.find(route => route.path === pathname && route.method === method)
if (route) {
return route.handler(req, res)
}
res.end('404 Not Found.')
}
module.exports = Router
注意:当前可以直接使用这些第三方包是因为本项目安装了 express,所以安装了它的依赖。如果要单独发布自己实现的 express,应该在这个包的目录下安装依赖:
# 进入到模拟 express 的模块目录下
cd express
npm init -y
# 安装依赖
npm i methods
# 建议安装和 Express 一样的版本
# npm i [email protected]
现在,在 express(模拟的)中加载 methods 的时候就会优先加载当前目录(/express
)下的 node_modules/methods
了。
实现更强大的路由路径匹配模式
源码:
node_modules\express\lib\router\layer.js
Express 内部支持路由路径的多种匹配模式:Route paths
路由示例
// app.js
app.get('/', function (req, res) {
res.end('root')
})
app.get('/about', function (req, res) {
res.end('about')
})
app.get('/random.text', function (req, res) {
res.end('random.text')
})
app.get('/ab?cd', function (req, res) {
res.end('ab?cd')
})
app.get('/ab+cd', function (req, res) {
res.end('ab+cd')
})
app.get('/ab*cd', function (req, res) {
res.end('ab*cd')
})
app.get('/ab(cd)?e', function (req, res) {
res.end('ab(cd)?e')
})
app.get(/a/, function (req, res) {
res.end('/a/')
})
app.get(/.*fly$/, function (req, res) {
res.end('/.*fly$/')
})
app.get('/users/:userId/books/:bookId', function (req, res) {
res.end(req.params)
})
PS:
response.send()
方法是 Express 在 nodejshttp.ServerResponse
对象的基础上扩展的方法,当前还没有定义,http.ServerResponse
本身没有.send()
方法,所以这里使用的res.end()
Express 源码
Express 内部使用的 path-to-regexp
模块实现路由匹配,并且可以解析动态参数。
// node_modules\express\lib\router\layer.js
var pathRegexp = require('path-to-regexp');
this.regexp = pathRegexp(path, this.keys = [], opts);
模拟实现
安装依赖
# 在 /express 目录下执行
# 注意:path-to-regexp 最新版已经不支持 Express 4 了,要安装老版本,版本号参考 Express 包下的 package.json
npm i [email protected]
处理路径
// router\index.js
const url = require('url')
const methods = require('methods')
const pathRegexp = require('path-to-regexp')
function Router() {
// 路由记录栈
this.stack = []
}
// 为每个请求方法添加处理函数
methods.forEach(method => {
Router.prototype[method] = function (path, handler) {
this.stack.push({
path,
method,
handler,
})
}
})
Router.prototype.handle = function (req, res) {
const {
pathname } = url.parse(req.url)
const method = req.method.toLowerCase()
const route = this.stack.find(route => {
const keys = []
// 基于配置的路由匹配规则,生成一个正则表达式
const regexp = pathRegexp(route.path, keys, {
})
// 匹配实际请求路径的结果
const match = regexp.exec(pathname)
return match && route.method === method
})
if (route) {
return route.handler(req, res)
}
res.end('404 Not Found.')
}
module.exports = Router
处理动态路径参数
假设访问 '/users/:userId/books/:bookId'
路由地址: http://localhost:3000/users/tom/books/123
。
path-to-regexp
会解析动态参数,将解析的参数名填充到传入的 keys
数组,参数值可以通过正则匹配后的结果中查看:
const keys = []
const regexp = pathRegexp(route.path, keys, {
})
const match = regexp.exec(pathname)
console.log('keys =>',keys)
console.log('match =>',match)
打印结果:
keys => [
{
name: 'userId', optional: false, offset: 8 },
{
name: 'bookId', optional: false, offset: 30 }
]
match => [
'/users/tom/books/123',
'tom',
'123',
index: 0,
input: '/users/tom/books/123',
groups: undefined
]
所以可以将参数名和参数值映射在一起,挂载到 req.params
上:
// router\index.js
const route = this.stack.find(route => {
const keys = []
// 基于配置的路由匹配规则,生成一个正则表达式
const regexp = pathRegexp(route.path, keys, {
})
// 匹配实际请求路径的结果
const match = regexp.exec(pathname)
// 解析动态参数
if (match) {
req.params = req.params || {
}
keys.forEach((key, index) => {
req.params[key.name] = match[index + 1]
})
}
return match && route.method === method
})
提取 Layer 处理模块
Express 源码
源码:
node_modules\express\lib\router\layer.js
Express 中是将路由路径匹配单独封装到了一个模块中管理的。
// node_modules\express\lib\router\index.js
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
layer.route = route;
// 路由栈里存储的 Layer 实例
this.stack.push(layer);
// 调用的 layer 的 match 方法进行匹配
layer = stack[idx++];
match = matchLayer(layer, path);
function matchLayer(layer, path) {
try {
return layer.match(path);
} catch (err) {
return err;
}
}
// node_modules\express\lib\router\layer.js
module.exports = Layer;
function Layer(path, options, fn) {
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
debug('new %o', path)
var opts = options || {
};
this.handle = fn;
this.name = fn.name || '<anonymous>';
this.params = undefined;
this.path = undefined;
// 实例化时就生成了路由的正则表达式
this.regexp = pathRegexp(path, this.keys = [], opts);
// set fast path flags
this.regexp.fast_star = path === '*'
this.regexp.fast_slash = path === '/' && opts.end === false
}
Layer.prototype.match = function match(path) {
...}
模拟实现
// express\lib\router\layer.js
const pathRegexp = require('path-to-regexp')
function Layer(path, handler) {
this.path = path
this.handler = handler
this.keys = []
this.regexp = pathRegexp(path, this.keys, {
})
this.params = {
}
}
Layer.prototype.match = function (pathname) {
const match = this.regexp.exec(pathname)
if (match) {
this.keys.forEach((key, index) => {
this.params[key.name] = match[index + 1]
})
return true
}
return false
}
module.exports = Layer
// router\index.js
const url = require('url')
const methods = require('methods')
const Layer = require('./layer')
function Router() {
// 路由记录栈
this.stack = []
}
// 为每个请求方法添加处理函数
methods.forEach(method => {
Router.prototype[method] = function (path, handler) {
const layer = new Layer(path, handler)
layer.method = method
this.stack.push(layer)
}
})
Router.prototype.handle = function (req, res) {
const {
pathname } = url.parse(req.url)
const method = req.method.toLowerCase()
const layer = this.stack.find(layer => {
const match = layer.match(pathname)
if (match) {
req.params = req.params || {
}
Object.assign(req.params, layer.params)
}
return match && layer.method === method
})
if (layer) {
return layer.handler(req, res)
}
res.end('404 Not Found.')
}
module.exports = Router
实现单个处理函数的中间件功能
路由示例
app.get('/', (req, res, next) => {
console.log(1)
next()
}, (req, res, next) => {
console.log(2)
next()
}, (req, res, next) => {
console.log(3)
next()
})
app.get('/', (req, res) => {
// 响应
res.end('get /')
})
app.get('/foo', (req, res, next) => {
console.log('foo 1')
next()
})
app.get('/foo', (req, res, next) => {
console.log('foo 2')
next()
})
app.get('/foo', (req, res) => {
// 响应
res.end('get /foo')
})
模拟实现
不能直接遍历匹配到的每个路由的 handler
,考虑到处理函数中可能有异步操作,可以自定义一个 next
方法(内部处理下一个路由),将其作为参数传递给 handler
,由 handler
自己决定处理下一个路由(调用 next
)的时机。
// router\index.js
const url = require('url')
const methods = require('methods')
const Layer = require('./layer')
function Router() {
// 路由记录栈
this.stack = []
}
// 为每个请求方法添加处理函数
methods.forEach(method => {
Router.prototype[method] = function (path, handler) {
const layer = new Layer(path, handler)
layer.method = method
this.stack.push(layer)
}
})
Router.prototype.handle = function (req, res) {
const {
pathname } = url.parse(req.url)
const method = req.method.toLowerCase()
let index = 0
const next = () => {
if (index >= this.stack.length) {
return res.end(`Cannot GET ${
pathname}`)
}
const layer = this.stack[index++]
const match = layer.match(pathname)
if (match) {
req.params = req.params || {
}
Object.assign(req.params, layer.params)
}
if (match && layer.method === method) {
return layer.handler(req, res, next)
}
// 如果当前请求没有匹配到路由,继续遍历下一个路由
next()
}
// 调用执行
next()
}
module.exports = Router
// 访问 `/foo` 打印结果:
foo 1
foo 2
// 访问 `/ 打印结果(只执行了每个路由的第一个处理函数):
1
实现多个处理函数的中间件
思路分析
当前路由表 stack 中每项存储的是单独的 layer,分别包含一个处理函数 handler。
实际上 Express 在路由表(Router)中还存储了一份路由表(Route),表中存放的是为每个处理函数创建的 layer,这样就可以遍历每个路由的多个处理函数。
数据结构类似:
Router
stack [
Layer 1 {
path: 'xx', // 请求路径
dispatch, // 处理方法,内部调用 Route 下的 dispatch
Route {
stack [
Layer {
path: 'xx', method: 'xxx', handler: 'xxx' },
Layer {
path: 'xx', method: 'xxx', handler: 'xxx' },
Layer {
path: 'xx', method: 'xxx', handler: 'xxx' }
],
dispatch // 遍历调用 stack 中的 handler
}
},
Layer 2 {
path: 'xx', // 请求路径
dispatch, // 处理方法
Route {
stack [
Layer {
path: 'xx', method: 'xxx', handler: 'xxx' },
Layer {
path: 'xx', method: 'xxx', handler: 'xxx' },
Layer {
path: 'xx', method: 'xxx', handler: 'xxx' }
],
dispatch // 遍历调用 stack 中的 handler
}
},
]
组织数据结构
// express\lib\router\route.js
const methods = require('methods')
const Layer = require('./layer')
function Route() {
this.stack = [
// { path, method, handler }
]
}
// 遍历执行当前路由对象中所有的处理函数
Route.prototype.dispatch = function () {
}
methods.forEach(method => {
Route.prototype[method] = function (path, handlers) {
handlers.forEach(handler => {
const layer = new Layer(path, handler)
layer.method = method
this.stack.push(layer)
})
}
})
module.exports = Route
// router\index.js
const url = require('url')
const methods = require('methods')
const Layer = require('./layer')
const Route = require('./route')
function Router() {
// 路由记录栈
this.stack = []
}
// 为每个请求方法添加处理函数
methods.forEach(method => {
Router.prototype[method] = function (path, handlers) {
const route = new Route()
const layer = new Layer(path, route.dispatch.bind(route))
layer.route = route
this.stack.push(layer)
route[method](path, handlers)
}
})
Router.prototype.handle = function (req, res) {
...}
module.exports = Router
// express\lib\application.js
...
methods.forEach(method => {
// 接受全部处理函数
App.prototype[method] = function (path, ...handlers) {
this._router[method](path, handlers)
}
})
...
可以打印路由看看结果:
// app.js
app.get('/foo', (req, res) => {
// 响应
res.end('get /foo')
})
console.log(app._router)
执行处理函数
Express 可以通过下面的 API 配置路由:
app.route('/foo')
.get((req, res) => {
})
.post((req, res) => {
})
在执行处理函数的时候,在外层路由匹配路径,在内层路由匹配请求方法,所以这里将匹配 method 去掉:
// router\index.js
Router.prototype.handle = function (req, res) {
...
const next = () => {
...
// 顶层只匹配请求路径,内层匹配请求方法
// if (match && layer.method === method) {
if (match) {
// 顶层调用的 handler 就是 route.dispatch 函数
return layer.handler(req, res, next)
}
next()
}
next()
}
// express\lib\router\route.js
const methods = require('methods')
const Layer = require('./layer')
...
// 遍历执行当前路由对象中所有的处理函数
// 遍历内层的 stack,内层遍历结束就要返回到外层,所以第三个参数名取名 out 而不是 next
Route.prototype.dispatch = function (req, res, out) {
const method = req.method.toLowerCase()
let index = 0
const next = () => {
if (index >= this.stack.length) {
return out()
}
const layer = this.stack[index++]
if (layer.method === method) {
return layer.handler(req, res, next)
}
next()
}
next()
}
...
module.exports = Route
再次访问 /
打印结果:
1
2
3
实现 use 方法
回顾使用规则
// 不验证请求方法和请求路径,任何请求都会处理
// app.use((req, res, next) => {
// res.end('hello')
// })
// 匹配的不是 `/foo`,而是以 `/foo` 作为前置路径的路由
// app.use('/foo', (req, res, next) => {
// res.end('foo')
// })
// 与不传第一个路径参数等效
// app.use('/', (req, res, next) => {
// res.end('hello')
// })
// 可以挂载多个请求处理函数
app.use('/', (req, res, next) => {
console.log(1)
next()
}, (req, res, next) => {
console.log(2)
next()
}, (req, res, next) => {
res.end('hello')
})
模拟实现
// express\lib\application.js
App.prototype.use = function (path, ...handlers) {
this._router.use(path, handlers)
}
// router\index.js
Router.prototype.use = function (path, handlers) {
// 没有传路径的时候
if (typeof path === 'function') {
handlers.unshift(path)
path = '/' // 任何路径都以它开头
}
handlers.forEach(handler => {
const layer = new Layer(path, handler)
// 标记是否是 use 类型的中间件
layer.isUseMiddleware = true
this.stack.push(layer)
})
}
// express\lib\router\layer.js
const pathRegexp = require('path-to-regexp')
function Layer(path, handler) {
this.path = path
this.handler = handler
this.keys = []
this.regexp = pathRegexp(path, this.keys, {
})
this.params = {
}
}
Layer.prototype.match = function (pathname) {
const match = this.regexp.exec(pathname)
if (match) {
this.keys.forEach((key, index) => {
this.params[key.name] = match[index + 1]
})
return true
}
// 匹配 use 中间件的路径
if (this.isUseMiddleware) {
if (this.path === '/') {
return true
}
if (pathname.startsWith(`${
this.path}/`)) {
return true
}
}
return false
}
module.exports = Layer