# 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
来调用啦。