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

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

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

下面舉一個 Koa 官網的 demo 例子,可以看到這些功能的具體實現(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 的實現(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!');
????}
??}
??//?返回一個閉包函數(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?{
????????//?將每一個?middleware?函數(shù)作為前一個函數(shù)的?next?參數(shù)
????????return?Promise.resolve(fn(context,?dispatch.bind(null,?i?+?1)));
??????}?catch?(err)?{
????????return?Promise.reject(err);
??????}
????}
??};
}
簡化掉判斷邏輯,compose執(zhí)行后就是類似下面這樣的結構:
//?這樣就可能更好理解了。
//?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();
????????????})
??????????);
????????})
??????);
????})
??);
};
實際上 koa-compose 返回的是一個 Promise,從中間件(傳入的數(shù)組)中取出第一個函數(shù),傳入context和第一個next函數(shù)來執(zhí)行。
第一個 next 函數(shù)也返回一個 Promise,從中間件(傳入的數(shù)組)中取出第二個函數(shù),傳入context和第二個next函數(shù)來執(zhí)行。
第二個 next 函數(shù)也返回一個 Promise,從中間件(傳入的數(shù)組)中取出第三個函數(shù),傳入context和第三個next函數(shù)來執(zhí)行。
第三個...
以此類推。最后一個中間件中如果調用了 next 函數(shù),則返回 Promise.resolve()。這樣就把所有中間件串聯(lián)起來了。類似棧的先進后出,每個中間件都有兩個切面,這就是洋蔥模型的實現(xiàn)原理。
Koa 最簡實現(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);
??}
??/**
????*?服務事件監(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);
??}
??/**
????*?中間件總回調方法
????*/
??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 的微服務 Node.js 框架設計
然而,Koa只是一個HTTP框架,在實際的業(yè)務場景中,業(yè)務方除了要編寫HTTP服務,還可能要編寫其他類型的服務,比如 Thrift 服務、WebSocket 服務、消息隊列的 Consumer 服務等等。應該如何設計這樣一個不僅支持HTTP,還支持其他服務類型的微服務 Node.js 框架呢?我們先從這些服務的共性出發(fā)。
設計思想
HTTP、Thrift、WebSocket 等服務雖然應用層協(xié)議不同,但歸根結底都是 C/S 結構的軟件系統(tǒng),其工作流程都可以劃分為請求和響應兩個階段,如下圖所示:

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

簡單實現(xiàn)
我們可以根據上述架構圖做一個簡單的實現(xiàn)(基于 AbstractServer 構建 HttpServer 與 ThriftServer ):
某些方法的細節(jié)部分這里先不做展開,感興趣的同學可以自行查閱更多資料。
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;
??}
}
總結
本文對 Koa、基于 Koa 的微服務 Node.js 框架在思想、原理、實現(xiàn)方面做了一些探討。
Koa 的核心思想是 AOP,AOP 中切面的概念可以類比于流水線上可以自由增加或減少的“環(huán)節(jié)”,對于這樣有固定流程的“環(huán)節(jié)”,我們都可以把它們當做AOP的切面,利用洋蔥模型的思想去處理。
參考資料
Koa設計模式:https://chenshenhai.github.io/koajs-design-note/
