前端微服務(wù)

介紹

前端領(lǐng)域一直在不斷的發(fā)展,傳統(tǒng)的 jQuery + Backbone + Bootstrap MVC 解決方案逐漸被 Angular、Ember、React、Vue 等 MVVM 框架替代,前后端分離和前端組件化的思想已經(jīng)達(dá)到了頂峰。

在傳統(tǒng)的系統(tǒng)中,通常會(huì)有一個(gè)站點(diǎn),所有的業(yè)務(wù)都在這個(gè)站點(diǎn)上,隨著業(yè)務(wù)復(fù)雜度的上升,打包的體積會(huì)迅速變大,發(fā)布時(shí)也會(huì)變慢。為了適應(yīng)業(yè)務(wù)的復(fù)雜度往往需要更多的開發(fā)者、更細(xì)粒度的團(tuán)隊(duì)組織。分組開發(fā)時(shí)大家的模塊解耦到各自完成,上線時(shí)糅合在一起運(yùn)行,這時(shí)會(huì)產(chǎn)生出層出不窮的分支合并、代碼回滾等,都會(huì)造成合作效率的驟降。

所有的業(yè)務(wù)聚合在一起還會(huì)造成頻繁發(fā)布,每個(gè)業(yè)務(wù)都會(huì)產(chǎn)生一定的更新頻率,每個(gè)業(yè)務(wù)都會(huì)導(dǎo)致整個(gè)項(xiàng)目一起升級(jí)、測(cè)試和上線,發(fā)布頻率的總和會(huì)非常高、非常頻繁。

以如此高的上線頻率、版本迭代速度來看,開發(fā)者也極難追溯哪個(gè)版本對(duì)應(yīng)哪個(gè)改動(dòng)。

框架層出不窮,版本更是迭代不窮,難免會(huì)出現(xiàn)前端項(xiàng)目技術(shù)棧不統(tǒng)一、所用框架版本不統(tǒng)一。

比如:有的項(xiàng)目使用了 Vue,有的項(xiàng)目使用了 React,Vue 的項(xiàng)目已經(jīng)穩(wěn)定運(yùn)行,若是沒有新的功能加入,但卻需要結(jié)合到其它的項(xiàng)目中時(shí),對(duì) Vue 的重構(gòu)的成本會(huì)很高,這時(shí)就需要去兼容不同類型的前端框架。

一家大的公司也可能有很多的應(yīng)用,這些應(yīng)用代表了公司的組織架構(gòu),在用戶眼里他們是一個(gè)產(chǎn)品。

聚合成為了一個(gè)技術(shù)趨勢(shì),體現(xiàn)在前端的聚合就是微服務(wù)化架構(gòu)

那么問題來了,什么是前端微服務(wù)?

一個(gè)集成了不同業(yè)務(wù)的大型應(yīng)用,將應(yīng)用拆分成多個(gè)模塊,每一個(gè)模塊可以單獨(dú)的開發(fā)、調(diào)試并上線,最后由應(yīng)用提供統(tǒng)一的入口。

有什么優(yōu)勢(shì)?
  • 每個(gè)模塊都是一個(gè)獨(dú)立的個(gè)體,如果有某個(gè)模塊出現(xiàn)問題了,不會(huì)導(dǎo)致整個(gè)應(yīng)用掛掉。

  • 由于每個(gè)模塊可以單獨(dú)上線,因此上線會(huì)更快,有利于更新迭代。

  • 由于有了服務(wù)注冊(cè)的功能,因此頁面都可以通過配置化的方式來動(dòng)態(tài)加載,對(duì)于功能的新增、回滾特別方便。

  • 框架無關(guān) (每個(gè)模塊都可以根據(jù)實(shí)際需求選擇不同的框架)

會(huì)遇到什么樣的難題?
  • 如何將不同業(yè)務(wù)模塊集中到一個(gè)大的應(yīng)用上,統(tǒng)一對(duì)外開放?

  • 如何給不同用戶賦予權(quán)限,讓其能夠訪問平臺(tái)的特定業(yè)務(wù)模塊,同時(shí)禁止其訪問無權(quán)限的業(yè)務(wù)模塊?

  • 如何快速接入新的模塊,并對(duì)模塊進(jìn)行版本管理,保證功能同步?

實(shí)現(xiàn)方案

我們已經(jīng)知道了什么是微服務(wù),那我們?cè)撊绾尉唧w去實(shí)現(xiàn)呢?

獨(dú)立開發(fā)

每個(gè)模塊單獨(dú)開發(fā),不需要和其他模塊保持完全一致的組織模型,也可以選擇適合自己的框架。

