微前端框架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實現html

前端

  • 在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.我這裏作乞丐版,目前就不作那麼正規了,設計框架原則你們不能忘記哈webpack

git

看樣式、腳本都生效了

  • 問題也暴露出來了,那麼如今咱們在子應用中寫的樣式代碼,污染到了基座全局,這樣是不能夠的,由於每一個子應用應該是沙箱環境
  • 若是是script相關的,能夠用proxy和defineproperty作處理
  • 若是是樣式相關,可使用shadow dow技術作樣式隔離
  • 這裏不得不說,web components技術也是能夠在某些技術去實現微前端
  • 咱們今天主要是實現乞丐版,爲了讓你們能瞭解微前端如何工做的,這裏也是開放了源碼

寫在最後

  • 本文gitHub源碼倉庫:https://github.com/JinJieTan/chunchao,記得給個star
  • 我是Peter,架構設計過20萬人端到端加密超級羣功能的桌面IM軟件,如今是一名前端架構師。

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

    另外歡迎收藏個人資料網站:前端生活社區:https://qianduan.life,感受對你有幫助,能夠右下角點個在看,關注一波公衆號:[前端巔峯]web

相關文章
相關標籤/搜索