上一篇文章咱们讲了怎么用 Node.js
原生 API 来写一个 web 服务器
,尽管代码比拟丑,然而基本功能还是有的。然而个别咱们不会间接用原生 API 来写,而是借助框架来做,比方本文要讲的Express
。通过上一篇文章的铺垫,咱们能够猜想,Express
其实也没有什么黑魔法,也仅仅是原生 API 的封装,次要是用来提供更好的扩展性,应用起来更不便,代码更优雅。本文照例会从 Express
的根本应用动手,而后本人手写一个 Express
来代替他,也就是源码解析。
本文可运行代码曾经上传 GitHub,拿下来一边玩代码,一边看文章成果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/Express
简略示例
应用 Express
搭建一个最简略的 Hello World
也是几行代码就能够搞定,上面这个例子起源官网文档:
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {res.send('Hello World!');
});
app.listen(port, () => {console.log(`Example app listening at http://localhost:${port}`);
});
能够看到 Express
的路由能够间接用 app.get
这种办法来解决,比咱们之前在 http.createServer
外面写一堆 if
优雅多了。咱们用这种形式来改写下上一篇文章的代码:
const path = require("path");
const express = require("express");
const fs = require("fs");
const url = require("url");
const app = express();
const port = 3000;
app.get("/", (req, res) => {res.end("Hello World");
});
app.get("/api/users", (req, res) => {
const resData = [
{
id: 1,
name: "小明",
age: 18,
},
{
id: 2,
name: "小红",
age: 19,
},
];
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(resData));
});
app.post("/api/users", (req, res) => {
let postData = "";
req.on("data", (chunk) => {postData = postData + chunk;});
req.on("end", () => {
// 数据传完后往 db.txt 插入内容
fs.appendFile(path.join(__dirname, "db.txt"), postData, () => {res.end(postData); // 数据写完后将数据再次返回
});
});
});
app.listen(port, () => {console.log(`Server is running on http://localhost:${port}/`);
});
Express
还反对中间件,咱们写个中间件来打印出每次申请的门路:
app.use((req, res, next) => {const urlObject = url.parse(req.url);
const {pathname} = urlObject;
console.log(`request path: ${pathname}`);
next();});
Express
也反对动态资源托管,不过他的 API 是须要指定一个文件夹来独自寄存动态资源的,比方咱们新建一个 public
文件夹来寄存动态资源,应用 express.static
中间件配置一下就行:
app.use(express.static(path.join(__dirname, 'public')));
而后就能够拿到动态资源了:
手写源码
手写源码才是本文的重点,后面的不过是铺垫,本文手写的指标就是本人写一个 express
来替换后面用到的express api
,其实就是源码解析。在开始之前,咱们先来看看用到了哪些API
:
express()
,第一个必定是express
函数,这个运行后会返回一个app
的实例,前面用的很多办法都是这个app
上的。app.listen
,这个办法相似于原生的server.listen
,用来启动服务器。app.get
,这是解决路由的 API,相似的还有app.post
等。app.use
,这是中间件的调用入口,所有中间件都要通过这个办法来调用。express.static
,这个中间件帮忙咱们做动态资源托管,其实是另外一个库了,叫 serve-static,因为跟Express
架构关系不大,本文就先不讲他的源码了。
本文所有手写代码全副参照官网源码写成,办法名和变量名尽量与官网保持一致,大家能够对照着看,写到具体的办法时我也会贴出官网源码的地址。
express()
首先须要写的必定是 express()
,这个办法是所有的开始,他会创立并返回一个app
,这个app
就是咱们的web 服务器
。
// express.js
var mixin = require('merge-descriptors');
var proto = require('./application');
// 创立 web 服务器的办法
function createApplication() {
// 这个 app 办法其实就是传给 http.createServer 的回调函数
var app = function (req, res) { };
mixin(app, proto, false);
return app;
}
exports = module.exports = createApplication;
上述代码就是咱们在运行 express()
的时候执行的代码,其实就是个空壳,返回的 app
临时是个空函数,真正的 app
并没在这里,而是在 proto
上,从上述代码能够看出 proto
其实就是 application.js
,而后通过上面这行代码将proto
上的货色都赋值给了app
:
mixin(app, proto, false);
这行代码用到了一个第三方库 merge-descriptors
,这个库总共没有几行代码,做的事件也很简略,就是将proto
下面的属性挨个赋值给 app
,对merge-descriptors
源码感兴趣的能够看这里:https://github.com/component/merge-descriptors/blob/master/index.js。
Express
这里之所以应用 mixin
,而不是一般的面向对象来继承,是因为它除了要mixin proto
外,还须要 mixin
其余库,也就是须要多继承,我这里省略了,然而官网源码是有的。
express.js
对应的源码看这里:https://github.com/expressjs/express/blob/master/lib/express.js
app.listen
下面说了,express.js
只是一个空壳,真正的 app
在application.js
外面,所以 app.listen
也是在这里。
// application.js
var app = exports = module.exports = {};
app.listen = function listen() {var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
下面代码就是调用原生 http
模块创立了一个服务器,然而传的参数是 this
,这里的this
是什么呢?回忆一下咱们应用 express
的时候是这样用的:
const app = express();
app.listen(3000);
所以 listen
办法的理论调用者是 express()
的返回值,也就是下面 express.js
外面 createApplication
的返回值,也就是这个函数:
var app = function (req, res) {};
所以这里的 this
也是这个函数,所以我在 express.js
外面就加了正文,这个函数是 http.createServer
的回调函数。当初这个函数是空的,实际上他应该是整个 web 服务器
的解决入口,所以咱们给他加上解决的逻辑,在外面再加一行代码:
var app = function(req, res) {app.handle(req, res); // 这是真正的服务器解决入口
};
app.handle
app.handle
也是挂载在 app
上面的,所以他理论也在 application.js
这个文件外面,上面咱们来看看他干了什么:
app.handle = function handle(req, res) {
var router = this._router;
// 最终的解决办法
var done = finalhandler(req, res);
// 如果没有定义 router
// 间接完结返回
if (!router) {done();
return;
}
// 有 router,就用 router 来解决
router.handle(req, res, done);
}
下面代码能够看出,理论解决路由的是 router
,这是Router
的一个实例,并且挂载在 this
上的,咱们这里还没有给他赋值,如果没有赋值的话,会间接运行 finalhandler
并且完结解决。finalhandler
也是一个第三方库,GitHub 链接在这里:https://github.com/pillarjs/finalhandler。这个库的性能也不简单,就是帮你解决一些收尾的工作,比方所有路由都没匹配上,你可能须要返回 404
并记录下error log
,这个库就能够帮你做。
app.get
下面说了,在具体解决网络申请时,实际上是用 app._router
来解决的,那么 app._router
是在哪里赋值的呢?事实上 app._router
的赋值有多个中央,一个中央就是 HTTP
动词解决办法上,比方咱们用到的 app.get
或者 app.post
。无论是app.get
还是 app.post
都是调用的 router
办法来解决,所以能够对立用一个循环来写这一类的办法。
// HTTP 动词的办法
var methods = ['get', 'post'];
methods.forEach(function (method) {app[method] = function (path) {this.lazyrouter();
var route = this._router.route(path);
route[method].apply(route, Array.prototype.slice.call(arguments, 1));
return this;
}
});
下面代码 HTTP
动词都放到了一个数组外面,官网源码中这个数组也是一个第三方库保护的,名字就叫 methods
,GitHub 地址在这里:https://github.com/jshttp/methods。我这个例子因为只须要两个动词,就简化了,间接用数组了。这段代码其实给app
创立了跟每个动词同名的函数,所有动词的处理函数都是一样的,都是去调 router
外面的对应办法来解决。这种将不同局部抽取进去,从而复用独特局部的代码,有点像我之前另一篇文章写过的设计模式 —- 享元模式。
咱们留神到下面代码除了调用 router
来解决路由外,还有一行代码:
this.lazyrouter();
lazyrouter
办法其实就是咱们给 this._router
赋值的中央,代码也比较简单,就是检测下有没有 _router
,如果没有就给他赋个值,赋的值就是Router
的一个实例:
app.lazyrouter = function lazyrouter() {if (!this._router) {this._router = new Router();
}
}
app.listen
,app.handle
和 methods
解决办法都在 application.js
外面,application.js
源码在这里:https://github.com/expressjs/express/blob/master/lib/application.js
Router
写到这里咱们发现咱们曾经应用了 Router
的多个API
,比方:
router.handle
router.route
route[method]
所以咱们来看下 Router
这个类,上面的代码是从源码中简化进去的:
// router/index.js
var setPrototypeOf = require('setprototypeof');
var proto = module.exports = function () {function router(req, res, next) {router.handle(req, res, next);
}
setPrototypeOf(router, proto);
return router;
}
这段代码对我来说是比拟奇怪的,咱们在执行 new Router()
的时候其实执行的是 new proto()
,new proto()
并不是我奇怪的中央,奇怪的是他设置原型的形式。我之前在讲 JS 的面向对象的文章提到过如果你要给一个类加上类办法能够这样写:
function Class() {}
Class.prototype.method1 = function() {}
var instance = new Class();
这样 instance.__proto__
就会指向 Class.prototype
,你就可应用instance.method1
了。
Express.js
的上述代码其实也是实现了相似的成果,setprototypeof
又是一个第三方库,作用相似 Object.setPrototypeOf(obj, prototype)
,就是给一个对象设置原型,setprototypeof
存在的意义就是兼容老规范的 JS,也就是加了一些polyfill
,他的代码在这里。所以:
setPrototypeOf(router, proto);
这行代码的意思就是让 router.__proto__
指向 proto
,router
是你在 new proto()
时的返回对象,执行了下面这行代码,这个 router
就能够拿到 proto
上的全副办法了。像 router.handle
这种办法就能够挂载到 proto
上了,成为proto.handle
。
绕了一大圈,其实就是 JS 面向对象的应用,给 router
增加类办法,然而为什么应用这么绕的形式,而不是像我下面那个 Class
那样用呢?这我就不是很分明了,可能有什么历史起因吧。
路由架构
Router
的根本构造晓得了,要了解 Router
的具体代码,咱们还须要对 Express
的路由架构有一个整体的意识。就以咱们这两个示例 API 来说:
get /api/users
post /api/users
咱们发现他们的 path
是一样的,都是 /api/users
,然而他们的申请办法,也就是method
不一样。Express
外面将 path
这一层提取进去作为了一个类,叫做 Layer
。然而对于一个Layer
,咱们只晓得他的path
,不晓得method
的话,是不能确定一个路由的,所以 Layer
上还增加了一个属性 route
,这个route
上也存了一个数组,数组的每个项存了对应的 method
和回调函数handle
。整个构造你能够了解成这个样子:
const router = {
stack: [
// 外面很多 layer
{
path: '/api/users'
route: {
stack: [
// 外面存了多个 method 和回调函数
{
method: 'get',
handle: function1
},
{
method: 'post',
handle: function2
}
]
}
}
]
}
晓得了这个构造咱们能够猜到,整个流程能够分成两局部:注册路由 和匹配路由 。当咱们写app.get
和app.post
这些办法时,其实就是在 router
上增加 layer
和route
。当一个网络申请过去时,其实就是遍历 layer
和route
,找到对应的 handle
拿进去执行。
留神 route
数组外面的构造,每个项按理来说应该应用一种新的数据结构来存储,比方 routeItem
之类的。然而 Express
并没有这样做,而是将它和 layer
合在一起了,给 layer
增加了 method
和handle
属性。这在首次看源码的时候可能造成困惑,因为 layer
同时存在于 router
的stack
上和 route
的stack 上
,肩负了两种职责。
router.route
这个办法是咱们后面注册路由的时候调用的一个办法,回顾下后面的注册路由的办法,比方app.get
:
app.get = function (path) {this.lazyrouter();
var route = this._router.route(path);
route.get.apply(route, Array.prototype.slice.call(arguments, 1));
return this;
}
联合下面讲的路由架构,咱们在注册路由的时候,应该给 router
增加对应的 layer
和route
,router.route
的代码就不难写出了:
proto.route = function route(path) {var route = new Route();
var layer = new Layer(path, route.dispatch.bind(route)); // 参数是 path 和回调函数
layer.route = route;
this.stack.push(layer);
return route;
}
Layer 和 Route 构造函数
下面代码新建了 Route
和Layer
实例,这两个类的构造函数其实也挺简略的。只是参数的申明和初始化:
// layer.js
module.exports = Layer;
function Layer(path, fn) {
this.path = path;
this.handle = fn;
this.method = '';
}
// route.js
module.exports = Route;
function Route() {this.stack = [];
this.methods = {}; // 一个放慢查找的 hash 表}
route.get
后面咱们看到了 app.get
其实通过上面这行代码,最终调用的是route.get
:
route.get.apply(route, Array.prototype.slice.call(arguments, 1));
也晓得了 route.get
这种动词处理函数,其实就是往 route.stack
上增加 layer
,那咱们的route.get
也能够写进去了:
var methods = ["get", "post"];
methods.forEach(function (method) {Route.prototype[method] = function () {
// 反对传入多个回调函数
var handles = flatten(slice.call(arguments));
// 为每个回调新建一个 layer,并加到 stack 上
for (var i = 0; i < handles.length; i++) {var handle = handles[i];
// 每个 handle 都应该是个函数
if (typeof handle !== "function") {var type = toString.call(handle);
var msg =
"Route." +
method +
"() requires a callback function but got a" +
type;
throw new Error(msg);
}
// 留神这里的层级是 layer.route.layer
// 后面第一个 layer 曾经做个 path 的比拟了,所以这里是第二个 layer,path 能够间接设置为 /
var layer = new Layer("/", handle);
layer.method = method;
this.methods[method] = true; // 将 methods 对应的 method 设置为 true,用于前面的疾速查找
this.stack.push(layer);
}
};
});
这样,其实整个 router
的构造就构建进去了,前面就看看怎么用这个构造来解决申请了,也就是 router.handle
办法。
router.handle
后面说了 app.handle
实际上是调用的 router.handle
,也晓得了router
的构造是在 stack
上增加了 layer
和router
,所以 router.handle
须要做的就是从 router.stack
上找出对应的 layer
和router
并执行回调函数:
// 真正解决路由的函数
proto.handle = function handle(req, res, done) {
var self = this;
var idx = 0;
var stack = self.stack;
// next 办法来查找对应的 layer 和回调函数
next();
function next() {
// 应用第三方库 parseUrl 获取 path,如果没有 path,间接返回
var path = parseUrl(req).pathname;
if (path == null) {return done();
}
var layer;
var match;
var route;
while (match !== true && idx < stack.length) {layer = stack[idx++]; // 留神这里先执行 layer = stack[idx]; 再执行 idx++;
match = layer.match(path); // 调用 layer.match 来检测以后门路是否匹配
route = layer.route;
// 没匹配上,跳出当次循环
if (match !== true) {continue;}
// layer 匹配上了,然而没有 route,也跳出当次循环
if (!route) {continue;}
// 匹配上了,看看 route 上有没有对应的 method
var method = req.method;
var has_method = route._handles_method(method);
// 如果没有对应的 method,其实也是没匹配上,跳出当次循环
if (!has_method) {
match = false;
continue;
}
}
// 循环完了还没有匹配的,就 done 了,其实就是 404
if (match !== true) {return done();
}
// 如果匹配上了,就执行对应的回调函数
return layer.handle_request(req, res, next);
}
};
下面代码还用到了几个 Layer
和Route
的实例办法:
layer.match(path): 检测以后
layer
的path
是否匹配。route._handles_method(method):检测以后
route
的method
是否匹配。layer.handle_request(req, res, next):应用
layer
的回调函数来解决申请。
这几个办法看起来并不简单,咱们前面一个一个来实现。
到这里其实还有个疑难。从他整个的匹配流程来看,他寻找的其实是 router.stack.layer
这一层,然而最终应该执行的回调却是在 router.stack.layer.route.stack.layer.handle
。这是怎么通过router.stack.layer
找到最终的 router.stack.layer.route.stack.layer.handle
来执行的呢?
这要回到咱们后面的 router.route
办法:
proto.route = function route(path) {var route = new Route();
var layer = new Layer(path, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
}
这里咱们 new Layer
的时候给的回调其实是 route.dispatch.bind(route)
,这个办法会再去route.stack
上找到正确的 layer
来执行。所以 router.handle
真正的流程其实是:
- 找到
path
匹配的layer
- 拿出
layer
上的route
,看看有没有匹配的method
layer
和method
都有匹配的,再调用route.dispatch
去找出真正的回调函数来执行。
所以又多了一个须要实现的函数,route.dispatch
。
layer.match
layer.match
是用来检测以后 path
是否匹配的函数,用到了一个第三方库 path-to-regexp
,这个库能够将path
转为正则表达式,不便前面的匹配,这个库在之前写过的 react-router
源码中也呈现过。
var pathRegexp = require("path-to-regexp");
module.exports = Layer;
function Layer(path, fn) {
this.path = path;
this.handle = fn;
this.method = "";
// 增加一个匹配正则
this.regexp = pathRegexp(path);
// 疾速匹配 /
this.regexp.fast_slash = path === "/";
}
而后就能够增加 match
实例办法了:
Layer.prototype.match = function match(path) {
var match;
if (path != null) {if (this.regexp.fast_slash) {return true;}
match = this.regexp.exec(path);
}
// 没匹配上,返回 false
if (!match) {return false;}
// 不然返回 true
return true;
};
layer.handle_request
layer.handle_request
是用来调用具体的回调函数的办法,其实就是拿出 layer.handle
来执行:
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
fn(req, res, next);
};
route._handles_method
route._handles_method
就是检测以后 route
是否蕴含须要的 method
,因为之前增加了一个methods
对象,能够用它来进行疾速查找:
Route.prototype._handles_method = function _handles_method(method) {var name = method.toLowerCase();
return Boolean(this.methods[name]);
};
route.dispatch
route.dispatch
其实是 router.stack.layer
的回调函数,作用是找到对应的 router.stack.layer.route.stack.layer.handle
并执行。
Route.prototype.dispatch = function dispatch(req, res, done) {
var idx = 0;
var stack = this.stack; // 留神这个 stack 是 route.stack
// 如果 stack 为空,间接 done
// 这里的 done 其实是 router.stack.layer 的 next
// 也就是执行下一个 router.stack.layer
if (stack.length === 0) {return done();
}
var method = req.method.toLowerCase();
// 这个 next 办法其实是在 router.stack.layer.route.stack 上寻找 method 匹配的 layer
// 找到了就执行 layer 的回调函数
next();
function next() {var layer = stack[idx++];
if (!layer) {return done();
}
if (layer.method && layer.method !== method) {return next();
}
layer.handle_request(req, res, next);
}
};
到这里其实 Express
整体的路由构造,注册和执行流程都实现了,贴下对应的官网源码:
Router 类:https://github.com/expressjs/express/blob/master/lib/router/index.js
Layer 类:https://github.com/expressjs/express/blob/master/lib/router/layer.js
Route 类:https://github.com/expressjs/express/blob/master/lib/router/route.js
中间件
其实咱们后面曾经隐含了中间件,从后面的构造能够看出,一个网络申请过去,会到 router
的第一个 layer
,而后调用next
到到第二个 layer
,匹配上layer
的path
就执行回调,而后始终这样把所有的 layer
都走完。所以中间件是啥?中间件就是一个 layer
,他的path
默认是/
,也就是对所有申请都失效。依照这个思路,代码就简略了:
// application.js
// app.use 就是调用 router.use
app.use = function use(fn) {
var path = "/";
this.lazyrouter();
var router = this._router;
router.use(path, fn);
};
而后在 router.use
外面再加一层 layer
就行了:
proto.use = function use(path, fn) {var layer = new Layer(path, fn);
this.stack.push(layer);
};
总结
Express
也是用原生 APIhttp.createServer
来实现的。Express
的次要工作是将http.createServer
的回调函数拆出来了,构建了一个路由构造Router
。- 这个路由构造由很多层
layer
组成。 - 一个中间件就是一个
layer
。 - 路由也是一个
layer
,layer
上有一个path
属性来示意他能够解决的 API 门路。 path
可能有不同的method
,每个method
对应layer.route
上的一个layer
。layer.route
上的layer
尽管名字和router
上的layer
一样,然而性能侧重点并不一样,这也是源码中让人困惑的一个点。layer.route
上的layer
的主要参数是method
和handle
,如果method
匹配了,就执行对应的handle
。- 整个路由匹配过程其实就是遍历
router.layer
的一个过程。 - 每个申请来了都会遍历一遍所有的
layer
,匹配上就执行回调,一个申请可能会匹配上多个layer
。 - 总体来看,
Express
代码给人的感觉并不是很完满,特地是Layer
类肩负两种职责,跟软件工程强调的繁多职责
准则不符,这也导致Router
,Layer
,Route
三个类的调用关系有点凌乱。而且对于继承和原型的应用都是很老的形式。可能也是这种不完满催生了Koa
的诞生,下一篇文章咱们就来看看Koa
的源码吧。 Express
其实还对原生的req
和res
进行了扩大,让他们变得更好用,然而这个其实只相当于一个语法糖,对整体架构没有太大影响,所以本文就没波及了。
本文可运行代码曾经上传 GitHub,拿下来一边玩代码,一边看文章成果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/Express
参考资料
Express 官网文档:http://expressjs.com/
Express 官网源码:https://github.com/expressjs/express/tree/master/lib
文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和 GitHub 小星星,你的反对是作者继续创作的能源。
作者博文 GitHub 我的项目地址:https://github.com/dennis-jiang/Front-End-Knowledges
我也搞了个公众号[进击的大前端],不打广告,不写水文,只发高质量原创,欢送关注~