微前端在小米 CRM 系統的實踐

原文地址:www.lishuaishuai.com/architectur…javascript

1、前言

大型組織的組織結構、軟件架構在不斷地發生變化。移動優先(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

2、實施微前端的六種方式

《前端架構從入門到微前端》一書中,將微前端的實施分爲六種:github

2.1 路由分發

路由分發式微前端,即經過路由將不一樣的業務分發到不一樣的、獨立前端應用上。其一般能夠經過 HTTP 服務器的反向代理來實現,又或者是應用框架自帶的路由來解決。如圖:web

2.2 前端微服務化

前端微服務化,是微服務架構在前端的實施,每一個前端應用都是徹底獨立(技術棧、開發、部署、構建獨立)、自主運行的,最後經過模塊化的方式組合出完成的應用。json

採用這種方式意味着,一個頁面上能夠同時存在兩個以上的前端應用在運行。如圖:bootstrap

目前主流的框架有 Single-SPAqiankunMooa,後二者都是基於 Single-SPA 的封裝。

2.3 微應用

微應用化是指在開發時應用都是以單1、微小應用的形式存在的,而在運行時,則是經過構建系統合併這些應用,並組合成一個新的應用。

微應用化大都是以軟件工程的方式來完成前端應用的聚合,所以又能夠稱之爲組合式集成

微應用化只能使用惟一的一種前端框架。

如圖:

2.4 微件化

微件化(Widget)是一段能夠直接嵌入應用上運行的代碼,它由開發人員預先編譯好,在加載時不須要再作任何修改或編譯。微前端下的微件化是指,每一個業務團第編寫本身的業務代碼,並將編譯好的代碼部署到指定的服務器上,運行時只須要加載指定的代碼便可。

如圖:

2.5 iframe

iFrame 做爲一個很是古老的,人人都以爲普通的技術,卻一直很管用。

HTML 內聯框架元素 <iframe> 表示嵌套的正在瀏覽的上下文,能有效地將另外一個 HTML 頁面嵌入到當前頁面中。

iframe 能夠建立一個全新的獨立的宿主環境,這意味着咱們的前端應用之間能夠相互獨立運行。採用 iframe 有幾個重要的前提:

  • 網站不須要 SEO 支持
  • 擁有相應的應用管理機制。

在不少業務場景下,不免會遇到一些難以解決的問題,那麼能夠引入 iframe 來解決。

2.6 Web Components

Web Components 是一套不一樣的技術,容許開發者建立可重用的定製元素(它們的功能封裝在代碼以外)而且在您 Web 應用中使用它們。

在真正的項目上使用 Web Components技術,離如今還有一些距離,結合 Web Components 來構建前端應用,是一種面向將來演進的架構。或者說在將來能夠採用這種方式來構建應用。

如圖:

在真實的業務場景中,每每是上面提到六種方式中的幾種的結合使用,或者是某種方式的變種。下面看我遇到的真實場景。

3、真實的業務場景

現有三個內部系統,下面稱之爲 old-a、old-b和C,其中,old-a和old-b是老舊的先後端未分離項目,C爲先後端分離的SPA應用(React + HIUI),三個系統的架構圖大體以下:

能夠看到,old-a 運行在一臺服務器1上,old-b運行在服務器2上,C系統的前端資源在服務器2上,而且C沒有本身的域名。

三個系統均在後端同窗維護和開發,他們的需求以下:

  • 統一的域名
  • 統一的界面和交互
  • 系統須要方便拓展
  • 不但願開發階段每一個系統有獨立的代碼倉庫
  • CI 構建

4、怎麼改造?

4.1 關鍵點

考慮開發同窗的需求和開發成本、維護成本、將來的可拓展性,系統改造關鍵點以下:

  1. 申請統一的域名(暫且稱之爲crm.mi.com)
  2. 將 old-a 和 old-b 兩個老舊的系統樣式調整,像系統C靠攏
  3. 三個系統使用統一的菜單和權限
  4. 三個系統使用統一的 SSO
  5. C 系統和正在開發的 X 個系統使用 CI 解決打包和手動 copy 的問題

4.2 微前端幾種方式對比

整體的改造方案使用微前端的思想進行。對上面提到的六種方式進行對比(點擊查看大圖):

4.3 實施

對於上面幾種方式,在具體的實施使用了路由分發、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 的方式處理。

如圖:

改造後微前端架構圖:

5、一些問題

5.1 子應用註冊方式

官方示例:

// 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"
  }
]
複製代碼

5.2 共享cookie

將域名統一的一大好處是 iframe 域名和主應用域名同源。沒有了跨域 能夠在 layout 統一 SSO 登陸,經過 cookie 共享讓其餘模塊拿到登陸信息。

5.3 應用之間數據共享及通訊

因爲這次改造,應用之間不涉及數據共享,因此沒有頂級 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)
})
複製代碼

5.4 css隔離

樣式的隔離有不少種處理方式,如: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}`)
		})
	}
})
複製代碼

5.5 js沙箱

有一個可嚴重可不嚴重的問題,如何確保子應用之間的全局變量不會互相干擾,實現js的隔離。廣泛的作法是給全局變量添加前綴,這種方式相似 css 的 BEM,經過約定的方式來避免衝突。這種方式簡單,但不是很靠譜。

qiankun 內部的實現方式是經過 Proxy 來實現的沙箱模式,即在應用的 bootstrapmount 兩個生命週期開始以前分別給全局狀態打下快照,而後當應用切出/卸載時,將狀態回滾至 bootstrap 開始以前的階段,確保應用對全局狀態的污染所有清零。有興趣的同窗能夠看源碼。

6、優化體驗 PWA

  1. 建立桌面圖標,快速訪問
  2. Service Worker 緩存,對大文件和不常常更改的文件緩存,優化加載。

7、 還能夠作什麼?

上面的改造已經基本知足了業務需求,針對此業務還有更進一步的作法,達到更好的體驗:

  • 可使用 lerna 統一管理全部項目,方便維護,或者讓每一個應用擁有獨立的倉庫,作到獨立開發。
  • 可使用 SystemJS 實現應用的動態加載、獨立部署。

8、總結

上面提到,這次的實踐方式是微前端實現方式中幾種的結合,或者是某種方式的變種。也許在理論上並非最優的,可是在具體的問題中要是最優解。架構設計必需要與當前要解決的問題相匹配,「沒有最優的架構,只有最合適的架構」

微前端不是一個框架或者工具,而是一套架構體系。

這套體系除了微前端的基礎設施外還須要具有微前端配置中心(版本管理、發佈策略、動態構建、中心化管理)、微前端觀察工具(應用狀態可見、可控)等。

整個體系的搭建將是一個龐大的工程,目前大部分團隊是在使用微前端的模式和思想來解決現有系統中的痛點。

9、推薦閱讀

前端微服務化解決方案

多是你見過最完善的微前端解決方案

微前端的那些事兒

Micro Frontends

single-spa.js.org/


公衆號:

相關文章
相關標籤/搜索