前言
Koa 是运行在 Node.js 中的 web 服务框架,小而美。
Koa2 是 Koa 框架的最新版本,Koa3 还没有正式推出,Koa1 正走在被替换的路上。
Koa2 与 Koa1 的最大不同,在于 Koa1 基于 co 管理 Promise/Generator 中间件,而 Koa2 紧跟最新的 ES 规范,支持到了 Async Function(Koa1 不支持),两者中间件模型表现一致,只是语法底层不同。
Koa2 正在蚕食 Express 的市场份额,最大的原因是 Javascript 的语言特性进化,以及 Chrome V8 引擎的升级,赋予了 Node.js 更大的能力,提升开发者的编程体验,满足开发者灵活定制的场景以及对于性能提升的需求,蚕食也就水到渠成,2018 年开始,Koa2 会超越 Express 成为本年最大普及量的 Node.js 框架。
以上就是 Koa2 的现状,以及它的趋势,站在 2018 年的节点来看,Koa2 的学习大潮已经到来,那么如果要掌握 Koa2,需要去学习它的哪些知识呢,这些知识跟 Node.js 以及语言规范有什么关系,它的内部组成是如何的,运行机制怎样,定制拓展是否困难,以及它的三方库生态如何,应用场景有哪些,跟前端有如何结合等等,这些问题本文将做简要的探讨,Koa2 详细的代码案例和深度剖析见这里 。
备注:如下提到的 Koa 均指代 Koa 2.x 版本
关于作者 TJ
了解过 TJ 的童鞋都知道,他以惊为天人的代码贡献速度、源源不断的开发热情和巧夺天工的编程模型而推动整个 Node.js/NPM 社区大步迈进,称为大神毫不过分,而大神的脑回路,向来与凡人不同。
关于大神的传说有很多,最有意思的是在国外著名程序员论坛 reddit 上,有人说,TJ 从来就不是一个人,一个人能有这么高效而疯狂的代码产出实在是太让人震惊了,他背后一定是一个团队,因为他从来都不参加技术会议,也不见任何人,而最后 TJ 离开 Node 社区去转向 Go,这种做事方式非常谷歌,所以 TJ 是谷歌的一个招牌,大家众说纷纭,吵的不可开交,不过有一点大家都是达成共识的,那就是非常肯定和感谢他对于 Nodejs 社区的贡献和付出。
Express 的架构和中间件模型
聊 Koa 之前,先对比下 Express,在 Express 里面,不同时期的代码组织方式虽然大为不同,比如早期是全家桶各种路由、表单解析都囊括到一个项目中,中后期做了大量的拆分,将大部分模块都独立出来官方自行维护,或者是采用社区其他开发者提供的中间件模块,但纵观 Express 多年的历程,他依然是相对大而全,API 较为丰富的框架,并且它的整个中间件模型是基于 callback 回调,而 callback 常年被人诟病。
对于一个 web 服务框架来说,它的核心流程,就是在整个 HTTP 进入到流出的过程中,从它的流入数据上采集所需要的参数素材,再向流出的数据结构上附加期望素材,无论是一个静态文件还是 JSON 数据,而在采集和附加的过程中,需要各个中间件大佬的参与,有的干的是记录日志的活儿,有的干的是解析表单的活儿,有的则是管理会话,既然是大佬,一般都脾气大,你不安排好他们的注册顺序,不通过一种机制管理他们的入场退场顺序,他们不仅不好好配合,还可能砸了你的场子。
那么 Express 里面,首先就是对于 HTTP 这个大家伙的管理(其他协议先不涉及),管理这个大家伙,Express 祭出了三件,哦不,其实是四件法宝。
首先是通过 express() 拿到的整个服务器运行实例,这个实例相当于是一个酒店,而你就是来访的客人 - HTTP 请求,酒店负责你一切需求,做到你满意。
在酒店里面,还有两个工作人员,一个是 req(request) 负责接待你的叫阿来吧,还有一个送你离开的狠角色 - res(response),叫阿去吧,阿来接待到你进酒店,门口的摄像头会你拍照(Log 记录来去时间,你的特征),收集你的指纹(老会员识别),引领你去前台签到(获取你的需求,比如你要拿走属于你的一套西服),然后酒店安排你到房间休息(等待响应),里面各种后勤人员忙忙碌碌接待不同的客人,其中有一个是帮你取西服的,取了后,交给阿来,阿来再把西服穿你身上,同时还可能帮你装饰一番,比如给你带个帽子(加个自定义头),然后送你出门,门口的摄像头还会拍你一下,就知道了酒店服务你的时间......实在编不下去了,想用物理世界的案例来对应到程序世界是蛮难的,严谨度不够,不过帮新手同学留下一个深刻印象倒是可取的。
Express 源码简要分析
上面酒店的 4 件法宝,其实就是服务器运行实例,req 请求对象,res 响应对象和中间件 middlewares,刚才负责照相的,签到的,分析需求的其实都是中间件,一个一个滤过去,他们根据自己的规则进行采集、分析、转化和附加,把这个 HTTP 客人,从头到脚捏一遍,客人就舒舒服服的离开了。
中间件是众多 web 框架中比较核心的概念,它们可以根据不同的场景,来集成到框架中,增强框架的服务能力,而框架则需要提供一套机制来保证中间件是有序执行,这个机制在不同的框架中则大为不同,在 Express 里面,我们通过 use(middlewares()) 逐个 use 下去,use 的顺序和规则都由 express 自身控制。
在 express/express.js 中,服务器运行实例 app 通过 handle 来把 Nodejs 的 req 和 res 传递给 handle 函数,赋予 handle 对于内部对象的控制权:
app = function(req, res, next) {
app.handle(req, res, next)
}
而在 express/application.js 中,拿到控制权的 handle 又把请求响应和回调,继续分派给了 express 的核心路由模块,也就是 router:
app.handle = function handle (req, res, callback) {
var router = this._router
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
})
router.handle(req, res, done)
}
这里的 router.handle 就持有到了 req, res 对象,可以理解为,express 把 Nodejs 监听到的请求三要素(req, res, cb) 下放给了内部的路由模块 router。
然后继续回到刚才 use(middlewares(),Express 每一次 use 中间件,都会把这个中间件也交给 router:
app.use = function use(fn) {
router.use('/', fn)
}
而 router 里面,有很重要一个概念,就是 layer 层,可以理解为中间件堆叠的层,一层层堆叠起来:
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn)
this.stack.push(layer)
以上是伪代码(删减了大部分),可以看做是 express 在启动运行的时候,注册好了一个中间件函数栈,里面堆叠好了待被调用的中间件,一旦请求进来,就会被 router handle 来处理:
proto.handle = function handle(req, res, out) {
next()
function next(err) {
var layer
var route
self.process_params(layer, paramcalled, req, res, function (err) {
if (route) {
return layer.handle_request(req, res, next)
}
trim_prefix(layer, layerError, layerPath, path)
})
}
function trim_prefix(layer, layerError, layerPath, path) {
if (layerError) {
layer.handle_error(layerError, req, res, next)
} else {
layer.handle_request(req, res, next)
}
}
}
handle 里面的 next 是整个中间件栈能够转起来的关键,在所有的中间件里面,都要执行这个 next,从而把当前的控制权以回调的方式往下面传递。
但是问题就是这种机制在最初的时候,如果没有事件的配合,是很难做到原路进去,再顺着原路回去,相当于是每个中间件都被来回滤了 2 遍,赋予中间件更灵活的控制权,这就是掣肘 Express 的地方,也是 Express 市场一定会被 Koa 蚕食的重要原因。
具体 Express 的代码比这里描述的要复杂好几倍,大家有兴趣可以去看源码,应该会有更多的收获,如果没有 Koa 这种框架存在的话,Express 的内部实现用精妙形容绝对不为过,只是这种相对复杂一些的内部中间件机制,未必符合所有人的口味,也说明了早些年限于 JS 的能力,想要做一些流程双向控制多么困难。
关于 Express 就分析到这里,这不是本文的重点,了解它内部的复杂度以及精妙而复杂都实现就可以了,因为这是特定历史阶段的历史产物,有它特定的历史使命。
早期的 Koa 模型 - 我们不一样
得益于大神非同寻常的脑回路,Koa 从一开始就选择了跟 Express 完全不同的架构方向,上面 Express 的部分大家没看懂也没关系,因为 Koa 这里的处理,会让你瞬间脑回路清晰。
首先要明白,Koa 与 Express 是在做同样事情上的不同实现,所以意味着他俩对外提供的能力大部分是相同的,这部分不赘述,我们看不同的地方:
Koa 内部也有几个神行太保,能力较大,首先 new Koa() 出来的服务器运行实例,它像青蛙一样,张大嘴吞食所有的请求,通过它可以把服务真正跑起来,跟 Express 一样,这个就跳过不提了,重点是它的 context,也就是 ctx,这货上面有很多引用,最核心的是 request 和 response,这俩可以对应到 Express 两个对立的 req 和 res,在 Koa 里面,把它俩都集中到 ctx 里面进行管理,分别通过 ctx.request 和 ctx.reponse 进行直接访问,原来 Express 两个独立对象做的事情,现在一个 ctx 就够了,上下文对象都在他手中,想要联系谁就能联系谁。
其次是它的中间件机制,Koa 真正的魅力所在,先看段代码:
const Koa = require('koa')
const app = new Koa()
const indent = (n) => new Array(n).join(' ')
const mid1 = () => async (ctx, next) => {
ctx.body = `<h3>请求 => 第一层中间件</h3>`
await next()
ctx.body += `<h3>响应 <= 第一层中间件</h3>`
}
const mid2 = () => async (ctx, next) => {
ctx.body += `<h3>${indent(4)}请求 => 第二层中间件</h3>`
await next()
ctx.body += `<h3>${indent(4)}响应 <= 第二层中间件</h3>`
}
app.use(mid1())
app.use(mid2())
app.use(async (ctx, next) => {
ctx.body += `<p style="color: #f60">${indent(12)}=> Koa 核心 处理业务 <=</p>`
})
app.listen(2333)
大家可以把这 22 行代码跑起来,浏览器里访问 localhost:2333 就能看到代码的执行路径,一个 HTTP 请求,从进入到流出,是两次穿透,每一个中间件都被穿透两次,这个按照次序的正向进入和反向穿透并不是必选项,而是 Koa 轻松具备的能力,同样的能力,在 Express 里面实现反而很费劲。
Koa2 源码简要分析
想要了解上面提到的能力,就要看下 Koa 核心的代码:
同样是 app.use(middlewares()),在 koa/application.js 里面,每一个中间件同样被压入到一个数组中:
use(fn) {
this.middleware.push(fn)
}
在服务器启动的时候,建立监听,同时注册回调函数:
listen(...args) {
server = http.createServer(this.callback()).listen(...args)
}
回调函数里面,返回了 (req, res) 给 Node.js 用来接收请求,在它内部,首先基于 req, res 创建出来 ctx,就是那个同时能管理 request 和 response 的家伙,重点是上面压到数组里面的 middlewares 被 compose 处理后,就扔给了 handleRequest:
callback() {
const fn = compose(this.middleware)
return handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
}
compose 就是 koa-compose,简单理解为通过它,以递归的方式实现了 Promise 的链式执行,因为我们都知道, async function 本质上会返回一个 Promise,这里 compose 跳过不说了,继续去看 handleRequest:
handleRequest(ctx, fnMiddleware) {
return fnMiddleware(ctx).then(respond(ctx))
}
实在是简洁的不像实力派,请求进来后,会把可以递归调用的中间件数组都执行一遍,每个中间件都能拿到 ctx,同时,因为 async function 的语法特性,可以中间件中,把执行权交给后面的中间件,这样逐层逐层交出去,最后再逐层逐层执行回来,就达到了请求沿着一条路进入,响应沿着同样的一条路反向返回的效果。
借用官方文档的一张图来表达这个过程:
我知道这张图还不够,再祭出官方的第二张图,著名的洋葱模型:
Koa2 要学习什么
从上面的对比,我们其实就发现了 Koa2 独具魅力的地方,这些魅力一方面跟框架设计理念有关,一方面跟语言特性有关,语言特性,无外乎下面几个:
- 箭头函数
- Promise 规范
- 迭代器生成器函数执行原理
- 异步函数 Async Function
- 以及 Koa2 的应用上下文 ctx 的常用 API(也即它的能力)
- koa-compose 工具函数的递归特征
- 中间件执行的进出顺序和用法
这些都是基础性的值得学习的,这些知识跟着语言规范有着非常亲近的关系,所以意味着学会这些以后,也需要去到 ES6/7/8 里面挑选更多的语法特性,早早入坑学习,限于篇幅本文均不再探讨,上面的基础知识学习如果有兴趣,可以跟着 Koa2解读+数据抓取+实战电影网站 了解更多实战姿势。
Koa2 和 Express 到底如何选择
能不能来个痛快话?其实可以的,选 Koa2 吧,2018 年了,不用等了。
同时一定非它不可么,其实也不是,我们可以更加客观的看待选择问题,再梳理下思绪:
Koa 是基于新的语法特性,实现了 Promise 链传递,错误处理更友好,Koa 不绑定任何中间件,是干干净净的裸框架,需要什么就加什么,Koa 对流支持度很好,通过上下文对象的交叉引用让内部流程与请求和响应串联的更紧凑,如果 Express 是大而全,那么 Koa 就是小而精,二者定位不同,只不过 Koa 扩展性非常好,稍微组装几个中间件马上就能跟 Express 匹敌,代码质量也更高,设计理念更先进,语法特性也更超前。
这是站在用户的角度比较的结果,如果站在内部实现的角度,Koa 的中间件加载和执行机制跟 Express 是截然不同的,他俩在这一点上的巨大差别也导致了一个项目可以完全走向两种不同的中间件设计和实现方式,不过往往我们是作为框架的使用者,业务的开发者来使用的,那么对于 Nodejs 的用户来说,Express 能满足你的,Koa 都可以满足你,Express 让你爽的,Koa 可以让你更爽。
这也是为什么,阿里的企业级框架 Eggjs 底层是 Koa 而不是 Express,360 公司的大而全的 thinkjs 底层也是 Koa,包括沃尔玛的 hapi 虽然没有用 Koa,但是他的核心开发者写博客说,受到 Koa 的冲击和影响, 也要升级到 async function,保持对语法的跟进,而这些都是 Koa 已经做好了整个底子,任何上层架构变得更简单了。
大家在选用 Express 的时候,或者从 Express 升级到 Koa 的时候,其实不用太纠结,只要成本允许,都可以使用,如果实现成本过高,那么用 Express 也没问题的,遇到其他新项目的时候,没有了历史包袱,在用 Koa 也不迟。
Koa 运行机制和 Nodejs 事件循环
其实通过上面的篇幅,我们对于内部组成基本了解了,运行机制其实就是中间件执行机制,而定制拓展性,我们上面提到了 Eggjs 和 Thinkjs 已经充分证明了它可定制的强大潜力,这里我们主要聊下跟运行机制相关的,一个是 Koajs 自身,另外的一个是通过它向下到 Node.js 底层,它的运行机制是怎样的,这块涉及到 Libuv 的事件循环,如果不了解的话,很难在 Node.js 这颗技能树上再进一台阶,所以它也非常重要。
而 Libuv 的事件循环,本质上决定了 Node.js 的异步属性和异步能力,提到异步,我们都知道 Node.js 的异步非阻塞 IO,但是大家对于 同步异步以及阻塞非阻塞,都有了自己的理解,说到异步 IO,其实往往我们说的是操作系统所提供的异步 IO 能力,那首先什么是 IO,说白了,就是数据进出,人机交互的时候,我们会把键盘鼠标这些外设看做是 Input,也就是输入,对应到主机上,会有专门流入数据或者信号的物理接口,显示器作为一个可视化的外设,对应到主机上,会有专门的输出数据的接口,这就是生活中我们可见的 IO 能力,这个接口再向下,会进入到操作系统这个层面,在操作系统层面,会提供诸多的能力,比如磁盘读写,DNS 查询,数据库连接,网络请求接收与返回等等,在不同的操作系统中,他们表现出来的特征也不一致,有的是纯异步的,非阻塞的,有的是同步的阻塞的,无论怎么样,我们都可以把这些 IO 看做是上层应用和下层系统之间的数据交互,上层依赖于下层,上层也可以进一步对这些能力进行定制改造,如果这个交互是异步的非阻塞的,那么这种就是 异步 IO 模型,如果是同步的阻塞的,那么就是同步 IO 模型。
在 Nodejs 里面,我们可以拿文件读写为例,Koa 只是一个上层的 web 应用服务框架而已,它所有与操作系统之家的沟通能力,都建立在 Node.js 整个的通信服务模型的基础之上,Nodejs 提供了 filesystem 也就是 fs 这个模块,模块中提供了文件读写的接口,比如 readFile 这个异步的接口,它就是一个典型的异步 IO 接口,反之 readFileSync 就是一个阻塞的同步 IO 接口,以这个来类推,我们站在上层的 web 服务这个层面,就很容易理解 Node.js 的异步非阻塞模型,异步 IO 能力。
那么 Node.js 的异步能力又是建立在 Libuv 这一层的几个阶段上的,什么?还有阶段?
是的,Node.js 的底层除了解释和执行 JS 代码的 Chrome V8 虚拟机,还有一大趴儿就是 Libuv,它跟操作系统交互,封装了不同平台的诸多接口,相当于抹平了操作系统的异步差异带来的兼容性,让 Node.js 对外提供一致的同异步 API,而 Libuv 的几个阶段,便是对于单线程的 JS 最有利的辅助实现,所有的异步都可以看做是任务,任务是耗时的,libuv 把这些任务分成不同类型,分到不同阶段,有他们各自的执行规律和执行优先级。
大家可以先预测下下面这段代码的执行结果:
const EventEmitter = require('events')
class EE extends EventEmitter {}
const yy = new EE()
yy.on('event', () => console.log('粗大事啦'))
setTimeout(() => console.log('0 毫秒后到期的定时器回调'), 0)
setTimeout(() => console.log('100 毫秒后到期的定时器回调'), 100)
setImmediate(() => console.log('immediate 立即回调'))
process.nextTick(() => console.log('process.nextTick 的回调'))
Promise.resolve().then(() => {
yy.emit('event')
process.nextTick(() => console.log('process.nextTick 的回调'))
console.log('promise 第一次回调')
})
.then(() => console.log('promise 第二次回调'))
你会发现你踏入了一个 【美好】 的世界,这就是我们通过了解 Koa 以后,如果想要继续往下学习,需要掌握的知识,这块知识才是真正的干货,一言半语的确说不清楚,我们保留思路往下走。
Koa2 的三方库生态如何
在 Koa1 时代和 Koa2 刚出的时候,的确它的三方库不多,需要自己动手包装,甚至还有 koa-convert 专门干这个活儿,把 1 代 koa 中间件转成可以兼容 2 代 koa 可以兼容的形式。
但是时至今日,Koa2 的生态已经相当完善了,尤其在 2018 年随着更多开发者切入到 Koa2 中,将会有大批量的业界优秀模块库进入到 Koa2 的大池子中,大家会发现可选择的越来越多,所以他的生态没问题。
跟前端如何结合
到这里,本文接近尾声了,我也感觉意犹未尽,但是再写下去怕是成飞流直下三千尺了,我想用一句话回答这个问题:
小而美是每一个工程师最终会选择自我修养,Koa2 是小而美的,能与它结合的必然也是小而美的,那么在 2018 年,就非 Parcel 莫属,小而美绝配,关于 Parcel 如何 AntDesign/React/Bootstrap 等这些前端框架库组合使用,可以关注 Koa2解读+数据抓取+实战电影网站 了解更多姿势。
回到本文的标题:Koa2 还有多久取代 Express?我想完全替代是不可能的,但是新项目使用 Koa2(以及基于它封装的框架)将会在数量上碾压 Express,时间呢,2018 - 2019 两年足矣,那么 2018 年起,但求不落后,加油!
封面图来自 codetrick