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