微前端方案 Qiankun 只是更完善的 Single-Spa
一個(gè)前端應(yīng)用能夠單獨(dú)跑,也能被作為一個(gè)模塊集成到另一個(gè)應(yīng)用里,這種架構(gòu)方式就叫做微前端。
它在前端領(lǐng)域能解決一些特定的問題:
- 中后臺(tái)系統(tǒng)中,有一些別的技術(shù)棧開發(fā)的歷史模塊,但是希望能夠在入口里集成進(jìn)來。
- sass 類的前端應(yīng)用,業(yè)務(wù)比較復(fù)雜,可能模塊很多,希望能拆分成多個(gè)應(yīng)用獨(dú)立維護(hù),也能夠集成到一起。
跨技術(shù)棧的應(yīng)用集成、大的項(xiàng)目拆分成獨(dú)立的小項(xiàng)目,這些是微前端解決的典型問題。
微前端的實(shí)現(xiàn)方案有很多,比較流行的是 single-spa 以及對(duì)它做了一層封裝的 qiankun。
今天我們就來了解下這兩個(gè)微前端實(shí)現(xiàn)方案:
single-spa
微前端的基本需求就是在 url 變化的時(shí)候,加載、卸載對(duì)應(yīng)的子應(yīng)用,single spa 就實(shí)現(xiàn)了這個(gè)功能。
它做的事情就是注冊(cè)微應(yīng)用、監(jiān)聽 URL 變化,然后激活對(duì)應(yīng)的微應(yīng)用:
注冊(cè)一個(gè)微應(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 是什么的時(shí)候,去加載子應(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 = () => {}
這部分邏輯還可以簡(jiǎn)化,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 變化的時(shí)候,加載、卸載對(duì)應(yīng)的子應(yīng)用。
但是 single spa 做的事情比較簡(jiǎn)單,不夠完善,比如說:
- 加載微應(yīng)用的時(shí)候要指定加載哪些 js、css,如果子應(yīng)用的打包邏輯發(fā)生了變化,這里也要跟著變
- 一個(gè)頁(yè)面可能有多個(gè)子應(yīng)用,之間會(huì)不會(huì)有樣式的沖突、JS 的沖突?
- 多個(gè)子應(yīng)用之間通信怎么處理?
這些都要使用 sigle-spa 的時(shí)候,自己去解決。
所以說 single-spa 并不夠完善,于是 qiankun 就出來了:
qiankun
qiankun 并不是新的微前端框架,它只是解決了 single-spa 沒解決的一些問題,是更完善的基于 single-spa 的微前端方案。
它解決了哪些問題呢?
我們一個(gè)個(gè)來看一下:
加載子應(yīng)用的資源的方式
用 single-spa 的時(shí)候,要在注冊(cè)的時(shí)候指定如何加載子應(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'
})
一般我們會(huì)結(jié)合 SystemJS 來用,簡(jiǎn)化加載的邏輯,但是依然要知道子應(yīng)用有哪些資源要加載,子應(yīng)用打包邏輯變了,這里加載的方式就要跟著變。
能不能把這個(gè)加載過程給自動(dòng)化了呢?
比如我根據(jù) url 加載子應(yīng)用的 html,然后解析出其中的 JS、CSS,自動(dòng)去加載。
qiankun 就是按照這個(gè)思路來解決的:
它會(huì)加載入口 html,解析出 scripts、styles 的部分,單獨(dú)去加載,而其余的部分,會(huì)做一些轉(zhuǎn)換之后放到 dom 里。
比如這樣一段 html:
qiankun 會(huì)把 head 部分轉(zhuǎn)換成 qiankun-head,把 script 部分提取出來自己加載,其余部分放到 html 里:
這樣也就不再需要開發(fā)者指定怎么去加載子應(yīng)用了,實(shí)現(xiàn)了解析 html 自動(dòng)加載的功能。
這個(gè)功能的實(shí)現(xiàn)放在 import-html-entry 這個(gè)包里。
single-spa 的實(shí)現(xiàn)叫做 Config Entry 或者 JS Entry,也就是要自己指定怎么加載子應(yīng)用,而 qiankun 這種叫做 Html Entry,會(huì)自動(dòng)解析 html 實(shí)現(xiàn)加載。
所以說,注冊(cè) qiankun 應(yīng)用的時(shí)候就更簡(jiǎn)單了一點(diǎn),只要指定 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ù)加載,會(huì)在空閑的時(shí)候加載解析出的 script 和 style:
除了實(shí)現(xiàn)了基于 html 的自動(dòng)加載,qiankun 還實(shí)現(xiàn)了 JS 和 CSS 的沙箱:
JS、CSS 沙箱
子應(yīng)用之間肯定要實(shí)現(xiàn)隔離,不能相互影響,也就是要實(shí)現(xiàn) JS 和 CSS 的隔離。
single-spa 沒有做這方面的處理,而 qiankun 實(shí)現(xiàn)了這個(gè)功能。
JS 的隔離也就是要隔離 window 這個(gè)全局變量,其余的不會(huì)有啥沖突,本來就是在不同函數(shù)的作用域執(zhí)行的。
qiankun 實(shí)現(xiàn) window 隔離有三種思路:
- 快照,加載子應(yīng)用前記錄下 window 的屬性,卸載之后恢復(fù)到之前的快照。
- diff,加載子應(yīng)用之后記錄對(duì) window 屬性的增刪改,卸載之后恢復(fù)回去。
- Proxy,創(chuàng)建一個(gè)代理對(duì)象,每個(gè)子應(yīng)用訪問到的都是這個(gè)代理對(duì)象。
這幾個(gè)實(shí)現(xiàn)思路都比較容易理解。
前兩種思路有個(gè)問題,就是不能同時(shí)存在多個(gè)子應(yīng)用,不然會(huì)沖突。一般常用的還是第三種 Proxy 的思路。
在 qiankun 里有這樣的策略選擇邏輯:
當(dāng)支持 Proxy,并且傳入的配置沒設(shè)置 loose,就會(huì)使用 Proxy 的方式。
而 CSS 的隔離就是使用 shadow dom 了,這是瀏覽器支持的特性,shadow root 下的 dom 的樣式是不會(huì)影響其他 dom 的。
當(dāng)然,也有另一種策略,就是 scoped css 的思路,在 css 選擇器里加一個(gè)前綴,并且在 dom 上也加一個(gè) ID。
不過這種還是實(shí)現(xiàn)性的,需要手動(dòng)開啟:
在源碼里可以看到這兩種方式:
總之,JS、CSS 的隔離都有多種方案,可以通過配置來選擇。
此外,qiankun 還內(nèi)置了應(yīng)用間狀態(tài)管理的方案:
應(yīng)用間的狀態(tài)管理
多個(gè)子應(yīng)用、子應(yīng)用和主應(yīng)用之間自然有一些狀態(tài)管理的需求,qiankun 也實(shí)現(xiàn)了這個(gè)功能。
使用起來是這樣的:
主應(yīng)用里做全局狀態(tài)的初始化,定義子應(yīng)用獲取全局狀態(tài)的方法 getGlobalState 和全局狀態(tài)變化時(shí)的處理函數(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'}})
}
綜上,其實(shí) qiankun 就是更完善一些的 signle-spa,通過 html entry 的方式解決了要手動(dòng)加載子應(yīng)用的各種資源的麻煩,通過沙箱實(shí)現(xiàn)了 JS、CSS 的隔離,還實(shí)現(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 會(huì)在跑子應(yīng)用之前在 window 沙箱設(shè)置 POWERED_BY_QIANKUN 的變量,如果有這個(gè)變量就不要直接渲染,在 mount 生命周期里做渲染,否則就直接渲染。
還要指定靜態(tài)資源的加載地址,通過 webpack_public_path 的全局變量。
其余的就和 single-spa 差不多了。
總結(jié)
前端應(yīng)用能夠單獨(dú)跑,也能被集成到另一個(gè)應(yīng)用中跑,這種架構(gòu)叫做微前端架構(gòu)。它在解決跨技術(shù)棧的應(yīng)用集成、大項(xiàng)目拆分的場(chǎng)景下是很有用的。
主流的微前端方案是 single-spa 以及基于 single-spa 的 qiankun:
single-spa 實(shí)現(xiàn)了路由切換的時(shí)候,對(duì)子應(yīng)用的加載、卸載。
但是它不夠完善,沒有解決資源加載、沙箱、全局狀態(tài)管理的問題,而 qiankun 做的更完善了一些:
- 基于 html 自動(dòng)分析 js、css,自動(dòng)加載,不需要開發(fā)者手動(dòng)指定如何加載。
- 基于快照、Proxy 的思路實(shí)現(xiàn)了 JS 隔離,基于 Shadow Dom 和 scoped css 的思路實(shí)現(xiàn)了 CSS 隔離。
- 提供了全局狀態(tài)管理的機(jī)制。
所以說,qiankun 基于 single-spa,使用方式差不多,但是各方面的功能更完善一些。