5分鐘帶你瞭解微前端(內含大量代碼示例)

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"如果說,2021年了你還不瞭解「微前端」,請自覺搬好板凳前排聽講,小編特地邀請了我們 LigaAI 團隊的前端負責人,帶你輕鬆玩轉微前端。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"什麼是微前端?","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Techniques, strategies and recipes for building a modern web app withmultiple teams that can ship features independently. – Micro Frontends","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微前端是一種多個團隊通過獨立發佈功能的方式,來共同構建現代化 web 應用的技術手段及方法策略。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不同於單純的前端框架/工具,微前端是一套架構體系,這個概念最早在2016年底由 ThoughtWorks 提出。 ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"微前端是一種類似於微服務的架構,它將微服務的理念應用於瀏覽器端,將 Web 應用從整個的「單體應用」轉變爲多個小型前端應用的「聚合體」。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"各個前端應用「原子化」,可以獨立運行、開發、部署,從而滿足業務的快速變化,以及分佈式、多團隊並行開發的需求。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"核心價值(爲什麼要使用微前端?)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"- 不限技術棧","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主應用不限制接入的子應用的技術棧,子應用擁有完全自主權。所接入的子應用之間也相互獨立,沒有任何直接或間接的技術棧、依賴、以及實現上的耦合。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"- 可獨立開發、部署","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微應用倉庫獨立,前後端均可獨立開發,部署完成後主框架自動完成同步更新。獨立部署的能力在微前端體系中至關重要,能夠縮小單次開發的變更範圍,進而降低相關風險。 各個微前端都應該有自己的持續交付管道,這些管道可以將微前端構建、測試並部署到生產環境中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"- 增量升級","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在面對各種複雜場景時,我們通常很難對一個已經存在的系統做全量的技術棧升級或重構。 因此,微前端是一種非常好的實施漸進式重構的手段和策略,它可以逐漸升級我們的架構、依賴關係和用戶體驗。","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"當主框架發生重大變化時,微前端的每個模塊可以獨立按需升級,不需要整體下線或一次性升級所有內容。","attrs":{}},{"type":"text","text":"如果我們想要嘗試新的技術或互動模式,也能在隔離度更好的環境下做試驗。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"- 簡單、解耦、易維護","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微前端架構下的代碼庫傾向於更小/簡單、更容易開發,避免無關組件之間不必要的耦合,讓代碼更簡潔。通過界定清晰的應用邊界來降低意外耦合的可能性,更好地避免這類無意間造成的耦合問題。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"在什麼場景下使用?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"微前端架構旨在解決單體應用在一個相對長的時間跨度下,由於參與人員、團隊的增多、變遷,從一個普通應用演變成一個巨石應用 (Frontend Monolith) 後應用不可維護的問題。這類問題在企業級 Web 應用中尤爲常見。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"- 兼容遺留系統","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現今技術不斷更迭,團隊想要保技術棧不落後,就需要在兼容原有系統的前提下,使用新框架去開發新功能。而遺留系統的功能早已完善,且運行穩定,團隊沒有必要也沒有精力去將遺留系統重構一遍。此時團隊如果需要使用新框架、新技術去開發新的應用,使用微前端是很好的解決方案。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"- 應用聚合","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大型的互聯網公司,或商業Saas平臺,都會爲用戶/客戶提供很多應用和服務。如何爲用戶呈現具有統一用戶體驗和一站式的應用聚合成爲必須解決的問題。前端聚合已成爲一個技術趨勢,目前比較理想的解決方案就是微前端。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"- 不同團隊間開發同一個應用,所用技術棧不同","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"團隊需要把第三方的SaaS應用進行集成或者把第三方私服應用進行集成(比如在公司內部部署的 gitlab等),以及在已有多個應用的情況下,需要將它們聚合爲一個單應用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/89/8913522ce15c29afb43b9cc9670c2531.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":9}}],"text":"圖源:https://micro-frontends.org/","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"什麼是qiankun?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"qiankun 是一個基於 single-spa 的微前端實現庫,旨在幫助大家能更簡單、無痛地構建一個生產可用微前端架構系統。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"qiankun 孵化自螞蟻金融科技基於微前端架構的雲產品統一接入平臺。在經過一批線上應用的充分檢驗及打磨後,該團隊將其微前端內核抽取出來並開源,希望能同時幫助有類似需求的產品更方便地構建自己的微前端系統,同時也希望通過社區的幫助將 qiankun 打磨得更加成熟完善。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前 qiankun 已在螞蟻內部服務了超過 200+ 線上應用,在易用性及完備性上,絕對是值得信賴的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"📦 基於 single-spa 封裝,提供了更加開箱即用的 API。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"📱 不限技術棧,任意技術棧的應用均可 使用/接入,不論是 React/Vue/Angular/JQuery 還是其他等框架。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"💪 HTML Entry 接入方式,讓你接入微應用像使用 iframe 一樣簡單。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"🛡 樣式隔離,確保微應用之間樣式互相不干擾。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"🧳 JS 沙箱,確保微應用之間 全局變量/事件 不衝突。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"⚡️ 資源預加載,在瀏覽器空閒時間預加載未打開的微應用資源,加速微應用打開速度。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 應用一鍵切換成微前端架構系統。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"遇到的問題及解決建議","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"子應用靜態資源404","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.所有圖片等靜態資源上傳至 cdn,css 中直接引用 cdn 地址(推薦)2.將字體文件和圖片打包成base64(適用於字體文件和圖片體積小的項目)(但總是有一些不符合要求的資源,請使用第三種)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// webpack config loader, 添加以下rule到rules中\n{\n test: /\\.(png|jpe?g|gif|webp|woff2?|eot|ttf|otf)$/i,\n use: [{\n loader: 'url-loader',\n options: {},\n }]\n}\n// chainWebpack\nconfig.module.rule('fonts').use('url-loader').loader('url-loader').options({}).end();\nconfig.module.rule('images').use('url-loader').loader('url-loader').options({}).end();\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.在打包時給其注入完整路徑(適用於字體文件和圖片體積比較大的項目)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const elementFromPoint = document.elementFromPoint;\ndocument.elementFromPoint = function (x, y) {\n const result = Reflect.apply(elementFromPoint, this, [x, y]);\n // 如果座標元素爲shadow則用該shadow再次獲取\n if (result && result.shadowRoot) {\n return result.shadowRoot.elementFromPoint(x, y);\n }\n return result;\n};\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"css樣式隔離","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"默認情況下,qiankun會自動開啓沙箱模式,但這個模式無法隔離主應用與子應用,也無法適應同時加載多子應用的場景。 qiankun還給出了shadow dom的方案,需要配置sandbox: { strictStyleIsolation: true }","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於 ShadowDOM 的嚴格樣式隔離並不是一個可以無腦使用的方案,大部分情況下都需要接入應用做一些適配後才能正常在 ShadowDOM 中運行起來。比如 react 場景下需要解決這些問題 ,使用者需要清楚開啓了 strictStyleIsolation 意味着什麼。下面會列出我解決ShadowDom的一些案例。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"fix shadow dom","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"getComputedStyle","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當獲取shadow dom的計算樣式的時候傳入的element是DocumentFragment,會報錯。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const getComputedStyle = window.getComputedStyle;\nwindow.getComputedStyle = (el, ...args) => {\n // 如果爲shadow dom則直接返回\n if (el instanceof DocumentFragment) {\n return {};\n }\n return Reflect.apply(getComputedStyle, window, [el, ...args]);\n};\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"elementFromPoint","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據座標(x, y)當獲取一個子應用的元素的時候,會返回shadow root,並不會返回真正的元素。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"const elementFromPoint = document.elementFromPoint;\ndocument.elementFromPoint = function (x, y) {\n const result = Reflect.apply(elementFromPoint, this, [x, y]);\n // 如果座標元素爲shadow則用該shadow再次獲取\n if (result && result.shadowRoot) {\n return result.shadowRoot.elementFromPoint(x, y);\n }\n return result;\n};\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"document 事件 target 爲 shadow","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當我們在document添加click、mousedown、mouseup等事件的時候,回調函數中的event.target不是真正的目標元素,而是shadow root元素。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// fix: 點擊事件target爲shadow元素的問題\nconst {addEventListener: oldAddEventListener, removeEventListener: oldRemoveEventListener} = document;\nconst fixEvents = ['click', 'mousedown', 'mouseup'];\nconst overrideEventFnMap = {};\nconst setOverrideEvent = (eventName, fn, overrideFn) => {\n if (fn === overrideFn) {\n return;\n }\n if (!overrideEventFnMap[eventName]) {\n overrideEventFnMap[eventName] = new Map();\n }\n overrideEventFnMap[eventName].set(fn, overrideFn);\n};\nconst resetOverrideEvent = (eventName, fn) => {\n const eventFn = overrideEventFnMap[eventName]?.get(fn);\n if (eventFn) {\n overrideEventFnMap[eventName].delete(fn);\n }\n return eventFn || fn;\n};\ndocument.addEventListener = (event, fn, options) => {\n const callback = (e) => {\n // 當前事件對象爲qiankun盒子,並且當前對象有shadowRoot元素,則fix事件對象爲真實元素\n if (e.target.id?.startsWith('__qiankun_microapp_wrapper') && e.target?.shadowRoot) {\n fn({...e, target: e.path[0]});\n return;\n }\n fn(e);\n };\n const eventFn = fixEvents.includes(event) ? callback : fn;\n setOverrideEvent(event, fn, eventFn);\n Reflect.apply(oldAddEventListener, document, [event, eventFn, options]);\n};\ndocument.removeEventListener = (event, fn, options) => {\n const eventFn = resetOverrideEvent(event, fn);\n Reflect.apply(oldRemoveEventListener, document, [event, eventFn, options]);\n};\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"js 沙箱","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主要是隔離掛載在window上的變量,而qiankun內部已經幫你處理好了。在子應用運行時訪問的window其實是一個Proxy代理對象。 所有子應用的全局變量變更都是在閉包中產生的,不會真正回寫到 window 上,這樣就能避免多實例之間的污染了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/91/91229b1e08d4cdc4b98a00e61814b6c4.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":9}}],"text":"圖源:前端優選","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"複用公共依賴","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如:企業中的util、core、request、ui等公共依賴,在微前端中,我們不需要每個子應用都加載一次,這樣既浪費資源並且還會導致本來單例的對象,變成了多例。 在webpack中配置externals。把需要複用的排除打包,然後在index.html中加載排除的lib外鏈(子應用需要在script或者style標籤加上ignore屬性,有了這個屬性,qiankun 便不會再去加載這個 js/css,而子項目獨立運行,這些 js/css 仍能被加載)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"externals: {\n 'element-ui': {\n commonjs: 'element-ui',\n commonjs2: 'element-ui',\n amd: 'element-ui',\n root: 'ElementUI' // 外鏈cdn加載掛載到window上的變量名\n }\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"父子共享(以國際化爲例)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"應用註冊時或加載時,將依賴傳遞給子項目","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// 註冊\nregisterMicroApps([\n {\n name: 'micro-1', \n entry: 'http://localhost:9001/micro-1', \n container: '#micro-1', \n activeRule: '/micro-1', \n props: { i18n: this.$i18n }\n },\n]);\n// 手動加載\nloadMicroApp({\n name,\n entry,\n container: `#${this.boxId}`,\n props: {\n i18n: this.$i18n\n }\n});\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"子應用啓動時獲取props參數初始化","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"let { i18n } = props;\nif (!i18n) {\n // 當獨立運行時或主應用未共享時,動態加載本地國際化\n const module = await import('@/common-module/lang');\n i18n = module.default;\n}\nnew Vue({\n i18n,\n router,\n render\n});\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主應用在註冊子應用或者手動加載子應用時把共享的變量通過props傳遞給子應用,子應用在bootstrap或者mount鉤子函數中獲取,如果沒有從props中獲取到該變量,子應用則動態加載本地變量。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"keep-alive(Vue)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"其實並不建議做keepAlive,但是我還是做了,我能說什麼…","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"網上有其他方案,我沒有采納,我在這裏說下我的方案吧(綜合了網上的方案),使用loadMicroApp手動加載和卸載子應用。這裏有幾個難點:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// microApp.js (可以走CI/CD運維配置,也可以通過接口從服務器獲取)\nconst apps = [{\n name: 'micro-1',\n activeRule: '/micro-1'\n}, {\n name: 'micro-2',\n activeRule: '/micro-2',\n prefetch: true\n}, {\n name: 'micro-3',\n activeRule: '/micro-3',\n prefetch: false, // 預加載資源\n preload: false, // 預渲染\n keepalive: true // 緩存子應用\n}];\n\nexport default apps.map(app => ({ ...app, entry: getEntryUrl(app.name) }));\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\n\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// App.vue (layout)\n\n\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"路由的響應,如果我們不卸載keepAlive的子應用,則子應用依然會響應路由的變化,從而導致子應用的當前路由已經不是離開時的路由了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"/**\n * 讓vue-router支持keepalive,當主路由變更時如果當前子應用沒有該路由則不做處理\n * 因爲通過瀏覽器前進後退會先觸發主路由的監聽,導致沒有及時通知到子應用deactivated,則子應用路由沒有及時停止監聽,則會處理本次主路由變更\n * @param router\n */\nconst supportKeepAlive = (router) => {\n const old = router.history.transitionTo;\n router.history.transitionTo = (location, cb) => {\n const matched = router.getMatchedComponents(location);\n if (!matched || !matched.length) {\n return;\n }\n Reflect.apply(old, router.history, [location, cb]);\n };\n};\n// 重寫監聽路由變更事件\nsupportKeepAlive(instance.$router);\n// 如果爲預掛載並且當前不爲激活狀態則停止監聽路由,並設置_startLocation爲空,爲了在激活的時候可以響應\nif (preload && !active) {\n // 如果當前子應用不是預加載(我這裏做了多個子應用並存且可以預加載),並且訪問的不是當前子應用則把路由停止\n instance.$router.history.teardown();\n instance.$router.history._startLocation = '';\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頁面的activated與deactivated觸發。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"// 在子應用創建的時候監聽激活與失效事件\nif (eventBus) {\n eventBus.$on(`activated:${appName}`, activated);\n eventBus.$on(`deactivated:${appName}`, deactivated);\n}\n/**\n * 獲取當前路由的組件\n * @returns {*}\n */\nconst getCurrentRouteInstance = () => {\n const {matched} = instance?.$route || {};\n if (matched?.length) {\n const { instances } = matched[matched.length - 1];\n if (instances) {\n return instances.default || instances;\n }\n }\n};\n\n/**\n * 觸發當前路由組件hook\n * @param hook\n */\nconst fireCurrentRouterInstanceHook = (hook) => {\n const com = getCurrentRouteInstance();\n const fns = com?.$options?.[hook];\n if (fns) {\n fns.forEach(fn => Reflect.apply(fn, com, [{ micro: true }]));\n }\n};\n\n/**\n * 激活當前子應用回調\n */\nconst activated = () => {\n instance?.$router.history.setupListeners();\n console.log('setupListeners');\n fireCurrentRouterInstanceHook('activated');\n};\n/**\n * 被 keep-alive 緩存的組件停用時調用。\n */\nconst deactivated = () => {\n instance?.$router.history.teardown();\n console.log('teardown');\n fireCurrentRouterInstanceHook('deactivated');\n};\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"vuex 全局狀態共享","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(慎用!破壞了vuex的理念, 不適用於大量的數據)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"子應用使用自己的vuex,並不是真正的使用主應用的vuex。需要共享的vuex模塊主應用與子應用理論來說是引用的相同的文件,我們在這個vuex模塊標記它是否需要共享,並watch主應用與子應用的該模塊。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當子應用中的state發生了改變則更新主應用的state,相反主應用的state變更後也同樣修改子應用的state。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"/**\n * 獲取命名空間狀態數據\n * @param state 狀態數據\n * @param namespace 命名空間\n * @returns {*}\n */\nconst getNamespaceState = (state, namespace) => namespace === 'root' ? state : get(state, namespace);\n\n/**\n * 更新狀態數據\n * @param store 狀態存儲\n * @param namespace 命名空間\n * @param value 新的值\n * @returns {*}\n */\nconst updateStoreState = (store, namespace, value) => store._withCommit(() => setVo(getNamespaceState(store.state, namespace), value));\n\n/**\n * 監聽狀態存儲\n * @param store 狀態存儲\n * @param fn 變更事件函數\n * @param namespace 命名空間\n * @returns {*}\n * @private\n */\nconst _watch = (store, fn, namespace) => store.watch(state => getNamespaceState(state, namespace), fn, { deep: true });\n\nconst updateSubStoreState = (stores, ns, value) => stores.filter(s => s.__shareNamespaces.has(ns)).forEach(s => updateStoreState(s, ns, value));\n\nexport default (store, mainStore) => {\n // 如果有主應用存儲則開啓共享\n if (mainStore) {\n // 多個子應用與主應用共享時判斷主應用存儲是否已經標記爲已共享\n if (mainStore.__isShare !== true) {\n // 所有子應用狀態\n mainStore.__subStores = new Set();\n // 已監聽的命名空間\n mainStore.__subWatchs = new Map();\n mainStore.__isShare = true;\n }\n // 把當前子應用存儲放入主應用裏面\n mainStore.__subStores.add(store);\n const shareNames = new Set();\n const { _modulesNamespaceMap: moduleMap } = store;\n // 監聽當前store,更新主應用store,並統計該子應用需要共享的所有命名空間\n Object.keys(moduleMap).forEach(key => {\n const names = key.split('/').filter(k => !!k);\n // 如果該命名空間的上級命名空間已經共享則下級不需要再共享\n const has = names.some(name => shareNames.has(name));\n if (has) {\n return;\n }\n const { _rawModule: { share } } = moduleMap[key];\n if (share === true) {\n const namespace = names.join('.');\n // 監聽當前子應用存儲的命名空間,發生變化後更新主應用與之同名的命名空間數據\n _watch(store, value => updateStoreState(mainStore, namespace, value), namespace);\n shareNames.add(namespace);\n }\n });\n\n // 存儲當前子應用需要共享的命名空間\n store.__shareNamespaces = shareNames;\n\n shareNames.forEach(ns => {\n // 從主應用同步數據\n updateStoreState(store, ns, getNamespaceState(mainStore.state, ns));\n if (mainStore.__subWatchs.has(ns)) {\n return;\n }\n // 監聽主應用的狀態,更新子應用存儲\n const w = mainStore.watch(state => getNamespaceState(state, ns), value => updateSubStoreState([...mainStore.__subStores], ns, value), { deep: true });\n console.log(`主應用store監聽模塊【${ns}】數據`);\n mainStore.__subWatchs.set(ns, w);\n });\n }\n return store;\n};\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看到這裏,你一定也驚歎於微前端的精妙吧!紙上得來終覺淺,期待各位的實踐行動,如果遇到任何問題,歡迎關注我們 ","attrs":{}},{"type":"link","attrs":{"href":"https://www.infoq.cn/u/ligaai/publish","title":"","type":null},"content":[{"type":"text","text":"LigaAI@infoQ","attrs":{}}]},{"type":"text","text":",一起交流,共同進步~更多詳情,請點擊我們的官方網站 ","attrs":{}},{"type":"link","attrs":{"href":"https://ligai.cn/","title":"","type":null},"content":[{"type":"text","text":"LigaAI-新一代智能研發管理系統","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":10}}],"text":"本文部分內容參考:Micro Frontends、Micro Frontends from martinfowler.com、微前端的核心價值、qiankun介紹","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"text":"本文作者","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":": Alone zhou","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"text":"本文鏈接","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":":","attrs":{}},{"type":"link","attrs":{"href":"https://blog.cn-face.com/2021/06/17/%E5%BE%AE%E5%89%8D%E7%AB%AF%EF%BC%88qiankun%EF%BC%89%E5%B0%9D%E9%B2%9C/","title":"","type":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"https://blog.cn-face.com/2021/06/17/微前端(qiankun)嚐鮮/","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}},{"type":"strong","attrs":{}}],"text":"版權聲明","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":": 本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章