微前端方案 Qiankun 只是更完善的 Single-Spa
一個前端應(yīng)用能夠單獨跑,也能被作為一個模塊集成到另一個應(yīng)用里,這種架構(gòu)方式就叫做微前端。
它在前端領(lǐng)域能解決一些特定的問題:
- 中后臺系統(tǒng)中,有一些別的技術(shù)棧開發(fā)的歷史模塊,但是希望能夠在入口里集成進(jìn)來。
- sass 類的前端應(yīng)用,業(yè)務(wù)比較復(fù)雜,可能模塊很多,希望能拆分成多個應(yīng)用獨立維護(hù),也能夠集成到一起。
跨技術(shù)棧的應(yīng)用集成、大的項目拆分成獨立的小項目,這些是微前端解決的典型問題。
微前端的實現(xiàn)方案有很多,比較流行的是 single-spa 以及對它做了一層封裝的 qiankun。
今天我們就來了解下這兩個微前端實現(xiàn)方案:
single-spa
微前端的基本需求就是在 url 變化的時候,加載、卸載對應(yīng)的子應(yīng)用,single spa 就實現(xiàn)了這個功能。
它做的事情就是注冊微應(yīng)用、監(jiān)聽 URL 變化,然后激活對應(yīng)的微應(yīng)用:
注冊一個微應(yīng)用是這樣的:
import { registerApplication } from 'single-spa';
registerApplication({
name: 'app',
app: () => {
loadScripts('./chunk-a.js');
loadScripts('./chunk-b.js');
return loadScripts('./entry.js')
}
activeWhen: '/appName'
})
singleSpa.start()
要指定當(dāng) url 是什么的時候,去加載子應(yīng)用,怎么加載。
它要求子應(yīng)用的入口文件導(dǎo)出 bootstrap、mount、unmount 的生命周期函數(shù),也就是在加載完成、掛載前、卸載前執(zhí)行的邏輯。
比如 react 的子應(yīng)用:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './index.tsx'
export const bootstrap = () => {}
export const mount = () => {
ReactDOM.render(<App/>, document.getElementById('root'));
}
export const unmount = () => {}
這部分邏輯還可以簡化,single-spa 提供了和 react、vue、angular 等集成的包,可以直接用:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './index.tsx';
import singleSpaReact from 'single-spa-react';
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App
});
export const bootstrap = reactLifecycles.bootstrap;
export const mount = reactLifecycles.mount;
export const unmount = reactLifecycles.unmount;
這就是完成了微前端的基本需求,能夠在 url 變化的時候,加載、卸載對應(yīng)的子應(yīng)用。
但是 single spa 做的事情比較簡單,不夠完善,比如說:
- 加載微應(yīng)用的時候要指定加載哪些 js、css,如果子應(yīng)用的打包邏輯發(fā)生了變化,這里也要跟著變
- 一個頁面可能有多個子應(yīng)用,之間會不會有樣式的沖突、JS 的沖突?
- 多個子應(yīng)用之間通信怎么處理?
這些都要使用 sigle-spa 的時候,自己去解決。
所以說 single-spa 并不夠完善,于是 qiankun 就出來了:
qiankun
qiankun 并不是新的微前端框架,它只是解決了 single-spa 沒解決的一些問題,是更完善的基于 single-spa 的微前端方案。
它解決了哪些問題呢?
我們一個個來看一下:
加載子應(yīng)用的資源的方式
用 single-spa 的時候,要在注冊的時候指定如何加載子應(yīng)用:
import { registerApplication } from 'single-spa';
registerApplication({
name: 'app',
app: () => {
loadScripts('./chunk-a.js');
loadScripts('./chunk-b.js');
return loadScripts('./entry.js')
}
activeWhen: '/appName'
})
一般我們會結(jié)合 SystemJS 來用,簡化加載的邏輯,但是依然要知道子應(yīng)用有哪些資源要加載,子應(yīng)用打包邏輯變了,這里加載的方式就要跟著變。
能不能把這個加載過程給自動化了呢?
比如我根據(jù) url 加載子應(yīng)用的 html,然后解析出其中的 JS、CSS,自動去加載。
qiankun 就是按照這個思路來解決的:
它會加載入口 html,解析出 scripts、styles 的部分,單獨去加載,而其余的部分,會做一些轉(zhuǎn)換之后放到 dom 里。
比如這樣一段 html:
qiankun 會把 head 部分轉(zhuǎn)換成 qiankun-head,把 script 部分提取出來自己加載,其余部分放到 html 里:
這樣也就不再需要開發(fā)者指定怎么去加載子應(yīng)用了,實現(xiàn)了解析 html 自動加載的功能。
這個功能的實現(xiàn)放在 import-html-entry 這個包里。
single-spa 的實現(xiàn)叫做 Config Entry 或者 JS Entry,也就是要自己指定怎么加載子應(yīng)用,而 qiankun 這種叫做 Html Entry,會自動解析 html 實現(xiàn)加載。
所以說,注冊 qiankun 應(yīng)用的時候就更簡單了一點,只要指定 html 的地址就行:
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'vue app',
entry: '//localhost:7100',
container: '#container-vue',
activeRule: '/micro-vue'
},
{
name: 'react app',
entry: '//localhost:7101',
container: '#container-react',
activeRule: '/micro-react'
},
]);
start();
而且 qiankun 還支持預(yù)加載,會在空閑的時候加載解析出的 script 和 style:
除了實現(xiàn)了基于 html 的自動加載,qiankun 還實現(xiàn)了 JS 和 CSS 的沙箱:
JS、CSS 沙箱
子應(yīng)用之間肯定要實現(xiàn)隔離,不能相互影響,也就是要實現(xiàn) JS 和 CSS 的隔離。
single-spa 沒有做這方面的處理,而 qiankun 實現(xiàn)了這個功能。
JS 的隔離也就是要隔離 window 這個全局變量,其余的不會有啥沖突,本來就是在不同函數(shù)的作用域執(zhí)行的。
qiankun 實現(xiàn) window 隔離有三種思路:
- 快照,加載子應(yīng)用前記錄下 window 的屬性,卸載之后恢復(fù)到之前的快照。
- diff,加載子應(yīng)用之后記錄對 window 屬性的增刪改,卸載之后恢復(fù)回去。
- Proxy,創(chuàng)建一個代理對象,每個子應(yīng)用訪問到的都是這個代理對象。
這幾個實現(xiàn)思路都比較容易理解。
前兩種思路有個問題,就是不能同時存在多個子應(yīng)用,不然會沖突。一般常用的還是第三種 Proxy 的思路。
在 qiankun 里有這樣的策略選擇邏輯:
當(dāng)支持 Proxy,并且傳入的配置沒設(shè)置 loose,就會使用 Proxy 的方式。
而 CSS 的隔離就是使用 shadow dom 了,這是瀏覽器支持的特性,shadow root 下的 dom 的樣式是不會影響其他 dom 的。
當(dāng)然,也有另一種策略,就是 scoped css 的思路,在 css 選擇器里加一個前綴,并且在 dom 上也加一個 ID。
不過這種還是實現(xiàn)性的,需要手動開啟:
在源碼里可以看到這兩種方式:
總之,JS、CSS 的隔離都有多種方案,可以通過配置來選擇。
此外,qiankun 還內(nèi)置了應(yīng)用間狀態(tài)管理的方案:
應(yīng)用間的狀態(tài)管理
多個子應(yīng)用、子應(yīng)用和主應(yīng)用之間自然有一些狀態(tài)管理的需求,qiankun 也實現(xiàn)了這個功能。
使用起來是這樣的:
主應(yīng)用里做全局狀態(tài)的初始化,定義子應(yīng)用獲取全局狀態(tài)的方法 getGlobalState 和全局狀態(tài)變化時的處理函數(shù) onGlobalStateChange:
import { initGlobalState } from 'qiankun'
const initialState = {
user: {
name: 'guang'
}
}
const actions = initGlobalState(initialState)
actions.onGlobalStateChange((newState, prev) => {
for (const key in newState) {
initialState[key] = newState[key]
}
})
actions.getGlobalState = (key) => {
return key ? initialState[key] : initialState
}
export default actions
子應(yīng)用里可以通過參數(shù)拿到 global state 的 get、set 方法:
export async function mount(props) {
const globalState = props.getGlobalState();
props.setGlobalState({user: {name: 'dong'}})
}
綜上,其實 qiankun 就是更完善一些的 signle-spa,通過 html entry 的方式解決了要手動加載子應(yīng)用的各種資源的麻煩,通過沙箱實現(xiàn)了 JS、CSS 的隔離,還實現(xiàn)了全局的狀態(tài)管理機(jī)制。
子應(yīng)用里大概這樣寫:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
function render(props) {
const { container } = props;
ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('[react16] react app bootstraped');
}
export async function mount(props) {
props.onGlobalStateChange((value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev), true);
props.setGlobalState({
ignore: props.name,
user: {
name: props.name,
},
});
render(props);
}
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
qiankun 會在跑子應(yīng)用之前在 window 沙箱設(shè)置 POWERED_BY_QIANKUN 的變量,如果有這個變量就不要直接渲染,在 mount 生命周期里做渲染,否則就直接渲染。
還要指定靜態(tài)資源的加載地址,通過 webpack_public_path 的全局變量。
其余的就和 single-spa 差不多了。
總結(jié)
前端應(yīng)用能夠單獨跑,也能被集成到另一個應(yīng)用中跑,這種架構(gòu)叫做微前端架構(gòu)。它在解決跨技術(shù)棧的應(yīng)用集成、大項目拆分的場景下是很有用的。
主流的微前端方案是 single-spa 以及基于 single-spa 的 qiankun:
single-spa 實現(xiàn)了路由切換的時候,對子應(yīng)用的加載、卸載。
但是它不夠完善,沒有解決資源加載、沙箱、全局狀態(tài)管理的問題,而 qiankun 做的更完善了一些:
- 基于 html 自動分析 js、css,自動加載,不需要開發(fā)者手動指定如何加載。
- 基于快照、Proxy 的思路實現(xiàn)了 JS 隔離,基于 Shadow Dom 和 scoped css 的思路實現(xiàn)了 CSS 隔離。
- 提供了全局狀態(tài)管理的機(jī)制。
所以說,qiankun 基于 single-spa,使用方式差不多,但是各方面的功能更完善一些。