深度:從零編寫一個微前端框架

寫在開頭:css

手寫框架體系文章,缺手寫vue和微前端框架文章,今日補上微前端框架,以爲寫得不錯,記得點個關注+在看,轉發更好html


對源碼有興趣的,能夠看我以前的系列手寫源碼文章前端

微前端框架是怎麼導入加載子應用的  【3000字精讀】 vue

原創:帶你從零看清Node源碼createServer和負載均衡整個過程 node

原創:從零實現一個簡單版React (附源碼)react

精讀:10個案例讓你完全理解React hooks的渲染邏輯 webpack

原創:如何本身實現一個簡單的webpack構建工具 【附源碼】 ios

從零解析webRTC.io Server端源碼nginx


正式開始: web

對於微前端,最近好像很火,以前我公衆號也發過比較多微前端框架文章

深度:微前端在企業級應用中的實踐  (1萬字,華爲)

萬字解析微前端、微前端框架qiankun以及源碼

那麼如今咱們須要手寫一個微前端框架,首先得讓你們知道什麼是微前端,如今微前端模式分不少種,可是大都是一個基座+多個子應用模式,根據子應用註冊的規則,去展現子應用。

這是目前的微前端框架基座加載模式的原理,基於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()這個源碼同樣,可參考我以前的文章

原創:從零實現一個簡單版React (附源碼)


那麼咱們先編寫一個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熱更新HMR

原創:如何本身實現一個簡單的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協議:

深度:手寫一個WebSocket協議    [7000字]

最後

  • 歡迎加我微信(CALASFxiaotan),拉你進技術羣,長期交流學習...
  • 歡迎關注「前端巔峯」,認真學前端,作個有專業的技術人...

點個贊支持我吧,轉發就更好了

相關文章
相關標籤/搜索