# koa中间件大揭秘
# 前言
我们在使用koa的时候发现,其实koa只是对req,res进行了封装,但是很多一些功能如路由、静态资源、模板引擎等都没有支持,看过源码都知道koa的源码就那么一点。但是丰富的第三方中间件弥补了这个不足。接下来我们来揭开一下一些常用中间件的内部原理,你会发现其实koa的中间件真的大同小异。
# koa-bodyparser
我们先来简单看下 koa-bodyparser 的基本用法
const Koa = require("koa")
const bodyParser = require("koa-bodyparser")
const app = new Koa()
app.use(bodyParser()) // 挂载中间件 koa-bodyparser
app.use(async (ctx, next) => { // 渲染一个表单,表单提交时候请求接口 /login
if (ctx.method === "GET" && ctx.path === "/form") {
ctx.body = `
<form action="/login" method="post">
<input type="text" name="username"/>
<input type="text" name="password"/>
<button>提交</button>
</form>
`;
} else {
await next();
}
});
app.use(async ctx => {
if (ctx.method === "POST" && ctx.path === "/login") {
ctx.body = ctx.request.body; //将接口得到的数据展示在页面上
}
});
app.listen(5050);
上面的代码其实很简单,/form 路由渲染一个表单组件,表单组件请求的是/login 路由,而/login将得到的数据渲染在页面,我们来看下运行效果:

知道 koa-bodyparser 的基本用法之后,我们试一下自己实现一个类似功能的中间件
const bodyParser = ()=>{
return async (ctx,next)=>{ // 返回一个async函数
await new Promise((resolve,reject)=>{
let arr = [];
ctx.req.on('data',(chunk)=>{
arr.push(chunk);
})
ctx.req.on('end',function(){
ctx.request.body = Buffer.concat(arr).toString(); // 将文件流赋值给 ctx.request.body
resolve();
})
})
await next();
}
}
其实你会发现,内部就是这么简单,而且不难发现,app.use() 方法在 koa 中是接受一个 async函数的,根据app.use(bodyParser()) 来挂载中间件我们可以知道 bodyParser() 返回的就是一个 async 函数,而中间件的核心原理则是利用 koa 中间件的洋葱机制,在一开始给 ctx 上挂载一些属性或者方法,则在后面的 arr.use() 中都可以通过 ctx来拿到对应挂载的方法。
# koa-static
koa-static 的作用用一句话来概括就是,起一个静态服务,下面看下具体用法
const Koa = require("koa");
const app = new Koa();
const static = require('koa-static');
app.use(static(__dirname))
app.listen(5050);
以上代码的意思是,以当前目录为根目录起一个静态服务,通过 http://localhost:5050/XXX 就可以获取到对应的资源了。
而且不难发现,koa-static 的用法和 koa-bodyparser 很像,都是app.use(xxx()) 的形式,那我们就来简单实现一个这个效果
function static(pathname) {
return async (ctx, next) => {
try { // 因为 fs.stat 主要通过报错来判断当前路径文件存不存在 所以try catch处理
let filePath = ctx.path
filePath = path.join(pathname, filePath) // 拿到绝对路径
let statObj = await fs.stat(filePath)
if (statObj.isDirectory()) {// 如果是目录,则拼接上 `index.html`
filePath = path.resolve(filePath, 'index.html')
}
ctx.body = await fs.readFile(filePath, 'utf-8') //读取处理过的路径,然后返回
}catch(e) {
return next() // 如果处理不了 就走下一个中间件
}
}
}
原理很简单,就是读当前传入路径的文件,有的话就赋值给 ctx.body , 没有的话走下一个中间件
# koa-router
顾名思义,一个路由的第三方中间件,我们直接看它的基础用法
const Koa = require('koa');
const Router = require('koa-router');
const router = new Router();
const app = new Koa();
router.get('/hello',async (ctx,next)=>{
ctx.body = 'hello';
next();
})
app.use(router.routes());
app.listen(5050);
上诉代码主要是起一个/hello的get请求接口,返回hello, 大大的简化了koa对路由的操作,如ctx.method === "GET" && ctx.path === "/hello" 类似的判断,我们自己实现的时候需要注意到一些细节,如 Router 是一个类,app.use()挂载的是类上routes方法返回的函数,我们试下来实现一款简单的koa-router.
class Layer{ //将栈中的结构抽取一个类 方便后序扩展
constructor(method,pathname,callback){
this.method = method;
this.pathname = pathname;
this.callback = callback;
}
match(path,method){ //将匹配方法抽离出来
return path === this.pathname && method.toLowerCase() === this.method;
}
}
class Router{
constructor(){
this.stack = [];// 这里面存放着所有的路由关系
}
get(pathname,callback){
// 我们次调用get方法都会像内部数组放一层
let layer = new Layer('get', pathname, callback)
this.stack.push(layer)
}
compose(fns,ctx,next){
// compose原理 和koa类似 先抽取栈中的第一个执行, 将下一个函数作为第一个函数的参数next,以此递归
let dispatch = (index)=>{
if(index === fns.length) return next(); // 边界值判断 避免爆了
let callback = fns[index].callback;// 拿到栈中每一项执行
return Promise.resolve(callback(ctx,()=>dispatch(index+1)))
}
return dispatch(0);
}
routes(){
return async (ctx,next)=>{
// 获取请求的路径
let path = ctx.path; // /hello
let method = ctx.method; // get
let fns = this.stack.filter(layer=>layer.match(path,method));//在存储的路由表中筛选出符合条件项
this.compose(fns,ctx,next) // 进行组合
}
}
}
module.exports = Router;
我们慢慢来体会一下 koa-router 的内部流程
首先我们使用route.get('/xxx', callback) 的时候会调用 Router类上的get方法,该方法会将 method path callback 通过 Layer 类组装一下,存入 stack中,我们 statck的结构如下
然后我们调用 route.routes() 的时候,会根据当前请求的方法和路径去 stack中筛选出符合条件的项,将他们组合成一个大的函数,内部原理是和koa的中间件原理一样的,先执行第一个函数,将下一个函数作为第一个函数的参数next

