✨ Micro Frontends ✨

微前端 - 将微服务理念扩展到前端开发

(via Jimmy Lv 🐵)

⏲️ | Agenda

  1. 微前端的概念缘由与定义
  2. 微前端的实践方案(4+)
  3. Demo
  4. 优缺点总结与思考

The Majestic Monolith

💡 | 微服务架构带来了哪些好处?

假设服务边界已经被正确地定义为可独立运行的业务领域,并确保在微服务设计中遵循诸多最佳实践。

  • 复杂性:服务可以更好地分离。
  • 可扩展性:服务可以独立伸缩。
  • 灵活性:服务可以独立失败。
  • 敏捷性:服务可以独立开发,测试和部署。

💡 | 那么前端的现状呢? —— 臃肿的前端

  • ✨ SPA:一个功能丰富,功能强大的浏览器应用程序。
  • 💔 随着时间的推移,往往由一个独立团队所开发的前端层越来越难以维护。

微前端的定义 - 将微服务理念扩展到前端开发

微前端(Micro Frontends)这个术语是微服务的衍生物。同时构建多个自包含的和松耦合的 UI 组件(服务),其中每个组件负责特定的 UI 元素和功能。

更详细的「微前端 Micro Frontends」

  • 根据领域特定功能将应用程序分解为更小的单元。每个单元是独立的,可以独立开发部署。
  • 将网站或 Web 应用程序视为由独立团队拥有的功能组合。每个团队都有一个独特的业务或任务关注和专业的任务。
  • 每一个团队是跨职能的,从数据库到用户界面端到端地开发其功能/特性。
  • 所有前端功能(身份验证,库存,购物车等)都是 Web 应用程序的一部分,并与后端(大部分时间通过 HTTP)进行通信,并将其分解为微服务。
  • 可以同时拥有后端,前端,数据访问层和数据库,即一个服务中的子域所需的所有内容。每一项服务都应该由一个独立的团队来完成。

微前端的核心思想

  • ✨ Be Technology Agnostic
  • ✨ Isolate Team Code
  • ✨ Establish Team Prefixes
  • ✨ Favor Native Browser Features over Custom APIs
  • ✨ Build a Resilient Site

微前端的实践方案

🔨 Creating small apps (rather than components) 🔨

# src/App.js
export default () =>
  <header>
    <h1>Logo</h1>
    ......
  </header>;
# server.js
const renderedApp = renderToString(React.createElement(App, null));

💞 Joining apps together

Option 1: SSR using template

# 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>

Problem: Some apps may take longer to load

💞 Joining apps together

Option 1.1: Progressive loading from the back-end

Option 2: iframe

<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>
  • 优点
    • 最强大的是隔离了组件和应用程序部分的运行时环境,因此每个部分都可以独立开发,并且可以与其他部分的技术无关
    • 可以在 React 中开发一些部分,在 Angular 中开发一些部分,在 vanilla Js 中开发更多或任何其他技术。
    • 只要 iframes 来自同一个来源,消息传递也就相当直接和强大。Window.postMessageAPI
  • 缺点
    • Bundle 的大小非常明显,因为你最终会多次发送相同的库,并且由于应用程序是分开的,所以在构建时不能提取公共依赖关系。
    • 至于浏览器的支持,你基本上不能嵌套两层以上的 iframes(parent - > iframe - > iframe),或者所有的地狱崩溃。
    • 如果任何嵌套的框架需要能够滚动或具有 Form 表单域,就会很痛苦。

Option 3: Client-Side JavaScript

var script = document.createElement("script");
script.setAttribute("src", nonExecutableScript.src);
script.setAttribute("type", "text/javascript");
element.appendChild(script);
  • This basically loads the apps through ajax and insert their content inside those divs. It also has to clone each script tag manually for them to work.

  • Note: to avoid problems with Javascript and CSS loading order, I suggest you to evolve this to a solution similar to facebook's bigpipe, returning a JSON like { html: ..., css: [...], js: [...] } so you can have full control of it.

Option 4: WebComponents

# 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>

Web Components 用于整合所有功能模块

Web Components 是一个 Web 标准,所以像 Angular,React,Preact,Vue 或 Hyperapp 这样的主流 JavaScript 框架都支持它们。

- 自定义元素
- Shadow DOM
- HTML 导入
- HTML 模板元素
<link rel="import" href="/components/tc-books/tc-books.html">
<link rel="import" href="/components/tc-books/tc-book-form.html">

Communication Between Apps

# 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);
}

More Options...

  • 组件库 - 根据主应用程序的技术栈,不同的组件和应用程序部分可以作为库和主应用程序,所以主应用程序是由不同组件组成的。
  • 将 “应用程序” 作为黑盒 React 组件分发给消费应用程序 - 应用程序的状态完全包含在组件中,API 只是通过 props 暴露出来。
  • Edge Side Includes(ESI)/Server Side Includes(SSI) - 通过特殊的文件后缀 (shtml,inc) 或简单的标记语言来对那些可以加速和不能加速的网页中的内容片断进行描述,将每个网页划分成不同的小部分分别赋予不同的缓存控制策略。

页面加载的问题与优化建议

  • 响应式布局:使用 skeleton screen
  • 加快初始渲染:浏览器异步加载
  • 视觉体验一致:共享 UI 组件库
  • Router:依赖反转(前端 BFF/Gateway)
  • 提取共同依赖作为 externals
  • ……

