手摸手實(shí)現(xiàn)基于 Koa 的微服務(wù)框架(實(shí)用!)
作者: 米澤,抖音前端團(tuán)隊(duì)國際化工具核心開發(fā)者。
Koa 官網(wǎng)的介紹是這樣介紹自己的:
Koa 是一個(gè)新的 web 框架,由 Express 幕后的原班人馬打造, 致力于成為 web 應(yīng)用和 API 開發(fā)領(lǐng)域中的一個(gè)更小、更富有表現(xiàn)力、更健壯的基石。通過利用 async 函數(shù),Koa 幫你丟棄回調(diào)函數(shù),并有力地增強(qiáng)錯(cuò)誤處理。Koa 并沒有捆綁任何中間件, 而是提供了一套優(yōu)雅的方法,幫助您快速而愉快地編寫服務(wù)端應(yīng)用程序。
從上面的描述中我們可以知道,Koa 是一種簡單好用的 Web 框架。它的特點(diǎn)是優(yōu)雅、簡潔、表達(dá)力強(qiáng)、自由度高。其本身代碼只有 1000 多行,所有功能都可以通過插件的方式擴(kuò)展,很符合 KISS 原則與 Unix 哲學(xué)。比較有名的 Node.js 業(yè)務(wù)框架 egg.js 就是是繼承自Koa。
但 Koa 的劣勢也很明顯,就是太過自由,并沒有內(nèi)置過多的功能,比如常見的請求體解析、路由、模板渲染等功能都沒有,需要加載第三方中間件來實(shí)現(xiàn)。另外 Koa 只支持 Http 服務(wù),無法滿足業(yè)務(wù)方對于 RPC 服務(wù)的需求。
本文將對基于 Koa 的微服務(wù) Node.js 框架設(shè)計(jì)思路做一些思考與探究,并且對實(shí)現(xiàn)方面做一些簡單補(bǔ)充。讓我們先從 Koa 的核心思想與原理開始。
Koa的核心思想與最簡實(shí)現(xiàn)
核心思想:AOP 面向切面編程
AOP技術(shù)的誕生并不算晚,早在1990年開始,來自Xerox Palo Alto Research Lab(即PARC)的研究人員就對面向?qū)ο笏枷氲木窒扌赃M(jìn)行了分析。他們研究出了一種新的編程思想,借助這一思想或許可以通過減少代碼重復(fù)模塊從而幫助開發(fā)人員提高工作效率。隨著研究的逐漸深入,AOP也逐漸發(fā)展成一套完整的程序設(shè)計(jì)思想,各種應(yīng)用AOP的技術(shù)也應(yīng)運(yùn)而生。
這個(gè)名詞聽起來很高大上,可能很多人都聽過,但是又沒有徹底搞懂,到底什么叫面向切面編程?這里先不解釋 AOP 的具體含義,而是舉個(gè)簡單的例子。
農(nóng)場的水果包裝流水線一開始只有 采摘 - 清洗 - 貼標(biāo)簽

為了提高銷量,想加上兩道工序 分類和包裝但又不能干擾原有的流程,同時(shí)如果沒增加收益可以隨時(shí)撤銷新增工序。

最后在流水線的中的空隙插上兩個(gè)工人去處理,形成 采摘 - 分類 - 清洗 - 包裝 - 貼標(biāo)簽的新流程,而且工人可以隨時(shí)撤回。
上面所說的每一道工序,都可以看作是一個(gè)切面。
回到 AOP 的含義:就是在現(xiàn)有代碼程序中,在程序的生命周期或橫向流程中,加入或減去一個(gè)或多個(gè)功能,使原本功能不受影響。
核心原理:koa-compose + Node.js http
Koa 可以被拆解為如下公式:
Koa = Node.js原生http服務(wù) + 中間件引擎koa-compose
通過把中間件用 Promise + async/await 的方式嵌套組合,Koa 實(shí)現(xiàn)了比 Express 的線性模型中間件多了一倍切面的洋蔥模型中間件,所以 Koa 能非常方便地實(shí)現(xiàn)類似響應(yīng)時(shí)間計(jì)算、日志打印、鑒權(quán)等等常用功能。

