Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

# koa中间件大揭秘 #21

Open
YIngChenIt opened this issue Jun 30, 2020 · 0 comments
Open

# koa中间件大揭秘 #21

YIngChenIt opened this issue Jun 30, 2020 · 0 comments

Comments

@YIngChenIt
Copy link
Owner

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

![koa1](https://user-images.githubusercontent.com/53081460/86111612-8dfccf00-baf9-11ea-9c5d-4f0739e4dbad.gif

知道 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的结构如下

stack

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

STACK2

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant