# 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将得到的数据渲染在页面,我们来看下运行效果:

An image

知道 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);

上诉代码主要是起一个/helloget请求接口,返回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的结构如下

An image

然后我们调用 route.routes() 的时候,会根据当前请求的方法和路径去 stack中筛选出符合条件的项,将他们组合成一个大的函数,内部原理是和koa的中间件原理一样的,先执行第一个函数,将下一个函数作为第一个函数的参数next

An image

其实这只是实现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来调用啦。