前言
說起微前端框架,很多人第一反應(yīng)就是 single-spa匈子。但是再問深入一點(diǎn):它是干嘛的住拭,它有什么用偷线,可能就回答不出來了摇零。
一方面沒多少人研究和使用微前端推掸。可能還沒來得及用微前端擴(kuò)展項(xiàng)目遂黍,公司就已經(jīng)倒閉了终佛。
另一方面是中文博客對微前端的研究少之又少俊嗽,很多文章只是簡單翻譯一下官方文檔雾家,讀幾個API,放個官方的 Demo 就完事了绍豁。很少有深入研究到底 single-spa 是怎么一回事的芯咧。
還有一方面是 single-spa 的文檔非常難看懂,和 Redux 文檔一樣喜歡造概念。講一個東西的時候敬飒,總是把別的庫拉進(jìn)來一起講邪铲,把一個簡單的東西變得非常復(fù)雜。最令人吐槽的一點(diǎn)就是官方的 sample code 都是只言片語无拗,完全拼湊不出來一個 Demo带到,而 Github 的 Demo 還賊復(fù)雜,沒解釋英染,光看完都要 clone 好幾個 repo揽惹。
最后,求人不如求己四康,剛完源碼再剛一下文檔搪搏。
這篇文章將不會聊怎么搭建一個 Demo,而是會從 “Why” 和 “How” 的角度來聊一下官方文檔的都講了哪些內(nèi)容闪金,相信看完這篇文章就能看懂 官方的 Demo 了疯溺。
一個需求
讓我們從一個最小的需求開始說起。有一天產(chǎn)品經(jīng)理突然說:我們要做一個 A 頁面哎垦,我看到隔壁組已經(jīng)做過這個 A 頁面了囱嫩,你把它放到我們項(xiàng)目里吧,應(yīng)該不是很難吧漏设?明天上線吧挠说。
此時,產(chǎn)品經(jīng)理想的是:應(yīng)該就填一個 URL 就好吧愿题?再不行损俭,復(fù)制粘貼也很快吧。而程序員想的卻是:又要看屎山了潘酗。又要重構(gòu)了杆兵。又要聯(lián)調(diào)了。測試數(shù)據(jù)有沒有白卸帷琐脏?等一下,聯(lián)調(diào)的后端是誰案淄谩日裙?
估計(jì)這是做大項(xiàng)目時經(jīng)常遇到的需求了:搬運(yùn)一個現(xiàn)有的頁面。我想大多數(shù)人都會選擇在自己項(xiàng)目里復(fù)制粘貼別人的代碼惰蜜,然后稍微重構(gòu)一下昂拂,再測試環(huán)境聯(lián)調(diào),最后上線抛猖。
但是格侯,這樣就又多了一份代碼了鼻听,如果別人的頁面改了,那么自己項(xiàng)目又要跟著同步修改联四,再聯(lián)調(diào)撑碴,再上線,非常麻煩朝墩。
所以程序員就想能不能我填一個 url醉拓,然后這個頁面就到項(xiàng)目里來了呢?所以收苏,<iframe/>
就出場了廉嚼。
iframe 的弊端
iframe 就相當(dāng)于頁面里再開個窗口加載別的頁面,但是它有很多弊端:
- 每次進(jìn)來都要加載倒戏,狀態(tài)不能保留
- DOM 結(jié)構(gòu)不共享怠噪。比如子應(yīng)用里有一個 Modal,顯示的時候只能在那一小塊地方展示杜跷,不能全屏展示
- 無法跟隨瀏覽器前進(jìn)后退
- 天生的硬隔離傍念,無法與主應(yīng)用進(jìn)行資源共享,交流也很困難
而 SPA 正好可以解決上面的問題:
- 切換路由就是切換頁面組件葛闷,組件的掛載和卸載非潮锘保快
- 單頁應(yīng)用肯定共享 DOM
- 前端控制路由,想前就前淑趾,想后就后
- React 通信有 Redux阳仔,Vue 通信有 Vuex,可與 App 組件進(jìn)行資源共享扣泊,交流很爽
這就給我們一個啟發(fā):能不能有這么一個巨型 SPA 框架近范,把現(xiàn)有的 SPA 當(dāng)成 Page Component 來組裝成一個新的 SPA 呢?這就是微前端的由來延蟹。
微前端是什么
微前端應(yīng)該有如下特點(diǎn):
- 技術(shù)棧無關(guān)评矩,主框架不限制接入應(yīng)用的技術(shù)棧,微應(yīng)用具備完全自主權(quán)
- 獨(dú)立開發(fā)阱飘、獨(dú)立部署斥杜,微應(yīng)用倉庫獨(dú)立,前后端可獨(dú)立開發(fā)沥匈,部署完成后主框架自動完成同步更新
- 增量升級蔗喂,在面對各種復(fù)雜場景時,我們通常很難對一個已經(jīng)存在的系統(tǒng)做全量的技術(shù)棧升級或重構(gòu)高帖,而微前端是一種非常好的實(shí)施漸進(jìn)式重構(gòu)的手段和策略
- 獨(dú)立運(yùn)行時缰儿,每個微應(yīng)用之間狀態(tài)隔離,運(yùn)行時狀態(tài)不共享
等一下等一下棋恼,說了一堆返弹,到底啥是 single-spa 啊。
嘿嘿爪飘,single-spa 框架并沒有實(shí)現(xiàn)上面任何特點(diǎn)义起,對的,一個都沒有师崎,Just Zero默终。
single-spa 到底是干嘛的
single-spa 僅僅是一個子應(yīng)用生命周期的調(diào)度者。single-spa 為應(yīng)用定義了 boostrap, load, mount, unmount 四個生命周期回調(diào):
只要寫過 SPA 的人都能理解犁罩,無非就是生齐蔽、老、病床估、死含滴。不過有幾個點(diǎn)需要注意一下:
- Register 不是生命周期,指的是調(diào)用
registerApplication
函數(shù)這一步 - Load 是開始加載子應(yīng)用丐巫,怎么加載由開發(fā)者自己實(shí)現(xiàn)(等會會說到)
- Unload 鉤子只能通過調(diào)用
unloadApplication
函數(shù)才會被調(diào)用
OK谈况,上面 4 個生命周期的回調(diào)順序是 single-spa 可以控制的,我能理解递胧,那什么時候應(yīng)該開始這一套生命周期呢碑韵?應(yīng)該是有一個契機(jī)來開始整套流程的,或者某幾個流程的缎脾。
契機(jī)就是當(dāng) window.location.href
匹配到 url 時祝闻,開始走對應(yīng)子 App 的這一套生命周期嘛。所以遗菠,single-spa 還要監(jiān)聽 url 的變化联喘,然后執(zhí)行子 app 的生命周期流程。
到此辙纬,我們就有了 single-spa 的大致框架了耸袜,無非就兩件事:
- 實(shí)現(xiàn)一套生命周期,在 load 時加載子 app牲平,由開發(fā)者自己玩堤框,別的生命周期里要干嘛的,還是由開發(fā)者造的子應(yīng)用自己玩
- 監(jiān)聽 url 的變化纵柿,url 變化時蜈抓,會使得某個子 app 變成 active 狀態(tài),然后走整套生命周期
畫個草圖如下:
是不是感覺 single-spa 很雞賊昂儒?雖然 single-spa 說自己是微前端框架沟使,但是一個微前端的特性都沒有實(shí)現(xiàn),都是需要開發(fā)者在加載自己子 App 的時候?qū)崿F(xiàn)的渊跋,要不就是通過一些第三方工具實(shí)現(xiàn)腊嗡。
注冊子應(yīng)用
有了上面的了解之后着倾,我們再來看 single-spa 里最重要的 API:registerApplication
,表示注冊一個子應(yīng)用燕少。使用如下:
singleSpa.registerApplication({
name: 'taobao', // 子應(yīng)用名
app: () => System.import('taobao'), // 如何加載你的子應(yīng)用
activeWhen: '/appName', // url 匹配規(guī)則卡者,表示啥時候開始走這個子應(yīng)用的生命周期
customProps: { // 自定義 props,從子應(yīng)用的 bootstrap, mount, unmount 回調(diào)可以拿到
authToken: 'xc67f6as87f7s9d'
}
})
singleSpa.start() // 啟動主應(yīng)用
上面注冊了一個子應(yīng)用 'taobao'客们。我們自己實(shí)現(xiàn)了加載子應(yīng)用的方法崇决,通過 activeWhen
告訴 single-spa 什么時候要掛載子應(yīng)用,好像就可以上手開擼代碼嘍底挫。
可以個鬼恒傻!請告訴我 System.import
是個什么鬼。哦建邓,是 SystemJS盈厘,誒,SystemJS 聽說過官边,它是個啥扑庞?為啥要用 SystemJS?憑啥要用 SystemJS拒逮?
SystemJS
相信很多人看過一些微前端的博客罐氨,它們都會說 single-spa 是基于 SystemJS 的。錯滩援!single-spa 和 SystemJS 一點(diǎn)關(guān)系都沒有栅隐!這里先放個主應(yīng)用和子應(yīng)用的關(guān)系圖:
single-spa 的理念是希望主應(yīng)用可以做到非常非常簡單的和輕量,簡單到只要一個 index.html + 一個 main.js 就可以完成微前端工程玩徊,連 Webpack 都不需要租悄,直接在瀏覽器里執(zhí)行 singleSpa.registerApplication
就收工了,這種執(zhí)行方式也就是 in-browser 執(zhí)行恩袱。
但是泣棋,瀏覽器里執(zhí)行 JS,別說實(shí)現(xiàn) import xxx from 'https://taobao.com'
了畔塔,我要是在瀏覽器里實(shí)現(xiàn) ES6 的 import/export 都不行疤侗病: import axios from 'axios'
。
其實(shí)澈吨,也不是不行把敢,只需要在 <script>
標(biāo)簽加上 type="module"
,也是可以實(shí)現(xiàn)的谅辣,例如:
<script type="module" src="module.js"></script>
<script type="module">
// or an inline script
import {helperMethod} from './providesHelperMethod.js';
helperMethod();
</script>
// providesHelperMethod.js
export function helperMethod() {
console.info(`I'm helping!`);
}
但是修赞,遇到導(dǎo)入模塊依賴的,像 import axios from 'axios'
這樣的桑阶,就需要 importmap 了:
<script type="importmap">
{
"imports": {
"vue": "https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js"
}
}
</script>
<div id="container">我是:{{name}}</div>
<script type="module">
import Vue from 'vue'
new Vue({
el: '#container',
data: {
name: 'Jack'
}
})
</script>
importmap 的功能就是告訴 'vue' 這個玩意要從 "https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js" 這里來的柏副。不過勾邦,importmap 現(xiàn)在只有 Chrome 是支持的。
所以割择,SystemJS 就將這一塊補(bǔ)齊了眷篇。當(dāng)然,除了 importmap锨推,它還有很多的功能铅歼,比如獲取當(dāng)前加載的所有模塊公壤、當(dāng)前模塊的 URL换可、可以 import html, import css,import wasm厦幅。
等等沾鳄,這在 Webpack 不也可以做到么?Webpack 還能 import less, import scss 呢确憨?這不比 SystemJS 牛逼译荞?對的,如果不是因?yàn)橐跒g覽器使用 import/export休弃,沒人會用 SystemJS吞歼。SystemJS 的好處和優(yōu)勢有且僅有一點(diǎn):那就是在瀏覽器里使用 ES6 的 import/export。
而正因?yàn)?SystemJS 可以在瀏覽器里可以使用 ES6 的 import/export 并支持動態(tài)引入塔猾,正好符合 single-spa 所提倡的 in-browser 執(zhí)行思路篙骡,所以 single-spa 文檔里才反復(fù)出現(xiàn) SystemJS 的身影,而且 Github Demo 里依然是使用 SystemJS 的 importmap 機(jī)制來引入不同模塊:
<script type="systemjs-importmap">
{
"imports": {
"@react-mf/root-config": "http://localhost:9000/react-mf-root-config.js"
}
}
</script>
<script>
singleSpa.registerApplication({
name: 'taobao', // 子應(yīng)用名
app: () => System.import('@react-mf/root-config'), // 如何加載你的子應(yīng)用
activeWhen: '/appName', // url 匹配規(guī)則丈甸,表示啥時候開始走這個子應(yīng)用的生命周期
customProps: { // 自定義 props糯俗,從子應(yīng)用的 bootstrap, mount, unmount 回調(diào)可以拿到
authToken: 'xc67f6as87f7s9d'
}
})
</script>
公共依賴
SystemJS 另一個好處就是可以通過 importmap 引入公共依賴。
假如睦擂,我們有三個子應(yīng)用得湘,它們都有公共依賴項(xiàng) antd,那每個子應(yīng)用打包出來都會有一份 antd 的代碼顿仇,就顯示很冗余淘正。
一個解決方法就是在主應(yīng)用里,通過 importmap 直接把 antd 代碼引入進(jìn)來臼闻,子應(yīng)用在 Webpack 設(shè)置 external 把 antd 打包時排除掉跪帝。子應(yīng)用打包就不會把 antd 打包進(jìn)去了,體積也變小了些阅。
有人會說了:我用 CDN 引入不行嘛伞剑?不行啊,因?yàn)樽討?yīng)用的代碼都是 import {Button} from 'antd'
的市埋,瀏覽器要怎么直接識別 ES6 的 import/export 呢黎泣?那還不得 SystemJS 嘛恕刘。
難道 Webpack 就沒有辦法可以實(shí)現(xiàn) importmap 的效果了么?Webpack 5 提出的 Module Federation 模塊聯(lián)邦就可以很好地做的 importmap 的效果抒倚。這是 Webpack 5 的新特性褐着,使用的效果和 importmap 差不多。關(guān)于模塊聯(lián)邦是個啥托呕,可以參考 這篇文章含蓉。
至于用 importmap 還是 Webpack 的 Module Federation,singles-spa 是推薦使用 importmap 的项郊,但是馅扣,文檔也沒有反對使用 Webpack 的 Module Federation 的理由。能用就OK着降。
SystemJS vs Webpack ES
有人可能會想:都 1202 年了差油,怎么還要在瀏覽器環(huán)境寫 JS 呢?不上個 Webpack 都不好意思說自己是前端開發(fā)了任洞。
沒錯蓄喇,Webpack 是非常強(qiáng)大的,而且可以利用 Webpack 很多能力交掏,讓主應(yīng)用變得更加靈活妆偏。比如,寫 less盅弛,scss钱骂,Webpack 的 prefetch 等等等等。然后在注冊子應(yīng)用時熊尉,完全可以利用 Webpack 的動態(tài)引入:
singleSpa.registerApplication({
name: 'taobao', // 子應(yīng)用名
app: () => import('taobao'), // 如何加載你的子應(yīng)用
activeWhen: '/appName', // url 匹配規(guī)則罐柳,表示啥時候開始走這個子應(yīng)用的生命周期
customProps: { // 自定義 props,從子應(yīng)用的 bootstrap, mount, unmount 回調(diào)可以拿到
authToken: 'xc67f6as87f7s9d'
}
})
那為什么 single-spa 還要推薦 SystemJS 呢狰住?個人猜測是因?yàn)?single-spa 希望主應(yīng)用應(yīng)該就一個空殼子张吉,只需要管內(nèi)容要放在哪個地方,所有的功能催植、交互都應(yīng)該交由 index.html 來統(tǒng)一管理肮蛹。
當(dāng)然,這僅僅是一種理念创南,可以完全不遵循它伦忠。像我個人還是喜歡用 Webpack 多一點(diǎn),SystemJS 還是有點(diǎn)多余稿辙,而且覺得有點(diǎn)奧特曼了昆码。不過,為了跟著文檔的節(jié)奏來,這里假設(shè)就用 SystemJS 來實(shí)現(xiàn)主應(yīng)用赋咽。
Root Config
由于 single-spa 非常強(qiáng)調(diào) in-browser 的方式來實(shí)現(xiàn)主應(yīng)用旧噪,所以 index.html 就充當(dāng)了靜態(tài)資源、子應(yīng)用的路徑聲明的角色脓匿。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Polyglot Microfrontends</title>
<meta name="importmap-type" content="systemjs-importmap" />
<script type="systemjs-importmap" src="https://storage.googleapis.com/polyglot.microfrontends.app/importmap.json"></script>
<% if (isLocal) { %>
<script type="systemjs-importmap">
{
"imports": {
"@polyglot-mf/root-config": "http://localhost:9000/polyglot-mf-root-config.js"
}
}
</script>
<% } %>
<script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.min.js"></script>
</head>
<body>
<script>
System.import('@polyglot-mf/root-config');
System.import('@polyglot-mf/styleguide');
</script>
<import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>
而 main.js 則實(shí)現(xiàn)子應(yīng)用注冊淘钟、主應(yīng)用啟動。
import { registerApplication, start } from "single-spa";
registerApplication({
name: "@polyglot-mf/navbar",
app: () => System.import("@polyglot-mf/navbar"),
activeWhen: "/",
});
registerApplication({
name: "@polyglot-mf/clients",
app: () => System.import("@polyglot-mf/clients"),
activeWhen: "/clients",
});
registerApplication({
name: "@polyglot-mf/account-settings",
app: () => loadWithoutAmd("@polyglot-mf/account-settings"),
activeWhen: "/settings",
});
start();
// A lot of angularjs libs are compiled to UMD, and if you don't process them with webpack
// the UMD calls to window.define() can be problematic.
function loadWithoutAmd(name) {
return Promise.resolve().then(() => {
let globalDefine = window.define;
delete window.define;
return System.import(name).then((module) => {
window.define = globalDefine;
return module;
});
});
}
像這樣的資源聲明 + 主子應(yīng)用加載的組件陪毡,single-spa 稱之為 Root Config米母。 它不是什么新概念,就只有兩個東西:
- 一個主應(yīng)用的 index.html
- 一個執(zhí)行
registerApplication
函數(shù)的 JS 文件
single-spa-layout
雖然一個 index.html 是完美的輕量微前端主應(yīng)用毡琉,但是就算再壓縮主應(yīng)用的交互铁瞒,那總得告訴子應(yīng)用放置的位置吧,那不還得 DOM API 一把梭绊起?一樣麻煩精拟?
為了解決這個問題燎斩,single-spa 說:沒事虱歪,我?guī)湍愀悖缓笤炝?single-spa-layout栅表。具體使用請看代碼:
<html>
<head>
<template id="single-spa-layout">
<single-spa-router>
<nav class="topnav">
<application name="@organization/nav"></application>
</nav>
<div class="main-content">
<route path="settings">
<application name="@organization/settings"></application>
</route>
<route path="clients">
<application name="@organization/clients"></application>
</route>
</div>
<footer>
<application name="@organization/footer"></application>
</footer>
</single-spa-router>
</template>
</head>
</html>
不能說和 Vue Router 很像笋鄙,只能說一模一樣吧。當(dāng)然上面這么寫很直觀怪瓶,但是瀏覽器并不認(rèn)識這些元素萧落,所以 single-spa-layout 把識別這些元素的邏輯都封裝成了函數(shù),并暴露給開發(fā)者洗贰,開發(fā)者只要調(diào)用一下就能識別出 appName 等信息了:
import { registerApplication, start } from 'single-spa';
import {
constructApplications,
constructRoutes,
constructLayoutEngine,
} from 'single-spa-layout';
// 獲取 routes
const routes = constructRoutes(document.querySelector('#single-spa-layout'));
// 獲取所有的子應(yīng)用
const applications = constructApplications({
routes,
loadApp({ name }) {
return System.import(name); // SystemJS 引入入口 JS
},
});
// 生成 layoutEngine
const layoutEngine = constructLayoutEngine({ routes, applications });
// 批量注冊子應(yīng)用
applications.forEach(registerApplication);
// 啟動主應(yīng)用
start();
沒什么好說的找岖,constrcutRoutes
, constructApplication
和 constructLayoutEngine
本質(zhì)上就是識別 single-spa-layout 定義的元素標(biāo)簽,然后獲取里面的屬性敛滋,再通過 registerApplication
函數(shù)一個個注冊就完事了许布。
改造子應(yīng)用
上面說的都是主應(yīng)用的事情,現(xiàn)在我們來關(guān)心一下子應(yīng)用绎晃。
子應(yīng)用最關(guān)鍵的一步就是導(dǎo)出 bootstrap, mount, unmount 三個生命周期鉤子蜜唾。
import SubApp from './index.tsx'
export const bootstrap = () => {}
export const mount = () => {
// 使用 React 來渲染子應(yīng)用的根組件
ReactDOM.render(<SubApp/>, document.getElementById('root'));
}
export const unmount = () => {}
single-spa-react, single-spa-vue, single-spa-angular, single-spa-xxx, ...
emmmm,怎么說的呢庶艾,上面三個 export 不太好看袁余,能不能有一種更直接的方法就實(shí)現(xiàn) 3 個生命周期的導(dǎo)出呢?
single-spa 說:可以啊咱揍,搞颖榜!所以有了 single-spa-react:
import React from 'react';
import ReactDOM from 'react-dom';
import SubApp from './index.tsx';
import singleSpaReact, {SingleSpaContext} from 'single-spa-react';
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: SubApp,
errorBoundary(err, info, props) {
return (
<div>出錯啦!</div>
);
},
});
export const bootstrap = reactLifecycles.bootstrap;
export const mount = reactLifecycles.mount;
export const unmount = reactLifecycles.unmount;
single-spa 說:我不能單給 react 搞啊,別的框架也要給它們整上一個掩完,一碗水端平蟹地,所以有這了這些牛鬼蛇神:
不禁感慨:這些小輪子是真能造啊。
導(dǎo)入子應(yīng)用的 CSS
不知道你有沒有注意到藤为,在剛剛的子應(yīng)用注冊里我們僅僅用 System.import
導(dǎo)入了一個 JS 文件怪与,那 CSS 樣式文件怎么搞呢?可能可以 System.import('xxx.css')
來導(dǎo)入缅疟。
但是分别,這又有問題了:在切換了應(yīng)用時,unmount 的時候要怎么把已有的 CSS 給刪掉呢存淫?官方說可以這樣:
const style = document.createElement('style');
style.textContent = `.settings {color: blue;}`;
export const mount = [
async () => {
document.head.appendChild(styleElement);
},
reactLifecycles.mount,
]
export const unmount = [
reactLifecycles.unmount,
async () => {
styleElement.remove();
}
]
我:single-spa耘斩,求求你做個人吧,搭個 Demo桅咆,還要我來處理 CSS括授?single-spa 說:好,等我再去造一個輪子岩饼。于是荚虚,就有了 single-spa-css。用法如下:
import singleSpaCss from 'single-spa-css';
const cssLifecycles = singleSpaCss({
// 這里放你導(dǎo)出的 CSS籍茧,如果 webpackExtractedCss 為 true版述,可以不指定
cssUrls: ['https://example.com/main.css'],
// 是否要使用從 Webpack 導(dǎo)出的 CSS,默認(rèn)為 false
webpackExtractedCss: false,
// 是否 unmount 后被移除寞冯,默認(rèn)為 true
shouldUnmount: true,
// 超時渴析,不廢話了,都懂的
timeout: 5000
})
const reactLifecycles = singleSpaReact({...})
// 加入到子應(yīng)用的 bootstrap 里
export const bootstrap = [
cssLifecycles.bootstrap,
reactLifecycles.bootstrap
]
export const mount = [
// 加入到子應(yīng)用的 mount 里吮龄,一定要在前面俭茧,不然 mount 后會有樣式閃一下的問題
cssLifecycles.mount,
reactLifecycles.mount
]
export const unmount = [
// 和 mount 同理
reactLifecycles.unmount,
cssLifecycles.unmount
]
這里要注意一下,上面的 https://example.com/main.css 并沒有看起來那么簡單易用漓帚。
假如你用了 Webpack 來打包母债,很有可能會用分包或者 content hash 來給 CSS 文件命名,比如 filename: "[name].[contenthash].css"
胰默。那請問 cssUrls
要怎么寫呀场斑,每次都要改 cssUrls
參數(shù)么?太麻煩了吧牵署。
single-spa-css 說:我可以通過 Webpack 導(dǎo)出的 __webpack_require__.cssAssetFileName
獲取導(dǎo)出之后的真實(shí) CSS 文件名漏隐。ExposeRuntimeCssAssetsPlugin 這個插件正好可以解決這個問題。這么一來 cssUrls
就可以不用指定了奴迅,直接把 Webpack 導(dǎo)出的真實(shí) CSS 名放到 cssUrls
里了青责。
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ExposeRuntimeCssAssetsPlugin = require("single-spa-css/ExposeRuntimeCssAssetsPlugin.cjs");
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
}),
new ExposeRuntimeCssAssetsPlugin({
// The filename here must match the filename for the MiniCssExtractPlugin
filename: "[name].css",
}),
],
};
子應(yīng)用 CSS 樣式隔離
雖然 single-spa-css 解決了子應(yīng)用的 CSS 引入和移除問題挺据,但是又帶來了另一個問題:怎么保證各個子應(yīng)用的樣式不互相干擾呢?官方給出的建議是:
第一種方法:使用 Scoped CSS脖隶,也即在子應(yīng)用的 CSS 選擇器上加前綴就好了嘛扁耐,像這樣:
.app1__settings-67f89dd87sf89ds {
color: blue;
}
要是嫌麻煩,可以在 Webpack 使用 PostCSS Prefix Selector 給樣式自動加前綴:
const prefixer = require('postcss-prefix-selector');
module.exports = {
plugins: [
prefixer({
prefix: "#single-spa-application\\:\\@org-name\\/project-name"
})
]
}
另一種方法是在加載子應(yīng)用的函數(shù)里产阱,將子應(yīng)用掛載到 Shadow DOM 上婉称,可以實(shí)現(xiàn)完美的樣式隔離。Shadow DOM 是什么构蹬,怎么玩可見 MDN這里王暗。
公共 CSS 樣式怎么處理
上面說的都是子應(yīng)用自己的 CSS 樣式,那如果子應(yīng)用之間要共享 CSS 怎么辦呢庄敛?比如有兩個子應(yīng)用都用了 antd俗壹,那都要 import 兩次 antd.min.css 了。
這個問題和上面提到的處理“公共依賴”的問題是差不多的藻烤。官方給出兩個建議:
- 將公共的 CSS 放到 importmap 里绷雏,也可以理解為在 index.html 里直接加個 link 獲取 antd 的 CSS 完事
- 將所有的公共的 UI 庫都 import 到 utility 里,將 antd 所有內(nèi)容都 export怖亭,再把 utility 包放到 importmap 里涎显,然后
import { Button } from '@your-org-name/utility';
去引入里面的組件
其實(shí)上面兩個方法都大同小異,思路都是在主應(yīng)用一波引入依许,只是一個統(tǒng)一引入CSS棺禾,另一個統(tǒng)一引入 UI 庫缀蹄。
子應(yīng)用的 JS 隔離
我們來想想應(yīng)用的 JS 隔離本質(zhì)是什么峭跳,本質(zhì)其實(shí)就是在 B 子應(yīng)用里使用 window 全局對象里的變量時,不要被 A 子應(yīng)用給污染了缺前。
一個簡單的解決思路就是:在 mount A 子應(yīng)用時蛀醉,正常添加全局變量,比如 jQuery 的 $
, lodash 的 _
衅码。在 unmount A 子應(yīng)用時拯刁,用一個對象記錄之前給 window 添加的全局變量,并把 A 應(yīng)用里添加 window 的變量都刪掉逝段。下一次再 mount A 應(yīng)用時垛玻,把記錄的全局變量重新加回來就好了。
single-spa 再次站出來:這個不用你自己手動記錄 window 的變更了奶躯。single-spa-leaked-globals 已經(jīng)實(shí)現(xiàn)好了帚桩,直接用就好了:
import singleSpaLeakedGlobals from 'single-spa-leaked-globals';
// 其它 single-spa-xxx 提供的生命周期函數(shù)
const frameworkLifecycles = ...
const leakedGlobalsLifecycles = singleSpaLeakedGlobals({
globalVariableNames: ['$', 'jQuery', '_'], // 新添加的全局變量
})
export const bootstrap = [
leakedGlobalsLifecycles.bootstrap, // 放在第一位
frameworkLifecycles.bootstrap,
]
export const mount = [
leakedGlobalsLifecycles.mount, // mount 時添加全局變量,如果之前有記錄在案的嘹黔,直接恢復(fù)
frameworkLifecycles.mount,
]
export const unmount = [
leakedGlobalsLifecycles.unmount, // 刪掉新添加的全局變量
frameworkLifecycles.unmount,
]
但是账嚎,這個庫的局限性在于:每個 url 只能加一個子 app,如果多個子 app 之間還是會訪問同一個 window 對象,也因此會互相干擾郭蕉,并不能做到完美的 JS 沙箱疼邀。
比如:一個頁面里,導(dǎo)航欄用 3.0 的 jQuery召锈,而頁面主體用 5.0 的 jQuery旁振,那就會有沖突了。
所以這個庫的場景也僅限于:首頁用 3.0 的 jQuery涨岁,訂單詳情頁使用 5.0 的 jQuery 這樣的場景规求。
子應(yīng)用的分類
上面我們說到了,當(dāng) url 匹配 activeWhen 參數(shù)時卵惦,就會執(zhí)行對應(yīng)子應(yīng)用的生命周期阻肿。那這樣就相當(dāng)于子應(yīng)用和 url 綁定在了一起了。
我們再來看 single-spa-leaked-globals沮尿,single-spa-css 這些庫丛塌,雖然它們也導(dǎo)出了生命周期,但這些生命周期與頁面渲染畜疾、url 變化沒有多大關(guān)系赴邻。
它們與普通的 application 唯一不同的地方就是:普通 application 的生命周期是通過 single-spa 來自動調(diào)度的,而這些庫是要通過手動調(diào)度的啡捶。只不過我們一般選擇在子應(yīng)用里的生命周期里手動調(diào)用它們而已姥敛。
這種與 url 無關(guān)的 “app” 在微前端也有著非常重要的作用,一般是在子應(yīng)用的生命周期里提供一些功能瞎暑,像 single-spa-css 就是在 mount 時添加 <link/>
標(biāo)簽彤敛。single-spa 將這樣的 “類子 app” 稱為 Parcel。
同時了赌,single-spa 還分出另一個類:Utility Modules墨榄。很多子應(yīng)用都用 antd, dayjs, axios 的,那么就可以搞一個 utility 集合這些公共庫勿她,然后統(tǒng)一做 export袄秩,然后在 importmap 里統(tǒng)一導(dǎo)入。子應(yīng)用就可以不需要在自己的 package.json 里添加 antd, dayjs, axios 的依賴了逢并。
總結(jié)一下之剧,single-spa 將微前端分為三大類:
分類 | 功能 | 導(dǎo)出 | 是否與 url 有關(guān) |
---|---|---|---|
Application | 子應(yīng)用 | bootstrap, mount, unmount | 是 |
Parcel | 功能組件,比如子應(yīng)用的生命周期打一些補(bǔ)丁 | bootstrap, mount, unmount, update | 否 |
Utility Module | 公共資源 | 所有公共資源 | 否 |
create-single-spa
上面介紹了一堆的與子應(yīng)用相關(guān)的庫砍聊,如果自己要從 0 開始慢慢地配置子應(yīng)用就比較麻煩背稼。所以,single-spa 說:不麻煩辩恼,有腳手架工具雇庙,一行命令生成子應(yīng)用谓形,都給您配好了。
npm install --global create-single-spa
# 或者
yarn global add create-single-spa
然后
create-single-spa
注意疆前!這里的 create-single-spa 指的是創(chuàng)建子應(yīng)用寒跳!
總結(jié)
以上就是 singles-spa 文檔里的所有內(nèi)容了(除了 SSR 和 Dev Tools,前者用的不多竹椒,后者自己看一下就會了童太,不多廢話)。由于本文是通過發(fā)現(xiàn)問題到解決問題來講述文檔內(nèi)容的胸完,所以從頭看到尾還是有點(diǎn)亂书释,這里就做一下總結(jié):
微前端概念
特點(diǎn):
- 技術(shù)棧無關(guān)
- 獨(dú)立開發(fā)、獨(dú)立部署
- 增量升級
- 獨(dú)立運(yùn)行時
single-spa
只做兩件事:
- 提供生命周期概念,并負(fù)責(zé)調(diào)度子應(yīng)用的生命周期
- 挾持 url 變化事件和函數(shù),url 變化時匹配對應(yīng)子應(yīng)用呻引,并執(zhí)行生命周期流程
三大分類:
- Application:子應(yīng)用秘豹,和 url 強(qiáng)相關(guān)位衩,交由 single-spa 調(diào)用生命周期
- Parcel:組件,和 url 無關(guān),手動調(diào)用生命周期
- Utility Module:統(tǒng)一將公共資源導(dǎo)出的模塊
“重要”概念
- Root Config:指主應(yīng)用的 index.html + main.js。HTML 負(fù)責(zé)聲明資源路徑熄阻,JS 負(fù)責(zé)注冊子應(yīng)用和啟動主應(yīng)用
- Application:要暴露 bootstrap, mount, umount 三個生命周期,一般在 mount 開始渲染子 SPA 應(yīng)用
- Parcel:也要暴露 bootstrap, mount, unmount 三個生命周期倔约,可以再暴露 update 生命周期秃殉。Parcel 可大到一個 Application,也可以小到一個功能組件浸剩。與 Application 不同的是 Parcel 需要開發(fā)都手動調(diào)用生命周期
SystemJS
可以在瀏覽器使用 ES6 的 import/export 語法钾军,通過 importmap 指定依賴庫的地址。
和 single-spa 沒有關(guān)系乒省,只是 in-browser import/export 和 single-spa 倡導(dǎo)的 in-browser run time 相符合巧颈,所以 single-spa 將其作為主要的導(dǎo)入導(dǎo)出工具。
用 Webpack 動態(tài)引入可不可以袖扛,可以,甚至可能比 SystemJS 好用十籍,并無好壞之分蛆封。
single-spa-layout
和 Vue Router 差不多,主要功能是可以在 index.html 指定在哪里渲染哪個子應(yīng)用勾栗。
single-spa-react, single-spa-xxx....
給子應(yīng)用快速生成 bootstrap, mount, unmount 的生命周期函數(shù)的工具庫惨篱。
single-spa-css
隔離前后兩個子應(yīng)用的 CSS 樣式。
在子應(yīng)用 mount 時添加子應(yīng)用的 CSS围俘,在 unmount 時刪除子應(yīng)用的 CSS砸讳。子應(yīng)用使用 Webpack 導(dǎo)出 CSS 文件時琢融,要配合 ExposeRuntimeCssAssetsPlugin
插件來獲取最終導(dǎo)出的 CSS 文件名。
算實(shí)現(xiàn)了一半的 CSS 沙箱簿寂。
如果要在多個子應(yīng)用進(jìn)行樣式隔離漾抬,可以有兩種方法:
- Shadow DOM,樣式隔離比較好的方法常遂,但是穿透比較麻煩
- Scoped CSS纳令,在子應(yīng)用的 CSS 選擇器上添加前綴做區(qū)分,可以使用
postcss-prefix-selector
這個包來快速添加前綴
single-spa-leaked-globals
在子應(yīng)用 mount 時給 window 對象恢復(fù)/添加一些全局變量克胳,如 jQuery 的 $
或者 lodash 的 _
平绩,在 unmount 時把 window 對象的變量刪掉。
實(shí)現(xiàn)了“如果主應(yīng)用一個url只有一個頁面”情況下的 JS 沙箱漠另。
公共依賴
有兩種方法處理:
- 造一個 Utility Module 包捏雌,在這個包導(dǎo)出所有公共資源內(nèi)容,并用 SystemJS 的 importmap 在主應(yīng)用的 index.html 里聲明
- 使用 Webpack 5 Module Federation 特性實(shí)現(xiàn)公共依賴的導(dǎo)入
哪個更推薦笆搓?都可以腹忽。
最后
single-spa 文檔就這些了嘛?沒錯砚作,就這些了窘奏。文檔好像給了很多“最佳實(shí)踐”,但真正將所有“最佳實(shí)踐”結(jié)合起來并落地的又沒多少葫录。
比如文檔說用 Shadow CSS 來做子應(yīng)用之間的樣式隔離着裹,但是 single-spa-leaked-globals 又不讓別人在一個 url 上掛載多個子應(yīng)用。感覺很不靠譜:這里行了米同,那里又不行了骇扇。
再說回 Shadow CSS 來做樣式隔離,但是也沒有詳細(xì)說明要具體要怎么做面粮。像這樣的例子還有很多:文檔往往只告訴了一條路少孝,怎么走還要看開發(fā)者自己。這就你給人一種 “把問題只解決一半” 的感覺熬苍。
如果真的想用 single-spa 來玩小 Demo 的稍走,用上面提到的小庫來搭建微前端是可以的,但是要用到生產(chǎn)環(huán)境真的沒那么容易柴底。
所以婿脸,為了填平 single-spa 遺留下來的坑,阿里基于 single-spa 造出了 qiankun 微前端框架柄驻,真正實(shí)現(xiàn)了微前端的所有特性狐树,不過這又是另外一個故事了。