前言
最近入職的一家公司采用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/lodash@4.17.10/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也有結合模塊聯合特性實現的例子。
不過這就不在本篇文章的涉及范圍內了,也許以后會寫下這塊的內容。
本篇博客到此結束。
希望這篇文章能給您帶來一些幫助,如有疏漏,也請不吝賜教。
