寫在開頭:css
手寫框架體系文章,缺手寫vue和微前端框架文章,今日補上微前端框架,以爲寫得不錯,記得點個關注+在看,轉發更好html
對源碼有興趣的,能夠看我以前的系列手寫源碼文章前端
微前端框架是怎麼導入加載子應用的 【3000字精讀】 vue
原創:帶你從零看清Node源碼createServer和負載均衡整個過程 node
精讀:10個案例讓你完全理解React hooks的渲染邏輯 webpack
原創:如何本身實現一個簡單的webpack構建工具 【附源碼】 ios
正式開始: web
對於微前端,最近好像很火,以前我公衆號也發過比較多微前端框架文章
那麼如今咱們須要手寫一個微前端框架,首先得讓你們知道什麼是微前端,如今微前端模式分不少種,可是大都是一個基座+多個子應用模式,根據子應用註冊的規則,去展現子應用。
這是目前的微前端框架基座加載模式的原理,基於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協議:
點個贊支持我吧,轉發就更好了