其实这只是实现koa-router 的一小部分内容,还有很多如二级路由啊、参数处理啥的比较恶心,这里就不写了
# koa-views
一句话来说 koa-views就是用来实现模板引擎的,很简单
const Koa = require('koa');
const Router = require('koa-router');
const views = require('koa-views');
const app = new Koa();
const path = require('path');
const router = new Router();
app.use(views(path.resolve(__dirname,'views'),{
map: {
html: 'ejs' //如果遇到.html 后缀的文件,用ejs模板处理
}
}));
router.get('/',async ctx=>{
await ctx.render('hello',{name:'zf'});
})
app.use(router.routes())
app.listen(5050);
先来理一下koa-views 的基本用法吧,首先挂载中间件,声明一些参数,如 遇到.html 后缀的文件用 ejs 模板引擎来渲染,最后在 ctx上挂载一个render()方法,传入对应的数据来渲染对应的页面。知道原理之后,我们不妨自己来实现一下
const views = (dirname,{map})=>{
return async (ctx,next)=>{
ctx.render = async (filename,data)=>{ // 将方法挂载到 ctx.render
let ejs = require(map.html);// 引用ejs模板 这里的做法有点low 其实应该遍历取出的
const renderFile = util.promisify(ejs.renderFile); // 调用ejs的方法渲染页面
// 渲染文件,成功后将结果返回去
ctx.body = await renderFile(path.join(dirname,filename+'.html'),data);
}
await next(); // 增加逻辑后 继续向下执行 koa-static
}
}
# 总结
通过对工作做经常用到的几个中间件进行了一波深入了解,相信大家对koa中间件的路子和形式都摸得差不多了。核心原理就是利用koa中的洋葱模型,一开始往ctx上挂载自己需要的属性或者方法,后序就可以通过ctx来调用啦。