寫在開頭
爲了讓大家更能理解微前端的工作模式,微前端的最佳實踐應該還需要探索
乞丐版微前端框架
chunchao
源碼開源,僅僅爲了讓大家學習微前端的工作模式而已,實際項目中,我們有使用Paas模式,web components,git submodule等模式都可以實現微前端,當然業內肯定有獨特的、優於這些模式的微前端實現
正式開始
推薦你先看我之前的幾篇文章,這樣才能更好的閱讀本文
如果你有什麼問題想跟我交流,可以加入我們的專業微前端交流羣/技術交流羣
往期我的原創推薦:
在上篇文章基礎上修改,加載子應用方式
首先修改插入dom形式,在請求回來子應用的html內容:
export async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
console.log(shouldMountApp, 'shouldMountApp');
fetch(shouldMountApp[0].entry)
.then(function (response) {
return response.text();
})
.then(function (text) {
const dom = document.createElement('div');
dom.innerHTML = text;
const subapp = document.querySelector('#subApp-content');
subapp && subapp.appendChild(dom);
});
}
直接將子應用的
dom
節點,渲染到基座的對應子應用節點中那麼子應用此時除了style、script標籤,都加載進來了
加載script
、style
標籤
❝樣式隔離、沙箱隔離並不是難題,這裏不着重實現,可以參考shadow dom,qiankun的proxy隔離代理window實現
❞
在qiankun源碼中,也是使用了
fetch
去加載·
script、style`標籤,然後用key-value形式緩存在一個對象中(方便緩存第二次直接獲取),他們的fetch還可以用閉包傳入或者使用默認的fetch,這裏不做過多源碼解析
加載script標籤
有直接寫在html文件裏的,有通過script標籤引入的(webpack等工程化產物),有async,preload,defer等特殊屬性
改造子應用1的html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>subapp1</title>
</head>
<body>
<div>subapp1</div>
</body>
<script src="/index.js"></script>
<script>
alert('subapp1')
</script>
</html>
此時有了
script
標籤,需要加載,根據加載方式,分爲html內部的和通過script標籤引入的例如:
<script src="/index.js"></script>
<script>
alert('subapp1')
</script>
那麼首先我們要對這個路徑做下處理,子應用entry中有完整url前綴路徑,那麼我們需要跟這個script標籤對src屬性拼接處理,然後發送fetch請求獲取內容
改造加載APP的函數,拉取script標籤(目前只考慮單實例)
export async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
const App = shouldMountApp.pop();
});
這裏有一個坑,如果子應用寫的是
script src="/index.js"
,但是讀取script標籤的src屬性,會自動+上主應用的前綴,所以要考慮下如何處理並且針對script標籤加載,都做了promise化,這樣可以確保拉取成功後再進行dom操作,插入到主應用基座中
一個是相對src,一個是絕對src,爲了不改變子應用的打包,我們使用相對src.
此時寫一段js代碼,獲取下當前的基座的完整url,用正則表達式替換掉即可
const url = window.location.protocol+"//"+window.location.host
這樣就能完整正確獲取到script標籤的內容了,發送fetch請求,獲取內容,然後集體promise化,得到真正的內容:
const res = await Promise.all(paromiseArr);
console.log(res, 'res');
if (res && res.length > 0) {
res.forEach((item) => {
const script = document.createElement('script');
script.innerText = item;
subapp.appendChild(script);
});
}
然後插入到subApp子應用的container中,腳本生效了
爲了優雅一些,我們把腳本抽離成單獨function,今天由於簡單點,乞丐版,爲了給你們學習,所以不講究太多,都用js寫代碼了,就不追求穩定和美觀了
完整的loadApp函數:
export async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
const App = shouldMountApp.pop();
fetch(App.entry)
.then(function (response) {
return response.text();
})
.then(async function (text) {
const dom = document.createElement('div');
dom.innerHTML = text;
const entryPath = App.entry;
const scripts = dom.querySelectorAll('script');
const subapp = document.querySelector('#subApp-content');
const paromiseArr =
scripts &&
Array.from(scripts).map((item) => {
if (item.src) {
const url = window.location.protocol + '//' + window.location.host;
return fetch(`${entryPath}/${item.src}`.replace(url, '')).then(
function (response) {
return response.text();
}
);
} else {
return Promise.resolve(item.textContent);
}
});
subapp.appendChild(dom);
const res = await Promise.all(paromiseArr);
if (res && res.length > 0) {
res.forEach((item) => {
const script = document.createElement('script');
script.innerText = item;
subapp.appendChild(script);
});
}
});
}
抽離腳本處理函數:
在loadApp函數中,插入dom後加載腳本
subapp.appendChild(dom);
handleScripts(entryPath,subapp,dom);
定義腳本處理函數:
export async function handleScripts(entryPath,subapp,dom) {
const scripts = dom.querySelectorAll('script');
const paromiseArr =
scripts &&
Array.from(scripts).map((item) => {
if (item.src) {
const url = window.location.protocol + '//' + window.location.host;
return fetch(`${entryPath}/${item.src}`.replace(url, '')).then(
function (response) {
return response.text();
}
);
} else {
return Promise.resolve(item.textContent);
}
});
const res = await Promise.all(paromiseArr);
if (res && res.length > 0) {
res.forEach((item) => {
const script = document.createElement('script');
script.innerText = item;
subapp.appendChild(script);
});
}
}
這樣loadApp函數就清晰了
export async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
const App = shouldMountApp.pop();
fetch(App.entry)
.then(function (response) {
return response.text();
})
.then(async function (text) {
const dom = document.createElement('div');
dom.innerHTML = text;
const entryPath = App.entry;
const subapp = document.querySelector('#subApp-content');
subapp.appendChild(dom);
handleScripts(entryPath, subapp, dom);
});
}
開始樣式文件處理
同理,我們此時要來一個複用,獲取所有的style標籤,以及link標籤,而且是rel="stylesheet"的,這樣的我們需要用fetch拉取回來,插入到subapp container中
首先在subApp1子應用中+上style標籤和樣式內容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>subapp1</title>
<style>
body {
color: red;
}
</style>
</head>
<body>
<div>subapp1</div>
</body>
<script src="/index.js"></script>
<script>
alert('subapp1')
</script>
</html>
然後在loadApp中加入handleStyles函數
handleScripts(entryPath, subapp, dom);
handleStyles(entryPath, subapp, dom);
定義handleStyles函數,20秒解決:
export async function handleStyles(entryPath, subapp, dom) {
const arr = [];
const styles = dom.querySelectorAll('style');
const links = Array.from(dom.querySelectorAll('link')).filter(
(item) => item.rel === 'stylesheet'
);
const realArr = arr.concat(styles,links)
const paromiseArr =
arr &&
Array.from(realArr).map((item) => {
if (item.rel) {
const url = window.location.protocol + '//' + window.location.host;
return fetch(`${entryPath}/${item.href}`.replace(url, '')).then(
function (response) {
return response.text();
}
);
} else {
return Promise.resolve(item.textContent);
}
});
const res = await Promise.all(paromiseArr);
if (res && res.length > 0) {
res.forEach((item) => {
const style = document.createElement('style');
style.innerHTML = item;
subapp.appendChild(style);
});
}
}
❝這裏可以做個promise化,如果加載失敗可以報個警告控制檯,封裝框架大都需要這個,否則無法debug.我這裏做乞丐版,目前就不做那麼正規了,設計框架原則大家不能忘記哈
❞
看樣式、腳本都生效了
問題也暴露出來了,那麼現在我們在子應用中寫的樣式代碼,污染到了基座全局,這樣是不可以的,因爲每個子應用應該是沙箱環境
如果是script相關的,可以用proxy和defineproperty做處理
如果是樣式相關,可以使用shadow dom技術做樣式隔離
這裏不得不說,web components技術也是可以在某些場景去實現微前端
我們今天主要是實現乞丐版,爲了讓大家能瞭解微前端如何工作的,這裏也是開放了源碼
寫在最後
本文gitHub源碼倉庫:https://github.com/JinJieTan/chunchao
,記得給個star
哦
我是Peter,架構設計過20萬人端到端加密超級羣功能的桌面IM軟件,現在是一名前端架構師。
如果你對性能優化有很深的研究,可以跟我一起交流交流,今天這裏寫得比較淺,但是大部分人都夠用,之前問我的朋友,我讓它寫了一個定時器定時消費隊列,最後也能用。哈哈
推薦閱讀
張一鳴:爲什麼 BAT 挖不走我們的人才?
2020年北京,上海擺攤夜市分佈
牛!月入2w,95後送外賣的程序員,送餐途中改bug
5 種將死的編程語言
再見,Kotlin !你好, Java !
回復 【關閉】學關閉微信朋友圈廣告
回復 【福利】獲取最新微信支付有獎勵
回復 【被刪】學查看你哪個好友刪除了你巧
回復 【聊天記錄】學備份/恢復聊天記錄
回復 【紅包】學發微信上的動態紅包
回復 【訪客】學微信查看朋友圈訪客記錄
掃碼加我微信進羣,內推和技術交流,大佬們零距離