介紹
前端領(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)用呢?

當(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 等。
- 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();
上面這段代碼主要做了如下工作:
主程序加載的時(shí)候,根據(jù)配置信息,創(chuàng)建模塊的 module 信息。
通過 xhr 加載拿到模塊的 js 代碼,并通過 new Function 的方式,將我們的模塊 module 信息傳進(jìn)去執(zhí)行 js 代碼,這樣 js 代碼導(dǎo)出的內(nèi)容就會(huì)掛載到 modules[name] 上。
調(diào)用模塊導(dǎo)出的 render 方法來渲染模塊內(nèi)容。
注意:
導(dǎo)出的模塊必須選用 CommonJs 打包類型,否則無法將我們自己的 module 傳進(jìn)去。
加載模塊的時(shí)候使用 xhr 請(qǐng)求,這樣才能拿到代碼的 source code。
- 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)方法:
主程序通過定義一個(gè) defineModule 方法,并將其掛載在 window 上來實(shí)現(xiàn)模塊定義。
業(yè)務(wù)模塊在開發(fā)的時(shí)候,通過 window.defineModule 方法來定義自己的模塊,并將自己的 render 方法導(dǎo)出。
主程序在加載模塊的時(shí)候,通過正常的創(chuàng)建 script 來加載。在加載完成后,根據(jù)模塊的配置信息可以拿到模塊的導(dǎo)出內(nèi)容。
調(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ù)這種方式來提高性能。
大的應(yīng)用可以拆分成多個(gè)子模塊獨(dú)立開發(fā)。
多個(gè)獨(dú)立的子模塊可以單獨(dú)發(fā)布。
可以通過接口的方式實(shí)現(xiàn)服務(wù)發(fā)現(xiàn),可以根據(jù)用戶不同的權(quán)限動(dòng)態(tài)返回配置信息。
動(dòng)態(tài)加載模塊可以使用 new Function + CommonJs 和 AMD 的實(shí)現(xiàn),具體哪種方法取決于個(gè)人。
在模塊銷毀的時(shí)候處理數(shù)據(jù),在模塊剛進(jìn)入的時(shí)候做一下準(zhǔn)備工作。