Micro Frontends + AEM/jQuery

  • AEM (of course), which contains web content only (no structured domain data).
  • React.js components that are hosted in AEM. AEM passes through different properties the components need e.g. ids, URLs of services.
  • Microservices that contain the structured domain data, and that are queried by the React.js components via Ajax.

Demo: Single-SPA “meta framework”

可以在一个页面将多个不同的框架整合,甚至在切换的时候都不需要刷新页面 (支持 React, Vue, Angular 1, Angular 2, etc)

  • Build micro frontends that coexist and can each be written with their own framework.
  • Use multiple frameworks on the same page without refreshing the page (React, AngularJS, Angular, Ember, or whatever you're using)
  • Write code using a new framework, without rewriting your existing app
  • Lazy load code for improved initial load time.
  • Hot reload entire chunks of your overall application (instead of individual files).

🐒 SHOW ME THE CODE!

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();

总结与思考:微前端的优缺点

优点

  • 敏捷性 - 独立开发和更快的部署周期
    • 开发团队可以选择自己的技术并及时更新技术栈。
    • 一旦完成其中一项就可以部署,而不必等待所有事情。
  • 降低错误和回归问题的风险,相互之间的依赖性急剧下降。
  • 更简单快捷的测试,每一个小的变化不必再触碰整个应用程序。
  • 有助于持续集成、持续部署以及持续交付。
  • 维护和 bugfix 非常简单,每个团队维护特定的区域。

缺点

  • 开发与部署环境分离
    • 一个复杂的开发环境
    • 有一个孤立的部署周期。
    • 需要在一个孤立的环境中运行。
  • 复杂的集成
    • 隔离 js,避免 css 冲突,根据需要加载资源,在团队之间共享公共资源的机制,处理数据获取并考虑用户的良好加载状态。
    • 微前端模块之间的 Contract Testing?
  • 第三方模块重叠 / 依赖冗余增加了管理的复杂性
  • 影响最终用户的体验
    • 初始 Loading 时间可能会增加
    • HTML 会需要服务器端的渲染
<script  src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.min.js"
crossorigin="anonymous"></script>

💡 | 持续思考…

  • (变幻莫测)前端的技术选型?
  • 在 Mobile/Mobile Web 上的悖论
  • 合理划分的边界:DDD
  • Don't use any of this if you don't need it
  • 软件架构到底在解决什么问题?

所谓架构,其实是解决人的问题;所谓敏捷,其实是解决沟通的问题;所谓精益,其实是在讨论如何榨干劳动力;

❤️ | Thank you!

- https://medium.com/@tomsoderlund/micro-frontends-a-microservice-approach-to-front-end-web-development-f325ebdadc16
- https://medium.com/@_rchaves_/building-microfrontends-part-i-creating-small-apps-710d709b48b7
- http://allegro.tech/2016/03/Managing-Frontend-in-the-microservices-architecture.html
- https://technologyconversations.com/2015/08/09/developing-front-end-microservices-with-polymer-web-components-and-test-driven-development-part-55-using-microservices/
- https://technologyconversations.com/2015/08/09/including-front-end-web-components-into-microservices/
- https://inbox.google.com/u/1/search/microfrontend
- https://inbox.google.com/u/1/search/micro%20frontend
- https://medium.com/@_rchaves_/building-microfrontends-bonus-part-rewriting-an-app-with-elm-97ddce415ff4
- https://m.signalvnoise.com/the-majestic-monolith-29166d022228
- https://medium.com/@_rchaves_/building-microfrontends-part-v-communication-between-apps-34ae3649d610
- https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
- https://medium.com/@_rchaves_/building-microfrontends-part-iv-using-cdns-tech-radar-for-consensus-7dd658c1edb7
- https://medium.com/@_rchaves_/building-microfrontends-part-iii-public-path-problem-1ce823be24c9
- https://medium.com/@_rchaves_/building-microfrontends-part-ii-joining-apps-together-dfa1b6f17d3e
- https://www.upwork.com/blog/2017/05/modernizing-upwork-micro-frontends/
- https://www.upwork.com/blog/2017/01/upwork-modernization/
- https://engineering.hellofresh.com/front-end-microservices-at-hellofresh-23978a611b87
- https://news.ycombinator.com/item?id=13009285
- https://www.thoughtworks.com/radar/techniques/micro-frontends
- http://www.agilechamps.com/microservices-to-micro-frontends/
- https://www.tikalk.com/js/introduction-to-micro-frontends/
- https://github.com/CanopyTax/single-spa
- http://www.infoq.com/cn/news/2015/08/netflix-falcor
- http://insights.thoughtworks.cn/frontend-future-radar/
- https://blog.prototypr.io/luke-wroblewski-introduced-skeleton-screens-in-2013-through-his-work-on-the-polar-app-later-fd1d32a6a8e7
- https://micro-frontends.org/
- https://blog.fundebug.com/2017/08/02/micro_frontend_development/
- https://www.quora.com/How-is-JavaScript-used-within-the-Spotify-desktop-application-Is-it-packaged-up-and-run-locally-only-retrieving-the-assets-as-and-when-needed-What-JavaScript-VM-is-used/answer/Mattias-Petter-Johansson