寫在開頭:
手寫框架體系文章,缺手寫vue和微前端框架文章,今日補上微前端框架,覺得寫得不錯,記得點個關注+在看,轉發更好
對原始碼有興趣的,可以看我之前的系列手寫原始碼文章
原創:帶你從零看清Node原始碼createServer和負載均衡整個過程
精讀:10個案例讓你徹底理解React hooks的渲染邏輯
原創:如何自己實現一個簡單的webpack構建工具 【附原始碼】
正式開始:
對於微前端,最近好像很火,之前我公眾號也發過比較多微前端框架文章
那麼現在我們需要手寫一個微前端框架,首先得讓大家知道什麼是微前端,現在微前端模式分很多種,但是大都是一個基座+多個子應用模式,根據子應用註冊的規則,去展示子應用。
這是目前的微前端框架基座載入模式的原理,基於single-spa封裝了一層,我看有不少公司是用Vue做載入器(有天然的keep-alive),還有用angular和web components技術融合的
首先專案基座搭建,這裡使用parcel:
mkdir pangu
yarn init
//輸入一系列資訊
yarn add parcel@next
然後新建一個index.html檔案,作為基座
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>
新建一個index.js檔案,作為基座載入配置檔案
新建src資料夾,作為pangu框架的原始碼資料夾,
新建example案例資料夾
現在專案結構長這樣
既然是手寫,就不依賴其他任何第三方庫
我們首先需要重寫hashchange popstate這兩個事件,因為微前端的基座,需要監聽這兩個事件根據註冊規則去載入不同的子應用,而且它的實現必須在React、vue子應用路由元件切換之前,單頁面的路由原始碼原理實現,其實也是靠這兩個事件實現,之前我寫過一篇單頁面實現原理的文章,不熟悉的可以去看看
https://segmentfault.com/a/1190000019936510
const HIJACK_EVENTS_NAME = /^(hashchange|popstate)$/i;
const EVENTS_POOL = {
hashchange: [],
popstate: [],
};
window.addEventListener('hashchange', loadApps);
window.addEventListener('popstate', loadApps);
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function (eventName, handler) {
if (
eventName &&
HIJACK_EVENTS_NAME.test(eventName) &&
typeof handler === 'function'
) {
EVENTS_POOL[eventName].indexOf(handler) === -1 &&
EVENTS_POOL[eventName].push(handler);
}
return originalAddEventListener.apply(this, arguments);
};
window.removeEventListener = function (eventName, handler) {
if (eventName && HIJACK_EVENTS_NAME.test(eventName)) {
let eventsList = EVENTS_POOL[eventName];
eventsList.indexOf(handler) > -1 &&
(EVENTS_POOL[eventName] = eventsList.filter((fn) => fn !== handler));
}
return originalRemoveEventListener.apply(this, arguments);
};
function mockPopStateEvent(state) {
return new PopStateEvent('popstate', { state });
}
// 攔截history的方法,因為pushState和replaceState方法並不會觸發onpopstate事件,所以我們即便在onpopstate時執行了reroute方法,也要在這裡執行下reroute方法。
const originalPushState = window.history.pushState;
const originalReplaceState = window.history.replaceState;
window.history.pushState = function (state, title, url) {
let result = originalPushState.apply(this, arguments);
reroute(mockPopStateEvent(state));
return result;
};
window.history.replaceState = function (state, title, url) {
let result = originalReplaceState.apply(this, arguments);
reroute(mockPopStateEvent(state));
return result;
};
// 再執行完load、mount、unmout操作後,執行此函式,就可以保證微前端的邏輯總是第一個執行。然後App中的Vue或React相關Router就可以收到Location的事件了。
export function callCapturedEvents(eventArgs) {
if (!eventArgs) {
return;
}
if (!Array.isArray(eventArgs)) {
eventArgs = [eventArgs];
}
let name = eventArgs[0].type;
if (!HIJACK_EVENTS_NAME.test(name)) {
return;
}
EVENTS_POOL[name].forEach((handler) => handler.apply(window, eventArgs));
}
上面程式碼很簡單,建立兩個佇列,使用陣列實現
const EVENTS_POOL = {
hashchange: [],
popstate: [],
};
如果檢測到是hashchange popstate兩種事件,而且它們對應的回撥函式不存在佇列中時候,那麼就放入佇列中。(相當於redux中介軟體原理)
然後每次監聽到路由變化,呼叫reroute函式:
function reroute() {
invoke([], arguments);
}
這樣每次路由切換,最先知道變化的是基座,等基座同步執行完(阻塞)後,就可以由子應用的vue-Rourer或者react-router-dom等庫去接管實現單頁面邏輯了。
那,路由變化,怎麼載入子應用呢?
像一些微前端框架會用import-html之類的這些庫,我們還是手寫吧
邏輯大概是這樣,一共四個埠,nginx反向代理命中基座伺服器監聽的埠(使用者必須首先訪問到根據域名),然後去不同子應用下的伺服器拉取靜態資源然後載入。
提示:所有子應用載入後,只是在基座的一個div標籤中載入,實現原理跟ReactDom.render()這個原始碼一樣,可參考我之前的文章
那麼我們先編寫一個registrApp方法,接受一個entry引數,然後去根據url變化載入子應用(傳入的第二個引數activeRule)
/**
*
* @param {string} entry
* @param {string} function
*/
const Apps = [] //子應用佇列
function registryApp(entry,activeRule) {
Apps.push({
entry,
activeRule
})
}
註冊完了之後,就要找到需要載入的app
export async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
console.log(shouldMountApp, 'shouldMountApp');
// const res = await axios.get(shouldMountApp.entry);
fetch(shouldMountApp.entry)
.then(function (response) {
return response.json();
})
.then(function (myJson) {
console.log(myJson, 'myJson');
});
}
shouldBeActive根據傳入的規則去判斷是否需要此時掛載:
export function shouldBeActive(app){
return app.activeRule(window.location)
}
此時的res資料,就是我們通過get請求獲取到的子應用相關資料,現在我們新增subapp1和subapp2資料夾,模擬部署的子應用,我們把它用靜態資源伺服器跑起來
subapp1.js作為subapp1的靜態資源伺服器
const express = require('express');
subapp2.js作為subapp2的靜態資源伺服器
const express = require('express');
const app = express();
const { resolve } = require('path');
app.use(express.static(resolve(__dirname, '../subapp1')));
app.listen(8889, (err) => {
!err && console.log('8889埠成功');
});
現在檔案目錄長這樣:
基座index.html執行在1234埠,subapp1部署在8889埠,subapp2部署在8890埠,這樣我們從基座去拉取資源時候,就會跨域,所以靜態資源伺服器、webpack熱更新伺服器等伺服器,都要加上cors頭,允許跨域。
const express = require('express');
const app = express();
const { resolve } = require('path');
//設定跨域訪問
app.all('*', function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'X-Requested-With');
res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS');
res.header('X-Powered-By', ' 3.2.1');
res.header('Content-Type', 'application/json;charset=utf-8');
next();
});
app.use(express.static(resolve(__dirname, '../subapp1')));
app.listen(8889, (err) => {
!err && console.log('8889埠成功');
});
⚠️:如果是dev模式,記得在webpack的熱更新伺服器中配置允許跨域,如果你對webpack不是很熟悉,可以看我之前的文章:
原創:如何自己實現一個簡單的webpack構建工具 【附原始碼】
這裡我使用nodemon啟用靜態資源伺服器,簡單為主,如果你沒有下載,可以:
npm i nodemon -g
或
yarn add nodemon global
這樣我們先訪問下8889,8890埠,看是否能訪問到。
訪問8889和8890都可以訪問到對應的資源,成功
正式開啟啟用我們的微前端框架pangu.封裝start方法,啟用需要掛載的APP。
export function start(){
loadApp()
}
註冊子應用subapp1,subapp2,並且手動啟用微前端
import { registryApp, start } from './src/index';
registryApp('localhost:8889', (location) => location.pathname === '/subapp1');
registryApp('localhost:8890', (location) => location.pathname === '/subapp2');
start()
修改index.html檔案:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<h1>基座</h1>
<div class="subapp">
<div>
<a href="/subapp1">子應用1</a>
</div>
<div>
<a href="/subapp2">子應用2</a>
</div>
</div>
<div id="subApp"></div>
</div>
</body>
<script src="./index.js"></script>
</html>
ok,執行程式碼,發現掛了,為什麼會掛呢?因為那邊返回的是html檔案,我這裡用的fetch請求,JSON解析不了
那麼我們去看看別人的微前端和第三方庫的原始碼吧,例如import-html-entry這個庫
由於之前我解析過qiankun這個微前端框架原始碼,我這裡就不做過度講解,它們是對fetch做了一個text()。
export async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
console.log(shouldMountApp, 'shouldMountApp');
// const res = await axios.get(shouldMountApp.entry);
fetch(shouldMountApp.entry)
.then(function (response) {
return response.text();
})
.then(function (myJson) {
console.log(myJson, 'myJson');
});
}
然後我們已經可以得到拉取回來的html檔案了(此時是一個字串)
由於現實的專案,一般這個html檔案會包含js和css的引入標籤,也就是我們目前的單頁面專案,類似下面這樣:
於是我們需要把指令碼、樣式、html檔案分離出來。用一個物件儲存
本想照搬某個微前端框架原始碼的,但是覺得它寫得也就那樣,今天又主要講原理,還是自己寫一個能跑的把,畢竟html的檔案都回來了,資料處理也不難
export async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
console.log(shouldMountApp, 'shouldMountApp');
// const res = await axios.get(shouldMountApp.entry);
fetch(shouldMountApp[0].entry)
.then(function (response) {
return response.text();
})
.then(function (text) {
const dom = document.createElement('div');
dom.innerHTML = text;
console.log(dom, 'dom');
});
}
先改造下,列印下DOM
發現已經能拿到dom節點了,那麼我先處理下,讓它展示在基座中
export async function loadApp() {
const shouldMountApp = Apps.filter(shouldBeActive);
console.log(shouldMountApp, 'shouldMountApp');
// const res = await axios.get(shouldMountApp.entry);
fetch(shouldMountApp[0].entry)
.then(function (response) {
return response.text();
})
.then(function (text) {
const dom = document.createElement('div');
dom.innerHTML = text;
const content = dom.querySelector('h1');
const subapp = document.querySelector('#subApp-content');
subapp && subapp.appendChild(content);
});
}
此時,我們已經可以載入不同的子應用了。
乞丐版的微前端框架就完成了,後面會逐步完善所有功能,向主流的微前端框架靠攏,並且完美支援IE11.記住它叫:pangu
推薦閱讀之前的手寫ws協議:
最後
- 歡迎加我微信(CALASFxiaotan),拉你進技術群,長期交流學習...
- 歡迎關注「前端巔峰」,認真學前端,做個有專業的技術人...
點個贊支援我吧,轉發就更好了