PK创意闹新春,我正在参加「春节创意投稿大赛」,详情请看:春节创意投稿大赛
介绍
Connect是一个基于Node可扩展的的HTTP服务器中间件框架, 通过以“中间件”的形式处理请求;
使用方法
安装Connect
yarn add connect
创建app
创建的app应用,将会存储后续添加的所有的中间件, 并且 connect() 返回的是一个函数,因为原生的Node的HTTP的API的createServer的参数就是一个函数
const connect = require('connect')
const app = connect()
// const app = require('connect')()
使用中间件
- 使用第三方中间件
比如: 引用
body-parser中间件,body-parser是一个对请求的req.body进行处理的中间件
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))
- 自定义中间件
const app = require('connect')()
// 使用中间件
app.use(function middleWare1(req, res, next) {
// 处理 用户的请求 和 响应
next()
})
app.use(function middleWare2(req, res, next) {
next()
})
app.use(function(req, res){
res.end('<h1>Hello word</h1>')
})
- 从app创建服务
// 第一种
const http = require('http')
http.createServer(app).listen(4000)
// 第二种
app.listen(4000)
第二种方式 其实也是 对第一种方式的封装
源码阅读
源码地址: Connect源码地址
引入依赖
// 第16 —— 21行 引入依赖
var debug = require('debug')('connect:dispatcher');
var EventEmitter = require('events').EventEmitter;
var finalhandler = require('finalhandler');
var http = require('http');
var merge = require('utils-merge');
var parseUrl = require('parseurl');
| 依赖名称 | 用途 | 来源 |
|---|---|---|
debug | 轻量级的调试工具 | 第三方 |
finalhandler | 作为响应的最后一步,触发HTTP的Response | 第三方 |
utils-merge | 合并 | 第三方 |
parseUrl | 转译URL | 第三方 |
events | 事件机制 | Node |
http | 创建HTTP服务 | Node |
// 第35 —— 36行
var env = process.env.NODE_ENV || 'development'; // 环境变量
var proto = {};
// 第39 —— 40行 延迟函数, 使用 setImmediate 或者 process.nextTick
var defer = typeof setImmediate === 'function'
? setImmediate
: function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
创建应用(createServer函数)
// 第28行 模块导出
module.exports = createServer;
// 第50 —— 57行 创建应用 并且返回
function createServer() {
function app(req, res, next){ app.handle(req, res, next); } // 当服务器有请求时
merge(app, proto);
merge(app, EventEmitter.prototype);
app.route = '/'; // 路由
app.stack = []; // 中间件
return app;
}
配置中间件(app.use)
// 第76 —— 110行
proto.use = function use(route, fn) {
var handle = fn;
var path = route;
// default route to '/'
// 如果 route 不为空的话, 那么 将 '/' 设置为 默认路径
if (typeof route !== 'string') {
handle = route;
path = '/';
}
// wrap sub-apps
if (typeof handle.handle === 'function') {
var server = handle;
server.route = path;
handle = function (req, res, next) {
server.handle(req, res, next);
};
}
// wrap vanilla http.Servers
if (handle instanceof http.Server) {
handle = handle.listeners('request')[0];
}
// strip trailing slash
// 去掉最后一个 '/'
if (path[path.length - 1] === '/') {
path = path.slice(0, -1);
}
// add the middleware
debug('use %s %s', path || '/', handle.name || 'anonymous');
this.stack.push({ route: path, handle: handle });
return this;
};
app.use的函数 将传入的参数 最终 处理成 { route: path, handle: handle }追加到 app.stack数组中, 参数如下:
app.use(route, fn)
app.use('/a', (req, res, next) => {
next()
})
app.use(fn)
app.use((req, res, next) => {
next()
})
app.use({ route, handle })
app.use({ handle: (req, res, next) => {
next()
}})
app.use(http.Server的实例)
const connect = require('connect')
const app = connect()
const http = require('http')
const server = http.createServer((req, res) => {
res.end()
})
app.use(server)
server.listeners(eventName): 返回 名为 eventName 的事件的 数组拷贝
备注
const server = http.createServer(function a() {}) // 等价于 const server = http.createServer() server.on('request', function a() {})
server.listeners('request') 就是拿到 ‘request’事件的所有的回调函数组成的数组
处理请求
// 第119 —— 187行
proto.handle = function handle(req, res, out) {
var index = 0;
var protohost = getProtohost(req.url) || '';
var removed = '';
var slashAdded = false;
var stack = this.stack;
// final function handler
// 最终处理函数
var done = out || finalhandler(req, res, {
env: env,
onerror: logerror
});
// store the original URL
// 保存原始URL
req.originalUrl = req.originalUrl || req.url;
// next函数
function next(err) {
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
if (removed.length !== 0) {
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
}
// next callback
var layer = stack[index++];
// all done
if (!layer) {
defer(done, err);
return;
}
// route data
var path = parseUrl(req).pathname || '/';
var route = layer.route;
// skip this layer if the route doesn't match
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}
// skip if route match does not border "/", ".", or end
var c = path.length > route.length && path[route.length];
if (c && c !== '/' && c !== '.') {
return next(err);
}
// trim off the part of the url that matches the route
if (route.length !== 0 && route !== '/') {
removed = route;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// ensure leading slash
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
}
// call the layer handle
call(layer.handle, route, err, req, res, next);
}
next();
};
这边是Connect框架的核心, 他会根据请求的pathname, 去遍历MiddleWare Stack, 当 匹配的到的路由route === pathname 或者 route 是 pathname路径上的一部分(换句话说就是 pathname 是 route的子路由, 那么 回调函数就会触发,
举个例子: 访问路径 localhost:3000/a/b/cd
const app = require('connect')()
app.use('/a/b', function(req, res, next) {
console.log('/a/b', '会触发, 因为/a/b/cd 是 /a/b的子路由')
next()
})
app.use('/a/b/c', function(req, res, next) {
console.log('/a/b/c', '不会触发, 因为 /a/b/c 与 /a/b/cd 不相等, 同时不是 /a/b/cd 也不是 /a/b/c 子路由')
next()
})
app.use('/a/b/cd', function(req, res) {
console.log('/a/b/cd', '会触发, 因为匹配到了')
res.end('/a/b/cd')
})
app.listen(3000)
重点
在我们看源码的时候,会看到如下的代码, c && c!=='/' && c!== '.' 也就是当 路径/a/b/c.d也被看到成 /a/b/c的子路由, 这样会调用 /a/b/c注册的中间件;
/a/b/c.d -> /a/b/c
var c = path.length > route.length && path[route.length];
if (c && c !== '/' && c !== '.') {
return next(err);
}
graph LR
/a/b/c.d(路径/a/b/c/d) -- 中间件 --> / -- 中间件 --> /a -- 中间件 --> /a/b -- 中间件 -->/a/b/c --> !(最终路由/a/b/c.d)
/a/b -- 不会到达 --> /a/b/d(/a/b/d)
通过app.listen直接创建应用
// 第215 —— 218行
proto.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
触发路由处理
// 第225 —— 249行
function call(handle, route, err, req, res, next) {
var arity = handle.length; // 函数的参数
var error = err; // 错误信息
var hasError = Boolean(err); // 是否有错误
debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);
try {
if (hasError && arity === 4) { // 如果有错,并且中间件的会掉函数是4个
// error-handling middleware // 错误优先
handle(err, req, res, next);
return;
} else if (!hasError && arity < 4) { // 如果没有错误, 并且中间件的 参数小于4个
// request-handling middleware
handle(req, res, next);
return;
}
} catch (e) { // 捕获错误
// replace the error
error = e;
}
// continue // 继续下一个 中间件
next(error);
}
错误日志
// 第258 —— 260行
function logerror(err) {
if (env !== 'test') console.error(err.stack || err.toString());
}
获取协议和主机
对传入的url,获取 协议和主机, 比如http://www.baid.com/ahha/12321 会返回 http://www.baid.com
// 第269 —— 279行
function getProtohost(url) {
// 如果长度为0 或 或者 以 '/' 开头的字符串 直接返回 undefined
if (url.length === 0 || url[0] === '/') {
return undefined;
}
var fqdnIndex = url.indexOf('://')
return fqdnIndex !== -1 && url.lastIndexOf('?', fqdnIndex) === -1
? url.substr(0, url.indexOf('/', 3 + fqdnIndex))
: undefined;
}
-
String.prototype.lastIndexOf(search, fromIndex): 从指定的索引fromIndex开始向前查询search最后一次出现的索引1.1
fromIndex大于 字符串的长度,fromIndex = s.length1.2fromIndex小于 0, 等同于fromIndex = 01.3search为空, 则返回fromIndex的值