内容简介:当我们在深入学习一个框架或者库时,为了了解它的思想及设计思路,也为了更好地使用和避免无意的 Bug,有时很有必要研究源码。对于 koa 这种极为简单,而应用却很广泛的框架/库更应该了解它的源码。而为了验证我们是否已足够了解它,可以实现一个仅仅具备核心功能的迷你的库。正所谓,麻雀虽小,五脏俱全。
当我们在深入学习一个框架或者库时,为了了解它的思想及设计思路,也为了更好地使用和避免无意的 Bug,有时很有必要研究源码。对于 koa 这种极为简单,而应用却很广泛的框架/库更应该了解它的源码。
而为了验证我们是否已足够了解它,可以实现一个仅仅具备核心功能的迷你的库。正所谓,麻雀虽小,五脏俱全。
删繁就简三秋树
,这里只用四十行代码实现一个小型的却具有其核心功能的 koa。
源码实现:https://github.com/shfshanyue/koa-mini
这是一个拥有 koa 几乎所有核心功能最简化的示例:
const Koa = require('koa') const app = new Koa() app.use(async (ctx, next) => { console.log('Middleware 1 Start') await next() console.log('Middleware 1 End') }) app.use(async (ctx, next) => { console.log('Middleware 2 Start') await next() console.log('Middleware 2 End') ctx.body = 'hello, world' }) app.listen(3000) // output // Middleware 1 Start // Middleware 2 Start // Middleware 2 End // Middleware 1 End
在这个最简化的示例中,可以看到有三个清晰的模块,分别如下:
-
Application: 基本服务器框架
-
Context: 服务器框架基本数据结构的封装,用以 http 请求解析及响应
-
Middleware: 中间件,也是洋葱模型的核心机制
我们开始逐步实现这三个模块:
抛开框架,来写一个简单的 server
我们先基于 node 最基本的 http API
来启动一个 http 服务,并通过它来实现最简版的 koa:
const http = require('http') const server = http.createServer((req, res) => { res.end('hello, world') }) server.listen(3000)
而要实现最简版的 koa
示例如下,我把最简版的这个 koa 命名为 koa-mini
const Koa = require('koa-mini') const app = new Koa() app.use(async (ctx, next) => { console.log('Middleware 1 Start') await next() console.log('Middleware 1 End') }) app.use(async (ctx, next) => { console.log('Middleware 2 Start') await next() console.log('Middleware 2 End') ctx.body = 'hello, world' }) app.listen(3000)
构建 Application
首先完成 Appliacation
的大体框架:
-
app.listen
: 处理请求及响应,并且监听端口 -
app.use
: 中间件函数,处理请求并完成响应
只有简单的十几行代码,示例如下:
const http = require('http') class Application { constructor () { this.middleware = null } listen (...args) { const server = http.createServer(this.middleware) server.listen(...args) } // 这里依旧调用的是原生 http.createServer 的回调函数 use (middleware) { this.middleware = middleware } }
此时调用 Application
启动服务的代码如下:
const app = new Appliacation() app.use((req, res) => { res.end('hello, world') }) app.listen(3000)
由于 app.use
的回调函数依然是原生的 http.crateServer
回调函数,而在 koa
中回调参数是一个 Context
对象。
下一步要做的将是构建 Context
对象。
构建 Context
在 koa 中, app.use
的回调参数为一个 ctx
对象,而非原生的 req/res
。因此在这一步要构建一个 Context
对象,并使用 ctx.body
构建响应:
-
app.use(ctx => ctx.body = 'hello, world') http.createServer Context
-
Context(req, res)
: 以request/response
数据结构为主体构造 Context 对象
核心代码如下,注意注释部分:
const http = require('http') class Application { constructor () {} use () {} listen (...args) { const server = http.createServer((req, res) => { // 构造 Context 对象 const ctx = new Context(req, res) // 此时处理为与 koa 兼容 Context 的 app.use 函数 this.middleware(ctx) // ctx.body 为响应内容 ctx.res.end(ctx.body) }) server.listen(...args) } } // 构造一个 Context 的类 class Context { constructor (req, res) { this.req = req this.res = res } }
此时 koa
被改造如下, app.use
可以正常工作:
const app = new Application() app.use(ctx => { ctx.body = 'hello, world' }) app.listen(7000)
实现以上的代码都很简单,现在就剩下一个最重要也是最核心的功能:洋葱模型
洋葱模型及中间件改造
上述工作只有简单的一个中间件,然而在现实中中间件会有很多个,如错误处理,权限校验,路由,日志,限流等等。因此我们要改造下 app.middlewares
-
app.middlewares
: 收集中间件回调函数数组,并并使用compose
串联起来
对所有中间件函数通过 compose
函数来达到抽象效果,它将对 Context
对象作为参数,来接收请求及处理响应:
// this.middlewares 代表所有中间件 // 通过 compose 抽象 const fn = compose(this.middlewares) await fn(ctx) // 当然,也可以写成这种形式,只要带上 ctx 参数 await compose(this.middlewares, ctx)
此时完整代码如下:
const http = require('http') class Application { constructor () { this.middlewares = [] } listen (...args) { const server = http.createServer(async (req, res) => { const ctx = new Context(req, res) // 对中间件回调函数串联,形成洋葱模型 const fn = compose(this.middlewares) await fn(ctx) ctx.res.end(ctx.body) }) server.listen(...args) } use (middleware) { // 中间件回调函数变为了数组 this.middlewares.push(middleware) } }
接下来,着重完成 compose
函数
完成 compose 函数的封装
koa 的洋葱模型指出每一个中间件都像是洋葱的每一层,当从洋葱中心穿过时,每层都会一进一出穿过两次,且最先穿入的一层最后穿出。
-
middleware
: 第一个中间件将会执行 -
next
: 每个中间件将会通过 next 来执行下一个中间件
我们如何实现所有的中间件的洋葱模型呢?
我们看一看每一个 middleware 的 API 如下
middleware(ctx, next)
而每个中间件中的 next
是指执行下一个中间件,我们来把 next
函数提取出来,而 next
函数中又有 next
,这应该怎么处理呢?
const next = () => nextMiddleware(ctx, next) middleware(ctx, next(0))
是了,使用一个递归完成中间件的改造,并把中间件给连接起来,如下所示:
// dispatch(i) 代表执行第 i 个中间件 const dispatch = (i) => { return middlewares[i](ctx, () => dispatch(i+1)) } dispatch(0)
dispatch(i)
代表执行第 i 个中间件,而 next()
函数将会执行下一个中间件 dispatch(i+1)
,于是我们使用递归轻松地完成了洋葱模型
此时,再把递归的终止条件补充上: 当最后一个中间件函数执行 next()
时,直接返回
const dispatch = (i) => { const middleware = middlewares[i] if (i === middlewares.length) { return } return middleware(ctx, () => dispatch(i+1)) } return dispatch(0)
最终的 compose
函数代码如下:
function compose (middlewares) { return ctx => { const dispatch = (i) => { const middleware = middlewares[i] if (i === middlewares.length) { return } return middleware(ctx, () => dispatch(i+1)) } return dispatch(0) } }
至此,koa 的核心功能洋葱模型已经完成,写个示例来体验一下吧:
const app = new Application() app.use(async (ctx, next) => { ctx.body = 'hello, one' await next() }) app.use(async (ctx, next) => { ctx.body = 'hello, two' await next() }) app.listen(7000)
此时还有一个小小的但不影响全局的不足:异常处理,下一步将会完成异常捕获的代码
异常处理
如果在你的后端服务中因为某一处报错,而把整个服务给挂掉了怎么办?
我们只需要对中间件执行函数进行一次异常处理即可:
try { const fn = compose(this.middlewares) await fn(ctx) } catch (e) { console.error(e) ctx.res.statusCode = 500 ctx.res.write('Internel Server Error') }
然而在日常项目中使用时,我们 必须 在框架层的异常捕捉之前就需要捕捉到它,来做一些异常结构化及异常上报的任务,此时会使用一个异常处理的中间件:
// 错误处理中间件 app.use(async (ctx, next) => { try { await next(); } catch (err) { // 1. 异常结构化 // 2. 异常分类 // 3. 异常级别 // 4. 异常上报 } })
小结
koa
的核心代码特别简单,如果你是一个 Node 工程师,非常建议在业务之余研究一下 koa 的源码,并且自己也实现一个最简版的 koa。
我源码实现的仓库为:koa-mini
以上所述就是小编给大家介绍的《使用四十行代码实现一个精简版 koa》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Rework
Jason Fried、David Heinemeier Hansson / Crown Business / 2010-3-9 / USD 22.00
"Jason Fried and David Hansson follow their own advice in REWORK, laying bare the surprising philosophies at the core of 37signals' success and inspiring us to put them into practice. There's no jarg......一起来看看 《Rework》 这本书的介绍吧!