下面舉一個(gè) Koa 官網(wǎng)的 demo 例子,可以看到這些功能的具體實(shí)現(xiàn)是多么的簡單:
const?Koa?=?require('koa');
const?app?=?new?Koa();
//?x-response-time
app.use(async?(ctx,?next)?=>?{
??const?start?=?Date.now();
??await?next();
??const?ms?=?Date.now()?-?start;
??ctx.set('X-Response-Time',?`${ms}ms`);
});
//?logger
app.use(async?(ctx,?next)?=>?{
??const?start?=?Date.now();
??await?next();
??const?ms?=?Date.now()?-?start;
??console.log(`${ctx.method}?${ctx.url}?-?${ms}`);
});//?response
app.use(async?ctx?=>?{
??ctx.body?=?'Hello?World';
});
app.listen(3000);
Node.js 原生 http 不用多說,下面著重講一下中間件引擎 koa-compose 的實(shí)現(xiàn),源碼非常精簡,核心代碼只有 30 行左右:
function?compose(middleware)?{
??//?如果middleware不是數(shù)組,或者元素不是函數(shù),則拋異常
??if?(!Array.isArray(middleware))?{
????throw?new?TypeError('Middleware?stack?must?be?an?array!');
??}
??for?(const?fn?of?middleware)?{
????if?(typeof?fn?!==?'function')?{
??????throw?new?TypeError('Middleware?must?be?composed?of?functions!');
????}
??}
??//?返回一個(gè)閉包函數(shù)
??return?function?(context,?next)?{
????//?last?called?middleware?#
????let?index?=?-1;
????return?dispatch(0);
????function?dispatch(i)?{
??????if?(i?<=?index)?{
????????return?Promise.reject(new?Error('next()?called?multiple?times'));
??????}
??????index?=?i;
??????let?fn?=?middleware[i];
??????if?(i?===?middleware.length)?{
????????fn?=?next;
??????}
??????if?(!fn)?{
????????return?Promise.resolve();
??????}
??????try?{
????????//?將每一個(gè)?middleware?函數(shù)作為前一個(gè)函數(shù)的?next?參數(shù)
????????return?Promise.resolve(fn(context,?dispatch.bind(null,?i?+?1)));
??????}?catch?(err)?{
????????return?Promise.reject(err);
??????}
????}
??};
}
簡化掉判斷邏輯,compose執(zhí)行后就是類似下面這樣的結(jié)構(gòu):
//?這樣就可能更好理解了。
//?simpleKoaCompose
const?[fn1,?fn2,?fn3]?=?stack;
const?fnMiddleware?=?function?(context)?{
??return?Promise.resolve(
????fn1(context,?function?next()?{
??????return?Promise.resolve(
????????fn2(context,?function?next()?{
??????????return?Promise.resolve(
????????????fn3(context,?function?next()?{
??????????????return?Promise.resolve();
????????????})
??????????);
????????})
??????);
????})
??);
};
實(shí)際上 koa-compose 返回的是一個(gè) Promise,從中間件(傳入的數(shù)組)中取出第一個(gè)函數(shù),傳入context和第一個(gè)next函數(shù)來執(zhí)行。
第一個(gè) next 函數(shù)也返回一個(gè) Promise,從中間件(傳入的數(shù)組)中取出第二個(gè)函數(shù),傳入context和第二個(gè)next函數(shù)來執(zhí)行。
第二個(gè) next 函數(shù)也返回一個(gè) Promise,從中間件(傳入的數(shù)組)中取出第三個(gè)函數(shù),傳入context和第三個(gè)next函數(shù)來執(zhí)行。
第三個(gè)...
以此類推。最后一個(gè)中間件中如果調(diào)用了 next 函數(shù),則返回 Promise.resolve()。這樣就把所有中間件串聯(lián)起來了。類似棧的先進(jìn)后出,每個(gè)中間件都有兩個(gè)切面,這就是洋蔥模型的實(shí)現(xiàn)原理。
Koa 最簡實(shí)現(xiàn)
const?http?=?require('http');
const?Emitter?=?require('events');
const?compose?=?require('koa-compose');?//?上面的?compose
/**
?*?通用上下文
?*/
const?context?=?{
??_body:?null,
??get?body()?{
????return?this._body;
??},
??set?body(val)?{
????this._body?=?val;
????this.res.end(this._body);
??},
};
class?MiniKoa?extends?Emitter?{
??constructor()?{
????super();
????this.middleware?=?[];
????this.context?=?Object.create(context);
??}
??/**
????*?服務(wù)事件監(jiān)聽
????*?@param?{*}?args
??*/
??listen(...args)?{
????const?server?=?http.createServer(this.callback());
????return?server.listen(...args);
??}
??/**
????*?注冊使用中間件
????*?@param?{Function}?fn
??*/
??use(fn)?{
????this.middleware.push(fn);
??}
??/**
????*?中間件總回調(diào)方法
????*/
??callback()?{
????if?(this.listeners('error').length?===?0)?{
??????this.on('error',?this.onerror);
????}
????const?handleRequest?=?(req,?res)?=>?{
??????let?context?=?this.createContext(req,?res);
??????let?{?middleware?}?=?this;
??????//?執(zhí)行中間件
??????compose(middleware)(context).catch(err?=>?this.onerror(err));
????};
????return?handleRequest;
??}
??/**
???*?異常處理監(jiān)聽
???*?@param?{EndOfStreamError}?err
???*/
??onerror(err)?{
????console.log(err);
??}
??/**
????*?創(chuàng)建通用上下文
????*?@param?{Object}?req
????*?@param?{Object}?res
????*/
??createContext(req,?res)?{
????let?context?=?Object.create(this.context);
????context.req?=?req;
????context.res?=?res;
????return?context;
??}
}
/**
?*?測試一下
?*/
const?app?=?new?MiniKoa();
const?PORT?=?3001;
app.use(async?ctx?=>?{
??ctx.body?=?'hello';
});
app.listen(PORT,?()?=>?{
??console.log(`started?at?port?${PORT}`);
});
基于 Koa 的微服務(wù) Node.js 框架設(shè)計(jì)
然而,Koa只是一個(gè)HTTP框架,在實(shí)際的業(yè)務(wù)場景中,業(yè)務(wù)方除了要編寫HTTP服務(wù),還可能要編寫其他類型的服務(wù),比如 Thrift 服務(wù)、WebSocket 服務(wù)、消息隊(duì)列的 Consumer 服務(wù)等等。應(yīng)該如何設(shè)計(jì)這樣一個(gè)不僅支持HTTP,還支持其他服務(wù)類型的微服務(wù) Node.js 框架呢?我們先從這些服務(wù)的共性出發(fā)。
設(shè)計(jì)思想
HTTP、Thrift、WebSocket 等服務(wù)雖然應(yīng)用層協(xié)議不同,但歸根結(jié)底都是 C/S 結(jié)構(gòu)的軟件系統(tǒng),其工作流程都可以劃分為請求和響應(yīng)兩個(gè)階段,如下圖所示:

