Hello to Koa!

499 阅读3分钟

最近有时候会用Koa 搭建一些小demo,一直比较好奇它是怎么完成洋葱模型这种效果的。于是这里我们来简单的实现一下类似的效果。

Hello world!Koa

我们首先使用 Koa搭建一个万能的Hello world。

先建立一个koa-service-demo 文件夹,npm i koa。安装完成以后,创建一个app.js文件,并添加下面的代码。

const koa = require('koa');
const app = new koa();

app.use(async function(ctx) {
    ctx.body = 'Hello world';
});

app.listen(3000);

然后命令行运行node app.js。浏览器访问http://localhost:3000 就可以在页面上看到我们的Hello world 了。

现在有一点小麻烦,如果我们把ctx.body = 'Hello world'; 改成了ctx.body = 'Hi world';。一定要重新运行node app.js 才会生效。

我们可以通过npm i --save-dev nodemon 安装一下 nodemon。安装好了以后,在我们项目的package.json 文件里面,添加下面的一条命令。

"scripts": {
  "koa": "nodemon app.js"
},

这时候运行npm run koa,修改Hello worldHi world。刷新页面,我们就会看到内容被更新了。

Koa 的洋葱模型

上面的代码是一个简单的Hello world。我们再稍微改复杂一点点。

const koa = require('koa');
const app = new koa();

app.use(async function(ctx, next) {
    ctx.body = 'Hello from first; ';
    await next();
    ctx.body += 'Bye from first; ';
});

app.use(async function(ctx, next) {
    ctx.body += 'Hello from second; ';
    await next();
    ctx.body += 'Bye from second; ';
});

app.listen(3000);

上面的代码在http://localhost:3000 的输出结果是

Hello from first; Hello from second; Bye from second; Bye from first; 

上面我们传入的两个async 函数被称为middleware。运行的顺序是,首先执行第一个middleware,运行到await 的时候,执行第二个middleware。第二个middlewareawait 的时候,如果有第三个middleware会执行第三个。但是这里没有,所以会一直运行到结束。然后返回到第一个middlewareawait 后面,继续运行。

乍一看这种执行流程还挺有趣的。函数运行到await next()时候,停止执行,进入到下一个middleware。运行完成以后,最后回到await next()的位置,继续执行。

我们怎么来简单实现一个类似的功能来运行下面的代码呢?

const app = new App();

app.use(async function(ctx, next) {
    ctx.body = 'Hello from first; ';
    await next();
    ctx.body += 'Bye from first; ';
});

app.use(async function(ctx, next) {
    ctx.body += 'Hello from second; ';
    await next();
    ctx.body += 'Bye from second; ';
});

const ctx = {body: ''};
app.run(ctx);

首先我们会有个自己的App 类,通过它构造了app 对象,使用app.use 加入了两个async 函数,最后调用app.run(ctx) 运行这两个async 函数。

为了简单一点,我们就不要app.listen 了,直接app.run

class App {
    middlewares = [];
    constructor() {
    }

    use(fn) {
        this.middlewares.push(fn);
    }
    run(ctx) {
        run(ctx, this.middlewares);
    }
}

这个App 通过use 方法,把传进来的fn 方法,添加到middlewares 里面。 app.run 的时候,调用一个run 方法,传入ctx 和属性middlewares

这里的这个run 方法怎么写?

function run(ctx, middlewares) {
}

middleware 方法里面,通过await next() 调用下一个middleware。那么run 函数开始的时候应该调用第一个middleware,也就是next(0);

function run(ctx, middlewares) {
    next(0);
}

然后我们定义这个next 函数。

function next(i) {
    const fn = middlewares[i];
    return fn(ctx, next.bind(null, i + 1));
}

这里的参数i 表示第imiddleware。在next 函数里面,拿到第imiddleware,然后执行它就可以了。但是i 的值增加是有极限的,不能超过middleware 的总数量。所以我们再添加一个退出条件。

if (i === middlewares.length) {
    return Promise.resolve();
}

表示当i === middlewares.length,返回一个resolvePromise。这是最后一个middlewareawait next() 的返回值。

最终的代码是

class App {
    middlewares = [];
    constructor() {
    }

    use(fn) {
        this.middlewares.push(fn);
    }
    run(ctx) {
        run(ctx, this.middlewares);
    }
}

function run(ctx, middlewares) {
    next(0);
    function next(i) {
        if (i === middlewares.length) {
            return Promise.resolve();
        }
        const fn = middlewares[i];
        return fn(ctx, next.bind(null, i + 1));
    }
}

我们尝试运行下面的代码:

const app = new App();
app.use(async function (ctx, next) {
    ctx.greeting = 'Hello from A;';
    await next();
    ctx.greeting += 'Bye from A;';
})
app.use(async function (ctx, next) {
    ctx.greeting += 'Hello from B;';
    await next();
    ctx.greeting += 'Bye from B;';
})
const ctx = {};
app.run(ctx);
setTimeout(() => {
    console.log(ctx);
});

打印的结果

{ greeting: 'Hello from A;Hello from B;Bye from B;Bye from A;' }

相较于这个简单实现,大家要是不满足,可以看看Koa 这个更加完善的版本。

总结

我们首先使用Koa 搭建了一个hello world的demo。基于这个demo,我们介绍了一个Koa 的洋葱模型。最后,我们简单实现了类似的功能。