獨(dú)立開發(fā)的優(yōu)點(diǎn):

  • 業(yè)務(wù)模塊分布式開發(fā),代碼倉庫更易管理。

  • 模塊內(nèi)的代碼高內(nèi)聚,更專注于業(yè)務(wù);

  • 新模塊的接入不需要修改已有模塊,不會(huì)影響其他模塊的功能;

  • 業(yè)務(wù)模塊移植性強(qiáng),可單獨(dú)部署,也可整合到大平臺(tái)下。

獨(dú)立部署

單體應(yīng)用的一大問題是發(fā)布非常慢,導(dǎo)致每天上線不了幾次,風(fēng)險(xiǎn)也很大。當(dāng)業(yè)務(wù)多的時(shí)候,不管有多少更新都要一起發(fā)布。

獨(dú)立部署解決了以上的問題,每次只需要發(fā)布對(duì)應(yīng)模塊的應(yīng)用即可。

服務(wù)發(fā)現(xiàn)

單體應(yīng)用拆分成多個(gè)模塊之后,一個(gè)項(xiàng)目里的方法分開部署了,那主程序怎么知道有哪些模塊,以及各個(gè)模塊對(duì)應(yīng)的配置信息( js / css 等配置信息)呢。

查找配置的模塊信息的過程,就叫做服務(wù)發(fā)現(xiàn)。

需要有個(gè)統(tǒng)一的注冊(cè)機(jī)構(gòu),把提供服務(wù)的各個(gè)應(yīng)用都查到。

假如:我們有三個(gè)不同的業(yè)務(wù)應(yīng)用,用戶如果想使用這三個(gè)業(yè)務(wù),如何去切換這三個(gè)應(yīng)用呢?

服務(wù)發(fā)現(xiàn)

當(dāng)用戶登錄時(shí),我們先調(diào)用接口根據(jù)用戶的身份、權(quán)限來返回不同的模塊配置信息,當(dāng)用戶點(diǎn)擊對(duì)應(yīng)的模塊后,通過 nginx 配置反向代理,來進(jìn)行路由分發(fā),從而實(shí)現(xiàn)前端微服務(wù)。

具體怎么實(shí)現(xiàn)呢?

每個(gè)模塊將自己的全量路由路徑傳入給主程序 ,而在主程序啟動(dòng)時(shí),主程序會(huì)調(diào)用接口從后端拉取當(dāng)前登錄用戶有權(quán)限的路由路徑,當(dāng)訪問某模塊的路由時(shí),會(huì)與有權(quán)限的路由路徑進(jìn)行比對(duì),比對(duì)失敗的路由路徑會(huì)自動(dòng)導(dǎo)向無權(quán)限的頁面視圖。

至于路由的權(quán)限維護(hù),可以做一個(gè)可視化配置路由的管理頁面,權(quán)限的細(xì)化程度根據(jù)自己的業(yè)務(wù)情況自定義即可。

下面是通過接口返回的一個(gè)模塊的配置信息,path 代表模塊對(duì)應(yīng)的路由地址,也就是說,當(dāng)前端匹配到了路由為 /home 的時(shí)候,就會(huì)加載對(duì)應(yīng)的 js 和 css 文件,并執(zhí)行 js 文件渲染模塊內(nèi)容。

[{
    name: 'home',
    path: '/home',
    js: 'https://XXX/home.js',
    css: 'https:XXX/home.css'
}]
動(dòng)態(tài)加載

當(dāng)前端匹配到了一個(gè)路由,需要確定當(dāng)前路由路徑對(duì)應(yīng)的是哪一個(gè)模塊,若對(duì)應(yīng)的模塊尚未注入路由信息,需要?jiǎng)討B(tài)加載模塊資源包,待加載并執(zhí)行了對(duì)應(yīng)的 js 腳本資源包后,再繼續(xù)執(zhí)行后續(xù)的渲染邏輯。

模塊的資源包可以有多種形式的打包方式,如 AMD、Commonjs、UMD 等。

  1. CommonJs

首先實(shí)現(xiàn)一個(gè)簡單的模塊功能:

import React from 'react';
import ReactDom from 'react-dom';


function App() {
 return React.createElement('div', null, 'hello world');
}


export const render = container => {
  ReactDom.render(React.createElement(App), container);
};

這段代碼在頁面上顯示了 hello world ,并導(dǎo)出了 render 方法。

// 全局模塊管理
const modules = {};

function loadModule() {
 const currentConfig = {
        name: 'home',
        path: '/home',
        js: './dist/main.js',
      };
 
 const { name, path, js } = currentConfig;
 
      modules[name] = {
        exports: {},
      };
 const ajax = new XMLHttpRequest();
      ajax.open('get', js);
      ajax.onload = function(event) {
 new Function('module', 'exports', this.responseText)(
       modules[name],
       modules[name].exports,
     );
 
        modules[name].exports.render(document.getElementById('app'));
      };
      ajax.send();
}

