connect 库的介绍、使用及源码分析
connect 是 node 中的一种用于拓展 http 服务的框架,支持 node 接入插件(或者说是中间件)。
使用 node http 模块搭建 http 服务:
const http = require('http');
http.createServer((req, res) => {
// 最初的中间件写法就是把这个回调函数拿出来,每个中间件在处理完之后都把req,res放入到下一个中间件里,
// 这样就形成了一个调用链条
}).listen(3000)
安装
npm i connect -D
使用
使用 connect 搭建一个中间件服务。
const http = require('http');
const connect = require('connect');
// app 是一个方法,里边有个 route、stack 等属性用来存储中间件
const app = connect();
// 添加新的中间件
app.use((req, res, next) => {
// 一定要添加 next(),不然下面的中间件无法使用
next();
})
// 只有请求url跟配置的route相同才会触发这个中间件
app.use(route ,(req, res, next) => {
next();
})
http.createServer(app);
api 介绍
通过 import 导入的是 connect 库是一个用于创建 app 的构造函数,因此我们需要调用 connect 方法来创建一个 app:
const connect = require('connect');
const app = connect();
以下介绍的 api 都是 app 对象的:
use
语法:
app.use([route,]fn)
用来添加中间件。
app 也可以当成是一个中间件
添加中间件的顺序很重要,存储中间件的数组就是按照调用 .use 方法进行组装。
- route:可选,以 route 作为 key 来保存对应路由要执行的中间件。
-
- 如果没有配置 route 参数,默认就是 /,那么每个请求都会触发这个中间件。
-
- 如果配置了 route 参数,就会判断 req.url 的开头是否为 route,是的话就会执行这个中间件(比如配置的 route 为 /foo,当 req.url 为 /foo/bar 时就会执行)。注意:如果 route 以 / 结尾,那么会移除 /(eg. /foo/ 会变成 /foo),在这种情况下,/foo、/foo/bar、/foo.bar 都能匹配成功。同时 req.url 移除掉 route 参数中的字符,原请求路径会添加到 req.originalUrl 中(eg. route: /foo,req.url: /foo/bar,那么 req.url 会被修改成 /bar,req.originalUrl 为 /foo/bar)。
- fn:中间件处理函数,接收三个参数:
-
- request:http 模块中的 request 对象。
-
- response:http 模块中的 response 对象。
-
- next:用于控制是否需要执行下一个中间件。如果已经满足需要,则不需要处理;如果还需要继续执行下面的中间件,需要调用 next()。
handle
用于处理其他 http 服务。接收三个参数:
- request:http 模块中的 request 对象。
- response:http 模块中的 response 对象。
- out:一个函数,当中间件没有处理 request 对象或者报错的时候触发。
listen
启动 http 服务。
原理讲解及源码解读
创建 app
function createServer() {
function app(req, res, next) { app.handle(req,res,next) };
merge(app, proto);
// 合并 EventEmitter 的原型
merge(app, EventEmitter.prototype);
app.route = '/';
app.stack = [];
return app;
}
可以看到 app 其实就是一个方法,其中合并 EventEmitter 的原型(使得 app 具有 emit 等功能 ),往 app 添加了 use、handle、listen 方法,同时添加了 route(用来处理嵌套 app 作为中间件的情况,此时这个 route 属性作为 key,要执行的中间件为这个嵌套 app)、stack(用来保存中间件) 属性。
添加中间件
前面提到了使用 use 方法添加中间件,并放在一个队列中,我们来看看 use 方法具体做了什么
proto.use = function use(route, fn) {
var handle = fn;
var path = route;
// 这个就是处理只传入一个中间件函数逻辑,将 route 设置成 /
if (typeof route !== "string") {
handle = route;
path = "/";
}
// 嵌套 app,就是把 app 也当作一个中间件使用
if (typeof handle.handle === "function") {
// 获取到嵌套 app 的 handle 属性
var server = handle;
// 保存请求路径
// TODO 好像没什么作用?
server.route = path;
// 中间件处理函数(调用 嵌套app 的 handle 方法)
handle = function (req, res, next) {
server.handle(req, res, next);
};
}
// 移除配置的 route 结尾的 / 字符
if (path[path.length - 1] === "/") {
path = path.slice(0, -1);
}
// 把中间件放入队列中
this.stack.push({ route: path, handle: handle });
return this;
}
中间件的执行方式
const app = connect();
http.createServer(app);
function app(req, res, next){ app.handle(req, res, next); }
app 作为一个回调函数传入到 createServer 方法中,当接受到请求时就会执行上面的 app 方法,核心就是 app.handle 方法:
执行 next 方法:
proto.handle = function handle(req, res, out) {
...
function next(err) {
// 下一个中间件
var layer = stack[index++];
// 全部中间件执行完毕
if (!layer) {
defer(done, err);
return;
}
// 解析请求路径
var path = parseUrl(req).pathname || "/";
// 中间件对应的路径
var route = layer.route;
// 路径不匹配直接换下一个中间件
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}
// 请求的路径长度 大于 中间件的路径,判断中间件路径是否包含在请求路径上
var c = path.length > route.length && path[route.length];
// route: /foo req.url: /foo.bar 和 /foo/bar 说明都符合设计,应该用当前的中间件处理
if (c && c !== "/" && c !== ".") {
return next(err);
}
// 删除 route,重新覆写 req.url 值(移除前面的 route)
if (route.length !== 0 && route !== "/") {
removed = route;
req.url = protohost + req.url.substr(protohost.length + removed.length);
}
// 执行中间件
call(layer.handle, route, err, req, res, next);
}
next();
}
根据当前的路由(key)来判断需要执行当前中间件。
使用 try…catch 来捕获中间件错误。
当存在错误的时候,中间件接收的第一个参数其实是 error,把错误暴露给中间件函数,由中间件函数自行处理逻辑。
function call(handle, route, err, req, res, next) {
try {
if (hasError && arity === 4) {
// 把错误暴露给中间件函数,由中间件函数自行处理逻辑
handle(err, req, res, next);
return;
} else if (!hasError && arity < 4) {
handle(req, res, next);
return;
}
} catch (e) {
// replace the error
error = e;
}
next(error);
}
通过 Function.length 来获取传入的参数个数。