express 和 koa 是同班人马打造,属于上一代的产物,然而如今还是有着大量使用,它和 koa 是有一些不同的哦。
express 对比 koa
- express 处理请求的时候全部采用的回调函数的方式,而 koa 采用的是 async + await
- express 内部采用的是 ES5 语法,koa 采用的是 es6 来编写的
- express 比 koa 丰富,也更重,多了一些内置的中间件(路由,静态服务,模板渲染等)
- koa 中为了扩展采用的方式是在 ctx 扩展了 request,response 对象,而 express 直接在原生的 req 和 res 的基础上进行了扩展
- express 中的特点是内部采用了回调的方式来组合代码,koa 支持转 promsie 串联
express 基础用法
const express = require('express');
// exprress 创建应用是通过 express() 来创建的
const app = express();
// 可以看出来 express 集成了路由功能
app.get('/', function(req, res) {
res.end('home');
});
app.get('/hello', function(req, res) {
res.end('hello');
});
app.all('*', function(req, res) {
res.end('all');
});
app.listen(3000, function() {
console.log('server start 3000');
});
实现基础版 express
- myExpress
- lib
- express.js
- index.js
index.js,入口文件,暴露 express 模块。
module.exports = require('./lib/express');
lib/express.js
- 根据用户调用的 app.get 方法(all 暂未处理)收集路由表
- 根据 listen 传递的参数,创建服务,路由查询,cb 调用。
myExpress/index.js,入口文件,暴露 express 模块。
lib/express.js
const http = require('http');
const url = require('url');
let routers = [{
method: 'all',
path: '*',
handler(req, res) {
res.end(`Cannot ${ req.method } ${ req.url }`);
}
}];
function createApplication() {
return {
get(path, handler) {
routers.push({
method: 'get',
path,
handler
});
},
listen(...args) {
// listen 创建服务
const server = http.createServer(function(req, res) {
// 最后一个参数 true 会把我们的 query 转成一个对象
let { pathname, query } = url.parse(req.url, true);
let requestMethod = req.method.toLowerCase();
// 在路由表中匹配 i 从 1 开始,过滤到顶部默认的 all 方法
for (let i = 1; i < routers.length; i++) {
let { method, path, handler } = routers[i];
// 匹配请求路径和请求方法,执行对应回调
console.log(pathname, method, routers);
if (pathname == path && method == requestMethod) {
return handler(req, res);
}
}
// 如果没有匹配到,则返回兜底 all 的处理方式,默认是抛错
return routers[0].handler(req, res);
});
server.listen(...args);
},
all() {
// 暂未处理 app.all 方法
}
}
}
module.exports = createApplication;
这样,就完成了基础的带有 get 路由的 express
演化一:工厂模式创建 express 应用
我们发现,createApplication 函数很冗余,我们把创建应用这一步提成一个工厂类。
lib/express
const Application = require('./application');
function createApplication() {
return new Application();
}
module.exports = createApplication;
lib/application.js
const http = require('http');
const url = require('url');
function Application() {
this._routers = [{
method: 'all',
path: '*',
handler(req, res) {
res.end(`Cannot ${ req.method } ${ req.url }`)
}
}];
}
Application.prototype.get = function (path, handler) {
this._routers.push({
path,
method: 'get',
handler
})
}
Application.prototype.listen = function (...args) {
// listen 创建服务
const server = http.createServer((req, res) => {
// 最后一个参数 true 会把我们的 query 转成一个对象
let { pathname, query } = url.parse(req.url, true);
let requestMethod = req.method.toLowerCase();
// 在路由表中匹配 i 从 1 开始,过滤到顶部默认的 all 方法
console.log(this._routers);
for (let i = 1; i < this._routers.length; i++) {
let { method, path, handler } = this._routers[i];
// 匹配请求路径和请求方法,执行对应回调
console.log(pathname, method, this._routers);
if (pathname == path && method == requestMethod) {
return handler(req, res);
}
}
// 如果没有匹配到,则返回兜底 all 的处理方式,默认是抛错
return this._routers[0].handler(req, res);
});
server.listen(...args);
}
module.exports = Application
演化二: 应用 & 路由分离
我们发现,虽然把应用代码抽离成一个类,但是路由的代码和应用的代码耦合在一起,这样也是不好的,我们继续拆。
- myExpress
- lib
- router
- index.js // 路由系统
- application.js
- express.js
- index.js
lib/application.js
const http = require('http');
const url = require('url');
const Router = require('./router');
// 每个应用默认创建一个路由系统
function Application() {
this.router = new Router();
}
// app.get,调用路由系统的 get 方法收集路由
Application.prototype.get = function (path, handler) {
this.router.get(path, handler);
}
// app.listen, 服务启动,路由系统暴露一个方法 handle,处理当前路由
Application.prototype.listen = function (...args) {
const server = http.createServer((req, res) => {
function done() {
res.end(`Cannot ${req.method} ${req.url}`)
}
// 交给路由系统处理 路由系统处理不了调用 done 方法
this.router.handle(req, res, done);
});
server.listen(...args);
}
module.exports = Application
lib/router/index.js
const url = require('url');
function Router() {
this.stack = [];
}
// 向路由的 stack 添加
Router.prototype.get = function (path, handler) {
this.stack.push({
path,
method: 'get',
handler
})
}
// 请求到来时,会匹配对应的路由
Router.prototype.handle = function (req, res, done) {
let { pathname, query } = url.parse(req.url, true);
let requestMethod = req.method.toLowerCase();
for (let i = 0; i < this.stack.length; i++) {
let { path, method, handler } = this.stack[i];
if (pathname === path && method === requestMethod) {
return handler(req, res);
}
}
done()
}
module.exports = Router
兼容路由中间件和同路径不同方法的路由串联写法
实际上我们开发的过程中,匹配到路由之后,执行回调之前,我们通常会调用一些中间件,比如鉴权中间件,使用方式如下
// 路由的中间件功能
const express = require('express');
const app = express();
function checkAuth(req, res, next) {
if (req.query.auth == '1') {
next();
} else {
res.end('no auth');
}
}
function logger(req, res, next) {
console.log('logger run');
next();
}
// 写法 1
app.get('/', logger, checkAuth, function(req, res, next) {
res.end('ok')
})
// 写法 2
app.get('/', [logger, checkAuth], function(req, res, next) {
res.end('ok')
})
app.listen(3000);
中间件的执行顺序
const express = require('express');
const app = express();
app.get('/', function(req, res, next) {
console.log(1);
next();
console.log(2)
}, function(req, res, next) {
console.log(3);
next();
console.log(4)
}, function(req, res, next) {
console.log(5);
next();
console.log(6)
})
app.get('/', function(req, res, next) {
console.log(7);
next();
})
app.listen(3000);
// 1 3 5 7 6 4 2
执行顺序跟 koa 是没区别的,但是 next 调用没有等待效果。
更恶心的是,express 提供了下面这种不推荐的写法,不过也可以执行(暂未实现)。
// app.route('/').post(function(req,res){
// res.end('post')
// }).get(function(req,res){
// res.end('get')
// })
ok,以上就是 express 中路由中间件和串联路由声明的写法,我们怎么设计路由,能兼容这么多种调用方式呢。
如下图所示:
1. 用户注册路由(比如 app.get)创建一个 layer 实例和一个 route 实例,并且 layer.route = route,layer 上保存有当前注册的路径和一个回调(route.dispatch,该回调会检查自身 stack,找到符合条件的 cb 依次执行),然后把当前 layer 放到 router 的栈内用作路径匹配。 2. 根据当前路由注册的请求方法,创建一个个 layer 实例,其上保存了当前的请求方式和回调函数(这个回调函数是用户本身的回调函数),然后存到 route 的栈中用作当请求触发,进行方法匹配,依次执行回调。 3. 优化一:route 中保存着针对当前路径,收集到的路由包含哪些注册的方法(get,post 等),如果没有当前请求对应的方法,直接下跳。 4. 优化二:express() 时不再直接创建路由,进行路由的懒初始化,可能有些人只创建应用而不想初始化路由。
实际请求到来时,先根据 path 匹配 router 中存储的 layer,然后调用 layer.hanlder(其实也就是它对应的 ruote.dispatch) 去依次遍历 route 中存储的已注册的路由回调,找到对应的请求方法的回调依次执行。
- myExpress
- lib
- router
- index.js // 路由系统
- layer.js // 存储路径 -> route.diapatch 方法的映射
- route.js // 存储请求 method、回调任务,并提供 diapatch 派发执行
- application.js
- express.js
- index.js
这个逻辑比较绕一点,好好理解一下哦,最好跟 手撕 koa2 对比学习。
lib/router/index.js
const url = require('url');
const Layer = require('./layer');
const Route = require('./route');
const methods = require('methods');
function Router() {
this.stack = [];
}
Router.prototype.route = function (path) {
let route = new Route();
let layer = new Layer(path, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
}
methods.forEach(method => {
Router.prototype[method] = function (path, handlers) { // 像路由的stack中添加
let route = this.route(path); // 创建route,并返回route
route[method](handlers)
}
})
Router.prototype.handle = function (req, res, done) {
// 要在路由的栈中查找 ,找不到就找下一个,找到了将下一个的执行权限传递进去
const { pathname } = url.parse(req.url);
const method = req.method.toLowerCase();
let i = 0
const next = () => {
if (i == this.stack.length) return done(); // 整个栈都筛了一遍没有找到,交给应用来处理
let layer = this.stack[i++];
if (layer.match(pathname)) {
if (layer.route.methods[method]) {
// 这个 next 传进去 route.dispatch 内部,当内部执行完,调用该 next
// 为了形象,在 route.dispatch 中我们把该 next 命名为 out
layer.handle_request(req, res, next); // route.dispatch
} else {
next();
}
} else {
next();
}
}
next();
}
module.exports = Router
lib/router/layer.js
// 存一个路径和 handler 方法的对象
function Layer(path,handler){
this.path = path;
this.handler = handler;
}
Layer.prototype.match = function(pathname){
return this.path == pathname;
}
// 其实就是 route.dispatch
Layer.prototype.handle_request = function(req, res, next){
return this.handler(req,res,next)
}
module.exports = Layer;
lib/router/route.js
const Layer = require('./layer');
const methods = require('methods')
function Route() {
this.stack = [];
// 用来标识route上包含哪些方法
// 比如 '/' 路径如果不包含 post 方法,那么如果 post 方式请求,直接下跳而不再循环 route 中的 layer 去做匹配
this.methods = {};
}
// 可以看到 express 中间件,是递归不断依次执行
Route.prototype.dispatch = function (req, res, out) {
let i = 0;
let next = () => {
if (i === this.stack.length) return out();
let layer = this.stack[i++];
if (layer.method === req.method.toLowerCase()) {
layer.handler(req, res, next); // 用户注册的回调
} else {
next();
}
}
next();
}
methods.forEach(method => {
Route.prototype[method] = function (handlers) {
handlers.forEach(handler => {
// 这里路径其实没有用到,不一定是 /,随便传哦。
let layer = new Layer('/', handler);
layer.method = method;// 给每一层都添加一个方法
this.methods[method] = true;
this.stack.push(layer);
})
}
})
module.exports = Route;
lib/application.js
const http = require('http');
const methods = require('methods'); // 第三方模块,别人提供好的 我安装了express
const Router = require('./router'); // 引入了路由系统
function Application() { // 每个应用默认创建一个路由系统, 有可能人家是用应用,不用路由系统
// this.router = new Router();
}
// 懒加载路由
Application.prototype.lazy_route = function () {
if (!this.router) {
this.router = new Router();
}
}
methods.forEach(method => {
// app.get,app.post 等
Application.prototype[method] = function (path, ...handlers) {
// 懒加载路由
this.lazy_route();
this.router[method](path, handlers); // 像路由系统中添加
}
})
// app.listen()
Application.prototype.listen = function (...args) {
const server = http.createServer((req, res) => {
function done() {
res.end(`Cannot ${req.method} ${req.url}`)
}
this.lazy_route();
this.router.handle(req, res, done); // 交给路由系统来处理,路由系统处理不了会调用 done 方法
});
server.listen(...args)
}
module.exports = Application
express 中间件 + 错误处理
如果我们路由中间件比较多,我们没必要 app.method(path, [fn1, fn2])依次把方法填到路由中间件上,那能不能把中间件单独抽离出来。
const express = require('express');
const app = express();
// 针对所有路由的中间件,可以决定是否向下执行,可以扩展方法和属性
app.use('/', function(req, res, next) {
console.log('all middleware1');
next();
}, function(req, res, next) {
console.log('all middleware2');
next();
});
// 针对 /user 路由开头的中间件
app.use('/user', function(req, res, next) {
console.log('user middleware');
// next 方法传递参数代表错误
next('error');
});
app.get('/user', function(req, res) {
res.end('user');
});
// 前缀匹配到也能执行哦
app.get('/user/info', function(req, res) {
res.end('user info');
});
// 错误处理中间件 一般放在页面最底部去做兜底错误处理
// 当 next 方法传递参数,代表发生错误
app.use((error, req, res, next) => {
});
app.listen(3000, function() {
console.log(`server start 3000`);
});
可以看到,use 方法用来注册中间件,而且 use 可能接收多个处理方法,且中间件执行是在路由匹配之前。
具体思路如下图所示:
- 中间件类似路由,每次创建一个新的 layer 存入 router 中,不传路径则默认 "/",如果一个中间件内部有多个方法,则按回调方法拆分,重新注册成一个个的 layer 塞进 router。
- 中间件不需要 route 实例,中间件内部没有请求方式,只存了路径 + 一个回调函数,只要匹配到当前路由的 layer 且 layer 上没有 route(说明是中间件),直接调用 handler 方法执行,这里 handler 方法就是用户传进来的中间件回调。
- 错误处理中间件写在程序底部,用于错误兜底,中间件中 next('some things') 代表错误传递,会被兜底的错误捕获,且中间件错误传递会跳过所有路由,直达错误处理中间件,注意路由的中间件也能通过 next 传递错误直达错误中间件(它和中间件的 next 不一样哦,一个是竖着传递,一个是横着传递)。
lib/router/index.js
const url = require('url');
const Layer = require('./layer');
const Route = require('./route');
const methods = require('methods');
function Router() {
this.stack = [];
}
Router.prototype.route = function (path) {
let route = new Route();
let layer = new Layer(path, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
}
Router.prototype.use = function (path) {
let args = Array.from(arguments);
let handlers = [];
// 如果第一个参数不传,默认是 '/'
if (typeof path === 'function') {
path = '/';
handlers = [...args]
} else {
handlers = args.slice(1)
}
handlers.forEach(handler => {
let layer = new Layer(path, handler);
layer.route = undefined; // 后面判断 如果layer上有route属性 说明是路由,没有说明是中间件
this.stack.push(layer);
})
}
methods.forEach(method => {
Router.prototype[method] = function (path, handlers) { // 像路由的stack中添加
let route = this.route(path); // 创建route,并返回route
route[method](handlers)
}
})
Router.prototype.handle = function (req, res, done) {
// 要在路由的栈种查找 ,找不到就找下一个,找到了将下一个的执行权限传递进去
const { pathname } = url.parse(req.url);
const method = req.method.toLowerCase();
let i = 0
const next = (err) => {
if (i == this.stack.length) return done(); // 整个栈都筛了一遍没有找到,交给应用来处理
let layer = this.stack[i++];
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)) { // layer种用来管理自己的匹配逻辑
if (!layer.route) { // 中间件不需要匹配方法
if (layer.handler.length === 4) { // 如果正常情况下是不执行错误处理中间件的
next();
} else {
layer.handle_request(req, res, next)
}
} else { // 路由需要匹配方法,在执行
if (layer.route.methods[method]) {
layer.handle_request(req, res, next); // route.dispatch
} else {
next();
}
}
} else {
next();
}
}
}
next();
}
module.exports = Router
lib/router/layer.js
// 存一个路径和 handler 方法的对象
function Layer(path,handler){
this.path = path;
this.handler = handler;
}
Layer.prototype.match = function(pathname) {
// 无论中间件layer 还是路由layer 只要一样肯定匹配到
if(this.path == pathname){
return true;
}
// 如果是中间件 我们开头匹配就可以
if(!this.route){
if(this.path == '/'){ // 中间件路径是/ 表示可以匹配到
return true;
}
// /user/info /user/
// 为了避免 /u 也被匹配,我们加 '/' 匹配
return pathname.startsWith(this.path + '/');
}
return false;
}
// 其实就是 route.dispatch
Layer.prototype.handle_request = function(req, res, next){
return this.handler(req,res,next)
}
module.exports = Layer;
lib/router/route.js
const Layer = require('./layer');
const methods = require('methods')
function Route() {
this.stack = [];
// 用来标识route上包含哪些方法
// 比如 '/' 路径如果不包含 post 方法,那么如果 post 方式请求,直接下跳而不再循环 route 中的 layer 去做匹配
this.methods = {};
}
// 可以看到 express 中间件,是递归不断依次执行
Route.prototype.dispatch = function(req,res,out){
let i = 0;
let next = (err) =>{ // 内部路由抛出错误 我就将错误派发到外层处理
if(err) return out(err);
if(i === this.stack.length) return out();
let layer = this.stack[i++];
if(layer.method === req.method.toLowerCase()){
layer.handler(req,res,next); // 用户注册的回调
}else{
next();
}
}
next();
}
methods.forEach(method => {
Route.prototype[method] = function (handlers) {
handlers.forEach(handler => {
// 这里路径其实没有用到,不一定是 /,随便传哦。
let layer = new Layer('/', handler);
layer.method = method;// 给每一层都添加一个方法
this.methods[method] = true;
this.stack.push(layer);
})
}
})
module.exports = Route;
lib/application.js
const http = require('http');
const methods = require('methods'); // 第三方模块,别人提供好的 我安装了express
const Router = require('./router'); // 引入了路由系统
function Application() { // 每个应用默认创建一个路由系统, 有可能人家是用应用,不用路由系统
// this.router = new Router();
}
// 懒加载路由
Application.prototype.lazy_route = function () {
if (!this.router) {
this.router = new Router();
}
}
methods.forEach(method => {
// app.get,app.post 等
Application.prototype[method] = function (path, ...handlers) {
// 懒加载路由
this.lazy_route();
this.router[method](path, handlers); // 像路由系统中添加
}
})
// app.use()
Application.prototype.use = function(path, ...handler) {
this.lazy_route();
this.router.use(...arguments);
}
// app.listen()
Application.prototype.listen = function (...args) {
const server = http.createServer((req, res) => {
function done() {
res.end(`Cannot ${req.method} ${req.url}`)
}
this.lazy_route();
this.router.handle(req, res, done); // 交给路由系统来处理,路由系统处理不了会调用 done 方法
});
server.listen(...args)
}
module.exports = Application
正则路由
我们注册路由时候的路径并不一定是单纯的字符串,我们可能想写正则,想去接收参数,比如
// 我的路径必须是 /user/随意的id/随意的名字/xxx
app.get('/user/:id/:name/xxx', function (req, res) {
// params: { id: 随意的id, name: 随意的名字 }
res.end(JSON.stringify(req.params))
})
访问 http://localhost:3000/user/1/ys/xxx,我期待页面输出 {"id":"1","name":"ys"},这个问题我们拆成两步。
- 当注册路由时,根据路由 path 生成正则,这里用了一个包,叫 "path-to-regexp"
- 请求到来时,通过正则去匹配路由,并把匹配到的动态参数解析为 params,挂载到 layer 上,然后路由处理时,req.params = layer.params
lib/router/layer.js
const pathToRegExp = require('path-to-regexp');
function Layer(path,handler){
this.path = path;
this.keys = [];
// 将请求路径转成用于匹配的正则,并记录出现的 keys
this.regexp = pathToRegExp(this.path, this.keys);
// keys: [{ name: 'id' }, { name: 'name' } ]
// regexp: /^\/user\/(?:([^\/]+?))\/(?:([^\/]+?))\/xxx\/?$/i 用于匹配真实 path
this.handler = handler;
}
Layer.prototype.match = function(pathname) {
const matches = pathname.match(this.regexp); // [1] id [2] name
// 如果 matches 存在,说明是正则路由,在 layer 上挂载解析好的 params 属性
if (matches){
// { id: '1', name: ':ys' }
this.params = this.keys.reduce((memo,key,index)=>(memo[key.name] = matches[index + 1],memo), {});
console.log('bingo', this.params);
return true;
}
if (this.path == pathname){
return true;
}
if (!this.route){
if(this.path == '/') {
return true;
}
return pathname.startsWith(this.path + '/');
}
return false;
}
Layer.prototype.handle_request = function(req, res, next){
return this.handler(req,res,next)
}
module.exports = Layer;
lib/router/index.js
// ...
Router.prototype.handle = function (req, res, done) {
const { pathname } = url.parse(req.url);
const method = req.method.toLowerCase();
let i = 0
const next = (err) => {
if (i == this.stack.length) return done();
let layer = this.stack[i++];
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)) { // layer 中用来管理自己的匹配逻辑
req.params = layer.params; // 挂载正则匹配的参数
if (!layer.route) {
if (layer.handler.length === 4) {
next();
} else {
layer.handle_request(req, res, next)
}
} else {
if (layer.route.methods[method]) {
layer.handle_request(req, res, next);
} else {
next();
}
}
} else {
next();
}
}
}
next();
}
module.exports = Router
二级路由实现(难点)
传统我们书写路由,都会这么写:
const express = require('./myExpress');
const app = express();
app.get('/user/add', function(req, res) {
req.end('user add');
});
// 重复的 /user 书写很麻烦,express 内置了二级路由来做抽离
app.get('/user/remove', function(req, res) {
req.end('user add');
});
app.listen(3000, function () {
console.log('server start 3000');
});
我们来尝试下 express 提供的二级路由:
- myExpress
- lib
- router
- index.js // 路由系统
- layer.js // 存储路径 -> route.diapatch 方法的映射
- route.js // 存储请求 method、回调任务,并提供 diapatch 派发执行
- application.js
- express.js
- index.js
- routerConfig
- user.js
新建 routerConifg/user.js
user.js
const express = require('express');
// 这也是 express 不能基于 class 实现的一个原因
// Router 技能作为构造函数,也能作为普通函数调用
const router = express.Router();
router.get('/add', function(req, res) {
res.end('user add');
});
router.get('/remove', function(req, res) {
res.end('user add');
});
module.exports = router;
修改 server.js
user.js
// const express = require('./myExpress');
const express = require('express');
const app = express();
const user = require('./routerConfig/user'); // 拿到 user 路由
app.use('/user', user);
app.listen(3000, function () {
console.log('server start 3000');
});
写起来既清晰又明了,那么怎么实现呢?
流程如下所示:
- app.use('/user', user),user 能作为一个 callback,说明 express.Router() 方法调用会返回一个函数。
function(req, res, next) {}
- express.Router 使用的也是 Router 构造函数,如果直接返回一个函数,则会影响 new 调用的 this,所以我们考虑一个变种写法,实例属性挂在返回的 function 上,声明一个对象 proto 收集原型链上的属性,然后返回的 function 的原型链指向 proto,就实现了这个能 new 能执行的 Router 方法。
代码示例
// 变种前
function Router() {
this.stack = [];
// 兼容直接二级路由调用的方式,但这样会影响 new Router 返回的 this
return function(req, res, next) {
}
}
// new 调用时 原型链东西也将丢失
Router.prototype.xx = function() {}
// -----------------------------变种后--------------------------------
function Router() {
let router = function(req, res, next) {
}
router.stack = [];
router.__proto__ = proto;
// 兼容直接二级路由调用的方式,但这样会影响 new Router 返回的 this
return router;
}
let proto = {};
proto.xx = function() {}
- 和路由中间件不同的是,route 中存储的 layer 也有二级路由的路径,请求到来时先匹配一级路由,匹配掉之后摘除一级路由路径,进入 route 中匹配二级路由,二级路由处理完毕后,重新拼接一级路由路径往下匹配。
lib/router/index.js
const url = require('url');
const Layer = require('./layer');
const Route = require('./route');
const methods = require('methods');
function Router() {
let router = function(req, res, next) {
// 二级路由也是请求来了从 route 中匹配 layer,用普通路由用的同一个方法哦
// 不过二级路由匹,要卸下一级路由再去 route 中匹配哦
router.handle(req, res, next);
}
router.stack = [];
router.__proto__ = proto;
// 兼容直接二级路由调用的方式
// 能 new 能执行,返回的函数即是实例也是普通调用时 Router 的返回结果
return router
}
let proto = {};
proto.route = function (path) {
let route = new Route();
let layer = new Layer(path, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
}
proto.use = function (path) {
let args = Array.from(arguments);
let handlers = [];
// 如果第一个参数不传,默认是 '/'
if (typeof path === 'function') {
path = '/';
handlers = [...args]
} else {
handlers = args.slice(1)
}
handlers.forEach(handler => {
let layer = new Layer(path, handler);
layer.route = undefined; // 后面判断 如果layer上有route属性 说明是路由,没有说明是中间件
this.stack.push(layer);
})
}
methods.forEach(method => {
proto[method] = function (path, handlers) { // 像路由的stack中添加
// 之前是注册路由时调用的 app.get,handlers 被处理成数组
// 但是二级路由时,我们使用的 express.Router().get 没处理
// 这里为了兼容,不是数组则转成数组
if (!Array.isArray(handlers)) {
handlers = Array.from(arguments).slice(1);
}
let route = this.route(path); // 创建route,并返回route
route[method](handlers)
}
})
proto.handle = function (req, res, done) {
// 要在路由的栈中查找 ,找不到就找下一个,找到了将下一个的执行权限传递进去
const { pathname } = url.parse(req.url);
const method = req.method.toLowerCase();
console.log('111', pathname, this.stack);
let i = 0;
let removed = '';
const next = (err) => {
if (i == this.stack.length) return done(); // 整个栈都筛了一遍没有找到,交给应用来处理
let layer = this.stack[i++];
// 拼回 remove
if (removed.length) {
req.url = removed + req.url;
removed = ''; // 从next方法出来的时候 需要增添前缀
}
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)) { // layer 中用来管理自己的匹配逻辑
req.params = layer.params; // 挂载正则匹配的参数
if (!layer.route) { // 中间件不需要匹配方法
if (layer.handler.length === 4) { // 如果正常情况下是不执行错误处理中间件的
next();
} else {
// 匹配到中间件时,需要删除中间件 path 的前缀,如果是 /,则不删除
// 然后进入 route 中查找,比如 /user/add -> /add,目的兼容二级路由
if (layer.path !== '/') {
removed = layer.path; // 保存原来的 path
req.url = req.url.slice(layer.path.length);
}
layer.handle_request(req, res, next)
}
} else { // 路由需要匹配方法,在执行
console.log(11);
if (layer.route.methods[method]) {
layer.handle_request(req, res, next); // route.dispatch
} else {
next();
}
}
} else {
next();
}
}
}
next();
}
module.exports = Router
lib/router/layer.js
const pathToRegExp = require('path-to-regexp');
// 存一个路径和 handler 方法的对象
function Layer(path,handler){
this.path = path;
this.keys = [];
// 将请求路径转成用于匹配的正则,并记录出现的 keys
this.regexp = pathToRegExp(this.path, this.keys);
// keys: [{ name: 'id' }, { name: 'name' } ]
// regexp: /^\/user\/(?:([^\/]+?))\/(?:([^\/]+?))\/xxx\/?$/i 用于匹配真实 path
this.handler = handler;
}
Layer.prototype.match = function(pathname) {
const matches = pathname.match(this.regexp); // [1] id [2] name
// 如果 matches 存在,说明是正则路由,在 layer 上挂载解析好的 params 属性
if (matches){
// { id: '1', name: ':ys' }
this.params = this.keys.reduce((memo,key,index)=>(memo[key.name] = matches[index + 1],memo), {});
// console.log('bingo', this.params);
return true;
}
// console.log(pathname, this.regexp);
// 无论中间件layer 还是路由layer 只要一样肯定匹配到
if(this.path == pathname){
return true;
}
// 如果是中间件 我们开头匹配就可以
if(!this.route){
if(this.path == '/'){ // 中间件路径是/ 表示可以匹配到
return true;
}
// /user/info /user/
// 为了避免 /u 也被匹配,我们加 '/' 匹配
return pathname.startsWith(this.path + '/');
}
return false;
}
// 其实就是 route.dispatch
Layer.prototype.handle_request = function(req, res, next){
return this.handler(req,res,next)
}
module.exports = Layer;
lib/router/route.js(该文件没修改)
const Layer = require('./layer');
const methods = require('methods')
function Route() {
this.stack = [];
// 用来标识route上包含哪些方法
// 比如 '/' 路径如果不包含 post 方法,那么如果 post 方式请求,直接下跳而不再循环 route 中的 layer 去做匹配
this.methods = {};
}
// 可以看到 express 中间件,是递归不断依次执行
Route.prototype.dispatch = function(req,res,out){
let i = 0;
let next = (err) =>{ // 内部路由抛出错误 我就将错误派发到外层处理
if(err) return out(err);
if(i === this.stack.length) return out();
let layer = this.stack[i++];
console.log(layer.method, req.method.toLowerCase());
if(layer.method === req.method.toLowerCase()){
layer.handler(req, res, next); // 用户注册的回调
}else{
next();
}
}
next();
}
methods.forEach(method => {
Route.prototype[method] = function (handlers) {
handlers.forEach(handler => {
// 这里路径其实没有用到,不一定是 /,随便传哦。
let layer = new Layer('/', handler);
layer.method = method;// 给每一层都添加一个方法
this.methods[method] = true;
this.stack.push(layer);
})
}
})
module.exports = Route;
lib/application.js
const http = require('http');
const methods = require('methods'); // 第三方模块,别人提供好的 我安装了express
const Router = require('./router'); // 引入了路由系统
function Application() { // 每个应用默认创建一个路由系统, 有可能人家是用应用,不用路由系统
// this.router = new Router();
}
// 懒加载路由
Application.prototype.lazy_route = function () {
if (!this.router) {
this.router = new Router();
}
}
methods.forEach(method => {
// app.get,app.post 等
Application.prototype[method] = function (path, ...handlers) {
// 懒加载路由
this.lazy_route();
this.router[method](path, handlers); // 像路由系统中添加
}
})
// app.use()
Application.prototype.use = function(path, ...handler) {
this.lazy_route();
this.router.use(...arguments);
}
// app.listen()
Application.prototype.listen = function (...args) {
const server = http.createServer((req, res) => {
function done() {
res.end(`Cannot ${req.method} ${req.url}`)
}
this.lazy_route();
this.router.handle(req, res, done); // 交给路由系统来处理,路由系统处理不了会调用 done 方法
});
server.listen(...args)
}
module.exports = Application
lib/express.js
const Application = require('./application');
const Router = require('./router');
function createApplication() {
return new Application();
}
// 可 new,可直接调用的 Route
createApplication.Router = Router;
module.exports = createApplication;
路由中的参数中间件(app.param)
express 提供了一个参数校验的中间件,它能针对请求的参数做特殊处理。
const express = require('./express');
const app = express();
const path = require('path');
const fs = require('fs')
app.param('id', function(req, res, next, value, key) {
console.log(value);
next();
})
app.param('id', function(req, res, next, value, key) {
console.log(value);
next();
})
app.param('name', function(req, res, next, value, key) {
console.log(value);
next();
})
app.param('name', function(req, res, next, value, key) {
console.log(value);
next();
})
app.get('/user/:id/:name/xxx', function(req, res) {
res.end('user');
})
app.listen(3000, function() {
console.log(`server start 3000`);
})
也就是说,比如我两个路由的处理都用到了相同格式的参数,我能把这个处理提取出来,作为一个中间件前置校验。
比如上例中,我们 id 注册了两个回调 [fn1, fn2],name 也注册了两个回调 [fn1, fn2],这时候我们之前收集到的 layer.keys 为 [{ name: 'id' }, { name: 'name' } ],遍历 keys,依次执行回调即可。
lib/application.js
// 参数处理中间件
Application.prototype.param = function(path, ...handler) {
this.lazy_route();
this.router.param(...arguments);
}
lib/router/index.js
const url = require('url');
const Layer = require('./layer');
const Route = require('./route');
const methods = require('methods');
function Router() {
let router = function (req, res, next) {
// 二级路由也是请求来了从 route 中匹配 layer,用普通路由用的同一个方法哦
// 不过二级路由匹,要卸下一级路由再去 route 中匹配哦
router.handle(req, res, next);
}
router.events = {};
router.stack = [];
router.__proto__ = proto;
// 兼容直接二级路由调用的方式
// 能 new 能执行,返回的函数即是实例也是普通调用时 Router 的返回结果
return router
}
// ...
proto.param = function (key, callback) {
// 收集参数对应的 cbs,{ id: [], name: []}
if (this.events[key]) {
this.events[key].push(callback);
} else {
this.events[key] = [callback]
}
}
// 遍历 layer.keys,依次执行参数的回调,最后执行路由回调
proto.handle_params = function (req, res, layer, out) {
let keys = layer.keys;
if (!keys || !keys.length) return out();
// 获取 ['id', 'name'] keys 列表
keys = keys.reduce((memo, current) => [...memo, current.name], []);
let events = this.events;
let i = 0;
let index = 0;
let key;
let fns;
const next = () => {
if (keys.length === i) return out()
key = keys[i++]
fns = events[key]; // {id:[fn1,fn2],name:[fn1,fn2]}
if (fns) {
processCallback();
} else {
next();
}
}
next();
function processCallback() {
let fn = fns[index++];
if (fn) {
fn(req, res, processCallback, layer.params[key], key)
} else {
index = 0;
next(); // 如果到头了 就执行下一个
}
}
}
// ...
express 实现 res.send, res.sendFile 扩展方法
我们知道,res.end 只能传入 buffer 或者 strng,express 基于 res 作了扩展。
app.get('/user/:id/:name/xxx', function(req, res) {
res.sendFile(path.resolve(__dirname,'note.md'));
res.json({ a: 1 });
res.send({ a: 2 });
})
可以看到,这个只是在 res 上使用中间件扩展了一个方法而已,就比如
app.use(function(req, res) {
res.send = function() {
// do something
}
});
app.get('/user/:id/:name/xxx', function(req, res) {
// res.send({});
res.sendFile(path.resolve(__dirname, '6.server.js'));
})
因为该中间件需要在所有路由注册之前最先注册进去,我们考虑把它防止路由初始化之后,emm,就放懒加载路由内部吧。
lib/application.js
// ...
Application.prototype.lazy_route = function () {
if (!this.router) {
this.router = new Router();
this.use((req, res, next) => {
res.send = function (data) {
if (typeof data == 'object') {
res.end(JSON.stringify(data))
} else if (typeof data == 'string' || Buffer.isBuffer(data)) {
res.end(data)
}
}
res.sendFile = function (filePath) {
fs.createReadStream(filePath).pipe(res);
}
next();
})
}
}
// ...
源码汇总
- myExpress
- lib
- router
- index.js
- layer.js
- route.js
- application.js
- express.js
- index.js
lib/router/index.js
const url = require('url');
const Layer = require('./layer');
const Route = require('./route');
const methods = require('methods');
function Router() {
let router = function (req, res, next) {
// 二级路由也是请求来了从 route 中匹配 layer,用普通路由用的同一个方法哦
// 不过二级路由匹,要卸下一级路由再去 route 中匹配哦
router.handle(req, res, next);
}
router.events = {};
router.stack = [];
router.__proto__ = proto;
// 兼容直接二级路由调用的方式
// 能 new 能执行,返回的函数即是实例也是普通调用时 Router 的返回结果
return router
}
let proto = {};
proto.route = function (path) {
let route = new Route();
let layer = new Layer(path, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
}
proto.use = function (path) {
let args = Array.from(arguments);
let handlers = [];
// 如果第一个参数不传,默认是 '/'
if (typeof path === 'function') {
path = '/';
handlers = [...args]
} else {
handlers = args.slice(1)
}
handlers.forEach(handler => {
let layer = new Layer(path, handler);
layer.route = undefined; // 后面判断 如果layer上有route属性 说明是路由,没有说明是中间件
this.stack.push(layer);
})
}
methods.forEach(method => {
proto[method] = function (path, handlers) { // 像路由的stack中添加
// 之前是注册路由时调用的 app.get,handlers 被处理成数组
// 但是二级路由时,我们使用的 express.Router().get 没处理
// 这里为了兼容,不是数组则转成数组
if (!Array.isArray(handlers)) {
handlers = Array.from(arguments).slice(1);
}
let route = this.route(path); // 创建route,并返回route
route[method](handlers)
}
})
proto.param = function (key, callback) {
// 收集参数对应的 cbs,{ id: [], name: []}
if (this.events[key]) {
this.events[key].push(callback);
} else {
this.events[key] = [callback]
}
}
// 遍历 layer.keys,依次执行参数的回调,最后执行路由回调
proto.handle_params = function (req, res, layer, out) {
let keys = layer.keys;
if (!keys || !keys.length) return out();
// 获取 ['id', 'name'] keys 列表
keys = keys.reduce((memo, current) => [...memo, current.name], []);
let events = this.events;
let i = 0;
let index = 0;
let key;
let fns;
const next = () => {
if (keys.length === i) return out()
key = keys[i++]
fns = events[key]; // {id:[fn1,fn2],name:[fn1,fn2]}
if (fns) {
processCallback();
} else {
next();
}
}
next();
function processCallback() {
let fn = fns[index++];
if (fn) {
fn(req, res, processCallback, layer.params[key], key)
} else {
index = 0;
next(); // 如果到头了 就执行下一个
}
}
}
proto.handle = function (req, res, done) {
// 要在路由的栈中查找 ,找不到就找下一个,找到了将下一个的执行权限传递进去
const { pathname } = url.parse(req.url);
const method = req.method.toLowerCase();
let i = 0;
let removed = '';
const next = (err) => {
if (i == this.stack.length) return done(); // 整个栈都筛了一遍没有找到,交给应用来处理
let layer = this.stack[i++];
// 拼回 remove
if (removed.length) {
req.url = removed + req.url;
removed = ''; // 从next方法出来的时候 需要增添前缀
}
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)) { // layer 中用来管理自己的匹配逻辑
req.params = layer.params; // 挂载正则匹配的参数
if (!layer.route) { // 中间件不需要匹配方法
if (layer.handler.length === 4) { // 如果正常情况下是不执行错误处理中间件的
next();
} else {
// 匹配到中间件时,需要删除中间件 path 的前缀,如果是 /,则不删除
// 然后进入 route 中查找,比如 /user/add -> /add,目的兼容二级路由
if (layer.path !== '/') {
removed = layer.path; // 保存原来的 path
req.url = req.url.slice(layer.path.length);
}
layer.handle_request(req, res, next)
}
} else { // 路由需要匹配方法,在执行
if (layer.route.methods[method]) {
// 执行路由回调之前,先执行 param 回调
// app.param('id', cb), layer 上有 keys
this.handle_params(req, res, layer, () => {
layer.handle_request(req, res, next); // route.dispatch
});
} else {
next();
}
}
} else {
next();
}
}
}
next();
}
module.exports = Router
lib/router/layer.js
const pathToRegExp = require('path-to-regexp');
// 存一个路径和 handler 方法的对象
function Layer(path,handler){
this.path = path;
this.keys = [];
// 将请求路径转成用于匹配的正则,并记录出现的 keys
this.regexp = pathToRegExp(this.path, this.keys);
// keys: [{ name: 'id' }, { name: 'name' } ]
// regexp: /^\/user\/(?:([^\/]+?))\/(?:([^\/]+?))\/xxx\/?$/i 用于匹配真实 path
this.handler = handler;
}
Layer.prototype.match = function(pathname) {
const matches = pathname.match(this.regexp); // [1] id [2] name
// 如果 matches 存在,说明是正则路由,在 layer 上挂载解析好的 params 属性
if (matches){
// { id: '1', name: ':ys' }
this.params = this.keys.reduce((memo,key,index)=>(memo[key.name] = matches[index + 1],memo), {});
// console.log('bingo', this.params);
return true;
}
// console.log(pathname, this.regexp);
// 无论中间件layer 还是路由layer 只要一样肯定匹配到
if(this.path == pathname){
return true;
}
// 如果是中间件 我们开头匹配就可以
if(!this.route){
if(this.path == '/'){ // 中间件路径是/ 表示可以匹配到
return true;
}
// /user/info /user/
// 为了避免 /u 也被匹配,我们加 '/' 匹配
return pathname.startsWith(this.path + '/');
}
return false;
}
// 其实就是 route.dispatch
Layer.prototype.handle_request = function(req, res, next){
return this.handler(req,res,next)
}
module.exports = Layer;
lib/router/route.js
const Layer = require('./layer');
const methods = require('methods')
function Route() {
this.stack = [];
// 用来标识route上包含哪些方法
// 比如 '/' 路径如果不包含 post 方法,那么如果 post 方式请求,直接下跳而不再循环 route 中的 layer 去做匹配
this.methods = {};
}
// 可以看到 express 中间件,是递归不断依次执行
Route.prototype.dispatch = function(req,res,out){
let i = 0;
let next = (err) =>{ // 内部路由抛出错误 我就将错误派发到外层处理
if(err) return out(err);
if(i === this.stack.length) return out();
let layer = this.stack[i++];
// console.log(layer.method, req.method.toLowerCase());
if(layer.method === req.method.toLowerCase()){
layer.handler(req, res, next); // 用户注册的回调
}else{
next();
}
}
next();
}
methods.forEach(method => {
Route.prototype[method] = function (handlers) {
handlers.forEach(handler => {
// 这里路径其实没有用到,不一定是 /,随便传哦。
let layer = new Layer('/', handler);
layer.method = method;// 给每一层都添加一个方法
this.methods[method] = true;
this.stack.push(layer);
})
}
})
module.exports = Route;
lib/application.js
const http = require('http');
const methods = require('methods'); // 第三方模块,别人提供好的 我安装了express
const fs = require('fs');
const Router = require('./router'); // 引入了路由系统
function Application() { // 每个应用默认创建一个路由系统, 有可能人家是用应用,不用路由系统
// this.router = new Router();
}
// 懒加载路由
Application.prototype.lazy_route = function () {
if (!this.router) {
this.router = new Router();
this.use((req, res, next) => {
res.send = function (data) {
if (typeof data == 'object') {
res.end(JSON.stringify(data))
} else if (typeof data == 'string' || Buffer.isBuffer(data)) {
res.end(data)
}
}
res.sendFile = function (filePath) {
fs.createReadStream(filePath).pipe(res);
}
next();
})
}
}
methods.forEach(method => {
// app.get,app.post 等
Application.prototype[method] = function (path, ...handlers) {
// 懒加载路由
this.lazy_route();
this.router[method](path, handlers); // 像路由系统中添加
}
})
// 参数处理中间件
Application.prototype.param = function (path, ...handler) {
this.lazy_route();
this.router.param(...arguments);
}
// app.use()
Application.prototype.use = function (path, ...handler) {
this.lazy_route();
this.router.use(...arguments);
}
// app.listen()
Application.prototype.listen = function (...args) {
const server = http.createServer((req, res) => {
function done() {
res.end(`Cannot ${req.method} ${req.url}`)
}
this.lazy_route();
this.router.handle(req, res, done); // 交给路由系统来处理,路由系统处理不了会调用 done 方法
});
server.listen(...args)
}
module.exports = Application
lib/express.js
const Application = require('./application');
const Router = require('./router');
function createApplication() {
return new Application();
}
// 可 new,可直接调用的 Route
createApplication.Router = Router;
module.exports = createApplication;
index.js
module.exports = require('./lib/express');