講真!開(kāi)發(fā)者要了解的微前端架構(gòu)
譯文【51CTO.com快譯】隨著前端技術(shù)的發(fā)展,針對(duì)此類(lèi)架構(gòu)的解決方案也不斷涌現(xiàn)。如下圖所示,前端架構(gòu)領(lǐng)域的主要思路是將前端分別獨(dú)立出來(lái),以便后期組合一個(gè)更大的整體,同時(shí)也方便獨(dú)立的團(tuán)隊(duì)對(duì)其進(jìn)行維護(hù)。
讓我們先來(lái)總結(jié)一下微服務(wù),給開(kāi)發(fā)應(yīng)用帶來(lái)的好處:
- 解耦的代碼庫(kù)。
- 自治的團(tuán)隊(duì)。
- 與技術(shù)和框架無(wú)關(guān)。
- 能夠獨(dú)立部署。
- 具有可擴(kuò)展性。
- 具有可重用性。
什么是微前端
如下圖所示,微前端(Micro-frontends)延續(xù)了上述僅在后端實(shí)現(xiàn)的微服務(wù)的各項(xiàng)優(yōu)勢(shì),能夠讓不同的團(tuán)隊(duì)實(shí)現(xiàn)端到端的代碼交付。
使用微前端的場(chǎng)景:
- 不同團(tuán)隊(duì)疲于應(yīng)付大量的代碼庫(kù)。
- 代碼的所有權(quán)頻繁發(fā)生變更。
- 產(chǎn)品的部署被應(yīng)用程序的其他部分拖延。
- 希望使用不同的前端(FE)框架。
編排(Orchestration)
借助業(yè)務(wù)流程,我們可以在服務(wù)器端和客戶(hù)端上,利用不同的方法將不同的微前端組合成為一個(gè)功能完備的應(yīng)用程序,以發(fā)揮最佳性能。
在此,我們介紹一種方案--構(gòu)建時(shí)間整合(Build time integration)。其特點(diǎn)是:
- 每個(gè)微前端應(yīng)用程序都代表一個(gè)npm軟件包。
- 主應(yīng)用程序(orchestrator/container)將自身構(gòu)建為具有所有依賴(lài)項(xiàng)(微前端)的最終捆綁包。
JavaScript
- {
- "name": "@super-app/container",
- "version": "1.0.0",
- "description": "My super app",
- "dependencies": {
- "@super-app/products-list": "2.0.0",
- "@super-app/header": "4.5.2",
- "@super-app/order": "1.0.0"
- }
- }
上述代碼似乎符合邏輯,但實(shí)際上卻暗藏著一個(gè)巨大的缺陷:每當(dāng)微前端應(yīng)用程序發(fā)生變更時(shí),整個(gè)orchestrator及其所有的依賴(lài)項(xiàng)都必須隨即重建,以創(chuàng)建出新的版本。那么,這樣會(huì)導(dǎo)致每個(gè)微前端的潛在延遲、回滾、甚至是錯(cuò)誤。與此同時(shí),由于每個(gè)團(tuán)隊(duì)都對(duì)相同的軟件包版本存在著依賴(lài)性,因此這實(shí)際上會(huì)導(dǎo)致新版本的創(chuàng)建更加困難。下面,我們來(lái)看看如何避免此類(lèi)情況的發(fā)生。
1.客戶(hù)端編排
- 客戶(hù)端路由。
- 狀態(tài)共享。
- 注冊(cè)所有的應(yīng)用程序。
- 如果有可能的話(huà),盡量解決共享依賴(lài)項(xiàng)。
- 初始化主應(yīng)用程序。
- 編寫(xiě)來(lái)自不同微前端應(yīng)用的程序片段。
為了達(dá)到上述功能,您可以使用如下程序庫(kù):
- single-spa:一種頂級(jí)的路由器。
- hinclude:包含了HTML程序片段。
- h-include:包括使用各種Web組件的HTML片段。
我們可以通過(guò)對(duì)不同API的簡(jiǎn)單Ajax調(diào)用,來(lái)完成程序片段的編寫(xiě)。為了返回預(yù)先渲染(pre-rendered)的HTML,API可以在前端被合并,或者僅返回所需的腳本標(biāo)簽,以及帶有ID的特定HTML標(biāo)簽,以方便加載那些程序片段可以呈現(xiàn)的ID。
此外,您也可以使用Vanilla JS、或其他類(lèi)型的框架,來(lái)自行實(shí)現(xiàn)編排的目的。
1.1路由
- 使用History API,來(lái)初始化應(yīng)用內(nèi)的路由器。
- 自定義瀏覽器事件、或PubSub庫(kù)。
- 將路由保留在Orchestrator應(yīng)用上。
1.2共享全局狀態(tài),并在應(yīng)用之間進(jìn)行通信
- 每個(gè)微前端都具有公開(kāi)狀態(tài)的Observable模式。RxJS非常適用于這種模式。
- 自定義瀏覽器事件。
- 實(shí)現(xiàn)Cookie、會(huì)話(huà)或本地存儲(chǔ)。
1.3共享主要UI庫(kù)的代碼
- 在選擇第三方庫(kù)時(shí),應(yīng)選擇一個(gè)在微前端中能夠支持所有可能用到的框架類(lèi)型的庫(kù)。
- 如果您想自行開(kāi)發(fā)庫(kù),則可以使用Web組件,以保持通用性。
- 團(tuán)隊(duì)成員各司其職對(duì)庫(kù)進(jìn)行開(kāi)發(fā)與維護(hù),而不必專(zhuān)門(mén)創(chuàng)建某個(gè)特定的團(tuán)隊(duì)。
1.4樣式?jīng)_突
- 為每個(gè)團(tuán)隊(duì)確定不同CSS類(lèi)的特定前綴。
- 使用BEM樣式。
- 使用JSS等樣式組件(styled-components),以避免在使用CSS和JS庫(kù)時(shí)發(fā)生沖突。
- 使用Web組件中的Shadow DOM。
1.5 SEO和UX
- Skeleton UI是一種在未加載內(nèi)容之前的預(yù)定義初始化屏幕。
- 在ESI或SSI的幫助下,進(jìn)行服務(wù)器端的渲染。
1.6 Web組件
它們包括4個(gè)規(guī)范定義:
1.6.1自定義元素(Custom Element)
- https://w3c.github.io/webcomponents/spec/custom/。
通過(guò)使用自定義元素的API,您可以使用各種生命周期、屬性變更處理程序、事件處理程序等方法,來(lái)創(chuàng)建功能齊全的自定義HTML元素。
如下是創(chuàng)建自定義元素的過(guò)程:
- 創(chuàng)建一個(gè)擴(kuò)展HTMLElement的類(lèi)。
- 自定義生命周期方法、及其屬性。
- 在connectedCallback()生命周期方法的內(nèi)部,將新元素與HTML模板相關(guān)聯(lián)。
- 使用自定義元素API注冊(cè)該元素。
- 在HTML中使用此元素。
JavaScript
- class MyIcon extends HTMLElement {
- constructor() {
- super();
- this._iconCode = null;
- }
- static get observedAttributes() {
- return ["code"];
- }
- attributeChangedCallback(name, oldValue, newValue) {
- // name will always be "code" due to observedAttributes
- this._iconCode = newValue;
- this._render();
- }
- connectedCallback() {
- this._render();
- }
- get code() {
- return this._iconCode;
- }
- set code(value) {
- this.setAttribute("code", value);
- }
- _render() {
- // append needed elements to the DOM or the shadow DOM
- }
- }
- customElements.define("my-icon", MyIcon);
- // Usage: <my-icon code="flower"></my-icon>
1.6.2 Shadow DOM
- https://w3c.github.io/webcomponents/spec/shadow/。
- https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM。
JavaScript
- Element.attachShadow();
上述的attachShadow方法僅采用了一個(gè)屬性為mode的對(duì)象作為參數(shù)。據(jù)此,您可以使用open和closed兩種模式,來(lái)創(chuàng)建在作用域樣式和獨(dú)立組件上隔離的DOM樹(shù)。其中,Open意味著您可以使用在主頁(yè)上下文中編寫(xiě)的JavaScript,去訪問(wèn)Shadow DOM。而closed則意味著僅使用自定義元素上下文中的Javascript,去訪問(wèn)Shadow DOM。可見(jiàn),當(dāng)您必須隔離CSS時(shí),此法非常實(shí)用。
JavaScript
- import React from 'react';
- import ReactDOM from 'react-dom';
- import App from './App';
- class MyCusotomElement extends HTMLElement {
- constructor() {
- super();
- this.shadow = this.attachShadow({ mode: 'closed' }); // Or open
- }
- connectedCallback() {
- ReactDOM.render(<App />, this.shadow);
- }
- }
- customElements.define("my-custom-element", MyCusotomElement);
1.6.3 ES模塊
- https://html.spec.whatwg.org/multipage/webappapis.html#integration-with-the-javascript-module-system。
在此,我們只需要導(dǎo)入、導(dǎo)出JS模塊即可。
1.6.4 HTML模板
- https://html.spec.whatwg.org/multipage/scripting.html#the-template-element/。
- https://html.spec.whatwg.org/multipage/scripting.html#the-slot-element。
使用HTML模板(如下面代碼中的),您可以創(chuàng)建在加載時(shí)未能呈現(xiàn)的HTML片段。當(dāng)然,您也可以在運(yùn)行時(shí),使用JavaScript對(duì)其進(jìn)行初始化。
HTML
- <div id="example"></div>
- <template id="example-template">
- <table>
- <tr>
- <td>What is this?</td>
- <td>My example template.</td>
- </tr>
- </table>
- </template>
JavaScript
- // Find our template
- var template = document.querySelector('#example-template');
- // Find our target element
- var target = document.querySelector('#example');
- // Clone the content of our template
- var content = document.importNode(template.content, true);
- // Append the template content to our target element
- target.appendChild(content);
如下面的代碼所示,另一個(gè)實(shí)用的元素是。它是Web組件技術(shù)的一部分,可用作Web組件內(nèi)的占位符,您可以使用自己的標(biāo)記來(lái)填充它。
HTML
- <template id="example-template">
- <div class="attributes">
- <h4><span>Attributes</span></h4>
- <slot name="attributes"><p>None</p></slot>
- </div>
- </template>
- <element-attributes>
- <span slot="attributes">Attributes from web component.</span>
- </element-attributes>
JavaScript
- customElements.define('element-attributes',
- class extends HTMLElement {
- constructor() {
- super();
- const template = document
- .getElementById('example-template')
- .content;
- this.attachShadow({ mode: 'open' })
- .appendChild(template.cloneNode(true));
- }
- }
其結(jié)果模板為:
具有slot屬性的已定義span元素,將在具有name屬性的slot元素內(nèi)被呈現(xiàn),其值與我們?cè)趕pan元素上的slot屬性值相匹配。
2.服務(wù)器端編排
- 具有代理請(qǐng)求的服務(wù)器路由。
- 注冊(cè)所有應(yīng)用程序。
- 如果有的話(huà),可解決共享依賴(lài)項(xiàng)。
- 服務(wù)與組合那些來(lái)自不同的微前端應(yīng)用的程序片段。
2.2 引導(dǎo)程序應(yīng)用
通常,我們可以將服務(wù)器端業(yè)務(wù)流程的方案,稱(chēng)為Bootstrap應(yīng)用。由于它們比較復(fù)雜,因此我們往往會(huì)用到如下兩個(gè)典型的方案。
2.2.1 Zalandos解決方案
Project Mosaic9(https://www.mosaic9.org/)。
用戶(hù)只需進(jìn)入其頁(yè)面,通過(guò)瀏覽器點(diǎn)擊其路由器,以決定是采用API調(diào)用、還是布局式調(diào)用。對(duì)于API調(diào)用而言,路由器會(huì)將請(qǐng)求代理到所需的API處。而在布局調(diào)用中,路由器會(huì)調(diào)用布局服務(wù),以了解所有可能的布局,進(jìn)而從不同的端點(diǎn)加載它們。
下圖展示了如何通過(guò)微前端工作流程,來(lái)創(chuàng)建一個(gè)開(kāi)源項(xiàng)目的完整步驟:
2.2.2 Facebook解決方案
上述Zalando中Tailor.js的靈感,實(shí)際上來(lái)源于Facebook的BigPipe。由于具有相似之處,我們不做過(guò)多的介紹,您可以參閱:https://www.facebook.com/notes/facebook-engineering/bigpipe-pipelining-web-pages-for-high-performance/389414033919/,以獲悉其工作原理。
2.3片段組成的可能性
針對(duì)服務(wù)器端的片段組成,我們可以使用Server side include(SSI)和Edge side include(ESI),這兩種傳統(tǒng)技術(shù),來(lái)輕松地將不同的HTML標(biāo)記組合為一個(gè)。當(dāng)然,這兩種技術(shù),都需要我們維護(hù)一個(gè)對(duì)應(yīng)著靜態(tài)HTML文件的URL映射。
2.3.1 Server side include(SSI)
- http://www.alticore.eu/wasd_root/doc/env/env_0400.html。
- https://www.owasp.org/index.php/Server-Side_Includes_(SSI)_Injection。
- https://www.w3.org/Jigsaw/Doc/User/SSI.html。
- http://httpd.apache.org/docs/current/howto/ssi.html#basic。
- 是一種簡(jiǎn)單的解釋型服務(wù)器端腳本語(yǔ)言。
- 得到了Apache和Nginx的支持。
下面是其主HTML文件的代碼:
HTML
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <title>SSI</title>
- </head>
- <body>
- <!--# include file="$PAGE.html" -->
- </body>
- </html>
在Nginx中的配置為:
Java
- server {
- listen 8080;
- server_name localhost;
- root /usr/share/nginx/html;
- index index.html;
- # Turn on the SSI ferature
- ssi on;
- # Set the $PAGE variable used inside the main HTML
- location /browse {
- set $PAGE 'browse';
- }
- location /profile {
- set $PAGE 'profile'
- }
- }
2.3.2 Edge side include(ESI)
- https://www.w3.org/TR/esi-lang。
- 使用到了小標(biāo)記(Small markup)語(yǔ)言。
- 目前只是建議,并非標(biāo)準(zhǔn)。
- 由包括Nginx和Varnish等不同的技術(shù)或庫(kù),來(lái)提供支持。
- 對(duì)于NodeJS而言,具有nodei的npm軟件包。
HTML
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <title>ESI</title>
- </head>
- <body>
- <esi:include src="http://example.com/1.html" alt="http://example.com/2.html" />
- </body>
- </html>
2.3.3自行實(shí)現(xiàn)
當(dāng)然,您也可以在服務(wù)器端實(shí)現(xiàn)自己的解析器、或某種標(biāo)記幫助器。幾乎每一個(gè)模板庫(kù)都可以實(shí)現(xiàn)自定義的標(biāo)簽解析。
結(jié)論
您可以根據(jù)自己所面對(duì)的問(wèn)題,通過(guò)分析,來(lái)選擇適合自己的微前端。常見(jiàn)的實(shí)現(xiàn)方式有如下兩種:
完全獨(dú)立
- 每個(gè)團(tuán)隊(duì)都選擇自己的技術(shù)棧,且不共享代碼。
- 每個(gè)片段都進(jìn)行自己的API調(diào)用。
- 每個(gè)視圖都由功能齊全的片段所組成。
- 每個(gè)微前端應(yīng)用都有自己的CI/CD。
戰(zhàn)略合作
- 大家同意技術(shù)棧,并共享公共庫(kù)。
- API調(diào)用流需經(jīng)過(guò)bootstrap應(yīng)用。
- 共享UI庫(kù)。
- 共享CI/CD。
原文標(biāo)題:Introduction to Micro-Frontend Architecture,作者:Mayur Ingle
【51CTO譯稿,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文譯者和出處為51CTO.com】