前言

最近入職的一家公司採用single-spa這個微前端框架,所以自學了此框架。

single-spa這個微前端框架雖然有中文文件,但是有些零散和晦澀。

所以我想在學習之餘,寫篇部落格拉平一下這個學習曲線。

什麼是微前端?

微前端的靈感來源於服務端微服務的理念。

可以簡單理解為,在開發一個複雜前端應用時,將其劃分為一系列更小更簡單的前端應用。

這些前端應用可以單獨開發、測試、部署,鬆耦合,可維護性強,還可以讓前端程式碼實現增量升級和使用不同的框架。

它的懶載入還能讓整個複雜應用載入速度變快。

常用微前端玩法和single-spa

在我之前的公司是使用iframe來實現微前端的,但是各個子應用間的通訊往往比較麻煩,而且很不靈活。

而最新的微前端理念,是由webpack5中模組聯合特性實現的,這裡就不多講了。

single-spa是一個比較流行的微前端框架,它並不是使用iframe來實現微前端,也不是通過模組聯合,而是通過路由路徑來在dom上載入不同的子應用。

Import maps和SystemJS

在具體講解single-spa前,我們得先了解一個東西:Import maps

這個功能是Chrome 89才支援的。

顧名思義,它是對import的一個對映處理,讓你控制在js中使用import時,到底從哪個url獲取這些庫。

比如通常我們會在js中,以下面這種方式引入模組:

    import moment from "moment"

正常情況下肯定是node_modules中引入,但是現在我們在html中加入下面的程式碼:

    <script type="importmap">
{
"imports": {
"moment": "/moment/src/moment.js"
}
}
</script>

這裡/moment/src/moment.js這個地址換成一個cdn資源也是可以的。最終達到的效果就是:

    import moment from "/moment/src/moment.js"

有了Import maps,import的語法就可以直接在瀏覽器中使用,而不再需要webpack來幫我們進行處理,不需要從node_modules中去載入庫。

Import maps甚至還有一個兜底的玩法:

    "imports": {
"jquery": [
"https://某CDN/jquery.min.js",
"/node_modules/jquery/dist/jquery.js"
]
}

當cdn無效時,再從本地庫中獲取內容。

它的功能還有很多,我就不一一列舉了,只需要對這個有一定的瞭解即可。

儘管Import maps非常強大,但是畢竟瀏覽器相容性還並不是很好,所以就有了我們的polifill方案:SystemJS

SystemJS同樣是一個模組載入器,可相容到IE11,同樣支援import對映,但是它的語法稍有不同:

    <script src="system.js"></script>
<script type="systemjs-importmap">
{
"imports": {
"lodash": "https://unpkg.com/[email protected]/lodash.js"
}
}
</script>

在瀏覽器中引入system.js後,會去解析type為systemjs-importmap的script下的import對映。

它和Import maps最終達到的效果是一致的。

single-spa

之所以我們要先講Import mapsSystemJS,就是因為single-spa的微前端往往需要結合SystemJS來實現。

single-spa框架中,基座會檢測瀏覽器url的變化,在變化時往往通過SystemJS的import對映,來載入不同的子應用js。

但是需要注意,single-spa並不是必須依賴SystemJS

剛剛我們提到了一個概念:基座,現在講下single-spa的兩個概念:基座應用

你可以簡單理解應用是一個個的單頁面應用,而基座是一個應用管理器,用來根據路由載入不同的應用

一般在基座中我們需要像下面這樣註冊一個應用:

import { registerApplication, start } from 'single-spa';

// 註冊應用1
registerApplication({
name:'app1',
app:() => import('./app1.js'),
activeWhen: '/app1',
customProps: { myTitle: "傳遞給應用的自定義引數的值" }
}); // 註冊應用2
registerApplication({
name:'app2',
app:() => import('./app2.js'),
activeWhen: '/app2'
}); start();

在上面的程式碼中,我們註冊了app1和app2兩個應用,分別匹配路由/app1和/app2。

