本文首发于技术雷达之「微前端」- 将微服务理念扩展到前端开发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:微前端 - 将微服务理念扩展到前端开发 - 吕立青的博客