微前端架構是一種相似於微服務的架構,它將微服務的理念應用於瀏覽器端,即將 Web 應用由單一的單體應用轉變爲多個小型前端應用聚合爲一的應用。javascript
由此帶來的變化是,這些前端應用能夠獨立運行、獨立開發、獨立部署。以及,它們應該能夠在共享組件的同時進行並行開發——這些組件能夠經過 NPM 或者 Git Tag、Git Submodule 來管理。css
注意:這裏的前端應用指的是先後端分離的單應用頁面,在這基礎才談論微前端纔有意義。html
結合我最近半年在微前端方面的實踐和研究來看,微前端架構通常能夠由如下幾種方式進行:前端
不一樣的方式適用於不一樣的使用場景,固然也能夠組合一塊兒使用。那麼,就讓咱們來一一瞭解一下,爲之後的架構演進作一些技術鋪墊。java
在一個單體前端、單體後端應用中,有一個典型的特徵,即路由是由框架來分發的,框架將路由指定到對應的組件或者內部服務中。微服務在這個過程當中作的事情是,將調用由函數調用變成了遠程調用,諸如遠程 HTTP 調用。而微前端呢,也是相似的,它是將應用內的組件調用變成了更細粒度的應用間組件調用,即原先咱們只是將路由分發到應用的組件執行,如今則須要根據路由來找到對應的應用,再由應用分發到對應的組件上。react
在大多數的 CRUD 類型的 Web 應用中,也都存在一些極爲類似的模式,即:首頁 -> 列表 -> 詳情:git
以下是一個 Spring 框架,用於返回首頁的示例:github
@RequestMapping(value="/") public ModelAndView homePage(){ return new ModelAndView("/WEB-INF/jsp/index.jsp"); }
對於某個詳情頁面來講,它多是這樣的:web
@RequestMapping(value="/detail/{detailId}") public ModelAndView detail(HttpServletRequest request, ModelMap model){ .... return new ModelAndView("/WEB-INF/jsp/detail.jsp", "detail", detail); }
那麼,在微服務的狀況下,它則會變成這樣子:bootstrap
@RequestMapping("/name") public String name(){ String name = restTemplate.getForObject("http://account/name", String.class); return Name" + name; }
然後端在這個過程當中,多了一個服務發現的服務,來管理不一樣微服務的關係。
在形式上來講,單體前端框架的路由和單體後端應用,並無太大的區別:依據不一樣的路由,來返回不一樣頁面的模板。
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 }, ];
而問題的關鍵就在於:怎麼將路由分發到這些不一樣的應用中去。與此同時,還要負責管理不一樣的前端應用。
路由分發式微前端,即經過路由將不一樣的業務分發到不一樣的、獨立前端應用上。其一般能夠經過 HTTP 服務器的反向代理來實現,又或者是應用框架自帶的路由來解決。
就當前而言,經過路由分發式的微前端架構應該是採用最多、最易採用的 「微前端」 方案。可是這種方式看上去更像是多個前端應用的聚合,即咱們只是將這些不一樣的前端應用拼湊到一塊兒,使他們看起來像是一個完整的總體。可是它們並非,每次用戶從 A 應用到 B 應用的時候,每每須要刷新一下頁面。
在幾年前的一個項目裏,咱們當時正在進行遺留系統重寫。咱們制定了一個遷移計劃:
整個系統並非一次性遷移過去,而是一步步往下進行。所以在完成不一樣的步驟時,咱們就須要上線這個功能,因而就須要使用 Nginx 來進行路由分發。
以下是一個基於路由分發的 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 /; } } }
在這個示例裏,不一樣的頁面的請求被分發到不一樣的服務器上。
隨後,咱們在別的項目上也使用了相似的方式,其主要緣由是:跨團隊的協做。當團隊達到必定規模的時候,咱們不得不面對這個問題。除此,還有 Angluar 跳崖式升級的問題。因而,在這種狀況下,用戶前臺使用 Angular 重寫,後臺繼續使用 Angular.js 等保持再有的技術棧。在不一樣的場景下,都有一些類似的技術決策。
所以在這種狀況下,它適用於如下場景:
而在知足上面場景的狀況下,若是爲了更好的用戶體驗,還能夠採用 iframe 的方式來解決。
iFrame 做爲一個很是古老的,人人都以爲普通的技術,卻一直很管用。
HTML 內聯框架元素
<iframe>
表示嵌套的正在瀏覽的上下文,能有效地將另外一個 HTML 頁面嵌入到當前頁面中。
iframe 能夠建立一個全新的獨立的宿主環境,這意味着咱們的前端應用之間能夠相互獨立運行。採用 iframe 有幾個重要的前提:
若是咱們作的是一個應用平臺,會在咱們的系統中集成第三方系統,或者多個不一樣部門團隊下的系統,顯然這是一個不錯的方案。一些典型的場景,如傳統的 Desktop 應用遷移到 Web 應用:
若是這一類應用過於複雜,那麼它必然是要進行微服務化的拆分。所以,在採用 iframe 的時候,咱們須要作這麼兩件事:
加載機制。在什麼狀況下,咱們會去加載、卸載這些應用;在這個過程當中,採用怎樣的動畫過渡,讓用戶看起來更加天然。
通信機制。直接在每一個應用中建立 postMessage
事件並監聽,並非一個友好的事情。其自己對於應用的侵入性太強,所以經過 iframeEl.contentWindow
去獲取 iFrame 元素的 Window 對象是一個更簡化的作法。隨後,就須要定義一套通信規範:事件名採用什麼格式、何時開始監聽事件等等。
有興趣的讀者,能夠看看筆者以前寫的微前端框架:Mooa。
無論怎樣,iframe 對於咱們今年的 KPI 怕是帶不來一絲的好處,那麼咱們就去造個輪子吧。
不管是基於 Web Components 的 Angular,或者是 VirtualDOM 的 React 等,現有的前端框架都離不開基本的 HTML 元素 DOM。
那麼,咱們只須要:
第一個問題,建立 DOM 是一個容易解決的問題。而第二個問題,則一點兒不容易,特別是移除 DOM 和相應應用的監聽。當咱們擁有一個不一樣的技術棧時,咱們就須要有針對性設計出一套這樣的邏輯。
儘管 Single-SPA 已經擁有了大部分框架(如 React、Angular、Vue 等框架)的啓動和卸載處理,可是它仍然不是適合於生產用途。當我基於 Single-SPA 爲 Angular 框架設計一個微前端架構的應用時,我最後選擇重寫一個本身的框架,即 Mooa。
雖然,這種方式的上手難度相對比較高,可是後期訂製及可維護性比較方便。在不考慮每次加載應用帶來的用戶體驗問題,其惟一存在的風險多是:第三方庫不兼容。
可是,不論怎樣,與 iFrame 相比,其在技術上更具備可吹牛逼性,更有看點。一樣的,與 iframe 相似,咱們仍然面對着一系列的不大不小的問題:
而咱們即又要拆分應用,又想 blabla……,咱們還能怎麼作?
組合式集成,即經過軟件工程的方式在構建前、構建時、構建後等步驟中,對應用進行一步的拆分,並從新組合。
從這種定義上來看,它可能算不上並非一種微前端——它能夠知足了微前端的三個要素,即:獨立運行、獨立開發、獨立部署。可是,配合上前端框架的組件 Lazyload 功能——即在須要的時候,才加載對應的業務組件或應用,它看上去就是一個微前端應用。
與此同時,因爲全部的依賴、Pollyfill 已經儘量地在首次加載了,CSS 樣式也不須要重複加載。
常見的方式有:
應用間的關係以下圖所示(其忽略圖中的 「前端微服務化」):
這種方式看上去至關的理想,即能知足多個團隊並行開發,又能構建出適合的交付物。
可是,首先它有一個嚴重的限制:必須使用同一個框架。對於多數團隊來講,這並非問題。採用微服務的團隊裏,也不會由於微服務這一個前端,來使用不一樣的語言和技術來開發。固然了,若是要使用別的框架,也不是問題,咱們只須要結合上一步中的自制框架兼容應用就能夠知足咱們的需求。
其次,採用這種方式還有一個限制,那就是:規範!規範!規範!。在採用這種方案時,咱們須要:
所以,這種方式看起來更像是一個軟件工程問題。
如今,咱們已經有了四種方案,每一個方案都有本身的利弊。顯然,結合起來會是一種更理想的作法。
考慮到現有及經常使用的技術的侷限性問題,讓咱們再次將目光放得長遠一些。
在學習 Web Components 開發微前端架構的過程當中,我嘗試去寫了我本身的 Web Components 框架:oan。在添加了一些基本的 Web 前端框架的功能以後,我發現這項技術特別適合於做爲微前端的基石。
Web Components 是一套不一樣的技術,容許您建立可重用的定製元素(它們的功能封裝在您的代碼以外)而且在您的 Web 應用中使用它們。
它主要由四項技術組件:
<template>
和 <slot>
元素,用於編寫不在頁面中顯示的標記模板。每一個組件由 link
標籤引入:
<link rel="import" href="components/di-li.html"> <link rel="import" href="components/d-header.html">
隨後,在各自的 HTML 文件裏,建立相應的組件元素,編寫相應的組件邏輯。一個典型的 Web Components 應用架構以下圖所示:
能夠看到這邊方式與咱們上面使用 iframe 的方式很類似,組件擁有本身獨立的 Scripts
和 Styles
,以及對應的用於單獨部署組件的域名。然而它並無想象中的那麼美好,要直接使用純 Web Components 來構建前端應用的難度有:
Web Components 中的 ShadowDOM 更像是新一代的前端 DOM 容器。而遺憾的是並非全部的瀏覽器,均可以徹底支持 Web Components。
Web Components 離如今的咱們太遠,但是結合 Web Components 來構建前端應用,則更是一種面向將來演進的架構。或者說在將來的時候,咱們能夠開始採用這種方式來構建咱們的應用。好在,已經有框架在打造這種可能性。
就當前而言,有兩種方式能夠結合 Web Components 來構建微前端應用:
前者是一種組件式的方式,或者則像是在遷移將來的 「遺留系統」 到將來的架構上。
現有的 Web 框架已經有一些能夠支持 Web Components 的形式,諸如 Angular 支持的 createCustomElement,就能夠實現一個 Web Components 形式的組件:
platformBrowser() .bootstrapModuleFactory(MyPopupModuleNgFactory) .then(({injector}) => { const MyPopupElement = createCustomElement(MyPopup, {injector}); customElements.define(‘my-popup’, MyPopupElement); });
在將來,將有更多的框架可使用相似這樣的形式,集成到 Web Components 應用中。
另一種方式,則是相似於 Stencil 的形式,將組件直接構建成 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();
在這種狀況之下,咱們就能夠構建出獨立於框架的組件。
一樣的 Stencil 仍然也只是支持最近的一些瀏覽器,好比:Chrome、Safari、Firefox、Edge 和 IE11
複合型,對就是上面的幾個類別中,隨便挑幾種組合到一塊兒。
我就不廢話了~~。
那麼,咱們應該用哪一種微前端方案呢?答案見下一篇《微前端快速選型指南》
相關資料: