實施微前端的六種方式,值得你看!
微前端架構(gòu)是一種類似于微服務的架構(gòu),它將微服務的理念應用于瀏覽器端,即將 Web 應用由單一的單體應用轉(zhuǎn)變?yōu)槎鄠€小型前端應用聚合為一的應用。
由此帶來的變化是,這些前端應用可以獨立運行、獨立開發(fā)、獨立部署。以及,它們應該可以在共享組件的同時進行并行開發(fā)——這些組件可以通過 NPM 或者 Git Tag、Git Submodule 來管理。
注意:這里的前端應用指的是前后端分離的單應用頁面,在這基礎(chǔ)才談論微前端才有意義。
結(jié)合我最近半年在微前端方面的實踐和研究來看,微前端架構(gòu)一般可以由以下幾種方式進行:
- 使用 HTTP 服務器的路由來重定向多個應用
- 在不同的框架之上設(shè)計通訊、加載機制,諸如 Mooa 和 Single-SPA
- 通過組合多個獨立應用、組件來構(gòu)建一個單體應用
- iFrame。使用 iFrame 及自定義消息傳遞機制
- 使用純 Web Components 構(gòu)建應用
- 結(jié)合 Web Components 構(gòu)建
不同的方式適用于不同的使用場景,當然也可以組合一起使用。那么,就讓我們來一一了解一下,為以后的架構(gòu)演進做一些技術(shù)鋪墊。
基礎(chǔ)鋪墊:應用分發(fā)路由 -> 路由分發(fā)應用
在一個單體前端、單體后端應用中,有一個典型的特征,即路由是由框架來分發(fā)的,框架將路由指定到對應的組件或者內(nèi)部服務中。微服務在這個過程中做的事情是,將調(diào)用由函數(shù)調(diào)用變成了遠程調(diào)用,諸如遠程 HTTP 調(diào)用。而微前端呢,也是類似的,它是將應用內(nèi)的組件調(diào)用變成了更細粒度的應用間組件調(diào)用,即原先我們只是將路由分發(fā)到應用的組件執(zhí)行,現(xiàn)在則需要根據(jù)路由來找到對應的應用,再由應用分發(fā)到對應的組件上。
后端:函數(shù)調(diào)用 -> 遠程調(diào)用
在大多數(shù)的 CRUD 類型的 Web 應用中,也都存在一些極為相似的模式,即:首頁 -> 列表 -> 詳情:
- 首頁,用于面向用戶展示特定的數(shù)據(jù)或頁面。這些數(shù)據(jù)通常是有限個數(shù)的,并且是多種模型的。
- 列表,即數(shù)據(jù)模型的聚合,其典型特點是某一類數(shù)據(jù)的集合,可以看到盡可能多的數(shù)據(jù)概要(如 Google 只返回 100 頁),典型見 Google、淘寶、京東的搜索結(jié)果頁。
- 詳情,展示一個數(shù)據(jù)的盡可能多的內(nèi)容。
如下是一個 Spring 框架,用于返回首頁的示例:
- @RequestMapping(value="/")
- public ModelAndView homePage(){
- return new ModelAndView("/WEB-INF/jsp/index.jsp");
- }
對于某個詳情頁面來說,它可能是這樣的:
- @RequestMapping(value="/detail/{detailId}")
- public ModelAndView detail(HttpServletRequest request, ModelMap model){
- ....
- return new ModelAndView("/WEB-INF/jsp/detail.jsp", "detail", detail);
- }
那么,在微服務的情況下,它則會變成這樣子:
- @RequestMapping("/name")
- public String name(){
- String name = restTemplate.getForObject("http://account/name", String.class);
- return Name" + name;
- }
而后端在這個過程中,多了一個服務發(fā)現(xiàn)的服務,來管理不同微服務的關(guān)系。
前端:組件調(diào)用 -> 應用調(diào)用
在形式上來說,單體前端框架的路由和單體后端應用,并沒有太大的區(qū)別:依據(jù)不同的路由,來返回不同頁面的模板。
- const appRoutes: Routes = [
- { path: 'index', component: IndexComponent },
- { path: 'detail/:id', component: DetailComponent },
- ];
而當我們將之微服務化后,則可能變成應用 A 的路由:
- const appRoutes: Routes = [
- { path: 'index', component: IndexComponent },
- ];
外加之應用 B 的路由:
- const appRoutes: Routes = [
- { path: 'detail/:id', component: DetailComponent },
- ];
而問題的關(guān)鍵就在于:怎么將路由分發(fā)到這些不同的應用中去。與此同時,還要負責管理不同的前端應用。
路由分發(fā)式微前端
路由分發(fā)式微前端,即通過路由將不同的業(yè)務分發(fā)到不同的、獨立前端應用上。其通??梢酝ㄟ^ HTTP 服務器的反向代理來實現(xiàn),又或者是應用框架自帶的路由來解決。
就當前而言,通過路由分發(fā)式的微前端架構(gòu)應該是采用最多、最易采用的 “微前端” 方案。但是這種方式看上去更像是多個前端應用的聚合,即我們只是將這些不同的前端應用拼湊到一起,使他們看起來像是一個完整的整體。但是它們并不是,每次用戶從 A 應用到 B 應用的時候,往往需要刷新一下頁面。
在幾年前的一個項目里,我們當時正在進行遺留系統(tǒng)重寫。我們制定了一個遷移計劃:
- 首先,使用靜態(tài)網(wǎng)站生成動態(tài)生成首頁
- 其次,使用 React 計劃棧重構(gòu)詳情頁
- ***,替換搜索結(jié)果頁
整個系統(tǒng)并不是一次性遷移過去,而是一步步往下進行。因此在完成不同的步驟時,我們就需要上線這個功能,于是就需要使用 Nginx 來進行路由分發(fā)。
如下是一個基于路由分發(fā)的 Nginx 配置示例:
- http {
- server {
- listen 80;
- server_name www.phodal.com;
- location /api/ {
- proxy_pass http://http://172.31.25.15:8000/api;
- }
- location /web/admin {
- proxy_pass http://172.31.25.29/web/admin;
- }
- location /web/notifications {
- proxy_pass http://172.31.25.27/web/notifications;
- }
- location / {
- proxy_pass /;
- }
- }
- }
在這個示例里,不同的頁面的請求被分發(fā)到不同的服務器上。
隨后,我們在別的項目上也使用了類似的方式,其主要原因是:跨團隊的協(xié)作。當團隊達到一定規(guī)模的時候,我們不得不面對這個問題。除此,還有 Angluar 跳崖式升級的問題。于是,在這種情況下,用戶前臺使用 Angular 重寫,后臺繼續(xù)使用 Angular.js 等保持再有的技術(shù)棧。在不同的場景下,都有一些相似的技術(shù)決策。
因此在這種情況下,它適用于以下場景:
- 不同技術(shù)棧之間差異比較大,難以兼容、遷移、改造
- 項目不想花費大量的時間在這個系統(tǒng)的改造上
- 現(xiàn)有的系統(tǒng)在未來將會被取代
- 系統(tǒng)功能已經(jīng)很完善,基本不會有新需求
而在滿足上面場景的情況下,如果為了更好的用戶體驗,還可以采用 iframe 的方式來解決。
使用 iFrame 創(chuàng)建容器
iFrame 作為一個非常古老的,人人都覺得普通的技術(shù),卻一直很管用。
HTML 內(nèi)聯(lián)框架元素 <iframe> 表示嵌套的正在瀏覽的上下文,能有效地將另一個 HTML 頁面嵌入到當前頁面中。
iframe 可以創(chuàng)建一個全新的獨立的宿主環(huán)境,這意味著我們的前端應用之間可以相互獨立運行。采用 iframe 有幾個重要的前提:
- 網(wǎng)站不需要 SEO 支持
- 擁有相應的應用管理機制。
如果我們做的是一個應用平臺,會在我們的系統(tǒng)中集成第三方系統(tǒng),或者多個不同部門團隊下的系統(tǒng),顯然這是一個不錯的方案。一些典型的場景,如傳統(tǒng)的 Desktop 應用遷移到 Web 應用:
如果這一類應用過于復雜,那么它必然是要進行微服務化的拆分。因此,在采用 iframe 的時候,我們需要做這么兩件事:
- 設(shè)計管理應用機制
- 設(shè)計應用通訊機制
加載機制。在什么情況下,我們會去加載、卸載這些應用;在這個過程中,采用怎樣的動畫過渡,讓用戶看起來更加自然。
通訊機制。直接在每個應用中創(chuàng)建 postMessage 事件并監(jiān)聽,并不是一個友好的事情。其本身對于應用的侵入性太強,因此通過 iframeEl.contentWindow 去獲取 iFrame 元素的 Window 對象是一個更簡化的做法。隨后,就需要定義一套通訊規(guī)范:事件名采用什么格式、什么時候開始監(jiān)聽事件等等。
有興趣的讀者,可以看看筆者之前寫的微前端框架:Mooa。
不管怎樣,iframe 對于我們今年的 KPI 怕是帶不來一絲的好處,那么我們就去造個輪子吧。
自制框架兼容應用
不論是基于 Web Components 的 Angular,或者是 VirtualDOM 的 React 等,現(xiàn)有的前端框架都離不開基本的 HTML 元素 DOM。
那么,我們只需要:
- 在頁面合適的地方引入或者創(chuàng)建 DOM
- 用戶操作時,加載對應的應用(觸發(fā)應用的啟動),并能卸載應用。
***個問題,創(chuàng)建 DOM 是一個容易解決的問題。而第二個問題,則一點兒不容易,特別是移除 DOM 和相應應用的監(jiān)聽。當我們擁有一個不同的技術(shù)棧時,我們就需要有針對性設(shè)計出一套這樣的邏輯。
盡管 Single-SPA 已經(jīng)擁有了大部分框架(如 React、Angular、Vue 等框架)的啟動和卸載處理,但是它仍然不是適合于生產(chǎn)用途。當我基于 Single-SPA 為 Angular 框架設(shè)計一個微前端架構(gòu)的應用時,我***選擇重寫一個自己的框架,即 Mooa。
雖然,這種方式的上手難度相對比較高,但是后期訂制及可維護性比較方便。在不考慮每次加載應用帶來的用戶體驗問題,其唯一存在的風險可能是:第三方庫不兼容。
但是,不論怎樣,與 iFrame 相比,其在技術(shù)上更具有可吹牛逼性,更有看點。同樣的,與 iframe 類似,我們?nèi)匀幻鎸χ幌盗械牟淮蟛恍〉膯栴}:
- 需要設(shè)計一套管理應用的機制。
- 對于流量大的 toC 應用來說,會在***加載的時候,會多出大量的請求
而我們即又要拆分應用,又想 blabla……,我們還能怎么做?
組合式集成:將應用微件化
組合式集成,即通過軟件工程的方式在構(gòu)建前、構(gòu)建時、構(gòu)建后等步驟中,對應用進行一步的拆分,并重新組合。
從這種定義上來看,它可能算不上并不是一種微前端——它可以滿足了微前端的三個要素,即:獨立運行、獨立開發(fā)、獨立部署。但是,配合上前端框架的組件 Lazyload 功能——即在需要的時候,才加載對應的業(yè)務組件或應用,它看上去就是一個微前端應用。
與此同時,由于所有的依賴、Pollyfill 已經(jīng)盡可能地在***加載了,CSS 樣式也不需要重復加載。
常見的方式有:
- 獨立構(gòu)建組件和應用,生成 chunk 文件,構(gòu)建后再歸類生成的 chunk 文件。(這種方式更類似于微服務,但是成本更高)
- 開發(fā)時獨立開發(fā)組件或應用,集成時合并組件和應用,***生成單體的應用。
- 在運行時,加載應用的 Runtime,隨后加載對應的應用代碼和模板。
應用間的關(guān)系如下圖所示(其忽略圖中的 “前端微服務化”):
這種方式看上去相當?shù)睦硐?,即能滿足多個團隊并行開發(fā),又能構(gòu)建出適合的交付物。
但是,首先它有一個嚴重的限制:必須使用同一個框架。對于多數(shù)團隊來說,這并不是問題。采用微服務的團隊里,也不會因為微服務這一個前端,來使用不同的語言和技術(shù)來開發(fā)。當然了,如果要使用別的框架,也不是問題,我們只需要結(jié)合上一步中的自制框架兼容應用就可以滿足我們的需求。
其次,采用這種方式還有一個限制,那就是:規(guī)范!規(guī)范!規(guī)范!。在采用這種方案時,我們需要:
- 統(tǒng)一依賴。統(tǒng)一這些依賴的版本,引入新的依賴時都需要一一加入。
- 規(guī)范應用的組件及路由。避免不同的應用之間,因為這些組件名稱發(fā)生沖突。
- 構(gòu)建復雜。在有些方案里,我們需要修改構(gòu)建系統(tǒng),有些方案里則需要復雜的架構(gòu)腳本。
- 共享通用代碼。這顯然是一個要經(jīng)常面對的問題。
- 制定代碼規(guī)范。
因此,這種方式看起來更像是一個軟件工程問題。
現(xiàn)在,我們已經(jīng)有了四種方案,每個方案都有自己的利弊。顯然,結(jié)合起來會是一種更理想的做法。
考慮到現(xiàn)有及常用的技術(shù)的局限性問題,讓我們再次將目光放得長遠一些。
純 Web Components 技術(shù)構(gòu)建
在學習 Web Components 開發(fā)微前端架構(gòu)的過程中,我嘗試去寫了我自己的 Web Components 框架:oan。在添加了一些基本的 Web 前端框架的功能之后,我發(fā)現(xiàn)這項技術(shù)特別適合于作為微前端的基石。
Web Components 是一套不同的技術(shù),允許您創(chuàng)建可重用的定制元素(它們的功能封裝在您的代碼之外)并且在您的 Web 應用中使用它們。
它主要由四項技術(shù)組件:
- Custom elements,允許開發(fā)者創(chuàng)建自定義的元素,諸如 <today-news></today-news>。
- Shadow DOM,即影子 DOM,通常是將 Shadow DOM 附加到主文檔 DOM 中,并可以控制其關(guān)聯(lián)的功能。而這個 Shadow DOM 則是不能直接用其它主文檔 DOM 來控制的。
- HTML templates,即 <template> 和 <slot> 元素,用于編寫不在頁面中顯示的標記模板。
- HTML Imports,用于引入自定義組件。
每個組件由 link 標簽引入:
- <link rel="import" href="components/di-li.html">
- <link rel="import" href="components/d-header.html">
隨后,在各自的 HTML 文件里,創(chuàng)建相應的組件元素,編寫相應的組件邏輯。一個典型的 Web Components 應用架構(gòu)如下圖所示:
可以看到這邊方式與我們上面使用 iframe 的方式很相似,組件擁有自己獨立的 Scripts 和 Styles,以及對應的用于單獨部署組件的域名。然而它并沒有想象中的那么美好,要直接使用純 Web Components 來構(gòu)建前端應用的難度有:
- 重寫現(xiàn)有的前端應用。是的,現(xiàn)在我們需要完成使用 Web Components 來完成整個系統(tǒng)的功能。
- 上下游生態(tài)系統(tǒng)不完善。缺乏相應的一些第三方控件支持,這也是為什么 jQuery 相當流行的原因。
- 系統(tǒng)架構(gòu)復雜。當應用被拆分為一個又一個的組件時,組件間的通訊就成了一個特別大的麻煩。
Web Components 中的 ShadowDOM 更像是新一代的前端 DOM 容器。而遺憾的是并不是所有的瀏覽器,都可以完全支持 Web Components。
結(jié)合 Web Components 構(gòu)建
Web Components 離現(xiàn)在的我們太遠,可是結(jié)合 Web Components 來構(gòu)建前端應用,則更是一種面向未來演進的架構(gòu)。或者說在未來的時候,我們可以開始采用這種方式來構(gòu)建我們的應用。好在,已經(jīng)有框架在打造這種可能性。
就當前而言,有兩種方式可以結(jié)合 Web Components 來構(gòu)建微前端應用:
- 使用 Web Components 構(gòu)建獨立于框架的組件,隨后在對應的框架中引入這些組件
- 在 Web Components 中引入現(xiàn)有的框架,類似于 iframe 的形式
前者是一種組件式的方式,或者則像是在遷移未來的 “遺留系統(tǒng)” 到未來的架構(gòu)上。
在 Web Components 中集成現(xiàn)有框架
現(xiàn)有的 Web 框架已經(jīng)有一些可以支持 Web Components 的形式,諸如 Angular 支持的 createCustomElement,就可以實現(xiàn)一個 Web Components 形式的組件:
- platformBrowser()
- .bootstrapModuleFactory(MyPopupModuleNgFactory)
- .then(({injector}) => {
- const MyPopupElement = createCustomElement(MyPopup, {injector});
- customElements.define(‘my-popup’, MyPopupElement);
- });
在未來,將有更多的框架可以使用類似這樣的形式,集成到 Web Components 應用中。
集成在現(xiàn)有框架中的 Web Components
另外一種方式,則是類似于 Stencil 的形式,將組件直接構(gòu)建成 Web Components 形式的組件,隨后在對應的諸如,如 React 或者 Angular 中直接引用。
如下是一個在 React 中引用 Stencil 生成的 Web Components 的例子:
- import React from 'react';
- import ReactDOM from 'react-dom';
- import './index.css';
- import App from './App';
- import registerServiceWorker from './registerServiceWorker';
- import 'test-components/testcomponents';
- ReactDOM.render(<App />, document.getElementById('root'));
- registerServiceWorker();
在這種情況之下,我們就可以構(gòu)建出獨立于框架的組件。
同樣的 Stencil 仍然也只是支持最近的一些瀏覽器,比如:Chrome、Safari、Firefox、Edge 和 IE11
復合型
復合型,對就是上面的幾個類別中,隨便挑幾種組合到一起。
我就不廢話了~~。
結(jié)論
那么,我們應該用哪種微前端方案呢?答案見下一篇《微前端快速選型指南》
相關(guān)資料: