前言
文章结构采用【指出阶段目标,然后以需解决问题为入口,以解决思路为手段】达到本文目标,若使诸君稍有启发,不枉此文心力^-^
(滴滴,长文警告,食用慎重)
实现提供http服务功能
先看使用
const express = require('./express');
const app = express();
app.get('/',function (req,res){
res.end('/')
})
app.get('/hello',function (req,res){
res.end('/hello');
})
app.listen(3000,function () {
console.log('server start 3000');
})
两个功能
- 执行
listen
方法时创建服务 - 访问方法符合时,访问对应路径,执行相应回调;均不匹配时,返回固定404信息;
实现思路
注意到express
是一个函数,其返回值是一个具有listen
、get
方法的对象,我们可以在express
的入口进行定义,从而目光转向对listen
、get
方法的实现了
listen
方法就是对原生的http
模块的一个封装,我们只要在执行时利用原生node模块http
创建一个服务就可以了get
方法和【均不匹配兼容】其实是一个路由功能,目前可以先简单的用一个队列去实现,每次执行get
等路由方法,就将路径和对应处理函数入队列,然后在请求来时进行遍历匹配即可。至于404兼容,我们可以在初始化时就存入一个处理函数,这样当所有都没有匹配上时就执行即可
具体实现
const http = require('http')
const url = require('url')
function createApplication() {
const router = [
{
path: '*',
method: '*',
handler(req,res){
res.end(`Cannot ${req.method} ${req.url}`)
}
}
]
return {
get(path,handler){
router.push({
path,
method: 'get',
handler
})
},
listen(port,cb){
let server = http.createServer(function (req,res) {
let {
pathname
} = url.parse(req.url); // 获取请求的路径
let requireMethod = req.method.toLowerCase();
for (let index = 1; index < router.length; index++) {
const {method,path,handler} = router[index];
if(pathname === path && requireMethod === method){
return handler(req, res);
}
}
return router[0].handler(req,res);
})
server.listen(...arguments)
}
}
}
module.exports = createApplication
实现拆分
实现应用的分离
在express
的入口函数中,只需要创建应用,而不应该关注应用本身的细节
express.js
const Application = require('./Application');
function createApplication() {
return new Application();
}
module.exports = createApplication
而在应用中,则将之前说的提供【http服务】、【路由服务】包裹在一个构造函数中即可
Application.js
const http = require('http')
const url = require('url')
function Application() {
// 路由表
this.router = [{
path: '*',
method: '*',
handler(req, res) {
res.end(`Cannot ${req.method} ${req.url}`)
}
}]
}
Application.prototype.get = function (path, handler) {
this.router.push({
path,
method: 'get',
handler
})
}
Application.prototype.listen = function (port, cb) {
let server = http.createServer( (req, res) => {
let {
pathname
} = url.parse(req.url); // 获取请求的路径
let requireMethod = req.method.toLowerCase();
for (let index = 1; index < this.router.length; index++) {
const {
method,
path,
handler
} = this.router[index];
if (pathname === path && requireMethod === method) {
return handler(req, res);
}
}
return this.router[0].handler(req, res);
})
server.listen(...arguments)
}
module.exports = Application;
此时我们完成了入口和应用层的抽离,但应用层和路由还是耦合的
完成路由的抽离
Router.js
const url = require('url')
function Router() {
this.stack = [];
}
Router.prototype.get = function (path, handler) {
this.stack.push({
path,
handler,
method: 'get'
})
}
Router.prototype.handle = function (req,res,out) {
let {
pathname
} = url.parse(req.url); // 获取请求的路径
let requireMethod = req.method.toLowerCase();
for (let index = 0; index < this.stack.length; index++) {
const {
method,
path,
handler
} = this.stack[index];
if (pathname === path && requireMethod === method) {
return handler(req, res);
}
}
return out(req, res);
}
module.exports = Router;
完善路由
先看应用
关于路由,上文只是简单的实现了单一请求匹配单一应答,对于express
的路由系统而言,其实还有更重要的两个特性没有体现
- 多匹配机制
- 多回调机制
多匹配机制
即同一个方法下的同一个路径被监听了多次,此时如果上层函数不执行next
方法,下层监听函数是不会被执行的;如下
app.flage = false;
app.get('/hello',function (req,res,next){
app.flage = !app.flage;
function isOk() {
// 判断是否向下执行的逻辑
return app.flage;
}
// res.end('/hello')
console.log('我是第一个监听函数',app.flage);
if (isOk()) {
next()
}else {
res.end('not continue')
}
})
app.get('/hello',function (req,res){
console.log('我是第二个监听函数');
res.end('/hello')
})
上面代码很简单,就是设定了一个开关,为true
时则执行next
函数继续向下执行,为false
则直接返回,启动服务请求接口,会发现如下现象
控制台显示如下
多回调机制
即回调函数传递多个,同样采用next
控制是否执行栈中的下一个
app.get('/', function (req,res,next) {
console.log(1);
next();
},function (req,res,next) {
console.log(2);
next();
},function (req,res,next) {
console.log(3);
next();
},function (req,res,next) {
console.log(4);
next();
res.end('/ matched')
})
效果和上文相似,调动next
则会继续向下执行
再看实现
理解next原理
要实现这些效果,我们先想想这样一个需求
有一个数组,组成数据结构如下,{match(){},handler(next){}} , 我希望当上一个handler调用next了之后,才执行数组的下一项
如何实现?
了解过CO
(就是大神TJ实现的【对generator
迭代器实现处理】的库,个人认为async+await
是这个库的一个官方版)就会发现,逻辑完全一致,都是【将是否继续循环的控制权反转给被循环者本身】
既然要控制,我们自然不能简单的循环了,而是得递归。举例论证:
let stack = [
{
handle(next){
console.log('1, begin')
setTimeout(() => {
next()
console.log('1, done');
}, 5000);
}
},
{
handle(next){
console.log('2,done');
}
}
]
那我们需要实现的就是
- 打印
'1, begin'
- 过五秒打印
1, done
和2, done
首先,我们很自然的能想到,如果next
就是下一个函数呢?这样不就满足【执行next时执行下一个函数】的需求了吗?于是我们写出如下代码
let index = 0; // 当前正在执行的数组项索引
function dispatch(stack){
let item = stack[index];
item.handle(stack[++index])// 先自增 再取值
}
这样,我们就执行了第一项和第二项;接着,我们要递归所有
let index = 0; // 当前正在执行的数组项索引
function dispatch(stack,out){
if(stack.length === index) return typeof out == 'Function' && out(); // 递归结束 ,如果存在结束钩子 则执行
let item = stack[index++]; // 取值完后自增
item.handle(dispatch.bind(null,stack))
}
有了这个基础,我们再去看express
的路由next
就能更轻松一点了。
实现express-router
先思考实现思路
- 每个【路径+方法】对应一个路由表(数组),路由表由处理函数组成,形成一个
层
(Layer) Router
持有路由表(数组),路由表由上面的Layer
组成
我们先来看张图
角色梳理
这里面有三个角色
- 路由表:由层组成,是个数组,其层的组成是指【处理函数】
- 路由对象:持有路由表,其路由表的层的组成是指【路径+方法 对应着 路由表】
- 层:为了统一数组组成而抽象出来的数据结构
Route.js
const Layer = require('./layer')
const methods = require('methods');
// 每个层都有一个route属性
function Route() {
this.stack = []
}
// 处理函数
Route.prototype.dispatch = function (req,res,out) {
let idx = 0;
let method = req.method.toLowerCase(); // 获取请求的方法
let dispatch = () => {
if (idx === this.stack.length) {
return out();
}
let layer = this.stack[idx++];
if (layer.method === method) { // 获取内部的第一层判断方法是否匹配
layer.handle_request(req,res,dispatch)
}else {
dispatch()
}
}
dispatch()
}
methods.forEach(method => {
// 被调用时 将对应的处理函数数组映射成多个层,并存入路由表对象持有的队列中
Route.prototype[method] = function (handlers) {
handlers.forEach(handler => {
let layer = new Layer('',handler);
layer.method = method;
this.stack.push(layer)
})
}
})
module.exports = Route;
Layer.js
function Layer(path,handler) {
this.path = path;
this.handler = handler;
this.method = ""
}
Layer.prototype.match = function (pathname) {
return this.path === pathname;
}
Layer.prototype.handler_request = function (req,res,next) {
this.handler((req,res,next))
}
module.exports = Layer;
逻辑梳理
首先,一个应用得有一个对应的路由
function Application() {
this._router = new Router();
}
其次,就是请求的订阅发布了。
订阅:得在应用初始化时进行方法的监听,即app
上的get、post
等路由方法交由路由处理
methods.forEach(method => {
Application.prototype[method] = function (path, ...handlers) {
this._router[method](path,handlers)
}
})
进入router
层,在方法被触发时做三件事
- 创建对应的层,存入自己的队列中
- 对层进行初始化,赋值上对应的【path+method】和处理函数
- 将处理逻辑交由层里面的路由表(
route
)进行处理
function Router() {
this.stack = [];
}
methods.forEach(method => {
Router.prototype[method] = function (path,handlers) { // 用户调[method]方法时,传递了多个处理函数
let route = this.route(path); // 构建一个route
// 将处理逻辑交由层里面的路由表进行处理
route[method](handlers);
}
})
Router.prototype.route = function (path) {
let route = new Route();
let layer = new Layer(path,route.dispatch.bind(route)); // 给当前调用get方法 放入一层
layer.route = route; // 每个层都有一个route属性
this.stack.push(layer);
return route;
}
发布:得改写应用对象的监听逻辑,将逻辑处理交由路由
Application.prototype.listen = function (port, cb) {
let server = http.createServer( (req, res) => {
// 应用提供一个对均无法匹配的兼容处理函数
function done() {
res.end(`Cannot ${req.method} ${req.url}`);
}
this._router.handle(req,res,done);
})
server.listen(...arguments)
}
路由的handle
方法只做一件事,递归存储的Layer
,所有匹配到的Layer
依次执行
重点【依次】,其实就是在说【多匹配机制】,这个时候我们就要采用next
的思路了。
Router.prototype.handle = function (req,res,out) {
let {
pathname
} = url.parse(req.url); // 获取请求的路径
let idx = 0;
let dispatch = () => { // exress 需要通过next函数来迭代
if (idx === this.stack.length) {
return out()
}
let layer = this.stack[idx++];
if (layer.match(pathname)) {
layer.handler_request(req,res,dispatch);
}else {
dispatch();
}
}
dispatch();
}
而Layer
的handler_request
其实就是调用了一下初始化Layer
时存入的handle
函数,回想一下上面,那不就是路由表的next
的吗?这就通了,相当了是:Application -> Router -> Layer -> Route -> Layer的处理逻辑
Layer间会需要一次next
逻辑,Route间也会需要一次,此时需要注意的是,Layer的out
函数是express在listen
中提供的done
方法,而Route的out
函数则是Layer的next方法
至此,我们就基本理解了epxress
的运作流程啦
中间件机制
先看应用
应用规则:
- express中use的第一个参数是匹配路径 不传相当于"/"
- 中间件匹配机制是惰性匹配,即匹配路径为
/a
的中间件,访问/aa
时同样会被执行(这也意味着不传匹配路径时即所有请求都会应用此中间件)
const express = require('./express');
const app = express();
// 第一个参数是匹配路径 不传相当于"/"
app.use(function (req,res,next) {
req.a = 1;
next()
})
app.use('/',function (req,res,next){
req.a++;
next();
})
app.get('/',function (req,res,next){
res.end(req.a + '');
})
app.use('/a',function (req,res,next){
req.a++;
next();
})
app.get('/a',function (req,res,next){
res.end(req.a + '');
})
app.listen(3000)
实现思路
结合之前的路由实现,其实中间件就是【没有路由表】的Layer,我们只需要
- 订阅监听时做下对“不传匹配路径”等情况的处理
- 请求发布时根据“是否具有路由表
route
属性”进行判断从而对中间件区分处理
如此即可
具体实现
订阅监听时做下对“不传匹配路径”等情况的处理
定义use
方法
Router.prototype.use = function(path, ...handlers) {
if (!handlers[0]) { // 只传递了一个函数
handlers.push(path); // app.use(function(){}) app.use()
path = '/'
}
handlers.forEach(handler => {
let layer = new Layer(path, handler);
layer.route = undefined; // 不写也是undefined , 主要告诉你 中间件没有route
this.stack.push(layer);
})
}
请求发布时根据“是否具有路由表route
属性”进行判断从而对中间件区分处理
改写handle
方法
Router.prototype.handle = function(req, res, done) {
let { pathname } = url.parse(req.url);
let method = req.method.toLowerCase()
let idx = 0;
const next = (err) => { // 中间件 和内部的next方法 出错都会走这个next
if (idx >= this.stack.length) return done(); // 路由处理不了 传递给应用层
let layer = this.stack[idx++];
// 无论是路由还是中间件 前提是路径必须匹配
if (layer.match(pathname)) { // match还没有更改
if (!layer.route) { // 没有说明是中间件 注意 此处就是对中间件的区分处理
layer.handle_request(req, res, next); // 直接执行中间件函数
} else {
// 路由必须匹配方法
if (layer.route.methods[method]) { // 这个next可以让路由层扫描下一个layer
layer.handle_request(req, res, next); // route.dispatch
} else {
next();
}
}
} else {
next();
}
}
next(); // 请求来了取出第一个执行
}
总结流程
错误处理
先看应用
应用规则:
- 首先进行错误中间件注册(错误中间件区别普通中间件就在于它有四个参数)
app.use((err,req,res,next) => {
res.header('Content-Type','text/html;charset=utf-8');
res.end(err)
})
- 应用时,只要在执行
next
函数时传递参数,就会执行到错误中间件的回调中,并且会将值传递给err
app.use(function (req,res,next){
let flag = Math.random() > 0.5;
if(flag){
return next('出错了');
}
next();
})
访问效果如下
实现思路
同样的,对错误中间件进行区别处理,判断逻辑
- 根据
route
属性的有无判断是不是中间件 - 根据
fn.length
是不是4判断是不是错误中间件 (函数的length
属性是参数个数)
具体实现
改写Router的handle
方法,注意两点:
- 如果next有参数,则查找错误中间件以执行
- 在执行中间件时要进行判断,从而避免执行了错误中间件
Router.prototype.handle = function(req, res, done) {
let { pathname } = url.parse(req.url);
let method = req.method.toLowerCase()
let idx = 0;
let removed = '';
const next = (err) => { // 中间件 和内部的next方法 出错都会走这个next
if (idx >= this.stack.length) return done(); // 路由处理不了 传递给应用层
let layer = this.stack[idx++];
if (err) {
// 如果有错误 , 找错误处理中间件
if(!layer.route){ // 中间件
if(layer.handler.length === 4){
layer.handler(err,req,res,next)
}else{
next(err);
}
}else{ // 路由
next(err);
}
} else {
// 无论是路由还是中间件 前提是路径必须匹配
if (layer.match(pathname)) { // match还没有更改
if (!layer.route) { // 没有说明是中间件
// 正常中间件不走错误
if(layer.handler.length !== 4){
layer.handle_request(req, res, next); // 直接执行中间件函数
}else{
next();
}
} else {
// 路由必须匹配方法
if (layer.route.methods[method]) { // 这个next可以让路由层扫描下一个layer
layer.handle_request(req, res, next); // route.dispatch
} else {
next();
}
}
} else {
next();
}
}
}
next(); // 请求来了取出第一个执行
}
路由参数
先看应用
应用规则:对于路由传参,通常有两种写法
- 请求时以
?key=val
的形式,获取时req.query
- 定义时以
/:key1/:key2
的形式,请求时以/val1/val2
的形式,获取时req.params
app.get('/wzy1/:id/:name',function (req,res){
console.log(req.params);
res.end(JSON.stringify(req.params))
})
app.get('/wzy2',function (req,res){
console.log(req.query);
res.end(JSON.stringify(req.query))
})
实现思路
第一种,其实只要进行下以?
截取,然后进行split
,组装成为对象赋值在req.query
属性上即可
重点实现第二种,关键是1. 如何获取请求路径上的参数 2. 如何根据这种规定进行路由的匹配
很自然的,我们可以想到用正则,正则中的分组模式可以让我们获取到符合条件情况下的某部分的值
具体实现
首先看个例子
let configUrl = '/wzy/:id/:name';
let keys = [];
configUrl = configUrl.replace(/:([^\/]+)/g,function (){
keys.push(arguments[1]);
return '([^\/]+)'
})
let reg = new RegExp(configUrl);
let requestUrl = '/wzy/1/2';
let [,...args] = requestUrl.match(reg);
let params = {};
keys.forEach((key ,i) => {
params[key] = args[i]
})
console.log(params);// {id: 1, name: 2}
其实这种功能是有第三方包的,path-to-regexp
,改写后如下
const {pathToRegexp} = require('path-to-regexp')
console.log(pathToRegexp);
let configUrl = '/wzy/:id/:name';
let keys = [];
regExp = pathToRegexp(configUrl,keys);
console.log(regExp, keys);
核心功能实现了,那我们开始接入express
中,主要是改写路由匹配逻辑
接入正则
Layer.js
const pathToRegExp = require('path-to-regexp')
function Layer(path, handler) {
this.path = path;
this.regExp = pathToRegExp(this.path, this.keys = []);
console.log(this.regExp, this.keys)
this.handler = handler;
}
改写macth
Layer.js
Layer.prototype.match = function(pathname) {
// pathname = /user /user
if (this.path == pathname) {
return true;
}
// 中间件只有开头就可以
let r = pathname.match(this.regExp);
if(r){
let [, ...matches] = r; // 正则匹配的结果 1个是匹配到的整个结果 2第一个分组 3 第二个分组
this.params = {};
this.keys.forEach((item, index) => {
this.params[item.name] = matches[index]
});
return true;
}
if (!this.route) {
if (this.path == '/') { // / 可以匹配任何路径
return true;
}
// /user/add
return pathname.startsWith(this.path + '/')
}
// todo ...
return false;
}
子路由
先看应用
应用规则:
- 创建子路由系统从而进行解耦合,项目更加模块化
const express = require('express');
const app = express();
const user = express.Router();
user.get('/add',function (req,res){
res.end('user add')
})
user.get('/remove',function (req,res){
res.end('user remove')
})
const article = express.Router();
article.get('/add',function (req,res){
res.end('article add')
})
article.get('/remove',function (req,res){
res.end('article remove')
})
app.use('/user', user);
app.use('/article', article);
app.listen(3000)
实现思路
首先,得明白它是一个中间件,这也就意味着express.Router
返回的是一个函数;且它是路由,意味着Router
函数不仅可以被new
还得支持直接执行时返回一个路由系统。其二,子路由需要拼接父路由在进行匹配
express.Router
返回的是一个函数,同时被new时又得返回一个对象
那我们就可以根据【函数被new时如果存在返回值且是一个引用类型的话,则返回此引用类型对象】的特点,首先定义Router
的返回值是一个中间件函数;然后定义一个对象,将所有之前的属性和方法放在这个对象身上,将这个对象放置在中间件函数的原型链上;这样就实现了new
和直接执行返回值都符合的情况。
实现父子路径拼接
在请求到来时,会符合中间件的匹配逻辑,这时我们直接执行子路由的handle
从而让请求进入子路由的处理中;
注意一点,子路由中定义的是不包含前缀的路径,所以需要记录下中间件的path
,从而先截取,再匹配;
具体实现
express.Router
返回的是一个函数,同时被new时又得返回一个对象
const url = require('url');
const Layer = require('./layer');
const Route = require('./route');
const methods = require('methods');
function Router() { // 能充当构造函数 也可以充当类 , 无论是new 还是call 都返回一个函数
router.stack = [];
router.methods = {};
let router = (req,res,next) => {
router.handle(req,res,next)
}
router.removed = ""
router.__proto__ = proto;
return router
}
let proto = {};
// 外层的layer 考虑路径 里层的layer考虑方法 = 同一个类
proto.prototype.route = function(path) {
let route = new Route();
let layer = new Layer(path, route.dispatch.bind(route)); // 每次调用get方法, 都会产生一个layer实例和一个route实例
// 这个关联目的是可以在layer获取route的信息
layer.route = route; // 路由中的layer 都有一个route属性 和 我们的route关联起来
this.stack.push(layer)
return route;
}
proto.prototype.use = function(path, ...handlers) {
if (!handlers[0]) { // 只传递了一个函数
handlers.push(path); // app.use(function(){}) app.use()
path = '/'
}
handlers.forEach(handler => {
let layer = new Layer(path, handler);
layer.route = undefined; // 不写也是undefined , 主要告诉你 中间件没有route
this.stack.push(layer);
})
}
// app.get
methods.forEach(method => {
// app.get => handlers rouer.get
proto.prototype[method] = function(path, handlers) { // handlers 是用户定义get时传递过来的所有执行函数 (数组)
if(!Array.isArray(handlers)){
handlers = Array.from(arguments).slice(1);
}
let route = this.route(path); // 创建一个route实例
// 创建一个layer 还要创建一个route,将handlers 传递给route
route[method](handlers);
}
})
proto.prototype.handle = function(req, res, done) {
let { pathname } = url.parse(req.url);
let method = req.method.toLowerCase()
let idx = 0;
let removed = '';
const next = (err) => { // 中间件 和内部的next方法 出错都会走这个next
if (idx >= this.stack.length) return done(); // 路由处理不了 传递给应用层
let layer = this.stack[idx++];
if(removed.length){
req.url = removed + req.url;
removed = '';
}
if (err) {
// 如果有错误 , 找错误处理中间件
if(!layer.route){ // 中间件
if(layer.handler.length === 4){
layer.handler(err,req,res,next)
}else{
next(err);
}
}else{ // 路由
next(err);
}
} else {
// 无论是路由还是中间件 前提是路径必须匹配
if (layer.match(pathname)) { // match还没有更改
req.params = layer.params
if (!layer.route) { // 没有说明是中间件
// 正常中间件不走错误
if(layer.handler.length !== 4){
// 进入到中间件的时候 需要将中间件的路径移除掉
//add
if(layer.path !== '/'){
removed = layer.path; // 要删除的部分 中间件要是/ 就不要删除了
req.url = req.url.slice(layer.path.length) ;
}
layer.handle_request(req, res, next); // 直接执行中间件函数
}else{
next();
}
} else {
// 路由必须匹配方法
if (layer.route.methods[method]) { // 这个next可以让路由层扫描下一个layer
layer.handle_request(req, res, next); // route.dispatch
} else {
next();
}
}
} else {
next();
}
}
}
next(); // 请求来了取出第一个执行
}
module.exports = Router;