本文首發於技術雷達之「微前端」- 將微服務理念擴展到前端開發javascript
歡迎關注知乎專欄 —— 前端的逆襲css
本文共計約 7k 字,預計閱讀時間 15mins前端
在傳統的軟件開發當中,大多數軟件都是單體式應用架構的。在瞬息萬變的商業時代背景下,企業必須學會適應咱們這個時代的不肯定性。快速試驗,快速失敗。更快地推出新產品和有效地改進當前產品,從而爲客戶提供有意義的數字體驗。vue
而單體應用這種軟件架構對於企業來講的致命缺點就是,企業對於市場的響應速度變慢。企業決策者在一年內須要作的決策數量很是有限,因爲依賴關係,其響應週期每每會變得很是漫長。每當開發或升級產品,都須要在一系列體量龐大的相關服務中同時增長新功能,這就須要全部利益相關方共同努力,以同步方式進行變動。java
假設服務邊界已經被正確地定義爲可獨立運行的業務領域,並確保在微服務設計中遵循諸多最佳實踐。那麼至少會如下幾個方面得到顯而易見的好處:react
每一個微服務是孤立的,獨立的「模塊」,它們共同爲更高的邏輯目的服務。微服務之間經過 Contract 彼此溝通,每一個服務都負責特定的功能。這使得每一個服務都可以保持簡單,簡潔和可測試性。git
從而微服務架構容許企業更自發地採起更深遠的業務決策,由於每一個微服務都是獨立運做的,並且每個管理團隊能夠很好地控制該服務的變動。github
在前端,每每由一個前端團隊建立並維護一個 Web 應用程序,使用 REST API 從後端服務獲取數據。這種方式若是作得好的話,它可以提供優秀的用戶體驗。但主要的缺點是單頁面應用(SPA)不能很好地擴展和部署。在一個大公司裏,單前端團隊可能成爲一個發展瓶頸。隨着時間的推移,每每由一個獨立團隊所開發的前端層愈來愈難以維護。web
特別是一個特性豐富、功能強大的前端 Web 應用程序,卻位於後端微服務架構之上。而且隨着業務的發展,前端變得愈來愈臃腫,一個項目可能會有 90% 的前端代碼,卻只有很是薄的後端,甚至這種狀況在 Serverless 架構的背景下還會愈演愈烈。
微前端(Micro Frontends)這個術語其實就是微服務的衍生物。將微服務理念擴展到前端開發,同時構建多個徹底自治的和鬆耦合的 App 模塊(服務),其中每一個 App 模塊只負責特定的 UI 元素和功能。
若是咱們看到微服務提供給後端的好處,那麼就能夠更進一步將這些好處應用到前端。與此同時,在設計微服務的時候,就能夠考慮不只要完成後端邏輯,並且還要完成前端的視覺部分。而對於微前端來講,與微服務的許多要求也是一致的:監控、日誌、HealthCheck、Analytics 等等。
這樣就能使各個前端團隊按照本身的步調迭代,並隨時準備就緒處於可發佈狀態,並隔離相互依賴所產生的風險,與此同時也更容易嘗試新技術。
首先讓咱們來建立一個典型 Web 應用程序的基本組件(Header、ProductList、ShoppingCart),以 Header 組件爲例:
# src/App.js
export default () =>
<header>
<h1>Logo</h1>
<nav>
<ul>
<li>About</li>
<li>Contact</li>
</ul>
</nav>
</header>;
複製代碼
而後須要注意的是咱們會用到 Express 對剛剛建立的 React 組件進行服務器端渲染,使之成爲一個 App 模塊:
# server.js
fs.readFile(htmlPath, 'utf8', (err, html) => {
const rootElem = '<div id="root">';
const renderedApp = renderToString(React.createElement(App, null));
res.send(html.replace(rootElem, rootElem + renderedApp));
});
複製代碼
再依次建立其餘 Apps 並獨立部署:
在每一個獨立團隊建立好各自的 App 模塊後,咱們就能夠將網站或 Web 應用程序視爲由各類模塊的功能組合。下文將介紹多種技術實踐方案來從新組合這些模塊(有時做爲頁面,有時做爲組件),而前端(無論是否是 SPA)將只須要負責路由器(Router)如何選擇和決定要導入哪些模塊,從而爲最終用戶提供一致性的用戶體驗。
# server.js
Promise.all([
getContents('https://microfrontends-header.herokuapp.com/'),
getContents('https://microfrontends-products-list.herokuapp.com/'),
getContents('https://microfrontends-cart.herokuapp.com/')
]).then(responses =>
res.render('index', { header: responses[0], productsList: responses[1], cart: responses[2] })
).catch(error =>
res.send(error.message)
)
);
複製代碼
# views/index.ejs
<head>
<meta charset="utf-8">
<title>Microfrontends Homepage</title>
</head>
<body>
<%- header %>
<%- productsList %>
<%- cart %>
</body>
複製代碼
可是,這種方案也存在弊端,即某些 App 模塊可能會須要相對較長的加載時間,而在前端整個頁面的渲染卻要取決於最慢的那個模塊。
好比說,可能 Header 模塊的加載速度要比其餘部分快得多,而 ProductList 則由於須要獲取更多 API 數據而須要更多時間。一般狀況下咱們但願儘快將網頁顯示給用戶,而在這種狀況下後臺加載時間就會變得更長。
固然,咱們也能夠經過修改一些後端代碼來漸進式地(Progressive)往前端發送 HTML,但與此同時卻徒增了後端複雜度,而且又將前端的渲染控制權交回了後端服務器。並且咱們的優化也取決於每一個模塊加載的速度,如果進行優化就必須按必定順序進行加載。
<body>
<iframe width="100%" height="200" src="https://microfrontends-header.herokuapp.com/"></iframe>
<iframe width="100%" height="200" src="https://microfrontends-products-list.herokuapp.com/"></iframe>
<iframe width="100%" height="200" src="https://microfrontends-cart.herokuapp.com/"></iframe>
</body>
複製代碼
咱們也能夠將每一個子應用程序嵌入到各自的 <iframe>
中,這使得每一個模塊可以使用任何他們須要的框架,而無需與其餘團隊協調工具和依賴關係,依然能夠藉助於一些庫或者 Window.postMessageAPI
來進行交互。
Window.postMessageAPI
parent - > iframe - > iframe
)。function loadPage (element) {
[].forEach.call(element.querySelectorAll('script'), function (nonExecutableScript) {
var script = document.createElement("script");
script.setAttribute("src", nonExecutableScript.src);
script.setAttribute("type", "text/javascript");
element.appendChild(script);
});
}
document.querySelectorAll('.load-app').forEach(loadPage);
複製代碼
<div class="load-app" data-url="header"></div>
<div class="load-app" data-url="products-list"></div>
<div class="load-app" data-url="cart"></div>
複製代碼
簡單來講,這種方式就是在客戶端瀏覽器經過 Ajax 加載應用程序,而後將不一樣模塊的內容插入到對應的 div
中,並且還必須手動克隆每一個 script 的標記才能使其工做。
須要注意的是,爲了不 Javascript 和 CSS 加載順序的問題,建議將其修改爲相似於 Facebook bigpipe
的解決方案,返回一個 JSON 對象 { html: ..., css: [...], js: [...] }
再進行加載順序的控制。
Web Components 是一個 Web 標準,因此像 Angular、React/Preact、Vue 或 Hyperapp 這樣的主流 JavaScript 框架都支持它們。你能夠將 Web Components 視爲使用開放 Web 技術建立的可重用的用戶界面小部件,也許會是 Web 組件化的將來。
Web Components 由如下四種技術組成(儘管每種技術均可以獨立使用):
<template>
)定義組件的 HTML 模板能力:一種用於保存客戶端內容的機制,該內容在頁面加載時不被渲染,但能夠在運行時使用 JavaScript 進行實例化。能夠將一個模板視爲正在被存儲以供隨後在文檔中使用的一個內容片斷。# src/index.js
class Header extends HTMLElement {
attachedCallback() {
ReactDOM.render(<App />, this.createShadowRoot());
}
}
document.registerElement('microfrontends-header', Header);
複製代碼
<body>
<microfrontends-header></microfrontends-header>
<microfrontends-products-list></microfrontends-products-list>
<microfrontends-cart></microfrontends-cart>
</body>
複製代碼
在微前端的實踐當中:
<microfrontends-header></microfrontends-header>
)。<link rel="import" href="/components/microfrontends/header.html">
<link rel="import" href="/components/microfrontends/products-list.html">
<link rel="import" href="/components/microfrontends/cart.html">
複製代碼
window
訂閱此事件並在應該刷新其數據時獲得通知。# angularComponent.ts
const event = new CustomEvent('addToCart', { detail: item });
window.dispatchEvent(event);
複製代碼
# reactComponent.js
componentDidMount() {
window.addEventListener('addToCart', (event) => {
this.setState({ products: [...this.state.products, event.detail] });
}, false);
}
複製代碼
lodash
、moment.js
等公共庫,或者跨多個團隊共同使用的 react
和 react-dom
。經過 Webpack 等構建工具就能夠把打包的時候將這些共同模塊排除掉,而只須要在 HTML <header>
中的 <script>
中直接經過 CDN 加載 externals 依賴。<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/react.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/react-dom.min.js" crossorigin="anonymous"></script>
複製代碼
咱們在「三靠譜」(已和諧客戶名稱)的 Marketplace 項目當中也曾經探索過 AEM + React 混合開發的解決方案,其中就涉及到如何在 AEM 當中嵌入 React 組件,甚至將 AEM 組件又強行轉化爲 React 組件進行嵌套。如今回過頭來其實也算是微前端的一種實踐:
<div id="cms-container-1">
<div id="react-input-container"></div>
<script> ReactDOM.render(React.createElement(Input, { ...injectProps }), document.getElementById('react-input-container')); </script>
</div>
<div id="cms-container-2">
<div id="react-button-container"></div>
<script> ReactDOM.render(React.createElement(Button, {}), document.getElementById('react-button-container')); </script>
</div>
複製代碼
開源的 single-spa
自稱爲「元框架」,能夠實如今一個頁面將多個不一樣的框架整合,甚至在切換的時候都不須要刷新頁面(支持 React、Vue、Angular 一、Angular 二、Ember 等等):
請看示例代碼,所提供的 API 很是簡單:
import * as singleSpa from 'single-spa';
const appName = 'app1';
const loadingFunction = () => import('./app1/app1.js');
const activityFunction = location => location.hash.startsWith('#/app1');
singleSpa.declareChildApplication(appName, loadingFunction, activityFunction);
singleSpa.start();
複製代碼
# single-spa-examples.js
declareChildApplication('navbar', () => import('./navbar/navbar.app.js'), () => true);
declareChildApplication('home', () => import('./home/home.app.js'), () => location.hash === "" || location.hash === "#");
declareChildApplication('angular1', () => import('./angular1/angular1.app.js'), hashPrefix('/angular1'));
declareChildApplication('react', () => import('./react/react.app.js'), hashPrefix('/react'));
declareChildApplication('angular2', () => import('./angular2/angular2.app.js'), hashPrefix('/angular2'));
declareChildApplication('vue', () => import('src/vue/vue.app.js'), hashPrefix('/vue'));
declareChildApplication('svelte', () => import('src/svelte/svelte.app.js'), hashPrefix('/svelte'));
declareChildApplication('preact', () => import('src/preact/preact.app.js'), hashPrefix('/preact'));
declareChildApplication('iframe-vanilla-js', () => import('src/vanillajs/vanilla.app.js'), hashPrefix('/vanilla'));
declareChildApplication('inferno', () => import('src/inferno/inferno.app.js'), hashPrefix('/inferno'));
declareChildApplication('ember', () => loadEmberApp("ember-app", '/build/ember-app/assets/ember-app.js', '/build/ember-app/assets/vendor.js'), hashPrefix('/ember'));
start();
複製代碼
(變幻莫測)前端的技術選型?
在 Mobile/Mobile Web 上的悖論
合理劃分的邊界:DDD(領域驅動開發)
Don't use any of this if you don't need it
軟件架構到底在解決什麼問題?—— 跨團隊溝通的問題
所謂架構,實際上是解決人的問題;所謂敏捷,實際上是解決溝通的問題;
本次技術雷達「微前端」主題的宣講 Slides 能夠在個人博客找到:「技術雷達」之 Micro Frontends:微前端 - 將微服務理念擴展到前端開發 - 呂立青的博客