框架設計:如何基於 Egg 設計 Node 的服務框架

Node 的工具化價值自很少言,而服務化價值須要長期探索,小菜前端在服務化路上依然是小學生,目前的嘗試是是 Cross 框架,嚐到了一些甜頭。前端

我想,幾乎沒有前端工程師會對 Node 不感興趣,但用它適合幹哪些事情,每一個人的答案都不一樣了,好比小菜前端,咱們對於 Node 的深度嘗試,能夠在這裏找到答案:《技術棧:爲何 Node 是前端團隊的核心技術棧》[1],但關於讓 Node 作服務端的事情,卻只有少數團隊有這樣的勇氣。git

之因此缺少自信和勇氣,本質的緣由在於 Node 尚未一個足夠順手的框架來讓你快速證實驅動業務的價值,也在於對 Node 缺少足夠的瞭解和信心,以及相對於服務端的強勢,每每前端在侵蝕服務端領域的時候,會受到這般那樣的挑戰甚至刁難,這也成爲了在團隊推廣 Node 常遇到的阻力,但願你們從小菜團隊身上能夠找到一些答案,其中答案的一部分就是要對 Node 要有足夠的瞭解和認知,才能夠爲通用問題抽象出通用的方案去實施,在小菜,就是對於 Node 框架的封裝,這個框架還沒有開源,名叫 Cross,寓意沒有邁不過的技術門檻。github


分清楚 Node 的邊界

先後端的團隊自己是相愛相殺的關係,是左右手的雙十合十,既有接口聯調上的上下游數據立場,也有必須與對方精誠合做才能一次次拿下項目的戰役,而在服務這件事情,前端就直接介入到了服務端的領域,並且從整個行業來看,這種介入在大中型公司已成爲不可阻擋的趨勢,不管是淘寶、天貓、支付寶、騰訊、網易、百度,包括創業獨角獸大搜車、貝貝網、Rokid,海內外不分國籍不分領域的衆多公司都有一個個團隊在深度耕耘,因此這裏的第一個邊界是先後端的邊界。面試

一千家公司可能就有一千種商業模式,一千種用戶畫像,一千種業務特徵,既有高依賴算法的高實時計算的井噴式訪問場景,也有日均幾十 UV 幾百 PV 的 toB 大客戶產品,什麼場景用 Node 合適,什麼不合適,這第二個邊界就是 Node 在業務領域裏的服務邊界。redis

只有弄清楚這兩個邊界,纔有 Node 的生存土壤,脫離了這兩個邊界,就不免到處碰壁沒法落地,針對先後端邊界,我從前寫過這樣一段話:算法

數據的控制權和與視圖所依賴的 API,這裏就是目前先後端的邊界,數據控制權屬於後端,API 屬於後端,把先後端簡單看作是一個完整的系統,這個系統中自 API 向下天然是後端的,API 向上則屬於前端。編程

在 API 下面,對於數據的業務流轉流轉邏輯,在上面對於數據的調用和組裝,這就是數據層面的自然分界點,而 Node 植入進去,也必須在 API 這一層與 Java 保持規範的統一和兼容,經過 RPC 無縫的調用才能來談邊界,而這個邊界個人理解它能夠是非強業務耦合的,好比獨立的內部協同系統,也能夠是非高計算型的,能夠是相對獨立的異步的高併發的模塊,好比消息堆棧的頻繁拉取推送,好比日誌的收集整理等等,總結起來就是非複雜業務流程的,非高計算型的這個地方能夠做爲 Node 進入的邊界。小程序

而對於業務的服務邊界,只要的小而美的相對獨立的系統,只要不是核心業務,均可以用 Node 快速開發,好比小菜這裏就有報表系統、打包系統、發佈系統、市調系統、日誌系統、可視化平臺、招聘面試系統、Bug 跟蹤系統等等。後端

以上的兩個邊界,你們在仔細評估的時候,必定不要忘了本身團隊人員的能力配置,能不能 Hold 住 Node,有沒有 Node 技術專家坐鎮,否則倉促使用可能還拔苗助長。bash


