微前端框架chunchao(春潮)開源啦

寫在開頭

  • 爲了讓大家更能理解微前端的工作模式,微前端的最佳實踐應該還需要探索

  • 乞丐版微前端框架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標籤,都加載進來了

加載scriptstyle標籤

樣式隔離、沙箱隔離並不是難題,這裏不着重實現,可以參考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軟件,現在是一名前端架構師。

如果你對性能優化有很深的研究,可以跟我一起交流交流,今天這裏寫得比較淺,但是大部分人都夠用,之前問我的朋友,我讓它寫了一個定時器定時消費隊列,最後也能用。哈哈

    

掃碼加我微信進羣,內推和技術交流,大佬們零距離

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章