前言
最近入職的一家公司採用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 maps和SystemJS,就是因為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:
- 基座的程式碼倉庫:https://gitee.com/vvjiang/single-spa-root-config-demo
- 應用的程式碼倉庫:https://gitee.com/vvjiang/single-spa-app-demo
同時執行起來即可,命令都是:
yarn start
如果確實想入門的話,對比一下我寫的和官方CLI工具初始化時的一些差異,可以瞭解到更多的一些小細節。
總結
總的來說,single-spa是一個非常優秀的微前端框架。
微前端領域最近的趨勢是用webpack5中模組聯合特性來實現,這與single-spa並不衝突,single-spa也有結合模組聯合特性實現的例子。
不過這就不在本篇文章的涉及範圍內了,也許以後會寫下這塊的內容。
本篇部落格到此結束。
希望這篇文章能給您帶來一些幫助,如有疏漏,也請不吝賜教。