loadModule();

上面這段代碼主要做了如下工作:

  1. 主程序加載的時(shí)候,根據(jù)配置信息,創(chuàng)建模塊的 module 信息。

  2. 通過 xhr 加載拿到模塊的 js 代碼,并通過 new Function 的方式,將我們的模塊 module 信息傳進(jìn)去執(zhí)行 js 代碼,這樣 js 代碼導(dǎo)出的內(nèi)容就會(huì)掛載到 modules[name] 上。

  3. 調(diào)用模塊導(dǎo)出的 render 方法來渲染模塊內(nèi)容。

注意:

  • 導(dǎo)出的模塊必須選用 CommonJs 打包類型,否則無法將我們自己的 module 傳進(jìn)去。

  • 加載模塊的時(shí)候使用 xhr 請(qǐng)求,這樣才能拿到代碼的 source code。

  1. AMD

AMD 語法:

define('module', [...deps], function () {
    ...
});

首先,定義一個(gè)簡單的模塊:

import React from 'react';
import ReactDom from 'react-dom';

function App() {
 return React.createElement('div', null, 'hello world');
}

const render = container => {
  ReactDom.render(React.createElement(App), container);
};

window.defineModule('home', {
  render,
});

通過 window.defineModule 這個(gè)方法來定義自己的模塊。

const namespace = Symbol('namespace');
window[namespace] = {};

function defineModule(name, exports) {
 window[namespace][name] = exports;
}

function getModule(name) {
 return window[namespace][name];
}

window.defineModule = defineModule;

function loadModule() {
 const currentConfig = {
        name: 'home',
        path: '/home',
        js: './dist/main.js',
      };
 
 const { name, path, js } = currentConfig;
 
 const scriptEle = document.createElement('script');
      scriptEle.src = js;
      scriptEle.onload = () => {
 const module = getModule(name);
 module.render(document.getElementById('app'));
      };
 
 document.body.appendChild(scriptEle);
}

loadModule();

主程序通過這種方式定義模塊,其他模塊就可以通過依賴項(xiàng)注入的方式來使用該模塊。

實(shí)現(xiàn)方法:

  1. 主程序通過定義一個(gè) defineModule 方法,并將其掛載在 window 上來實(shí)現(xiàn)模塊定義。

  2. 業(yè)務(wù)模塊在開發(fā)的時(shí)候,通過 window.defineModule 方法來定義自己的模塊,并將自己的 render 方法導(dǎo)出。

  3. 主程序在加載模塊的時(shí)候,通過正常的創(chuàng)建 script 來加載。在加載完成后,根據(jù)模塊的配置信息可以拿到模塊的導(dǎo)出內(nèi)容。

  4. 調(diào)用模塊導(dǎo)出的 render 方法來渲染模塊內(nèi)容。

生命周期管理

在切換各模塊時(shí),上一個(gè)模塊的 DOM 會(huì)被替換,但相關(guān)的事件并未正確清除。比如使用 React 框架的模塊,當(dāng)我們替換掉 DOM 內(nèi)容時(shí),并未正確觸發(fā) React 組件的 UnMount 事件。

所以,我們需要為模塊添加 destroy 和 ready 接口:

class App {

  ready() {
    // 在當(dāng)前模塊切換進(jìn)來時(shí)調(diào)用
  }

  destroy() {
    // 在當(dāng)前模塊切換出去時(shí)調(diào)用
  }

}

在切換模塊時(shí),需自動(dòng)調(diào)用上一個(gè)模塊的銷毀接口,然后在渲染新的模塊后,再自動(dòng)調(diào)用當(dāng)前模塊的準(zhǔn)備接口。

總結(jié)

當(dāng)一個(gè)應(yīng)用非常龐大且復(fù)雜時(shí),可以考慮使用前端微服務(wù)這種方式來提高性能。

  1. 大的應(yīng)用可以拆分成多個(gè)子模塊獨(dú)立開發(fā)。

  2. 多個(gè)獨(dú)立的子模塊可以單獨(dú)發(fā)布。

  3. 可以通過接口的方式實(shí)現(xiàn)服務(wù)發(fā)現(xiàn),可以根據(jù)用戶不同的權(quán)限動(dòng)態(tài)返回配置信息。

  4. 動(dòng)態(tài)加載模塊可以使用 new Function + CommonJs 和 AMD 的實(shí)現(xiàn),具體哪種方法取決于個(gè)人。

  5. 在模塊銷毀的時(shí)候處理數(shù)據(jù),在模塊剛進(jìn)入的時(shí)候做一下準(zhǔn)備工作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容