📌
Koa -- 基于 Node.js 平台的下一代 web 开发框架。
上面的那句话是Koa官方讲的。首先Koa是一个新的web框架,由Express幕后的原班人马打造。我们都知道现在市面上很多的Node 服务框架或者是功能,都是基于Express来进行开发的,比如说(webpack-dev-server,Nest,NodeBB等等)。这主要是因为Express出来的时间比较久,而且相对来说比较稳定;Express的生态相对的会完善一些;另外一点就是Express相对于来说比较容易入门。可以基于官方的工具直接生成,天生的自带路由和模板解析插件,比较容易上手。
当然,既然说Koa是下一代的web开发框架,就说明Koa肯定存在有比Express更优秀的地方:首先更小;另外Koa利用async函数,来丢弃Express中的回调函数,并且,更有力的增强错误处理。由于Koa中没有绑定任何的中间件,因此要搭建一套完整的功能,就需要自己去添加较多的中间件。对于初学者来说,相对的麻烦一些。
接下来就通过实践来逐渐进入到Koa的世界里吧。
Koa的发展历史
在Koa的发展历史中,存在两个版本,一个Koa 1.x一个是Koa 2.x。但是聊到Koa的发展,就不得不聊一下Express,因为Koa 同样出自 Express 团队之手。
Express
Express是 Node.js 的第一代流行 web 开发框架,主要是对 http 模块进行封装,并提供了路由、模板渲染等 web 开发常用功能,功能齐全但需要全量引用
由于出现早于 Promise,错误处理使用 Node.js 的 callback 风格,相应有了回调地狱问题,不过随着 Promise 流行,现在 Express 4.x、5.x 已经没了这个问题
Express 中间件是线性执行的,每一个中间件处理完成之后只有两个选择
- 交给下一个中间件
- 返回 response
只要是离开中间件后就再也无法返回,这样的设计让逻辑非常简单,但在很多需要多次处理请求的实现上变得复杂:比如统计一个请求的耗时,在 Express 中充斥着大量利用事件或者回调来 hack 这种需求的处理方式
exports.responseTime = function () {
return function (req, res, next) {
req.startTime = new Date(); // 开始时间
const trackTime = function () {
const endTime = new Date(); // 结束时间
const duration = endTime - req.startTime;
console.log('X-Response-Time: ', duration + 'ms');
}
res.once('finish', trackTime);
res.once('close', trackTime);
return next();
}
}
因为其简单、功能高度集成的特性,Express 现在仍然是最流行的 Node.js web 开发框架,但功能高度集成带来的不灵活和中间件的线性调用特性让越来越多企业级 web 框架封装都选择了使用 koa。
Koa 1.x
Koa 同样出自 Express 团队之手,除了 API 变化,相对于 Express 做了两个最重要的变更
- 不再内置任何中间件,所有 web 处理中间件都需要引用,灵活性和复杂性相伴而来(从这个角度讲 Express 才更像是 web 框架)
- 利用 generator 特性实现洋葱模型中间件,异步处理不再依赖 callback(主要靠 co 模块实现)
异步的代码书写起来更像是同步代码了,但使用 generator 中间件编写风格现在看起来会感觉怪怪的
app.use(function *(next) {
const startTime = new Date();
yield next;
const duration = new Date - startTime;
console.log('X-Response-Time: ', duration + 'ms');
})
Koa 2.x
理念和 Koa 1.x 一致,不过推荐异步处理变成了 async/await,其中间件实现也就是前面介绍过的 koa-compose,中间件编写风格好理解了很多。
app.use(async (ctx, next) => {
ctx; // is the Context
ctx.request; // is a koa Request
ctx.response; // is a koa Response
});
koa 2.x 也把之前使用 this 获取请求、响应等对象修改为了使用 ctx 对象。
接下来的内容,主要就是基于Koa 2.x来进行介绍的。有些人在使用的过程中,可能会因为某些包的使用出现为,这可能是因为版本选择原因。
新建一个Koa的基础项目
对于这一步,其实相对来说比较简单,就是按照官方给出的步骤,依样画葫芦就行了。首先你得有Node环境(这都2022年了,你得Node不会还停留在7.6以下吧,如果这样,建议你升级,否则后面会有很多的意想不到的问题)。然后选择一个自己喜欢的Node包管理工具,npm,yarn,pnpm,cnpm,tnpm等等都行,按照自己熟练的步骤,进行初始化一个项目,然后安装koa,新建一个名为index.js的文件,然后使用你喜爱的编辑器在文件中输入以下内容(反正我这边使用的vscode)。
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
然后使用控制台,进入该目录下面,直接运行:
node ./index.js
不出意外的话(有一种情况就是你本地的3000端口被占用了),项目就启动了,然后打开浏览器,输入:http://localhost:3000,即可在浏览器中看到有个Hello World的字样。就像下面这样:
这样就说明你已经使用了Koa创建了项目了,并且该项目能够正常的运行起来了。
中间件的概念
上面说到,Koa自身是没有绑定任何的中间件的。这里就会有人不清楚中间件到底是个什么鬼。从字面的理解上,就是在某个过程中间所存在工具。那这个过程对于Koa来说,这个过程就是一次http通信的过程,即Request和Response之间,那么这里的中间件,主要是用来处理请求数据和响应数据的。
请求报文是由请求方法、请求URI、协议版本、可选的请求首部字段和内容实体构成的。
响应报文基本上由协议版本、状态码(表示请求成功或失败的数字代码)、用以解释状态码的原因短语、可选的响应首部字段以及实体主体构成。
凡是处理请求报文和响应报文的,都可以被称之为中间件,我们可以使用中间件来做什么:
- URI处理(也就是所谓的路由处理)
- 重置HTTP请求路由
- 统一安全限制、信息上报
- Header操作、http请求认证
- 屏蔽爬虫
- 提供调试信息
- 请求日志记录
- 对相应结果进行处理(视图渲染,格式化响应数据)
- ……
Koa官方也给收集了不少的中间件(Home · koajs/koa Wiki · GitHub),我们常用的中间件主要包括有:
- 路由的处理:koa-router
- 静态资源处理:koa-static
- cookie,session处理:koa-session
- 日志处理:koa-logger
- ……
更多的中间件,可以在自己的平时开发的时候,根据业务的需要,进行查找和自己编写(其实不要把这个东西看的很高大上,其实只要你明白这中间的一切,你就会发现,原来不过如此)。
中间件的使用
上面聊了中间件的概念了,但是到现在还是对Koa的中间件一窍不通,可能还是不会使用。但是我们从上面的概念中,可以看出来,其实Koa中间件一定是一个函数。呢么对于Koa来说应该怎么去使用中间件呢?
- app.use()
这个方法将给定的中间件方法添加到应用程序中, 并且这个方法会返回 this,因此 这个use是能够进行链式调用的。
像我们的的Demo中使用的use,本质上也是在使用了一个Koa的中间件。
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
Koa上下文(ctx)
从上面的Demo中,我们会看到,在中间件中,会有一个参数ctx,这个参数呢,我们称之为上下文。如果之前了解过express的同学,应该知道我们在使用express定义一个中间建的时候会这样去写:
app.get('/', (req, res, next) => {
res.send('Hello World!')
})
我们对比会发现,相对于Express来说,Koa使用了一个叫做Context的对象,将Node中的request
和 response
对象封装在了一起。为编写 Web 应用程序和 API 提供了许多有用的方法。 这些操作在 HTTP 服务器开发中频繁使用,它们被添加到此级别而不是更高级别的框架,这将强制中间件重新实现此通用功能。
其实Context中不仅仅只有request和response两个对象,还有其他很多的对象(可以去官网),当我们在使用一些中间件的时候,也会给ctx注入一些属性或者是方法之类的东西。下面列举一些我们常用的一些属性和方法:
- ctx.request: koa 封装的 request 对象,中间件应该尽量使用
- ctx.req:Node.js 原生的 request 对象
- ctx.response:koa 封装的 response 对象,中间件应该尽量使用
- ctx.res:Node.js 原生的 response 对象
- ctx.state:koa 推荐的命名空间,用于通过中间件传递信息到前端视图
- ctx.app:对应用实例 app 的引用
- ctx.cookies:cookie 操作对象
- ctx.throw:通过 http status 抛出错误,让 koa 可以正确处理
Koa的级联(洋葱模型)
有关Koa的级联,说白了就是我们常说的Koa的洋葱模型:
先来通过一个例子来看一下这个级联的效果吧:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log(' 中间件 1开始');
await next();
console.log('中间件 1结束');
// ctx.body = JSON.stringify(ctx);
});
app.use(async (ctx, next) => {
console.log(' 中间件 2开始');
await next();
console.log('中间件 2结束');
})
app.listen(3000);
我们在运行后,访问页面,能够在运行的控制台中,看到如下的内容输出:
也就是说,在中间件1执行完成next之后,到中间件2中,中间件2执行完next之后,输出中间件2中的结束内容,然后再返回到中间件1中,进行后续的操作。这样的整个过程,就像洋葱一样,一层包裹一层,因此被称为洋葱模型。
洋葱模型的好处
按照正常的操作逻辑,就是事情不是需要一件一件的做么,所以,只需要保持顺序的执行过程就行了,但是为对于Koa来说,却以next函数的执行完成作为分界线,要在执行完成之后又回到了当前这个中间件中。
Koa之所以这么设计肯定是有他的用途,以及使用场景的,也就是说,这样的设计肯定是为了解决一些平时常见但是又不太容易去处理的东西。
举个例子,如果我们想要统计这次请求的在执行过重用了多长的时间,如果没有Koa模型,我们实现起来会很复杂,并且还会再执行过程中注入一些内容,然后在执行完成之后,再将注入的内容给卸载掉,这样的一个过程可能会来一些不可预知的问题,比如说在请求过程中会污染请求对象或者是参数等、在不能够准确的判断我们应该在什么时候去卸载我们注入的内容等……
当然还有别的一些问题,因此,在Koa中引入了洋葱模型,对于解决这样的问题,相对来说算是一种比较好的解决思路和方案。
洋葱模型的实现方式
从上面有关Koa的洋葱模型的效果,我们大致了解了有关洋葱模型的执行过程,但是这里想聊一句,如果让你去实现一个洋葱模型你会怎么去实现呢。下面是我的一个实现思路:
class App {
constructor () {
this.middleware = [];
this.ctx = {};
}
use(asynFun) {
this.middleware.push(asynFun);
}
async run(ctx, next) {
const middleware = this.middleware;
const that = this;
let index = -1;
dispatch(0);
function dispatch(index) {
let fun;
if (index === that.middleware.length) {
fun = next;
} else {
fun = that.middleware[index];
}
if (!fun) {
return Promise.resolve();
}
try {
return fun(ctx, async () => {
await dispatch(index + 1);
})
} catch (err) {
return Promise.reject(err)
}
}
}
}
const app = new App();
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');
})
// app.dispatch(0);
app.run({}, async () => {
console.log('run ……');
})
如果在控制台中进行执行的话,则执行的结果如下所示:
这个结果是不是很熟悉,没错,这个就是Koa的核心的级联的实现的基础版。接下来我们打开Koa的源码,能够看到有以下的内容:
- use方法:维护得到 middleware 中间件数组
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this
}
2. listen方法:Node.js 原生 http 模块 createServer 方法创建了一个服务。
listen (...args) {
debug('listen')
// 创建一个服务
const server = http.createServer(this.callback())
return server.listen(...args)
}
3. 如果我们继续往深处看,会看到Koa引入了一个叫做koa-compose的模块,然后我们打开koa-compose这个库,能够看到他的底层实现。
function compose(middleware) {
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next() {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
是不是很熟悉,没错,这个方法确实是和我们的自己实现的结果是一样的,如果我们将上述的结果进行分装,也就是将Http的listen,监听有请求过来之后,就执行我们的run方法,然后在对context加以封装,实现一些Koa的基础方法,是不是就会觉得,其实Koa也不过如此。如果有时间或者说有机会,我们也能够实现一个简易版的Koa。
小结
经过以上的分析,使我们对Koa有一个简单的认识,并且能够使用Koa的来创建项目,而且对Koa中的中间件和洋葱模型有一个简单的认识。其实我们会发现,很多Koa这个高大上的东西背后,其实也不过如此。