原文地址:www.lishuaishuai.com/architectur…javascript
大型組織的組織結構、軟件架構在不斷地發生變化。移動優先(Mobile First)、App中臺(One App)、中臺戰略等,各類口號在不斷的提出、修改和演進。同時,業務也在不斷地發展,致使應用不斷膨脹,進一步映射到軟件架構上。css
現有Web應用(SPA)不能很好的拓展和部署,隨着時間的推移,各個項目變得愈來愈臃腫,web應用變得愈來愈難以維護。html
微前端是一種相似於微服務的架構,它將微服務的理念應用於瀏覽器端,即將 Web 應用由單一的單體應用轉變爲多個小型前端應用聚合爲一的應用。各個前端應用還能夠獨立運行、獨立開發、獨立部署。前端
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. — Micro Frontendsjava
關鍵詞:解耦、聚合、技術棧無關、獨立運行、獨立開發、獨立部署、易拓展git
《前端架構從入門到微前端》一書中,將微前端的實施分爲六種:github
路由分發式微前端,即經過路由將不一樣的業務分發到不一樣的、獨立前端應用上。其一般能夠經過 HTTP 服務器的反向代理來實現,又或者是應用框架自帶的路由來解決。如圖:web
前端微服務化,是微服務架構在前端的實施,每一個前端應用都是徹底獨立(技術棧、開發、部署、構建獨立)、自主運行的,最後經過模塊化的方式組合出完成的應用。json
採用這種方式意味着,一個頁面上能夠同時存在兩個以上的前端應用在運行。如圖:bootstrap
目前主流的框架有 Single-SPA、qiankun、Mooa,後二者都是基於 Single-SPA
的封裝。
微應用化是指在開發時應用都是以單1、微小應用的形式存在的,而在運行時,則是經過構建系統合併這些應用,並組合成一個新的應用。
微應用化大都是以軟件工程的方式來完成前端應用的聚合,所以又能夠稱之爲組合式集成。
微應用化只能使用惟一的一種前端框架。
如圖:
微件化(Widget)是一段能夠直接嵌入應用上運行的代碼,它由開發人員預先編譯好,在加載時不須要再作任何修改或編譯。微前端下的微件化是指,每一個業務團第編寫本身的業務代碼,並將編譯好的代碼部署到指定的服務器上,運行時只須要加載指定的代碼便可。
如圖:
iFrame 做爲一個很是古老的,人人都以爲普通的技術,卻一直很管用。
HTML 內聯框架元素
<iframe>
表示嵌套的正在瀏覽的上下文,能有效地將另外一個 HTML 頁面嵌入到當前頁面中。
iframe 能夠建立一個全新的獨立的宿主環境,這意味着咱們的前端應用之間能夠相互獨立運行。採用 iframe 有幾個重要的前提:
在不少業務場景下,不免會遇到一些難以解決的問題,那麼能夠引入 iframe 來解決。
Web Components 是一套不一樣的技術,容許開發者建立可重用的定製元素(它們的功能封裝在代碼以外)而且在您 Web 應用中使用它們。
在真正的項目上使用 Web Components技術,離如今還有一些距離,結合 Web Components 來構建前端應用,是一種面向將來演進的架構。或者說在將來能夠採用這種方式來構建應用。
如圖:
在真實的業務場景中,每每是上面提到六種方式中的幾種的結合使用,或者是某種方式的變種。下面看我遇到的真實場景。
現有三個內部系統,下面稱之爲 old-a、old-b和C,其中,old-a和old-b是老舊的先後端未分離項目,C爲先後端分離的SPA應用(React + HIUI),三個系統的架構圖大體以下:
能夠看到,old-a 運行在一臺服務器1上,old-b運行在服務器2上,C系統的前端資源在服務器2上,而且C沒有本身的域名。
三個系統均在後端同窗維護和開發,他們的需求以下:
考慮開發同窗的需求和開發成本、維護成本、將來的可拓展性,系統改造關鍵點以下:
整體的改造方案使用微前端的思想進行。對上面提到的六種方式進行對比(點擊查看大圖):
對於上面幾種方式,在具體的實施使用了路由分發、iFrame、應用微服務化、微應用化的融合方式。或者說是某種方案的變種,由於改造以後同時具有了這幾種方案的特色。
對於C系統和正在開發的x個系統使用 single-spa 作改造,對於老舊的系統 old-a 和 old-b 使用 iframe 接入。
改造後以下圖:
此時,兩個老系統分別部署在各自的服務器,C 和將來的多個應用部署在同一臺服務器。而後,在 Nginx 層 爲老系統分配了兩個路由(暫且稱之爲 old-a 和old-b),分別將請求打到各自的服務器,根路由打到 C 和 xx 應用的服務器。
使用React 框架的 C 和 xx 應用基於 single-spa 改造後,那麼老系統 iframe 如何接入?
在配置菜單時,老系統路由會被帶上標識,統一交給其中一個應用以 iframe 的方式處理。
如圖:
改造後微前端架構圖:
官方示例:
// single-spa-config.js
import { registerApplication, start } from 'single-spa'
registerApplication("applicationName", loadingFunction, activityFunction)
start()
function loadingFunction() {
return import("src/app1/main.js")
}
function activityFunction(location) {
return location.pathname.indexOf("/app1/") === 0
}
複製代碼
當增長一個應用的時候,就須要對 single-spa-config.js
文件進行修改。
經過可配置的方式實現子應用註冊:
// single-spa-config.js
import * as singleSpa from 'single-spa'
import config from './manifest.json'
registerApp(config)
singleSpa.start()
function registerApp(conf) {
conf.forEach(application => {
singleSpa.registerApplication(
application.name,
() => import(`./${application.name}.app/index.js`),
pathPrefix(application.activeRule, application.strict),
)
})
}
function pathPrefix(prefix, strict) {
return function(location) {
if (strict) {
return location.pathname === prefix
}
return location.pathname.startsWith(`${prefix}`)
}
}
複製代碼
// manifest.json
[
{
"name": "layout",
"activeRule": "/"
},
{
"name": "welcome",
"activeRule": "/",
"strict": true
},
{
"name": "iframe",
"activeRule": "/link"
},
{
"name": "app1",
"activeRule": "/app1"
},
{
"name": "app2",
"activeRule": "/app2"
}
]
複製代碼
將域名統一的一大好處是 iframe 域名和主應用域名同源。沒有了跨域 能夠在 layout
統一 SSO 登陸,經過 cookie 共享讓其餘模塊拿到登陸信息。
因爲這次改造,應用之間不涉及數據共享,因此沒有頂級 store 的概念。模塊之間的簡單通訊 能夠經過 postMessage
或基於瀏覽器原生事件作通訊。
// 應用 A
window.dispatchEvent(
new CustomEvent('iframe:change', { detail: { path: '/a/b/c'} })
)
// 應用 B
window.addEventListener('iframe:change', (event) => {
console.log(event.detail.path)
})
複製代碼
樣式的隔離有不少種處理方式,如:BEM、CSS Module、css前綴、動態加載/卸載樣式表、Web Components自帶隔離機制等。
這次採用添加 css 前綴來隔離樣式,好比 postcss 插件:postcss-plugin-namespace
。可是這個插件並不知足需求,咱們的應用分佈在 src/
下,並以 name.app
的方式命名,須要給不一樣的應用添加不一樣的前綴。所以使用本身定製的插件:
postcss.plugin('postcss-plugin-namespace', function() {
return function(css) {
css.walkRules(rule => {
if (rule.parent && rule.parent.type === 'atrule' && rule.parent.name !== 'media') return
const filePath = rule.source && rule.source.input.file
const appName = /src\/(\S*?)\//.exec(filePath)[1] || ''
const namespace = appName.split('.')[0] || ''
rule.selectors = rule.selectors.map(s => `#${namespace} ${s === 'body' ? '' : s}`)
})
}
})
複製代碼
有一個可嚴重可不嚴重的問題,如何確保子應用之間的全局變量不會互相干擾,實現js的隔離。廣泛的作法是給全局變量添加前綴,這種方式相似 css 的 BEM,經過約定的方式來避免衝突。這種方式簡單,但不是很靠譜。
qiankun 內部的實現方式是經過 Proxy
來實現的沙箱模式,即在應用的 bootstrap
及 mount
兩個生命週期開始以前分別給全局狀態打下快照,而後當應用切出/卸載時,將狀態回滾至 bootstrap
開始以前的階段,確保應用對全局狀態的污染所有清零。有興趣的同窗能夠看源碼。
上面的改造已經基本知足了業務需求,針對此業務還有更進一步的作法,達到更好的體驗:
上面提到,這次的實踐方式是微前端實現方式中幾種的結合,或者是某種方式的變種。也許在理論上並非最優的,可是在具體的問題中要是最優解。架構設計必需要與當前要解決的問題相匹配,「沒有最優的架構,只有最合適的架構」。
微前端不是一個框架或者工具,而是一套架構體系。
這套體系除了微前端的基礎設施外還須要具有微前端配置中心(版本管理、發佈策略、動態構建、中心化管理)、微前端觀察工具(應用狀態可見、可控)等。
整個體系的搭建將是一個龐大的工程,目前大部分團隊是在使用微前端的模式和思想來解決現有系統中的痛點。
公衆號: