qiankun是基于 single-spa 做的二次封裝蕴掏,主要解決了single-spa 的一些痛點(diǎn)和不足。
single-spa存在的問題调鲸?
- 1盛杰、對(duì)微應(yīng)用侵入性太強(qiáng)
微應(yīng)用的改造步驟:- 微應(yīng)用路由改造,添加一個(gè)特定的前綴
- 微應(yīng)用入口改造藐石,掛載點(diǎn)變更和生命周期函數(shù)導(dǎo)出
- 打包工具配置更改
single-spa 采用JS Entry 的方式接入微應(yīng)用即供。也就是說single-spa 接入微應(yīng)用需要將微應(yīng)用整個(gè)打包成一個(gè)JS文件,發(fā)布到靜態(tài)資源服務(wù)器于微,然后再主應(yīng)用中配置該JS文件的地址告訴 single-spa 去這個(gè)地址加載微應(yīng)用逗嫡。問題出現(xiàn)了,如按需加載株依、首屏資源加載優(yōu)化驱证、css獨(dú)立打包等優(yōu)化沒有了。
2勺三、樣式隔離
single-spa 沒有做雷滚。怎么做到主應(yīng)用和微應(yīng)用之間的樣式需曾,微應(yīng)用和微應(yīng)用的樣式互不影響吗坚?這個(gè)只能通過約定命名規(guī)范來實(shí)現(xiàn)祈远,比如應(yīng)用樣式以自己的應(yīng)用名稱開頭。3商源、JS隔離
single-spa 沒有做车份。JS全局對(duì)象污染,A應(yīng)用在window上加一個(gè)自己的屬性window.A牡彻,微應(yīng)用B 也能訪問到扫沼。4、資源預(yù)加載
single-spa 沒有做庄吼。例如怎么實(shí)現(xiàn)在第一個(gè)微應(yīng)用加載完后缎除,后臺(tái)悄悄加載其他微應(yīng)用。5总寻、應(yīng)用間通信
single-spa 沒有做器罐。它只在注冊(cè)微應(yīng)用時(shí)給微應(yīng)用注入一些狀態(tài)信息,后續(xù)就不管了渐行,沒有任何通信的手段轰坊。
qiankun 如何解決以上問題
1、HTML Entry
qiankun 通過HTML Entry 的方式來解決JS Entry帶來的問題2祟印、樣式隔離
采用shadow dom 包裹沒一個(gè)微應(yīng)用肴沫,從而確保微應(yīng)用的樣式互不干擾
采用css scoped 方式(實(shí)驗(yàn)性)動(dòng)態(tài)改寫 css 選擇器來實(shí)現(xiàn)3、運(yùn)行時(shí)沙箱
qiankun的運(yùn)行時(shí)沙箱分為 JS 沙箱和樣式沙箱4蕴忆、資源預(yù)加載
qiankun 實(shí)現(xiàn)預(yù)加載的思路有兩種颤芬,一種是當(dāng)主應(yīng)用執(zhí)行 start 方法啟動(dòng) qiankun 以后立即去預(yù)加載微應(yīng)用的靜態(tài)資源,另一種是在第一個(gè)微應(yīng)用掛載以后預(yù)加載其它微應(yīng)用的靜態(tài)資源套鹅,這個(gè)是利用 single-spa 提供的 single-spa:first-mount 事件來實(shí)現(xiàn)的5驻襟、應(yīng)用間通信
qiankun 通過發(fā)布訂閱模式來實(shí)現(xiàn)應(yīng)用間通信
示例項(xiàng)目
yarn examples:install
yarn examples:start
qiankun 提供了6種實(shí)例,vue芋哭、vue3沉衣、react15、react16减牺、angular9豌习、purehtml。
主應(yīng)用在 examples/main 目錄下拔疚,提供了兩種實(shí)現(xiàn)方式肥隆,基于路由配置的 registerMicroApps 和 手動(dòng)加載微應(yīng)用的loadMicroApp。通過 webpak.config.js 的 entry 可以知道有兩個(gè)入口文件 multiple.js 和 index.js稚失。
- 1栋艳、基于路由配置
在 examples/main/index.js 中,將微應(yīng)用關(guān)聯(lián)到一些 url 規(guī)則句各,實(shí)現(xiàn)當(dāng)瀏覽器 url 發(fā)生變化時(shí)吸占,自動(dòng)加載相應(yīng)的微應(yīng)用晴叨。主應(yīng)用可以使用react進(jìn)行運(yùn)行,也可以使用vue進(jìn)行運(yùn)行矾屯。
registerMicroApps(
[
{
name: 'vue',
entry: '//localhost:7101',
container: '#subapp-viewport',
loader,
activeRule: '/vue',
},
],
{
beforeLoad: [
app => {
console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
},
],
beforeMount: [
app => {
console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
},
],
afterUnmount: [
app => {
console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
},
],
},
);
- 2兼蕊、手動(dòng)加載微應(yīng)用
在 examples/main/multiple.js 中有l(wèi)oadMicroApp實(shí)現(xiàn)的例子
function mount() {
app = loadMicroApp(
{ name: 'react15', entry: '//localhost:7102', container: '#react15' },
{ sandbox: { experimentalStyleIsolation: true } },
);
}
vue微應(yīng)用引入,需要修改 vue.config.js 和 mian.js 件蚕、public-path.js
{
...
// publicPath 沒在這里設(shè)置孙技,是通過 webpack 提供的全局變量 __webpack_public_path__ 來即時(shí)設(shè)置的,webpackjs.com/guides/public-path/
devServer: {
...
// 設(shè)置跨域排作,因?yàn)橹鲬?yīng)用需要通過 fetch 去獲取微應(yīng)用引入的靜態(tài)資源的牵啦,所以必須要求這些靜態(tài)資源支持跨域
headers: {
'Access-Control-Allow-Origin': '*',
},
},
output: {
// 把子應(yīng)用打包成 umd 庫格式
library: `${name}-[name]`, // 庫名稱,唯一
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
}
...
}
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
render: h => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
console.log('[vue] props from main framework', props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
運(yùn)行時(shí)沙箱
運(yùn)行時(shí)沙箱包括 JS 沙箱 和 樣式沙箱
JS 沙箱
JS 沙箱是通過 proxy 代理 window 對(duì)象妄痪,記錄window對(duì)象上屬性的增刪改查
- 單例模式
直接代理了原生 window 對(duì)象蕾久,記錄原生 window 對(duì)象的增刪改查,當(dāng) window 對(duì)象激活時(shí)恢復(fù) window 對(duì)象到上次即將失活時(shí)的狀態(tài)拌夏,失活時(shí)恢復(fù) window 對(duì)象到初始初始狀態(tài) - 多例模式
代理了一個(gè)全新的對(duì)象僧著,這個(gè)對(duì)象是復(fù)制的 window 對(duì)象的一部分不可配置屬性,所有的更改都是基于這個(gè) fakeWindow 對(duì)象障簿,從而保證多個(gè)實(shí)例之間屬性互不影響
樣式沙箱
樣式沙箱實(shí)際做的事情其實(shí)很簡(jiǎn)單盹愚,就是將動(dòng)態(tài)添加的 script、link站故、style 這三個(gè)元素插入到對(duì)的位置皆怕,屬于主應(yīng)用的插入主應(yīng)用,屬于微應(yīng)用的插入到對(duì)應(yīng)的微應(yīng)用中西篓,方便微應(yīng)用卸載的時(shí)候一起刪除愈腾,當(dāng)然樣式沙箱還額外做了兩件事:
(1)在卸載之前為動(dòng)態(tài)添加樣式做緩存,在微應(yīng)用重新掛載時(shí)再插入到微應(yīng)用內(nèi)
(2)將 proxy 對(duì)象傳遞給 execScripts 函數(shù)岂津,將其設(shè)置為微應(yīng)用的執(zhí)行上下文
-
樣式隔離
qiankun 的樣式隔離有兩種方式虱黄,一種是嚴(yán)格樣式隔離,通過 shadow dom 來實(shí)現(xiàn)吮成,另一種是實(shí)驗(yàn)性的樣式隔離橱乱,就是 scoped css,兩種方式不可共存粱甫。在 qiankun 中的嚴(yán)格樣式隔離泳叠,就是在這個(gè) createElement 方法中做的,通過 shadow dom 來實(shí)現(xiàn)茶宵, shadow dom 是瀏覽器原生提供的一種能力危纫,在過去的很長(zhǎng)一段時(shí)間里,瀏覽器用它來封裝一些元素的內(nèi)部結(jié)構(gòu)。以一個(gè)有著默認(rèn)播放控制按鈕的 <video> 元素為例种蝶,實(shí)際上契耿,在它的 Shadow DOM 中,包含來一系列的按鈕和其他控制器
實(shí)驗(yàn)性樣式隔離
實(shí)驗(yàn)性樣式的隔離方式其實(shí)就是 scoped css蛤吓,qiankun 會(huì)通過動(dòng)態(tài)改寫一個(gè)特殊的選擇器約束來限制 css 的生效范圍
HTML Entry
HTML Entry 是由 import-html-entry 庫實(shí)現(xiàn)的宵喂,通過 http 請(qǐng)求加載指定地址的首屏內(nèi)容即 html 頁面糠赦,然后解析這個(gè) html 模版得到 template, scripts , entry, styles会傲。
{
template: 經(jīng)過處理的腳本,link拙泽、script 標(biāo)簽都被注釋掉了,
scripts: [腳本的http地址 或者 { async: true, src: xx } 或者 代碼塊],
styles: [樣式的http地址],
entry: 入口腳本的地址淌山,要不是標(biāo)有 entry 的 script 的 src,要不就是最后一個(gè) script 標(biāo)簽的 src
}
然后遠(yuǎn)程加載 styles 中的樣式內(nèi)容顾瞻,將 template 模版中注釋掉的 link 標(biāo)簽替換為相應(yīng)的 style 元素泼疑。然后向外暴露一個(gè) Promise 對(duì)象。
{
// template 是 link 替換為 style 后的 template
template: embedHTML,
// 靜態(tài)資源地址
assetPublicPath,
// 獲取外部腳本荷荤,最終得到所有腳本的代碼內(nèi)容
getExternalScripts: () => getExternalScripts(scripts, fetch),
// 獲取外部樣式文件的內(nèi)容
getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
// 腳本執(zhí)行器退渗,讓 JS 代碼(scripts)在指定 上下文 中運(yùn)行
execScripts: (proxy, strictGlobal) => {
if (!scripts.length) {
return Promise.resolve();
}
return execScripts(entry, scripts, proxy, { fetch, strictGlobal });
}
}
HTML Entry 最終會(huì)返回一個(gè) Promise 對(duì)象,qiankun 就用了這個(gè)對(duì)象中的 template蕴纳、assetPublicPath 和 execScripts 三項(xiàng)会油,將 template 通過 DOM 操作添加到主應(yīng)用中,執(zhí)行 execScripts 方法得到微應(yīng)用導(dǎo)出的生命周期方法古毛,并且還順便解決了 JS 全局污染的問題翻翩,因?yàn)閳?zhí)行 execScripts 方法的時(shí)候可以通過 proxy 參數(shù)指定 JS 的執(zhí)行上下文。
內(nèi)容來源
微前端框架 之 qiankun 從入門到源碼分析
qiankun 2.x 運(yùn)行時(shí)沙箱 源碼分析
HTML Entry 源碼分析