爲何要封裝 Cross

在弄清楚上述的邊界後,小菜前端在 1 年多的時間裏,對 Node 進行深度的使用,從基建系統到相對獨立的業務系統,整個走下來,團隊更多同窗掌握了 Node 的使用,同時每一個系統之間的差別性也愈來愈大,有的用的是 Koa 有的是 Koa2,有的是 Thinkjs 有的是 Express,還有的是原生 NodeJS。

顯然每一個人的偏好都不一樣,代碼質量也不一樣,工程架構方式也不一樣,這爲後期的維護帶來巨大的麻煩,尤爲是作 Node 監控時候,發現無法用一套方案作批量的部署,也一樣不能作水平的快速擴展,須要挑選一個框架基於它作統一的封裝,從而把前端參與的全部服務端建設能夠統一塊兒來,並且現實是咱們的前端和 Node 應用因爲整個工程的構建與服務部署方式的不一樣,已經散落到各個服務器上,致使維護成爲了瓶頸,也必須到作出改變的時候了,這是當時的部分零散的應用圖:



爲何選擇 Eggjs

小菜前端在使用 Eggjs 做爲 Nodejs 的基礎服務框架以前使用過諸如 Koa、Express、Koa二、Thinkjs 等框架,其中與 Eggjs 最接近的當屬奇舞團開源的 Thinkjs[2] , 一樣的約定大於配置,一樣的基於 Koa2 進行包裝完善,一樣的採用多級分層的設計方式(Controller, Service 等等),讓應用開發變得更加清晰明瞭,然而有趣的是, Thinkjs 的開源時間(2013)早於 Eggjs 的開源時間,其在 github 上的 star 的增加速度倒是遠遠落後於 Eggjs,NPM 下載數亦然,雖然 thinkjs 開發體驗也不錯,小菜最後會選定 Eggjs 做爲 Nodejs 服務框架的緣由,除了上述提到的優勢以外,還有以下幾點 :

  • 高度可擴展的插件機制

  • 方便定製上層框架

  • 豐富且活躍的社區生態

  • 漸進式開發

  • 多進程管理

小菜前端從 18 年年初就開始使用 Eggjs 了,咱們的不少項目都是基於 Eggjs 搭建的,其中包括咱們的報表系統、GraphQL 網關、小程序後臺服務等。在使用 Eggjs 開發這些項目的過程當中咱們逐漸造成了本身的一套適用於宋小菜的基於 Eggjs 的上層框架,基於小菜特定業務場景長出來的 Framework,它的定製程度很高,你們能夠參考咱們實現這套框架時用到的技巧與方法,這些套路應該是通用的。


秉承怎樣的設計理念

考慮授人以魚不如授人以漁嘛,咱們先分享下咱們的設計理念,這是最簡單卻也最重要的開始部分,咱們的目標是風格統1、上手容易、維護方便:


而後就是總體需求的整理和開發集成,在開發集成個過程當中不斷調優:


image.png

定完目標,設計好流程,就要準備具體的實施了,咱們實施涉及到過程,主要從下面四個方面着手:

  • 框架關係

  • 通用 API

  • 插件定製

  • 工程管理


如何設計 Framework


框架關係

咱們將全部通用的 API 和經常使用工具函數以及經常使用的插件(redis、gateway)等統一集成在基礎框架 baseFramework 中,因爲 Egg 支持多級框架繼承,因此咱們能夠根據基礎框架 baseFramework 衍生出其餘框架如 GraphQL 相關的框架、微服務相關的框架,它至關因而一顆框架種子,能夠往不一樣的方向定製:


image.png


通用 API


1. 請求參數統一獲取

假定某個 HomeController 有成員函數 testAction 既要處理 post 請求又要處理 get 請求,就有可能出現如下狀況:

const { Controller } = require('egg');