也就是說當路由是/app1或者/app1/home時,會直接載入app1這個應用。

註冊應用後,需要start()來開始掛載應用,否則只會下載應用,而不會掛載應用。

那麼應用應該如何設定呢?

我們上面程式碼引用的./app1.js並沒有匯出一個真的單頁面應用,而一般是如下:

console.info('第一步 下載應用階段')
export function bootstrap(props) {
const {
name, // 應用名稱
singleSpa, // singleSpa例項
mountParcel, // 手動掛載的函式
myTitle // 我們之前在註冊應用時傳遞給customProps的屬性
} = props; // Props 會傳給每個生命週期函式 // 這個生命週期函式會在應用第一次掛載前執行一次。
return Promise.resolve().then(() => {
console.info('第二步 初始化', myTitle)
})
}
export function mount(props) {
// 每當應用路由匹配成功,但該應用處於未掛載狀態時,掛載的生命週期函式就會被呼叫。呼叫時,函式會根據URL來確定當前被啟用的路由,建立DOM元素、監聽DOM事件等以向用戶呈現渲染的內容。任何子路由的改變(如hashchange或popstate等)不會再次觸發mount,需要各應用自行處理。
return Promise.resolve().then(() => {
console.info('第三步 掛載應用', props.name)
document.getElementById('root').innerHTML = "我是app1啊"
})
} export function unmount(props) {
// 每當應用路由匹配不成功,但該應用已掛載時,解除安裝的生命週期函式就會被呼叫。解除安裝函式被呼叫時,會清理在掛載應用時被建立的DOM元素、事件監聽、記憶體、全域性變數和訊息訂閱等。
return Promise.resolve().then(() => {
console.info('第四步 解除安裝應用', props.name)
document.getElementById('root').innerHTML = ""
})
} export function unload(props) {
// 移除”生命週期函式的實現是可選的,它只有在unloadApplication被呼叫時才會觸發。如果一個已註冊的應用沒有實現這個生命週期函式,則假設這個應用無需被移除。
// 移除的目的是各應用在移除之前執行部分邏輯,一旦應用被移除,它的狀態將會變成NOT_LOADED,下次啟用時會被重新初始化。
// 移除函式的設計動機是對所有註冊的應用實現“熱下載”,不過在其他場景中也非常有用,比如想要重新初始化一個應用,且在重新初始化之前執行一些邏輯操作時。
return Promise.resolve().then(() => {
console.info('第五步 移除應用', props.name)
})
}

可以看到我們的app1.js這個應用中的程式碼,並沒有匯出一個單頁面應用元件,而是匯出了幾個生命週期函式,然後通過這幾個生命週期函式來控制組件的初始化,載入和解除安裝。

它這個操作是從現在我們的react或者vue這些框架的元件生命週期中獲得靈感,將生命週期應用於整個應用程式。

single-spa 與 SystemJS實現微前端

看了上面的程式碼之後你可能有點疑惑,你這個東西也沒什麼用嘛,不就是個懶載入嗎?

哪來的微前端?

我用個React.lazy(() => import('./app1.js'))來個懶載入怎麼了,你不要說一大堆把我繞暈了。

上面這些實際上還真的沒有實現微前端,但是,你可以結合我們之前講的微前端,想象一下:

如果./app1.js不是基座這個專案內的程式碼,而是另一個專案呢?

我們將這個app1.js放在一個單獨的專案中,它用react來寫了一個單頁面應用。

再將app2.js放在另一個單獨的專案中,它用vue來寫了一個單頁面應用。

通過我們現在的這個基座專案再來處理這兩個應用呢?

我們的基座專案是不是就可以寫成下面這樣:

import { registerApplication, start } from 'single-spa';

// 註冊應用1
registerApplication({
name:'app1',
app:() => import('@曉組織/app1'),
activeWhen: '/app1',
customProps: { myTitle: "傳遞給應用的自定義引數的值" }
}); // 註冊應用2
registerApplication({
name:'app2',
app:() => import('@曉組織/app2'),
activeWhen: '/app2'
}); start();

