摘要:插拔式應(yīng)用架構(gòu)方案和傳統(tǒng)前端架構(gòu)相比有以下幾個(gè)優(yōu)勢業(yè)務(wù)模塊分布式開發(fā),代碼倉庫更易管理。
背景
隨著互聯(lián)網(wǎng)云的興起,一種將多個(gè)不同的服務(wù)集中在一個(gè)大平臺上統(tǒng)一對外開放的概念逐漸為人熟知,越來越多與云相關(guān)或不相關(guān)的中后臺管理系統(tǒng)或企業(yè)級信息系統(tǒng)曾經(jīng)或開始采用了這種「統(tǒng)一平臺」的形式。同時(shí),前端領(lǐng)域保持著高速發(fā)展,早期的 jQuery+Backbone+Bootstrap 的 MVC 解決方案支撐起了業(yè)務(wù)相當(dāng)長的一段時(shí)間;后來,Angular、Ember 等 MVVM 框架開始嶄露頭角,前后端分離和前端組件化的思想在此時(shí)達(dá)到了鼎盛期。而在國內(nèi),Vue 框架憑著其簡潔易懂的 API 和出色的周邊生態(tài)支持獨(dú)領(lǐng)鰲頭,越來越多的中小型企業(yè)和開發(fā)者們開始轉(zhuǎn)向 Vue 陣營;與此同時(shí),在設(shè)計(jì)上獨(dú)樹一幟的純 View 層框架 React 開始興起,其充滿技術(shù)感的 Diff DOM 思想吸引了大批開發(fā)者,成為各大技術(shù)社區(qū)最火爆的話題,其周邊生態(tài)也隨之快速發(fā)展,成為了各大公司搭建技術(shù)棧時(shí)的首選框架。
回到平臺的話題。一個(gè)集成了不同業(yè)務(wù)的大平臺,很多情況下都是將業(yè)務(wù)拆分成多個(gè)子系統(tǒng)進(jìn)行開發(fā),最后由平臺提供統(tǒng)一的入口。而在當(dāng)前快速變化的前端大環(huán)境下,此類平臺需要考慮以下幾個(gè)難題:
怎樣將不同業(yè)務(wù)子系統(tǒng)集中到一個(gè)大平臺上,統(tǒng)一對外開放?
如何給不同用戶賦予權(quán)限讓其能夠訪問平臺的特定業(yè)務(wù)模塊同時(shí)禁止其訪問無權(quán)限的業(yè)務(wù)模塊?
如何快速接入新的子系統(tǒng),并對子系統(tǒng)進(jìn)行版本管理,保證功能同步?
針對于老系統(tǒng),如何實(shí)現(xiàn)從 Backbone 技術(shù)棧到 React 技術(shù)?;?Vue 技術(shù)棧的平滑升級?
接下來,我將分別基于這幾個(gè)問題介紹我們的實(shí)現(xiàn)方案。
產(chǎn)品模型首先我們來討論第一個(gè)問題:怎樣將不同業(yè)務(wù)子系統(tǒng)集中到一個(gè)大平臺上,統(tǒng)一對外開放?
如下圖所示,假設(shè)我們有三個(gè)業(yè)務(wù)子系統(tǒng),用戶如果要使用三個(gè)系統(tǒng)中的不同功能,他就需要同時(shí)在三個(gè)系統(tǒng)中登錄然后來回切換進(jìn)行操作。
而實(shí)際上理想的狀態(tài)是:A、B、C 三個(gè)子系統(tǒng)在同一個(gè)大平臺上,通過菜單提供入口進(jìn)入,用戶可以自由訪問任意一個(gè)子系統(tǒng)的頁面。如下圖所示:
注意到上圖中我們給 A、B、C 都標(biāo)記了 App(Application),把大平臺標(biāo)記為了 Product,以下為了方便說明,我們把每個(gè)子系統(tǒng)都稱為 App,把集成子系統(tǒng)的平臺稱為 Product。
事實(shí)上,對于真正的業(yè)務(wù)場景,除了用戶體驗(yàn)的改善,圖 2 所示系統(tǒng)還有很多優(yōu)勢,比如果企業(yè)想按業(yè)務(wù)模塊售賣產(chǎn)品,第二種方式顯然更好,用戶支付模塊費(fèi)用后賦予其模塊權(quán)限就可以使用新模塊了,而不是提供給用戶一個(gè)新系統(tǒng)。除此以外,對企業(yè)來說避免部署獨(dú)立的業(yè)務(wù)系統(tǒng)也就意味著省掉了域名、服務(wù)器、運(yùn)維方面的資源,節(jié)省了企業(yè)成本。
架構(gòu)方案確定了 Product 包含 App 的產(chǎn)品模型后,我們接下來要考慮以怎樣的一種形式,讓每個(gè) App 的訪問都能夠在 Product 下實(shí)現(xiàn)無縫切換。
如下圖所示,在訪問頁面時(shí),我們?yōu)樵L問路徑附加上了應(yīng)用前綴,標(biāo)識當(dāng)前訪問的是哪個(gè) App,App 路徑前綴之后才是當(dāng)前訪問的頁面路徑,這是一個(gè)前提約定。
而從 Product 角度來看,我們希望用戶在使用平臺時(shí),感受不到各個(gè) App 在切換時(shí)是在切換各系統(tǒng)模塊,所以 Product 需要控制所有 App 的視圖渲染時(shí)機(jī),即:Product 需統(tǒng)一管理所有 App 的視圖路由。
同時(shí),為了給不同權(quán)限用戶展現(xiàn)不同的視圖頁面,我們把從后端返回的用戶權(quán)限數(shù)據(jù)也傳入 Product,Product 會自動過濾掉沒有權(quán)限的路由,如下圖所示:
這里,因?yàn)樾枰尭?App 之間的切換對用戶來說就如同切換一個(gè)系統(tǒng)應(yīng)用的各個(gè)頁面,我們采用了單頁面應(yīng)用(SPA)的形式實(shí)現(xiàn) Product 的路由控制。
整個(gè)方案的架構(gòu)如下圖所示:
在這個(gè)架構(gòu)方案下,各子業(yè)務(wù)模塊可以根據(jù)需要動態(tài)加入大平臺下,不需要時(shí)屏蔽訪問路徑前綴即可;對平臺系統(tǒng)而言,各子業(yè)務(wù)模塊如同一個(gè)個(gè)功能插件,即插即用,不用即拔。這種插拔式的思想由來已久,我們稱之為「插拔式應(yīng)用架構(gòu)」。插拔式應(yīng)用架構(gòu)方案和傳統(tǒng)前端架構(gòu)相比有以下幾個(gè)優(yōu)勢:
業(yè)務(wù)模塊分布式開發(fā),代碼倉庫更易管理。
業(yè)務(wù)模塊(App)移植性強(qiáng),可多帶帶部署,也可整合到大平臺(Product)下。
模塊代碼高內(nèi)聚,更專注業(yè)務(wù)。
符合開閉原則,新模塊的接入不需要修改已有模塊,不會影響其他模塊的功能。
資源權(quán)限管理在介紹架構(gòu)方案的具體實(shí)現(xiàn)之前,我們需要先做些準(zhǔn)備工作,先來看下開頭我們提出的第二、三兩個(gè)問題。
首先是第二個(gè)問題:如何給不同用戶賦予權(quán)限讓其能夠訪問平臺的特定業(yè)務(wù)模塊同時(shí)禁止其訪問無權(quán)限的業(yè)務(wù)模塊?
上文中簡單提到了后端將訪問權(quán)限數(shù)據(jù)傳入 Product,我們的具體做法是每個(gè) App 將自己的全量路由路徑傳入 Product ,而在啟動平臺(Product)時(shí),Product 會從后端根據(jù)當(dāng)前登錄用戶獲取其有權(quán)限的路由路徑,當(dāng)訪問 App 任一路由時(shí),會在首次與有權(quán)限的路由路徑進(jìn)行比對,比對失敗的路由路徑會自動導(dǎo)向無權(quán)限的頁面視圖。
至于路由的權(quán)限維護(hù),可以做一個(gè)可視化配置路由的管理頁面,權(quán)限的細(xì)化程度根據(jù)自己的業(yè)務(wù)情況自定義即可。
其次是第三個(gè)問題:如何快速接入新的子系統(tǒng),并對子系統(tǒng)進(jìn)行版本管理,保證功能同步?
要回答這個(gè)問題,我們就要清楚每個(gè) App 具體的接入方式。上文中有提到每個(gè) App 的訪問依賴于當(dāng)前的路徑前綴,我們的具體做法是后端維護(hù)所有 App 基于 webpack 打出的 bundle 包的地址,并將這些包地址的配置映射關(guān)系傳入 Product,當(dāng)首次訪問到某個(gè) App 時(shí),Product 會首先加載該 App 相關(guān)的 bundle 包,而其 js bundle 包內(nèi)會調(diào)用全局的 Product 注入自己的路由信息,然后將后續(xù)的路由處理交給 Product 執(zhí)行。
當(dāng)然,上述的實(shí)現(xiàn)會涉及到渲染 App 視圖時(shí)的一些問題,在接下來的實(shí)現(xiàn)方案中我們會介紹到。
實(shí)現(xiàn)方案上面我們討論了很多理論性的內(nèi)容,接下來進(jìn)入干貨環(huán)節(jié):如何實(shí)現(xiàn)一個(gè)插拔式應(yīng)用框架?
根據(jù)上文中介紹一些實(shí)現(xiàn)思路,我們對將要實(shí)現(xiàn)的插拔式框架會先有一個(gè)大概的功能輪廓:
自實(shí)現(xiàn)一個(gè) Router,該 Router 需要在路由時(shí)根據(jù)路徑自動解析出 App 標(biāo)識,然后基于標(biāo)識動態(tài)加載 App 對應(yīng)的資源包。
App 加載其 js 資源包后立即執(zhí)行,自動向 Product 內(nèi)注入 App 相關(guān)的路由信息。
Router 在 App 加載完資源包后(script 腳本會在加載后立即執(zhí)行),嘗試根據(jù)路徑渲染 App 視圖頁面。
切換路由后,如果切換至了其他子 App,原 App 應(yīng)基于自身的生命周期,清除相關(guān) DOM 和事件等邏輯。
簡單歸納一下,我們的插拔式應(yīng)用框架應(yīng)在實(shí)現(xiàn)上做出以下幾個(gè)功能點(diǎn):動態(tài)路由、腳本加載和調(diào)度、子應(yīng)用視圖渲染、應(yīng)用生命周期管理。
接下來我們分別一一介紹各功能點(diǎn)的實(shí)現(xiàn)思路。
動態(tài)路由說起路由,對于不同的技術(shù)棧,有著不同的實(shí)現(xiàn)方案。如 Vue 有 vue-router,React 有 react-router 等。而為了適配各子 App 采用不同的技術(shù)體系開發(fā)的情形,我們需要將路由配置加以規(guī)范和統(tǒng)一管理。所以,我們需要重新設(shè)計(jì)一個(gè) Router,這個(gè) Router 必須能夠做到:動態(tài)注入路由且同時(shí)支持不同技術(shù)體系組件的渲染。
這里,我們采用了靈活性較強(qiáng)的 universal-router,其 path 和 action 的配置方式能夠讓我們很方便地進(jìn)行自定義的路由邏輯處理。雖然它不支持動態(tài)注入路由,但其代碼組織合理,配合大名鼎鼎的 history 庫,我很容易便實(shí)現(xiàn)了滿足自己需求的 Router。
如下圖所示:
腳本加載和調(diào)度在完成動態(tài)路由的基本功能后,我們就要開始處理路由邏輯的第一步了:動態(tài)加載當(dāng)前訪問 App 的腳本等資源包。
首先我們先分析出處理流程:在開始路由時(shí),我們需要根據(jù)請求路徑的第一段路徑名(如 /a/b 的第一段為 a)確定當(dāng)前要路由的路徑對應(yīng)的是哪一個(gè) App,若對應(yīng)的 App 尚未注入路由信息,就需要動態(tài)加載 App 的資源包,待執(zhí)行了 js 腳本資源包后,再繼續(xù)執(zhí)行后續(xù)的渲染邏輯。
App 的資源包可以有多種形式的打包方式,如 AMD、Commonjs、UMD 等。而為了兼容 App 能夠分別多帶帶部署和集成至平臺兩種情況,且保持最簡化的依賴,我們?nèi)耘f采用基于 webpack 打出 UMD 包的形式——讓 JS 加載后立即執(zhí)行即可,省去了如對 AMD 包加載器如 Requirejs 的依賴。
那么,依托于瀏覽器自身的腳本加載機(jī)制,我們的資源包加載器就很好實(shí)現(xiàn)了:分別使用 link 和 script 標(biāo)簽在 head 和 body 標(biāo)簽下動態(tài)插入資源包地址即可。
當(dāng)然,也有人會考慮到資源包先后順序加載依賴的問題。一般情況下,webpack 打包時(shí)會自行處理依賴關(guān)系,如果對多個(gè)資源包插件有先后執(zhí)行順序的依賴需求(如 jQuery 插件依賴),可在加載時(shí)做特殊的串行處理。
App 腳本加載流程如下圖所示:
應(yīng)用視圖渲染處理了 App 資源包的動態(tài)加載后,我們就要實(shí)現(xiàn)路由模塊最核心的功能了:應(yīng)用視圖的渲染。
首先,在上文介紹方案時(shí),我們提到每個(gè)子 App 既要能支持多帶帶部署,又需要能夠接入 Product 內(nèi),在平臺上運(yùn)行。所以,我們應(yīng)該意識到:各 App 視圖的渲染應(yīng)該交由每個(gè)子 App 自己完成,而不是由框架統(tǒng)一完成。
如果你對上面的結(jié)論感覺太突兀,那么,請思考以下兩個(gè)問題:
如果框架統(tǒng)一渲染路由結(jié)果,那么如何保證對 React Component、Backbone View 等各種不同形式組件的兼容?
如果框架統(tǒng)一渲染路由結(jié)果,就需要引入渲染接口,那么如何保證兼容各子 App 的接口版本(如 ReactDOM 版本等)?
所以,為了體現(xiàn)框架兼顧不同技術(shù)體系 App 的插拔式設(shè)計(jì)思想,我們必須要將應(yīng)用視圖的渲染從框架內(nèi)抽離出去。
那么,框架的路由在視圖渲染邏輯上還需要做什么事呢?
我們很快就會想到視圖渲染邏輯抽離出去后存在的問題:各子 App 要自己實(shí)現(xiàn)渲染了,那框架提效的作用體現(xiàn)在了何處?渲染接口又該如何統(tǒng)一?
前文中提到了開閉原則,開閉原則最主要的設(shè)計(jì)思想就是面向?qū)ο笤O(shè)計(jì)。我們的解決方案就是:
提供一個(gè) Application 基類,規(guī)范渲染接口,各子 App 在注入應(yīng)用時(shí)必須注入繼承自 Application 基類的應(yīng)用實(shí)例。
默認(rèn)提供使用較廣的 React Application 和適用性較強(qiáng)的 Backbone Application 兩個(gè)渲染實(shí)現(xiàn)應(yīng)用類(均繼承自 Application 基類)。
在各子 App 的入口 JS 文件內(nèi),可以根據(jù)自己的技術(shù)體系直接實(shí)例化 ReactApplication 或 BackboneApplication,也可以繼承自 Application 基類自實(shí)現(xiàn)渲染接口。當(dāng)然,如果自己的應(yīng)用類使用較多,可以作為插件貢獻(xiàn)出去。
Application 基類的示例代碼:
// application/index.js class Application { static DEFAULTS = { // ... } constructor(options = {}) { this._options = Object.assign({}, DEFAULTS, options); } start() { // 啟動應(yīng)用,開啟 view 的路徑變化監(jiān)聽事件 } stop() { // 停止路徑變化監(jiān)聽事件 } renderLayout() { // 渲染布局的接口 } render() { // 渲染主體內(nèi)容的接口 } // ... }
ReactApplication 類的實(shí)現(xiàn)示例代碼:
// application/react/index.js import Application from "../index.js"; class ReactApplication extends Application { render(err, children, params = {}) { if (err) { // 渲染錯(cuò)誤頁 throw err; } // React 和 ReactDOM 在實(shí)例化時(shí)由 App 自己傳入,便于各 App 自己控制 React 版本 const { React, ReactDOM } = this._options; ReactDOM.render(children, this._container); } }
BackboneApplication 類的實(shí)現(xiàn)示例代碼:
// application/backbone/index.js import Application from "../index.js"; class BackboneApplication extends Application { render(err, viewAction, params = {}) { if (err) { // 渲染錯(cuò)誤頁 throw err; } if (viewAction.prototype && isFunction(viewAction.prototype.render)) { this._currentView = new viewAction(params); return this._currentView.render(); } if (typeof viewAction.render === "function") { return viewAction.render(params); } } }
將渲染邏輯交給各子 App 自己實(shí)現(xiàn)后,我們就可以避免在框架的 View 類中根據(jù)不同技術(shù)體系實(shí)現(xiàn)不同的渲染邏輯。如果子 App 換了 Backbone 和 React 之外的其他渲染方式,我們也不必修改框架的實(shí)現(xiàn)重新發(fā)布新的版本。
另外,除了應(yīng)用實(shí)例外,我們還需要構(gòu)造一個(gè) Product 類,提供注入應(yīng)用實(shí)例的入口。示例代碼如下:
class Product { static registerApplication = (app) => { // 緩存 app 實(shí)例,并注入 app 路由 } }
在各子 App 的入口 JS 文件內(nèi),調(diào)用 Product 類注入當(dāng)前 app 實(shí)例(以 React App 為例):
// src/app.js import React from "react"; import ReactDOM from "react-dom"; import { Product, ReactApplication } from "plugin-pkg"; const app = new ReactApplication({ React, ReactDOM, // ... }); Product.registerApplication(app);應(yīng)用生命周期管理
到這里,從動態(tài)路由到視圖渲染,我們都已經(jīng)有了具體的實(shí)現(xiàn)思路,現(xiàn)在考慮實(shí)際應(yīng)用時(shí)的一個(gè)問題:在切換各子 App 時(shí),上一個(gè) App 的 DOM 會被替換,但相關(guān)的事件并未正確清除。拿 React 來說,我們直接替換掉 DOM 內(nèi)容,但未正確觸發(fā) React 組件的 UnMount 事件,Backbone View 的 destroy 回調(diào)同理。
所以,我們需要為 Application 類添加 destroy 接口:
class Application { destroy() { // 在當(dāng)前 App 實(shí)例切換出去時(shí)調(diào)用 } }
除了銷毀事件,有時(shí)在 App 切換進(jìn)來后也會需要一些統(tǒng)一處理,我們同時(shí)需要添加 ready 接口:
class Application { ready() { // 在當(dāng)前 App 實(shí)例切換進(jìn)來時(shí)調(diào)用 } }
生命周期的處理實(shí)現(xiàn),各 App 實(shí)例根據(jù)自己的實(shí)際情況自行實(shí)現(xiàn)相關(guān)邏輯即可。
框架在切換 App 時(shí),需自動調(diào)用上一個(gè)應(yīng)用實(shí)例的銷毀接口,然后在渲染 App 后,再自動調(diào)用當(dāng)前 App 的準(zhǔn)備接口。
構(gòu)建配置上面的內(nèi)容都是插拔式框架需要實(shí)現(xiàn)的功能,另外,各子 App 在打包時(shí)也要統(tǒng)一配置。如框架的依賴應(yīng)設(shè)為 external 的形式,在打包時(shí)不打入資源包。因?yàn)槲覀兊母?App JS 資源包都是 UMD 包直接執(zhí)行的形式,在實(shí)際運(yùn)行時(shí)使用 Product 統(tǒng)一引入的框架包的全局變量即可。
webpack 配置的示例代碼如下:
// webpack.config.js const path = require("path"); const resolveApp = relativePath => path.join(process.cwd(), relativePath); module.exports = { entry: { bundle: resolveApp("src/app.js"); }, module: { // ... }, plugins: [ // ... ], externals: { "plugin-pkg": "Plugin", }, };
這樣,不但能兼容獨(dú)立部署和集成入平臺兩種形式,也能在插入平臺模式下統(tǒng)一用平臺的插拔式框架包,便于平臺的統(tǒng)一升級。
總結(jié)以上的插拔式應(yīng)用設(shè)計(jì)是因?yàn)榭紤]到了兼容不同技術(shù)體系的子業(yè)務(wù)模塊,路由的實(shí)現(xiàn)稍顯繁復(fù),腳本的動態(tài)加載也比較簡單。在實(shí)際業(yè)務(wù)需求中,如果已經(jīng)確定了統(tǒng)一技術(shù)體系,大部分情況下就不必考慮兼容不同子業(yè)務(wù)模塊的問題了,完全可以選定一種技術(shù)體系(如 Vue 或 React)來實(shí)現(xiàn),多做的可能也只有權(quán)限處理這一小塊。
所以,以上內(nèi)容僅作參考,根據(jù)實(shí)際業(yè)務(wù)不同,設(shè)計(jì)出適合自己業(yè)務(wù)的插拔式方案,才是最好用的方案。
參考single-spa
文章可隨意轉(zhuǎn)載,但請保留此 原文鏈接 。
非常歡迎有激情的你加入 ES2049 Studio,簡歷請發(fā)送至 caijun.hcj(at)alibaba-inc.com 。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/108310.html
摘要:月日,在中國信息通信研究院中國通信標(biāo)準(zhǔn)化協(xié)會聯(lián)合主辦為期兩天的可信云大會上,主辦方頒發(fā)了年上半年可信云系列評估認(rèn)證,以及公布了可信云相關(guān)技術(shù)服務(wù)能力與應(yīng)用案例最佳實(shí)踐評選活動榜單。7月27日,在中國信息通信研究院、中國通信標(biāo)準(zhǔn)化協(xié)會聯(lián)合主辦為期兩天的2021 可信云大會上,主辦方頒發(fā)了2021年上半年可信云系列評估認(rèn)證,以及公布了可信云相關(guān)技術(shù)、服務(wù)能力與應(yīng)用案例最佳實(shí)踐評選活動榜單。UCl...
摘要:按鈕方面按鈕通過自定義指令綁定其特定的操作接口信息如產(chǎn)品上傳按鈕,需要擁有產(chǎn)品上傳的信息,才可以繼續(xù)執(zhí)行按鈕的業(yè)務(wù)邏輯。 開篇啰嗦幾句 在傳統(tǒng)單體項(xiàng)目中,通常會有一些框架用來管理熟知的權(quán)限。如耳濡目染的 Shiro 或者 Spring Security 。然而,到了現(xiàn)在這個(gè)時(shí)代,新開始的項(xiàng)目會更多的才用后端微服務(wù) + 前端 mvvm 的架構(gòu)開始書寫項(xiàng)目。權(quán)限控制方面將變得有些許晦澀。當(dāng)...
摘要:而從技術(shù)實(shí)現(xiàn)角度,微前端架構(gòu)解決方案大概分為兩類場景單實(shí)例即同一時(shí)刻,只有一個(gè)子應(yīng)用被展示,子應(yīng)用具備一個(gè)完整的應(yīng)用生命周期。為了解決產(chǎn)品研發(fā)之間各種耦合的問題,大部分企業(yè)也都會有自己的解決方案。 原文鏈接:https://zhuanlan.zhihu.com/p/... Techniques, strategies and recipes for building a modern ...
摘要:什么是單頁面應(yīng)用單頁面應(yīng)用是指用戶在瀏覽器加載單一的頁面,后續(xù)請求都無需再離開此頁目標(biāo)旨在用為用戶提供了更接近本地移動或桌面應(yīng)用程序的體驗(yàn)。流程第一次請求時(shí),將導(dǎo)航頁傳輸?shù)娇蛻舳?,其余請求通過獲取數(shù)據(jù)實(shí)現(xiàn)數(shù)據(jù)的傳輸通過或遠(yuǎn)程過程調(diào)用。 什么是單頁面應(yīng)用(SPA)? 單頁面應(yīng)用(SPA)是指用戶在瀏覽器加載單一的HTML頁面,后續(xù)請求都無需再離開此頁 目標(biāo):旨在用為用戶提供了更接近本地...
閱讀 2630·2021-11-17 09:33
閱讀 4015·2021-10-19 11:46
閱讀 945·2021-10-14 09:42
閱讀 2291·2021-09-22 15:41
閱讀 4288·2021-09-22 15:20
閱讀 4683·2021-09-07 10:22
閱讀 2346·2021-09-04 16:40
閱讀 842·2019-08-30 15:52