如果把整個(gè)客戶端與服務(wù)端之間的交互過程看成是一個(gè)完整流水線的話,那么請求和響應(yīng)自然就可以作為整個(gè)請求過程中的兩個(gè)切面,因此 Koa 的洋蔥模型也同樣適用于除 HTTP 之外其他類型的服務(wù)。所以我們可以基于 Koa進(jìn)行封裝和改造,構(gòu)造一個(gè)通用的服務(wù)中間件處理模型,這樣我們就可以用 Koa 的形式來編寫任意類型的服務(wù)程序。
框架的基本架構(gòu)如下圖所示:

簡單實(shí)現(xiàn)
我們可以根據(jù)上述架構(gòu)圖做一個(gè)簡單的實(shí)現(xiàn)(基于 AbstractServer 構(gòu)建 HttpServer 與 ThriftServer ):
某些方法的細(xì)節(jié)部分這里先不做展開,感興趣的同學(xué)可以自行查閱更多資料。
AbstractServer
import?compose?from?'koa-compose';
import?http?from?'http';
export?abstract?class?AbstractServer?extends?EventEmitter?{
????public?middlewares:?any[];
????public?context;
????public?request;
????public?response;
????/**
?????*?Initialize?a?new?application.
?????*
?????*?@constructor
?????*/
????constructor(options)?{
????????super();
????????this.middlewares?=?[];
????????this.context?=?Object.create(options.context);
????????this.request?=?Object.create(options.request);
????????this.response?=?Object.create(options.response);
????}
????/**
?????*?Listen?to?specific?port.
?????*/
????public?listen(...args)?{
????????const?server?=?this.createServer(this.callback());
????????return?server.listen(...args);
????}
????/**
?????*?Use?the?given?middleware?`fn`.
?????*
?????*?@param?fn?-?middleware
?????*/
????public?use(fn):?this?{
????????if?(typeof?fn?!==?'function')?{
????????????throw?new?Error('middleware?must?be?a?function!');
????????}
????????this.middlewares.push(fn);
????????return?this;
????}
????/**
?????*?Return?a?request?handler?callback.
?????*/
????public?callback()?{
????????const?fn?=?compose(this.middlewares);
????????return?(req,?res)?=>?{
????????????const?ctx?=?this.createContext(req,?res);
????????????return?this.handleRequest(ctx,?fn);
????????};
????}
????/**
?????*?Handle?request?in?callback.
?????*
?????*?@param?ctx
?????*?@param?fn
?????*/
????public?handleRequest(ctx,?fn):?Promise<void>?{
????????return?fn(ctx)
????????????.then(()?=>?this.handleResponse(ctx))
????????????.catch((err)?=>?ctx.onerror(err));
????}
????/**
?????*?Initialize?a?new?context.
?????*
?????*?@param?{Object}?req?-?request
?????*?@param?{Object}?res?-?response
?????*/
????public?createContext(
????????req,
????????res,
????)?{
????????const?context?=?Object.create(this.context);
????????const?request?=?Object.create(this.request);
????????const?response?=?Object.create(this.response);
????????context.app?=?this;
????????context.request?=?request;
????????context.response?=?response;
????????context.req?=?req;
????????context.res?=?res;
????????context.state?=?{};
????????request.app?=?this;
????????request.ctx?=?context;
????????request.req?=?req;
????????request.res?=?res;
????????request.response?=?response;
????????response.app?=?this;
????????response.ctx?=?context;
????????response.req?=?req;
????????response.res?=?res;
????????response.request?=?request;
????????return?context;
????}
????/**
?????*?Default?error?handler
?????*
?????*?@param?err?-?error
?????*/
????public?onerror(err:?Error):?void?{
????????const?msg?=?err.stack?||?err.toString();
????????console.error();
????????console.error(msg.replace(/^/gm,?'??'));
????????console.error();
????}
????/**
?????*?Create?server
?????*
?????*?@param?callback?-?server?request?callback
?????*/
????public?abstract?createServer(callback);
????/**
?????*?Handle?response?after?all?middlewares?have?been?executed
?????*
?????*?@param?ctx?-?context
?????*/
????public?abstract?handleResponse(ctx):?void;
}
HttpServer
export?class?HttpServer?extends?AbstractServer?{
???/**
?????*?initialize?http?server
?????*?@param?options
?????*/
??constructor(options)?{
????//?more?detail...
????const?{?context,?request,?response?}?=?options;
????super({
??????context,
??????request,
??????response,
????});
??}
??/**
?????*?Handle?request.
?????*
?????*?@param?ctx?-?context
?????*?@param?fn?-?composed?middleware
?????*/
??handleRequest(ctx,?fn)?{
????//?more?detail...
????return?super.handleRequest(ctx,?fn);
??}
??/**
?????*?Create?context.
?????*
?????*?@param?req?-?raw?request
?????*?@param?res?-?raw?response
?????*/
??createContext(req,?res)?{
????const?context?=?super.createContext(req,?res);
????//?more?detail...
????return?context;
??}
??/**
?????*?Handle?response?after?all?middlewares?have?been?executed.
?????*
?????*?@param?ctx
?????*/
??handleResponse(ctx)?{
????let?{?body?}?=?ctx;
????const?{?res?}?=?ctx;
????const?code?=?ctx.status;
????//?more?detail...
????body?=?JSON.stringify(body);
????return?res.end(body);
??}
??/**
?????*?Error?handler.
?????*
?????*?@param?err
?????*/
??onerror(err)?{
????super.onerror(err);
??}
??/**
?????*?Create?a?http?server.
?????*
?????*?@param?callback?-?request?handler
?????*/
??createServer(callback,?options?)?{
????//?more?detail...
????return?http.createServer(callback)?as?any;
??}
}
總結(jié)
本文對 Koa、基于 Koa 的微服務(wù) Node.js 框架在思想、原理、實(shí)現(xiàn)方面做了一些探討。
Koa 的核心思想是 AOP,AOP 中切面的概念可以類比于流水線上可以自由增加或減少的“環(huán)節(jié)”,對于這樣有固定流程的“環(huán)節(jié)”,我們都可以把它們當(dāng)做AOP的切面,利用洋蔥模型的思想去處理。
參考資料
Koa設(shè)計(jì)模式:https://chenshenhai.github.io/koajs-design-note/
最后
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「qianyu443033099」拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
關(guān)注公眾號「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