然後再在基座專案的模板頁中來個引入對映:

<script type="systemjs-importmap">
{
"imports": {
"@曉組織/app1": "//某網站/app1.js",
"@曉組織/app2": "//另一個網站/app2.js"
}
}
</script>

以後我們要做app1模組的部分,只需要在app1這個專案中維護就可以了,不會干擾到其他的應用。

以後React20,React30出來,或者部分專案升級webpack,或者給一個專案大調整,我們可以一個個小應用嘗試升級修改,不用所有專案同時調整。不僅風險變小了很多,也更加可控。

上面是結合SystemJS實現的微前端,其實還有使用npm包和單專案的玩法,但是不推薦,有興趣的可以參考官網的這篇文章:拆分應用

single-spa-react

看到這裡的朋友一定會想,這個東西好是好,怎麼把它和react的單頁面應用結合起來?

讓我自己寫載入和解除安裝react的單頁面應用?這也太挫了吧。

當然不可能,single-spa的生態可是很好的。

single-spa-react就是一個輔助庫,它可以幫助React應用程式實現single-spa 需要的生命週期函式(bootstrap、mount 和 unmount)。

import React from 'react';
import ReactDOM from 'react-dom';
import rootComponent from './path-to-root-component.js'; import singleSpaReact from 'single-spa-react';
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent,
errorBoundary(err, info, props) {
// https://reactjs.org/docs/error-boundaries.html
return (
<div>This renders when a catastrophic error occurs</div>
);
},
});
export const bootstrap = reactLifecycles.bootstrap;
export const mount = reactLifecycles.mount;
export const unmount = reactLifecycles.unmount;

這是官網提供的示例程式碼,那個rootComponent就是我們的頂層React元件。

其它的就不用多說了,畢竟都很容易理解,想要了解詳情可以看下:詳情

其它的一些語言比如vue和Angular都有自己對應的輔助庫,具體可以查閱:輔助庫列表

single-spa的Parcels

Parcels是single-spa的一個高階特性,是一個與框架無關的元件,與應用的不同之處在於parcel元件需要手動掛載,而不是通過匹配路由的activity方法被啟用。

官網說:只有在涉及到跨框架的應用之間進行元件呼叫時,我們才需要考慮parcel的使用。

一想到我們公司只用react,那麼打擾了,再見。

CLI工具:create-single-spa和可定製化的webpack配置

上面很多都是在講原理,現在到了實際應用的時候了。

single-spa的相關配置有些繁瑣,所以我推薦依賴現有的CLI去新建專案,可以去改造,而不是自己從零開始去搭建。

npx create-single-spa

執行上面這行命令後,就會讓你做一系列選擇,那些選擇就不多說,只說最關鍵的。

create-single-spa 可以讓你建立三種專案,分別是:

  • single-spa application/parcel :應用和parcel。
  • single-spa root config:基座。
  • in-browser utility module: 通用元件,工具函式,樣式指引。

你可以根據需要去建立不同的專案。

single-spa還提供了一些推薦的webpack配置庫,不用自己操心去設定webpack配置。

不過我建議最後輸出得到webpack配置你還是稍微打印出來看一下,做到心中有數,然後才可以再根據它的配置去做相應修改。

Demo分享

光看不做假把式,下面是我自己根據single-spa的CLI工具搭建的兩個簡易Demo:

同時執行起來即可,命令都是:

yarn start

如果確實想入門的話,對比一下我寫的和官方CLI工具初始化時的一些差異,可以瞭解到更多的一些小細節。

總結

總的來說,single-spa是一個非常優秀的微前端框架。

微前端領域最近的趨勢是用webpack5中模組聯合特性來實現,這與single-spa並不衝突,single-spa也有結合模組聯合特性實現的例子。

不過這就不在本篇文章的涉及範圍內了,也許以後會寫下這塊的內容。

本篇部落格到此結束。

希望這篇文章能給您帶來一些幫助,如有疏漏,也請不吝賜教。