<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從微服務(wù)到微前端:淺談微前端的設(shè)計思想

          共 13625字,需瀏覽 28分鐘

           ·

          2021-07-04 09:44


          記得作為實(shí)習(xí)生剛到公司的第一天就被什么monorepo、微前端等名詞搞得一頭霧水,經(jīng)過一段時間的學(xué)習(xí)終于摸清了一點(diǎn)門道,那么今天就來和大家聊一下我對微前端設(shè)計的看法和理解。

          1、引入:什么是微服務(wù)?

          微服務(wù)是近幾年在互聯(lián)網(wǎng)業(yè)界內(nèi)非常?? 的一個詞,在俺們大學(xué)沸點(diǎn)工作室Java組也已經(jīng)有Spring Cloud微服務(wù)的實(shí)踐先例,那我們作為前端的角度,該怎么理解微服務(wù)呢?

          可以看看下面這個?? :

          一個系統(tǒng)有PC Web端、手機(jī)H5、和后臺管理系統(tǒng),那么整個系統(tǒng)的結(jié)構(gòu)大概就像這樣:

          image.png

          這樣會造成什么問題嘞?

          • 單體服務(wù)端項目過大,不利于快速上手打包編譯
          • 不同系統(tǒng)會有相同的功能點(diǎn),導(dǎo)致產(chǎn)生大量重復(fù)的無意義的接口
          • 數(shù)據(jù)庫設(shè)計復(fù)雜

          那么微服務(wù)又是怎么解決的嘞?

          核心就是就是將系統(tǒng)拆分成不同的服務(wù),通過網(wǎng)關(guān)和controller來進(jìn)行簡單的控制和調(diào)用,各服務(wù)分而治之、互不影響。

          我們現(xiàn)在再看一哈新的項目結(jié)構(gòu):

          image.png

          通過服務(wù)的拆分后,我們的系統(tǒng)是不是更加清晰了?? ,那么問題來了?

          這和我們本次的主題,微前端有什么關(guān)系嗎

          前端的微前端思想其實(shí)同樣來自于此:通過拆分服務(wù),實(shí)現(xiàn)邏輯的解耦

          2、前端微服務(wù)設(shè)計

          2.1 為什么前端需要微服務(wù)?

          當(dāng)我們create一個新項目后,想必各位都有以下體會:

          寫項目的第一天:打包 20s

          寫項目的一周:打包 1min

          寫項目的一個月:打包 5min

          之前體驗過公司老項目,代碼量非常大,可讀性不高,打包需要10+分鐘

          隨著項目體量的增加,一個巨大的單體應(yīng)用是難以維護(hù)的,從而導(dǎo)致:開發(fā)效率低、上線困難等一系列問題。

          2.2 微前端的應(yīng)用場景

          對于一個管理系統(tǒng),它的頁面通常是長這個樣子的:

          image.png

          側(cè)邊欄的每一個tab,下面可能還有若干的二級節(jié)點(diǎn)甚至是三級節(jié)點(diǎn),久而久之,這樣的一個管理系統(tǒng),終究也會像前面提到的服務(wù)端一樣,難以維護(hù)。

          如果我們用微前端該如何設(shè)計呢?

          每一個tab就是一個子應(yīng)用,有自己的狀態(tài);自己的作用域;并且單獨(dú)打包發(fā)布。在全局層面只需要用一個主應(yīng)用(master)就可以實(shí)現(xiàn)管理和控制。

          一句話來講就是:應(yīng)用分發(fā)路由->路由分發(fā)應(yīng)用。

          2.3 早期微前端思路——iFrame

          Why not iframe ?

          對于路由分發(fā)應(yīng)用這件事:我們只需要通過iFrame就可以實(shí)現(xiàn)了,當(dāng)點(diǎn)擊不同的tab時,view區(qū)域展示的是iFrame組件,根據(jù)路由動態(tài)的改變iframe的src屬性,那不是so easy?

          它的好處有哪些?

          • 自帶樣式
          • 沙盒機(jī)制(環(huán)境隔離)
          • 前端之間可以相互獨(dú)立運(yùn)行

          那我們?yōu)槭裁礇]有使用iFrame做微前端呢?

          • CSS問題(視窗大小不同步)
          • 子應(yīng)用通信(使用postMessage并不友好)
          • 組件不能共享
          • 使用創(chuàng)建 iframe,可能會對性能或者內(nèi)存造成影響

          微前端的設(shè)計構(gòu)思:不僅能繼承iframe的優(yōu)點(diǎn),又可以解決它的不足。

          3、微前端核心邏輯

          3.1 子應(yīng)用加載(Loader)

          先來看看微前端的流程:

          image.png

          我們可以達(dá)成的共識是:需要先加載基座(master),再把選擇權(quán)交給主應(yīng)用,由主應(yīng)用根據(jù)注冊過的子應(yīng)用來抉擇加載誰,當(dāng)子應(yīng)用加載成功后,再由vue-router或react-router來根據(jù)路由渲染組件。

          3.1.1 注冊

          如果精簡代碼邏輯,在基座中實(shí)際上只需要做三件事:

          // 假設(shè)我們的微前端框架叫hailuo

          import Hailuo from './lib/index';



          // 1. 聲明子應(yīng)用

          const routers = [

              {

                  path'http://localhost:8081',

                  activeWhen'/subapp1'

              },

              {

                  path'http://localhost:8082',

                  activeWhen'/subapp2'

              }

          ];



          // 2. 注冊子應(yīng)用

          Hailuo.registerApps(routers);



          // 3. 運(yùn)行微前端

          Hailuo.run();

          注冊非常好理解,用一個數(shù)組維護(hù)所有已經(jīng)注冊了的子應(yīng)用:

              registerApps(routers: Router[]) {

                  (routers || []).forEach((r) => {

                      this.Apps.push({

                          entry: r.path,

                          activeRule(location) => (location.href.indexOf(r.activeWhen) !== -1)

                      });

                  });

              }

          3.1.2 攔截

          我們需要通過攔截注冊路由事件以保證主/子應(yīng)用的邏輯處理時機(jī)。

          import Hailuo from ".";



          // 需要攔截的實(shí)踐

          const EVENTS_NAME = ['hashchange''popstate'];

          // 實(shí)踐收集

          const EVENTS_STACKS = {

              hashchange: [],

              popstate: []

          };



          // 基座切換路由后的邏輯

          const handleUrlRoute = (...args) => {

              // 加載對應(yīng)的子應(yīng)用

              Hailuo.loadApp();

              // 執(zhí)行子應(yīng)用路由的方法

              callAllEventListeners(...args);

          };



          export const patch = () => {

              // 1. 先保證基座的事件監(jiān)聽路由的變化

              window.addEventListener('hashchange', handleUrlRoute);

              window.addEventListener('popstate', handleUrlRoute);



              // 2. 重寫addEventListener和removeEventListener

              // 當(dāng)遇到路由事件后:收集到stack中

              // 如果是其他事件:執(zhí)行original事件監(jiān)聽方法

              const originalAddEventListener = window.addEventListener;

              const originalRemoveEventListener = window.removeEventListener;



              window.addEventListener = (name, handler) => {

                  if(name && EVENTS_NAME.includes(name) && typeof handler === "function") {

                      EVENTS_STACKS[name].indexOf(handler) === -1 && EVENTS_STACKS[name].push(handler);

                      return;

                  }

                  return originalAddEventListener.call(this, name, handler);

              };



              window.removeEventListener = (name, handler) => {

                  if(name && EVENTS_NAME.includes(name) && typeof handler === "function") {

                      EVENTS_STACKS[name].indexOf(handler) === -1 && 

                      (EVENTS_STACKS[name] = EVENTS_STACKS[name].filter((fn) => (fn !== handler)));

                      return;

                  } 

                  return originalRemoveEventListener.call(this, name, handler);

              };



              // 手動給pushState和replaceState添加上監(jiān)聽路由變化的能力

              // 有點(diǎn)像vue2中數(shù)組的變異方法

              const createPopStateEvent = (state: any, name: string) => {

                  const evt = new PopStateEvent("popstate", { state });

                  evt['trigger'] = name;

                  return evt;

              };



              const patchUpdateState = (updateState: (data: any, title: string, url?: string)=>voidname: string) => {

                  return function({

                      const before = window.location.href;

                      updateState.apply(thisarguments);

                      const after = window.location.href;

                      if(before !== after) {

                          handleUrlRoute(createPopStateEvent(window.history.state, name));

                      }

                  };

              }



              window.history.pushState = patchUpdateState(

                  window.history.pushState,

                  "pushState"

              );

              window.history.replaceState = patchUpdateState(

                  window.history.replaceState,

                  "replaceState"

              );

          }

          3.1.3 加載

          通過路由可以匹配到符合的子應(yīng)用后,那么該如何將它加載到頁面呢?

          我們知道SPA的html文件只是一個空模板,實(shí)質(zhì)是通過js驅(qū)動的頁面渲染,那么我們把某一個頁面的js文件,全都剪切到另一個html的<script>標(biāo)簽中執(zhí)行,就實(shí)現(xiàn)了A頁面加載B的頁面。

              async loadApp() {

                  // 加載對應(yīng)的子應(yīng)用

                  const shouldMountApp = this.Apps.filter(this.isActive);

                  const app = shouldMountApp[0];

                  const subapp = document.getElementById('submodule');

                  await fetchUrl(app.entry)

                  // 將html渲染到主應(yīng)用里

                  .then((text) => {

                      subapp.innerHTML = text;

                  });

                  // 執(zhí)行 fetch到的js

                  const res = await fetchScripts(subapp, app.entry);

                  if(res.length) {

                      execScript(res.reduce((t, c) => (t+c), ''));

                  } 

              }

          Better實(shí)踐 ——html-entry

          它是一個加載并處理html、js、css的庫。

          它不是去加載一個個的js、css資源,而是去加載微應(yīng)用的入口html。

          • 第一步 :發(fā)送請求,獲取子應(yīng)用入口HTML。
          • 第二步 :處理該html文檔,去掉html、head標(biāo)簽,處理靜態(tài)資源。
          • 第三步 :處理sourceMap;處理js沙箱;找到入口js。
          • 第四步 :獲取子應(yīng)用provider內(nèi)容

          同時,約束了子應(yīng)用提供加載和銷毀函數(shù)(這個結(jié)構(gòu)是不是很眼熟):

          export function provider({ dom, basename, globalData }{



              return {

                  render() {

                      ReactDOM.render(

                          <App basename={basename} globalData={globalData} />,

                          dom ? dom.querySelector('#root') : document.querySelector('#root')

                      );

                  },

                  destroy({ dom }) {

                      if (dom) {

                          ReactDOM.unmountComponentAtNode(dom);

                      }

                  },

              };

          }

          3.2 沙箱(Sandbox)

          沙箱是什么:你可以理解為對作用域的一種比喻,在一個沙箱內(nèi),我的任何操作不會對外界產(chǎn)生影響。

          Why we need sandbox?

          當(dāng)我們集成了很多子應(yīng)用到一起后,勢必會出現(xiàn)沖突,如全局變量沖突樣式?jīng)_突,這些沖突可能會導(dǎo)致應(yīng)用樣式異常,甚至功能不可用。所以想讓微前端達(dá)到生產(chǎn)可用的程度,讓每個子應(yīng)用之間達(dá)到一定程度隔離的沙箱機(jī)制是必不可少的。

          實(shí)現(xiàn)沙箱,最重要的是:控制沙箱的開啟和關(guān)閉。

          3.2.1 快照沙箱

          原理就是運(yùn)行在某一環(huán)境A時,打一個快照,當(dāng)從別的環(huán)境B切換回來的時候,我們通過這個快照就可以立即恢復(fù)之前環(huán)境A時的情況,比如:

          // 切換到環(huán)境A

          window.a = 2;



          // 切換到環(huán)境B

          window.a = 3;



          // 切換到環(huán)境A

          console.log(a);    // 2

          實(shí)現(xiàn)思路,我們假設(shè)有Sandbox這個類:

          class Sandbox {

              private original;

              private mutated;

              sandBoxActive: () => void;

              sandBoxDeactivate: () => void;

          }
          const sandbox = new Sandbox();

          const code = "...";

          sandbox.activate();

          execScript(code);

          sandbox.sandBoxDeactivate();

          來理一下這個邏輯:

          1. 在sandBoxActive的時候,把變量存到original里;
          2. 在sandBoxDeactivate的時候,把當(dāng)前變量和original對比,不同的存到mutated(保存了快照),然后把變量的狀態(tài)恢復(fù)到original;
          3. 當(dāng)該沙箱再次觸發(fā)sandBoxActive,就可以把mutated的變量恢復(fù)到window上,實(shí)現(xiàn)沙箱的切換。

          3.2.2 VM沙箱

          類似于node中的vm模塊(可在 V8 虛擬機(jī)上下文中編譯和運(yùn)行代碼):http://nodejs.cn/api/vm.html#vm_vm_executing_javascript

          快照沙箱的缺點(diǎn)是無法同時支持多個實(shí)例。 但是vm沙箱利用proxy就可以解決這個問題。

          class SandBox {

              execScript(code: string) {

                  const varBox = {};

                  const fakeWindow = new Proxy(window, {

                      get(target, key) {

                          return varBox[key] || window[key];

                      },

                      set(target, key, value) {

                          varBox[key] = value;

                          return true;

                      }

                  })

                  const fn = new Function('window', code);

                  fn(fakeWindow);

              }

          }



          export default SandBox;



          // 實(shí)現(xiàn)了隔離

          const sandbox = new Sandbox();

          sandbox.execScript(code);



          const sandbox2 = new Sandbox();

          sandbox2.execScript(code2);



          // map

          varBox = {

              'aWindow''...',

              'bWindow''...'

          }

          我們把各個子應(yīng)用的window放到map中,通過proxy代理,當(dāng)訪問時,直接就是訪問到的各個子應(yīng)用的window對象;如果沒有,比如使用window.addEventListener,就會去真正的window中尋找。

          3.2.3 CSS沙箱

          • 前提:webpack在構(gòu)建的時候,最終是通過appendChild去添加style標(biāo)簽到html里的

          解決方案:劫持appendChild,增加namespace。

          瀏覽 79
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品6区 | 大香蕉,97| 成人网导航 | 成人看片黄a免费看视频 | 伊人操逼逼|