本文将带大家实现轻量级 web 框架 connect 的主要功能,只要 82 行代码就能搞定。
我并没有标题党,因为 Express 在 v3 版本之前就是基于 connect 进行封装的,不过在 v4 版本就将 connect 依赖移除了,代码被搬到 Express 仓库里,并做了一些细微调整。因此某种程度上,学习 connect 就是在学习 Express。
connect 的 repo 描述是:"Connect is a middleware layer for Node.js",也就是一个 Node.js 的中间件层。中间件层是一个非常有用的机制,它类似一个插件系统,让我们可以通过插拔的方式组合不同功能来处理请求。
基本使用
先来看看 connect 的使用。
javascript
const connect = require('connect')
const app = connect()
// respond to all requests
app.use(function(req, res){
res.end('Hello from Connect!\n')
})
// create node.js http server and listen on port
http.createServer(app).listen(3000)
跟 Express 一样。
另外,app
上还提供了 .listen()
方法,用于替代 http.createServer(app).listen(3000)
的冗长写法。
javascript
app.listen(3000) // 等价于 http.createServer(app).listen(3000)
再看看中间件的使用。
javascript
app.use(function middleware1(req, res, next) {
// middleware 1
next()
});
app.use(function middleware2(req, res, next) {
// middleware 2
next()
});
我们通过 app.use()
方法收集并使用中间件。
中间件就是一个函数,包含 3 个参数:req
、res
还有 next()
。在一个中间件内调用 next()
,就进入到下一个中间件的执行。
同时,我们还可以为中间件指定路由,这样中间件只在特定路径下起作用。
javascript
app.use('/foo', function fooMiddleware(req, res, next) {
// req.url starts with "/foo"
next()
})
app.use('/bar', function barMiddleware(req, res, next) {
// req.url starts with "/bar"
next()
})
本质上,纯中间件的写法就是在设置根路由 ('/'
),所以会对所有请求有效。
javascript
app.use(function middleware1(req, res, next) {
// middleware 1
next()
})
// 等同于
app.use('/', function middleware1(req, res, next) {
// middleware 1
next()
})
不过还有一类特殊中间件------异常中间件,专门用于处理前面流程里的异常错误。
javascript
// regular middleware
app.use(function (req, res, next) {
// i had an error
next(new Error('boom!'));
});
// error middleware for errors that occurred in middleware
// declared before this
app.use(function onerror(err, req, res, next) {
// an error occurred!
});
异常中间件必须是 4 个参数,第一个参数就是 error
,对应前面流程中传递给 next()
的 Error
对象。
以上,我们就讲完了 connect 库的基本使用。接下来,就着手实现。
代码实现
刚学 Node.js 的时候,我们学到第一个例子,可能就是启动一个会说"Hello World"的服务器了。
javascript
const http = require('node:http')
const hostname = '127.0.0.1'
const port = 3000
const server = http.createServer((req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.end('Hello World\n')
})
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`)
})
回顾 connect 的使用。
javascript
const connect = require('connect')
const app = connect()
// respond to all requests
app.use(function(req, res){
res.end('Hello from Connect!\n')
})
// create node.js http server and listen on port
app.listen(3000)
实现 app.listen()
我们已经知道 app.listen(3000)
内部实现就是 http.createServer(app).listen(3000)
。
因此,我们先实现 .listen()
方法。
javascript
module.exports = function createApplication() {
const app = {}
app.listen = function listen(...args) {
const server = require('node:http').createServer(/* ? */)
return server.listen(args);
}
return app
}
假设 app
是一个对象。不过,http.createServer(/* ? */)
中的 ?
内容该如何实现呢?
实现 app.use()
前一步,我们做了 app.use()
的调用。
javascript
// respond to all requests
app.use(function(req, res){
res.end('Hello from Connect!\n')
})
所以,当服务启动后,访问 localhost:3000 时,应该返回 "Hello from Connect!"
的文本。
同时,app.use()
又支持重复调用。
javascript
// respond to all requests
app.use(function(req, res, next) {
console.log('log req.url', req.url)
next()
})
// respond to all requests
app.use(function(req, res) {
res.end('Hello from Connect!\n')
})
那我们就考虑先用个数组,把通过 app.use()
调用传入进来的回调函数存起来。
javascript
module.exports = function createApplication() {
const app = {}
app.stack = []
app.use = function use(route, fn) {
let path = route
let handle = fn
// default route to '/'
if (typeof route !== 'string') {
path = '/'
handle = route
}
this.stack.push({ route: path, handle })
return this
}
app.listen = function listen() {
const server = http.createServer(/* ? */)
return server.listen.apply(server, arguments)
}
return app
}
我们把调用 app.use()
传入的中间件都存到了 app.stack
里。
根据定义可知,http.createServer(/* ? */)
中的 ?
内容应该是一个函数。针对当前场景,它是用来处理 stack
中的这些中间件的。
实现 app.handle()
我们把这些逻辑写在 app.handle()
内。
javascript
module.exports = function createApplication() {
const app = {}
app.stack = []
// ...
app.handle = function handle(res, res) {
// TODO
}
return app
}
每当请求来临,都由 app.handle
负责处理。
app.handle
的主要逻辑主要是处理 3 件事情。
- 获取当前要处理的路由,没有的话就交由最终处理函数
done
- 路由不匹配就跳过
- 路由匹配就执行当前中间件
javascript
app.handle = function handle(req, res) {
let index = 0
const done = function (err) { /* ... */ }
function next(err) {
// next callback
const layer = app.stack[index++]
// 1) all done
if (!layer) {
setImmdiate(done, err)
return
}
// route data
const path = require('node:url').parse(req.url).pathname
const route = layer.route
// 2) skip this layer if the route doesn't match
if (!path.toLowerCase().startsWith(route.toLowerCase())) {
return next(err)
}
// 3) call the layer handle
const arity = handle.length
const hasError = !!err
let error = err
try {
if (hasError && arity === 4) {
// error-handling middleware
layer.handle(err, req, res, next)
return
} else if (!hasError && arity < 4) {
// request-handling middleware
layer.handle(req, res, next)
return
}
} catch (e) {
error = e
}
next(error)
}
next()
}
以上的关键处理就封装在 next()
函数中。而 next()
函数就是传递给 connect 中间件的 next
参数。
这样,每次请求进来,我们都会从 app.stack
的第一个中间件(stack[0]
)开始处理,就实现了以 next
参数为连接桥梁的中间件机制。
值得注意的是调用当前中间件的逻辑,当我们调用 layer.handle(err, req, res, next)
/layer.handle(req, res, next)
时,处理流程会流入中间件内部,当内部调用 next()
函数后,控制权会重新回到 app.handle
,继续处理队列中的下一个中间件。
当请求最终没有任何中间件可以处理时,就会流入到 done
,这是最终处理器。处理器内部,会根据是否存在错误,分别返回 404
或 5xx
响应。
javascript
const done = function (err) {
if (err) {
res.statusCode = err.status ?? err.statusCode ?? 500
res.statusMessage = require('node:http').STATUS_CODES[404]
} else {
res.statusCode = 404
res.statusMessage = `Cannot ${req.method} ${require('node:url').parse(req.url).pathname}`
}
res.end(`${res.statusCode} ${res.statusMessage}`)
}
至此,我们基本写完了所有的逻辑。
当然,有一个地方,可以做一个小小的优化。将 http.createServer(app.handle.bind(app))
简化成 http.createServer(this)
,不过此时 app
就不能是对象,而是函数了。
javascript
module.exports = function createApplication() {
function app(req, res) { app.handle(req, res) }
// ...
app.listen = function listen(...args) {
const server = require('node:http').createServer(app)
return server.listen(...args)
}
// ...
return app
}
最后,我们整体来回顾一下。
javascript
module.exports = function createApplication() {
function app(req, res) { app.handle(req, res) }
app.stack = []
app.use = function use(route, fn) {
let path = route
let handle = fn
// default route to '/'
if (typeof route !== 'string') {
path = '/'
handle = route
}
this.stack.push({ route: path, handle })
return this
}
app.listen = function listen(...args) {
const server = require('node:http').createServer(app)
return server.listen(...args)
}
app.handle = function handle(req, res) {
let index = 0
const done = function (err) {
if (err) {
res.statusCode = err.status ?? err.statusCode ?? 500
res.statusMessage = require('node:http').STATUS_CODES[404]
} else {
res.statusCode = 404
res.statusMessage = `Cannot ${req.method} ${require('node:url').parse(req.url).pathname}`
}
res.end(`${res.statusCode} ${res.statusMessage}`)
}
function next(err) {
// next callback
const layer = app.stack[index++]
// 1) all done
if (!layer) {
setImmediate(done, err)
return
}
const path = require('node:url').parse(req.url).pathname
const route = layer.route
// 2) skip this layer if the route doesn't match
if (!path.toLowerCase().startsWith(route.toLowerCase())) {
return next(err)
}
// 3) call the layer handle
const arity = handle.length
const hasError = !!err
let error = err
try {
// error-handling middleware
if (hasError && arity === 4) {
layer.handle(err, req, res, next)
return
// request-handling middleware
} else if (!hasError && arity < 4) {
layer.handle(req, res, next)
return
}
} catch (e) {
error = e
}
next(error)
}
next()
}
return app
}
连上注释,我们只用了 82 行代码,就实现了 connect 的主要功能。
总结
本文带大家实现了轻量级 Web 框架 connect 的主要功能,同样这也是一个简易版本 Express!
实现核心是 2 个函数。
app.use(route, fn)
:用于收集中间件app.handle(res, req)
:用于消费中间件。主要逻辑位于next()
函数,这是传递给中间件的next
参数。每一次接收请求来临时,都由app.handle
负责处理
而这两个函数之间的桥梁就是 app.stack
。
行文最后,给大家留一个思考题。
connect()
实例的真实实现,是支持作为子应用,挂载到父应用之上的,也就是下面的用法。
javascript
const connect = require('connect')
const app = connect()
const blogApp = connect()
app.use('/blog', blogApp)
app.listen(3000)
甚至 http.Server
实例也支持挂载。
javascript
const connect = require('connect')
const app = connect()
const blog = http.createServer(function(req, res){
res.end('blog')
})
app.use('/blog', blog)
那是如何实现呢?
大家可以参照 app.use()
函数的源码进行学习。
感谢的你的阅读,再见~