module.exports = class HomeController extends Controller {
	testAction(){
    const { ctx } = this;
    const { method } = ctx.request;
    const id = method === 'GET'? ctx.request.query.id : ctx.request.body.id;
    ...
	}
}複製代碼

咱們能夠將其優化爲:

/* yourapp/app/controller/home.js */
const { BaseController } = require('egg');
// 或者
const { BaseController } = require('your-egg-framework');

module.exports = class HomeController extends BaseController {
	testAction(){
    const id = this.getParam('id');//
    ...
	}
}
  
/* egg-baseframework/core/base_controller.js */
const { Controller } = require('egg')

module.exports = class BaseController extends Controller {
	getParam(key) {
  	const { ctx } = this;
    const { method } = ctx.request;
    if (method === 'GET') {
    	if(key) {
      	...
      } else {
      	...
      }
    } else {
			...
    }
  }
}
/* your-egg-baseframework/lib/index.js */
const { BaseController } = require('../core/base_controller');

module.exports = {
  BaseController,
  ...
}

/* your-egg-framework/app.js */
module.exports = (app) => {
	require('egg').BaseController = BaseController
}複製代碼


2. 返回數據格式化

方法同上,咱們能夠在 BaseController 中定義統一的調用成功和調用失敗返回函數,並在函數中處理返回數據從而避免返回數據不規範的問題


3. 通用工具函數

咱們能夠將平時業務開發中可能會用到的工具函數統一經過框架擴展的額形式定義到內置對象helper 上,這些均可以以框架擴展(extend)的方式集成進來,好比參數轉化啊,錯誤信息格式化等等。


4. 增長參數校驗層

咱們能夠將參數校驗這一步抽離出來成爲 logic 層。有兩種方式能夠作到:

  • 在框架加載時調用 app.loader.loadToContext 將全部 controller 對應的參數校驗函數掛載到 context 上,在 controller 執行相應的處理函數時調用

  • 在你的框架繼承的 appWorkerLoader 中覆寫 eggjs 的 loadController , 對每個 controller 的處理函數都使用對應的 logic 進行代理


插件定製

Egg 的擁有着豐富的插件生態,然而總有些咱們須要用到的插件不太符合咱們的要求,好比:

  • egg-redis 長久不支持哨兵模式

  • egg-graphql 不支持鏈接其餘 graphql 服務

  • egg-kafka 長久沒有維護

這個時候就須要咱們本身動手編寫或修改相應的插件了,而有些在公司層面上通用的功能如:Java 服務端網關請求(egg-gateway)、用戶鑑權(egg-auth)等咱們也將其封裝爲插件集成到基礎框架中,講實話,整個框架開發中,讓人最開心最後成就感的部分就是寫插件的時候:


image.png


工程管理

因爲插件和插件之間,插件和框架之間,框架和框架之間存在相互依賴的關係,代碼管理就成爲了比較頭疼的問題,推薦使用目前比較火的 monorepo 來進行管理。規範版本發佈流程,避免出現不兼容問題。


總結

關於 Cross 的建設咱們差很少投入了一個多月的週期,從投入產出比來看仍是很划算的一次嘗試,可是在落地時候也會遇到很多問題,從人和團隊的角度來看,這樣的一套 Framework 須要有必定的 Node 編程能力的同窗才能較好的用起來,對於全部人依然有必定的心智成本,有沒有可能把這個成本繼續下降呢,走向 Pass 跟高階的只關心業務邏輯不關心背後實現的階段呢,這是一個很值得研究的課題,另外就是從事情的角度,若是業務中沒有那麼多的場景來承載這個框架,事實上它是很難繼續進階的,由於沒有足夠的應用和測試場景來暴露問題,這也是咱們當下遇到的一個實際困難,缺乏 Node 好手掣肘了咱們前進的步子,不過好消息是接下來的業務場景已經鋪開了,團隊也剛剛進了一個 Node 選手,接下來看看應用後發力效果如何。

相關文章
相關標籤/搜索