express
官方expressjs.com/ 的描述为
Fast, unopinionated, minimalist web framework for Node.js
翻译过来就是基于 Node.js 平台,快速、开放、极简的 Web 开发框架
如果使用原生的http模块,则像这样
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.end("home");
}
// handleUserGet()
// handleUserPost()
if (req.url === '/user' && req.method === 'GET') {
res.end("get user");
}
if (req.url === '/user' && req.method === 'POST') {
res.end("post user");
}
});
server.listen(3000, () => {
console.log('server start 3000');
});
可以看到我们需要针对不同的请求路径判断,很繁琐且不易于维护。
而express提供了路由系统,使用起来结构清晰易读,更易于维护
const express = require('express');
const app = express();
app.get('/', (req, res) => res.end('home'));
app.get('/user', (req, res) => res.end('get user'));
app.post('/user', (req, res) => res.end('post user'));
app.listen(3000, () => {
console.log('server start 3000');
});
而且express还扩展了req和res对象,添加了一些实用的方法,比如req.path、res.json和res.send等,这些烦方法在原生的http模块中并没有,并且express还有中间件机制,更加方便我们做请求处理。下面我们来手动实现一个简易版的express,以此来了解express的大致原理。
express基本实现
创建目录结构
index.js是入口文件
module.exports = require('./lib/express');
从express的使用可以看出,当调用get、post方法时可以将path、method和对应的handler存起来,当请求到来时再根据路径和请求方法取出对应的handler执行即可
const http = require('http');
const url = require('url');
const routes = [];
function createApplication() {
return {
get(path, handler) {
routes.push({
path,
handler,
method: 'get',
});
},
listen() {
function done(req, res) {
res.end(`Cannot ${req.method} ${req.url}`);
}
const server = http.createServer((req, res) => {
const requestMethod = req.method.toLowerCase();
const { pathname: requestUrl } = url.parse(req.url);
for (let i = 0; i < routes.length; i++) {
const route = routes[i];
if (route.path === requestUrl && route.method === requestMethod) {
return route.handler(req, res);
}
}
done(req, res);
});
server.listen(...arguments);
},
};
}
// express = createApplication
module.exports = createApplication;
创建应用和应用本身没有关系,不应该将两者耦合在一起,我们可以将创建应用和应用本身分离
lib/express.js
const Application = require('./application');
// 每次调用函数都产生一个应用的实例 工厂模式
function createApplication() {
return new Application
}
module.exports = createApplication
lib/application.js
const http = require('http');
const routes = [];
function Application() {}
// 为什么不使用class, 因为express诞生非常早,当时还没有es6
Application.prototype.get = function (path, handler) {
routes.push({
path,
handler,
method: 'get',
});
};
Application.prototype.listen = function () {
function done(req, res) {
res.end(`Cannot ${req.method} ${req.url}`);
}
const server = http.createServer((req, res) => {
function done(req, res) {
res.end(`Cannot ${req.method} ${req.url}`);
}
const server = http.createServer((req, res) => {
const requestMethod = req.method.toLowerCase();
const { pathname: requestUrl } = url.parse(req.url);
for (let i = 0; i < routes.length; i++) {
let route = routes[i];
if (route.path === requestUrl && route.method === requestMethod) {
return route.handler(req, res);
}
}
done(req, res);
});
server.listen(...arguments);
});
server.listen(...arguments);
};
module.exports = Application;
我们已经将创建应用方法和应用本身做了分离,但是还存在一个问题,那就是所有的应用都使用一个routes变量来路由信息,这样如果有多个application时会发生混乱, 比如
const app = express();
app.get('/app', (req, res) => res.end('app'));
app.listen(3000)
const app1 = express();
app1.get('/app1',(req, res) => res.end('app1');
当我访问domain:3000/app1时,可以看到返回了app1,但是理论上应该拿不到app1,这就是两个applicaion中的路由相互干扰了,所以我们需要为每个applicaion单独创建一个路由,而且怎么做路径匹配的职责也都交由路由去处理,applicaion并不关心,做到应用和路由隔离
lib/applicaion.js
const http = require('http');
const Router = require('./router');
function Application() {
this.router = new Router();
}
Application.prototype.get = function (path, handler) {
this.router.get(path, handler);
};
Application.prototype.listen = function () {
function done(req, res) {
res.end(`Cannot ${req.method} ${req.url}`);
}
const server = http.createServer((req, res) => {
this.router.handle(req, res, done);
});
server.listen(...arguments);
};
module.exports = Application;
lib/router/index.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) {
const requestMethod = req.method.toLowerCase();
const { pathname: requestUrl } = url.parse(req.url);
for (let i = 0; i < this.stack.length; i++) {
const route = this.stack[i];
if (route.path === requestUrl && route.method === requestMethod) {
return route.handler(req, res);
}
}
out(req, res);
};
module.exports = Router;
现在我们已经实现了express的核心功能了,但是现在还比较弱。
express 路由系统
在原生express中,一个路径可以对应多个handler,比如
// next表示是否向下执行
app.get(
'/',
function (req, res, next) {
console.log('111');
next();
},
function (req, res, next) {
console.log('222');
// 如果注释掉这个next,则将不会往下执行了
next();
},
function (req, res, next) {
console.log('333');
next();
}
);
app.get('/', function (req, res) {
res.end('ok');
});
此时的执行顺序应该为 111 -> 222 -> 333 -> ok
如果注释掉222中的next(), 执行顺序为 111 -> 222
按照我们之前实现的方式,无法满足这样的需求
先来分析一下应该怎么实现
在注册路由的时候,一个路径可以对应一个handler方法,将用户的回调再单独存放在后边,当路径匹配到的时候,调用这个handler方法去一次执行用户的回调fn,执行完之后再出来接着走下一个处理方法。
可以再进行细化
router中的路径加handler可以封装成一个个layer,一个路径对应的用户的所有真实回调可以封装成一个route,将一个真实的用户回调fn又封装成一个layer存入route的stack中。需要注意的是外层的layer路径是有用的,里层的layer路径是没有的。外层的layer中的handler就是route中的dispatch,这样就可以将外层的layer和route联系起来了,当调用route.dispatch时,就依次执行route.stack中的用户回调fn。
注意图中标明请求方法存在里层的layer中而不存在外层的layer,这是为什么呢?因为express还有一种古老的写法
const app = express();
// 一个路径对应多个不同请求方法的fn回调
app
.route('/')
.get(function (req, res) {})
.post(function () {});
只有存在里层才能支持这种写法,但是不推荐使用这种写法,写法繁琐且不好维护。
说完了原理,下面来代码实现
lib/router/layer.js
function Layer(path, handler) {
this.path = path;
this.handler = handler;
}
// 匹配路径的方法
Layer.prototype.match = function (pathname) {
return this.path === pathname;
};
Layer.prototype.handleRequest = function (req, res, next) {
// 其实这里对应的就是route.dispatch
this.handler(req, res, next);
};
module.exports = Layer;
lib/router/route.js
// methods 这个包包含了所有的http请求方法
const methods = require('methods');
const Layer = require('./layer');
function Route() {
this.stack = [];
this.methods = {};
}
methods.concat('all').forEach((method) => {
Route.prototype[method] = function (handlers) {
// 增加一个是否有该请求方法的标识,如果route中所有的fn都没有对应的请求方法,则直接跳过,优化性能
this.methods[method] = true;
handlers.forEach((handler) => {
const layer = new Layer('用不到', handler);
layer.method = method;
this.stack.push(layer);
});
};
});
Route.prototype.dispatch = function (req, res, out) {
// 稍后调用dispatch方法会去stack中迭代用户的回调来执行
let idx = 0;
const next = () => {
if (idx >= this.stack.length) return out();
const layer = this.stack[idx++];
if (layer.method === req.method.toLowerCase() || layer.method === 'all') {
layer.handleRequest(req, res, next); // 用户的回调
} else {
next();
}
};
next();
};
module.exports = Route;
lib/applicaion.js
const http = require('http');
const methods = require('methods')
const Router = require('./router');
function Application() {
this.router = new Router();
}
methods.concat('all').forEach((method) => {
Application.prototype[method] = function (path, ...handlers) {
this.router[method](path, handlers);
};
});
Application.prototype.listen = function () {
function done(req, res) {
res.end(`Cannot ${req.method} ${req.url}`);
}
const server = http.createServer((req, res) => {
this.router.handle(req, res, done);
});
server.listen(...arguments);
};
module.exports = Application;
lib/router/index.js
const url = require('url');
const methods = require('methods');
const Layer = require('./layer');
const Route = require('./route');
function Router() {
this.stack = [];
}
Router.prototype.route = function (path) {
const route = new Route(); // 创建route和layer产生关联
const layer = new Layer(path, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer); // 并且将layer放到路由系统中
return route;
};
methods.concat('all').forEach((method) => {
Router.prototype[method] = function (path, handlers) {
// 需要先产生route才能创建layer
const route = this.route(path); // 创建一个route
route[method](handlers); // 将用户的回调传递给了route中
};
});
Router.prototype.handle = function (req, res, out) {
const { pathname: requestUrl } = url.parse(req.url);
let idx = 0;
const next = () => {
if (idx >= this.stack.length) return out(req, res);
const layer = this.stack[idx++];
// 路径匹配,且匹配到的layer.route中有对应http请求方法的fn
if (
layer.match(requestUrl) &&
layer.route.methods[req.method.toLowerCase()]
) {
// 路径匹配到了交给route来处理,如果route处理完后,可以调用next,从上一个layer到下一个layer
layer.handleRequest(req, res, next); // 这里调用的其实是route.dispatch
} else {
next();
}
};
next();
};
module.exports = Router;
express普通中间件
有时候在调接口前我们通常需要做一些前置的逻辑,比如用户权限校验或扩展属性和方法等等,按照上一节我们知道可以在真实的回调执行前传入这些自定义逻辑
app.get('/user', '自定义逻辑fn',(req, res) => res.end('user'));
但是这样写这些自定义逻辑只能对一个路径中的一个方法生效,如果同时有多个路径需要这些自定义逻辑,那么则需要写多次, 非常繁琐和冗余
app.get('/user', '自定义逻辑fn',(req, res) => res.end('user'));
app.get('/manager', '自定义逻辑fn',(req, res) => res.end('manager'));
express也考虑到这一点,所以实现了中间件机制
// 中间如果不写路径默认就是匹配所有 默认就是/
// 中间件一定要写在想要拦截的路径前面
// 中间件没有方法,只匹配路径
app.use('/', function (req, res, next) {
console.log('middleware');
next();
})
app.get('/user', function (req, res,next) {
next()
res.end('user')
})
app.get('/manager', function (req, res) {
res.end('manager')
})
中间件的执行顺序
app.use(
'/',
(req, res, next) => {
// 不需要识别是get 还是post
console.log(1);
next();
console.log(2);
},
(req, res, next) => {
console.log(7);
next();
console.log(8);
}
);
app.use('/', (req, res, next) => {
console.log(3);
next();
console.log(4);
});
app.use('/', (req, res, next) => {
console.log(5);
next();
console.log(6);
});
此时的执行顺序为 1735642,next()相当于将下一个回调函数替换过来执行,这种模型也被称为洋葱模型。
中间件和路由都存在router的stack中
lib/application.js
+Application.prototype.use = function () {
+ this.router.use(...arguments); // 让路由系统来注册中间件
+};
lib/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);
+ layer.route = undefined; // 中间不存在route属性,可以根据这个属性来判断是不是路由
+ this.stack.push(layer);
+ });
+};
Router.prototype.handle = function (req, res, out) {
const { pathname: requestUrl } = url.parse(req.url);
let idx = 0;
const next = () => {
if (idx >= this.stack.length) return out(req, res);
const layer = this.stack[idx++];
- // 路径匹配,且匹配到的layer.route中有对应http请求方法的fn
- if (
- layer.match(requestUrl) &&
- layer.route.methods[req.method.toLowerCase()]
- ) {
- // 路径匹配到了交给route来处理,如果route处理完后,可以调用next,从上一个layer到下一个layer
- layer.handleRequest(req, res, next); // 这里调用的其实是route.dispatch
- } else {
- next();
- }
+ // 路由的逻辑要匹配方法和路径,但是中间件要求路径匹配就可以
+ if (layer.match(requestUrl)) {
+ // 这里要区分路由还是中间件,匹配规则不一样
+ if (layer.route) {
+ if (layer.route.methods[req.method.toLowerCase()]) {
+ layer.handleRequest(req, res, next);
+ } else {
+ next();
+ }
+ } else {
+ // 中间件
+ layer.handleRequest(req, res, next);
+ }
+ } else { // 如果路径不匹配
+ next();
+ }
};
next();
};
还有一个小问题,现在我们的匹配方法是完全路径完全相等才能匹配成功,但是中间件的匹配模式是开头路径能匹配到才就可以
lib/router/layer.js
Layer.prototype.match = function (pathname) {
// 无论中间件还是路由只要完全一样肯定就匹配到了
if (this.path === pathname) return true;
if (!this.route) {
// 中间件
if (this.path === '/') return true; // /表示匹配所有
return pathname.startsWith(this.path + '/'); // 要求必须以路径开头
}
return;
};
错误处理中间件
现在我们所有路由和中间件都是基于回调的形式,处理错误比较麻烦,目前的实现是需要每个回调函数自己进行错误处理。但在原生的express中,我们可以注册一个错误处理中间件,错误处理中间件必须要在所有中间件和路由的后面。错误处理中间件和普通的中间件的区别是错误处理中间件的回调函数参数为4个,比普通中间件多一个。
app.use('/', (req, res, next) => {
console.log(1);
next();
});
app.use('/', (req, res, next) => {
console.log(3);
// 传入错误信息,
next('got error');
});
app.use('/', (req, res, next) => {
console.log(5);
next();
});
// 路由回调中也可以抛出错误
// app.get('/user', function (req, res, next) {
// next('error');
// // res.end('user')
// });
app.get('/manager', function (req, res) {
res.end('user');
});
// 错误处理中间件也可以继续向下传递错误
app.use('/', function (err, req, res, next) {
next(err);
});
app.use('/', function (err, req, res, next) {
console.log(err);
res.end(error);
});
当路由或者普通中间件中某个回调错误时,我们可以在next中添加一个参数便是此回调出错了,这样就会将错误抛到后面的错误处理中间件,以上代码的执行顺序为 1 -> 3 -> got error
lib/router/index.js
Router.prototype.handle = function (req, res, out) {
const { pathname: requestUrl } = url.parse(req.url);
let idx = 0;
const next = (err) => {
// 里层调用next出错就走到外层的next来
if (idx >= this.stack.length) return out(req, res);
const layer = this.stack[idx++];
if (err) {
// 处理err 一直向下找找到错误处理中间件
if (!layer.route) {
// 中间件
layer.handleError(err, req, res, next);
} else {
next(err); // 不是中间件就直接跳过
}
} else {
// 路由的逻辑要匹配方法和路径,但是中间件要求路径匹配就可以
if (layer.match(requestUrl)) {
// 这里要区分路由还是中间件,匹配规则不一样
if (layer.route) {
if (layer.route.methods[req.method.toLowerCase()]) {
layer.handleRequest(req, res, next);
} else {
next();
}
} else {
// 没有错误的情况下遇到错误处理中间件直接跳过
if (layer.handler.length === 4) return next();
layer.handleRequest(req, res, next);
}
} else {
// 如果路径不匹配直接跳过
next();
}
}
};
next();
};
lib/router/route.js
Route.prototype.dispatch = function (req, res, out) {
// 稍后调用dispatch方法会去stack中迭代用户的回调来执行
let idx = 0;
const next = (err) => {
if (err) return out(err);
if (idx >= this.stack.length) return out();
let layer = this.stack[idx++];
if (layer.method === req.method.toLowerCase() || layer.method === 'all') {
layer.handleRequest(req, res, next); // 用户的回调
} else {
next();
}
};
next();
};
lib/router/layer.js
// 错误处理
Layer.prototype.handleError = function (err, req, res, next) {
if (this.handler.length === 4) {
// 检查一下 参数格式是不是4个
return this.handler(err, req, res, next); // 是四个就执行
}
// 不是继续将错误向下传递
next(err);
};
带参数的路由
在express中,我们可以声明带参数的路由,比如
app.get('/name/:id/:age',function(req, res, next){
console.log(req.params);
sres.end('end');
})
express会将匹配的参数添加到req.params对象中, 然后再回调中就可以直接使用。如果我的路径为/name/1/20, 则req.params就是{'id': 1, 'age': 20}.
我们可以将参数路由转成一个正则表达式,然后使用实际的路由去匹配这个正则,就能获取到params了,比如
const path = '/name/:id/:age';
const keys = [];
const regStr = path.replace(/:([^/]+)/g, function () {
keys.push(arguments[1]);
return '([^/]+)';
});
const reg = new RegExp(regStr);
const matched = '/name/1/20'.match(reg);
const params = keys.reduce((memo, k, i) => {
memo[k] = matched[i + 1];
return memo
}, {});
console.log(params);
lib/router/layer.js
// path-to-regexp是一个将参数路径转为正则表达式的库
const pathToRegExp = require('path-to-regexp');
function Layer(path, handler) {
this.path = path;
this.handler = handler;
this.regExp = pathToRegExp(this.path, (this.keys = []), true);
}
Layer.prototype.match = function (pathname) {
if (this.path === pathname) return true;
if (this.route) {
const matches = pathname.match(this.regExp);
if (matches) {
// 正则路由匹配
const values = matches.slice(1);
// 匹配到的参数都在this.keys中
this.params = values.reduce((memo, current, index) => {
memo[this.keys[index].name] = values[index];
return memo;
}, {});
return true;
}
}
if (!this.route) {
// 中间件
if (pathname === '/') return true;
return pathname.startsWith(this.path + '/');
}
return false;
};
lib/router/index.js
Router.prototype.handle = function (req, res, out) {
const { pathname: requestUrl } = url.parse(req.url);
let idx = 0;
const next = (err) => {
// 里层调用next出错就走到外层的next来
if (idx >= this.stack.length) return out(req, res);
const layer = this.stack[idx++];
if (err) {
// 处理err 一直向下找找到错误处理中间件
if (!layer.route) {
// 中间件
layer.handleError(err, req, res, next);
} else {
next(err); // 不是中间件就直接跳过
}
} else {
// 路由的逻辑要匹配方法和路径,但是中间件要求路径匹配就可以
if (layer.match(requestUrl)) {
+ req.params = layer.params;
// 这里要区分路由还是中间件,匹配规则不一样
if (layer.route) {
if (layer.route.methods[req.method.toLowerCase()]) {
layer.handleRequest(req, res, next);
} else {
next();
}
} else {
// 没有错误的情况下遇到错误处理中间件直接跳过
if (layer.handler.length === 4) return next();
layer.handleRequest(req, res, next);
}
} else {
// 如果路径不匹配直接跳过
next();
}
}
};
next();
};
二级路由
在使用express时,我们最常使用到的就是二级路由。在应用中,通常会有很多模块,每个模块由不同的团队维护,每个模块又存在很多路由,此时就需要二级路由了
const express = require('express');
const app = express();
const user = require('./routes/user');
const manager = require('./routes/manager');
// user应该是一个中间件, 即 user = express.Router() = function(req,res,next){}
app.use('/user',user)
app.use('/manager',manager)
app.listen(3000, function () {
console.log('server start 3000')
})
// user routes
// 其实这里等价于 const router = new Router();
const router = express.Router();
router.get('/add',function(req,res){
res.end('user add')
})
router.get('/remove',function(req,res,next){
res.end('user remove');
})
module.exports = router;
// manager routes
const router = express.Router();
router.get('/add', function (req, res) {
res.end('manager add');
});
router.get('/remove', function (req, res) {
res.end('manager remove');
});
module.exports = router;
当访问 user/add时会进入user中的add路由,当访问 manager/add时会进入manager中的add路由
lib/express.js
const Application = require('./application');
function createApplication() {
return new Application();
}
+ createApplication.Router = require('./router');
module.exports = createApplication;
因为之前时间的Router是一个构造函数,我们都是new Router进行使用,现在同时要支持express.Router()且返回的是一个中间件形式的使用
lib/router/index.js
function Router() {
- this.stack = [];
+ // 此router 既能在初始化的时候 new Router 也能 express.Router
+ const router = (req, res, next) => {
+ // 如果函数在new的时候 返回了一个引用类型,那么this会变成这个引用类型
+ // 请求来的时候会执行此回调方法, 应该去路由系统中拿出来一个个执行
+ router.handle(req, res, next);
+ };
+ // 设置原型对象为proto,不然执行express.Router()后,原型对象丢失
+ Object.setPrototypeOf(router, proto);
+ router.stack = []; // 一个函数既能new 又能执行
+ return router;
}
+ const proto = {};
- Router.prototype.route = function (path) {
+ Router.prototype.route = function (path) {
- Router.prototype.use = function (path, ...handlers) {
+ proto.use = function (path, ...handlers) {
- Router.prototype.handle = function (req, res, out) {
+ proto.handle = function (req, res, out) {
lib/router/route.js
methods.concat('all').forEach((method) => {
Route.prototype[method] = function (handlers) {
this.methods[method] = true;
+ // 当使用二级路由后, handlers不是数组
+ // const router = express.Router();
+ // router.get('/add', function (req, res) {
+ // res.end('manager add');
+ // });
+ if (!Array.isArray(handlers)) handlers = [...arguments]; // 不是数组就转化成数组
handlers.forEach((handler) => {
let layer = new Layer('用不到', handler);
layer.method = method;
this.stack.push(layer);
});
};
});
ps: 其实只要将
express.Router()的使用改成new Router()就没这么多事了🐶
这样二级路由就实现好了,但是现在有一个小问题还可以进行优化。在二级路由中的路径必须要加上一级路由的路径才能正常匹配,相当于写了两次/user
const app = express();
app.use('/user', user)
// user router
const router = express.Router();
router.get('/user/add',function(req,res){
res.end('user add')
})
但是在原生的express中二级路由可以不写一级路由的路径
// user router
const router = express.Router();
router.get('/add',function(req,res){
res.end('user add')
})
其实思路也很简单,进入二级路由匹配前我们可以将一级路由的路径减去,二级路由匹配完成出来的时候再加上之前删掉的一级路径就可以了。
比如/user/add, 进入二级路有前可以先将/user去掉,拿/add去二级路由中匹配就能匹配上了,出来的时候再加上/user, 继续匹配剩下的一级路由。
lib/router/index.js
proto.handle = function (req, res, out) {
// let { pathname: requestUrl } = url.parse(req.url)
let idx = 0;
+ let removed = '';
let next = (err) => {
// 里层调用next出错就走到外层的next来
if (idx >= this.stack.length) return out(err);
const layer = this.stack[idx++];
+ if (removed) {
+ // 如果从上一个layer到下一个layer我就需要将删掉的在补回来
+ req.url = removed + req.url;
+ removed = '';
}
if (err) {
// 处理err 一直向下找找到错误处理中间件
if (!layer.route) {
// 中间件
layer.handleError(err, req, res, next);
} else {
next(err); // 不是中间件就直接跳过
}
} else {
// 路由的逻辑要匹配方法和路径, 但是中间件要求路径匹配就可以
if (layer.match(req.url)) {
// 这里要区分路由还是中间件,匹配规则不一样
req.params = layer.params;
if (layer.route) {
if (layer.route.methods[req.method.toLowerCase()]) {
layer.handleRequest(req, res, next);
} else {
next();
}
} else {
// 中间件
if (layer.handler.length === 4) return next();
+ removed = layer.path === '/' ? '' : layer.path; // 记录删除的部分
+ req.url = req.url.slice(removed.length); // 删除需要删掉的部分
layer.handleRequest(req, res, next);
}
} else {
// 如果路径不匹配直接跳过
next();
}
}
};
next();
};