最近有时候会用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 world 为Hi 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。第二个middleware 到await 的时候,如果有第三个middleware会执行第三个。但是这里没有,所以会一直运行到结束。然后返回到第一个middleware 的await 后面,继续运行。
乍一看这种执行流程还挺有趣的。函数运行到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 表示第i个middleware。在next 函数里面,拿到第i 个middleware,然后执行它就可以了。但是i 的值增加是有极限的,不能超过middleware 的总数量。所以我们再添加一个退出条件。
if (i === middlewares.length) {
return Promise.resolve();
}
表示当i === middlewares.length,返回一个resolve 的Promise。这是最后一个middleware 的await 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 的洋葱模型。最后,我们简单实现了类似的功能。