其他分享
首页 > 其他分享> > 技术雷达之「微前端」- 将微服务理念扩展到前端开发

技术雷达之「微前端」- 将微服务理念扩展到前端开发

作者:互联网

文章大纲

本文共计约 7k 字,预计阅读时间 15mins

微前端的缘由:单体应用与微服务架构

 

 

在传统的软件开发当中,大多数软件都是单体式应用架构的。在瞬息万变的商业时代背景下,企业必须学会适应我们这个时代的不确定性。快速试验,快速失败。更快地推出新产品和有效地改进当前产品,从而为客户提供有意义的数字体验。

而单体应用这种软件架构对于企业来说的致命缺点就是,企业对于市场的响应速度变慢。企业决策者在一年内需要做的决策数量非常有限,由于依赖关系,其响应周期往往会变得非常漫长。每当开发或升级产品,都需要在一系列体量庞大的相关服务中同时增加新功能,这就需要所有利益相关方共同努力,以同步方式进行变更。

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

 

 

假设服务边界已经被正确地定义为可独立运行的业务领域,并确保在微服务设计中遵循诸多最佳实践。那么至少会以下几个方面获得显而易见的好处:

每个微服务是孤立的,独立的「模块」,它们共同为更高的逻辑目的服务。微服务之间通过 Contract 彼此沟通,每个服务都负责特定的功能。这使得每个服务都能够保持简单,简洁和可测试性。

从而微服务架构允许企业更自发地采取更深远的业务决策,因为每个微服务都是独立运作的,而且每一个管理团队可以很好地控制该服务的变更。

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

 

 

在前端,往往由一个前端团队创建并维护一个 Web 应用程序,使用 REST API 从后端服务获取数据。这种方式如果做得好的话,它能够提供优秀的用户体验。但主要的缺点是单页面应用(SPA)不能很好地扩展和部署。在一个大公司里,单前端团队可能成为一个发展瓶颈。随着时间的推移,往往由一个独立团队所开发的前端层越来越难以维护。

特别是一个特性丰富、功能强大的前端 Web 应用程序,却位于后端微服务架构之上。并且随着业务的发展,前端变得越来越臃肿,一个项目可能会有 90% 的前端代码,却只有非常薄的后端,甚至这种情况在 Serverless 架构的背景下还会愈演愈烈。

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

 

 

微前端(Micro Frontends)这个术语其实就是微服务的衍生物。将微服务理念扩展到前端开发,同时构建多个完全自治的和松耦合的 App 模块(服务),其中每个 App 模块只负责特定的 UI 元素和功能。

如果我们看到微服务提供给后端的好处,那么就可以更进一步将这些好处应用到前端。与此同时,在设计微服务的时候,就可以考虑不仅要完成后端逻辑,而且还要完成前端的视觉部分。而对于微前端来说,与微服务的许多要求也是一致的:监控、日志、HealthCheck、Analytics 等等。

拆分微前端所带来的好处

这样就能使各个前端团队按照自己的步调迭代,并随时准备就绪处于可发布状态,并隔离相互依赖所产生的风险,与此同时也更容易尝试新技术。

微前端的核心思想

 

 

微前端的可选实践方案(4 种+)

创建更小的 Apps(而不是 Components)

首先让我们来创建一个典型 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 模块?

在每个独立团队创建好各自的 App 模块后,我们就可以将网站或 Web 应用程序视为由各种模块的功能组合。下文将介绍多种技术实践方案来重新组合这些模块(有时作为页面,有时作为组件),而前端(不管是不是 SPA)将只需要负责路由器(Router)如何选择和决定要导入哪些模块,从而为最终用户提供一致性的用户体验。

Option 1: 使用后端模板引擎插入 HTML

# 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[], 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 数据而需要更多时间。通常情况下我们希望尽快将网页显示给用户,而在这种情况下后台加载时间就会变得更长。

Option 1.1: 渐进式从后端进行加载

当然,我们也可以通过修改一些后端代码来渐进式地(Progressive)往前端发送 HTML,但与此同时却徒增了后端复杂度,并且又将前端的渲染控制权交回了后端服务器。而且我们的优化也取决于每个模块加载的速度,若是进行优化就必须按一定顺序进行加载。

 

 

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>

我们也可以将每个子应用程序嵌入到各自的<iframe>中,这使得每个模块能够使用任何他们需要的框架,而无需与其他团队协调工具和依赖关系,依然可以借助于一些库或者Window.postMessageAPI来进行交互。

Option 3: 客户端 JavaScript 异步加载

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 加载顺序的问题,建议将其修改成类似于Facebookbigpipe的解决方案,返回一个 JSON 对象{ html: ..., css: [...], js: [...] }再进行加载顺序的控制。

Option 4: WebComponents 整合所有功能模块

Web Components 是一个 Web 标准,所以像 Angular、React/Preact、Vue 或 Hyperapp 这样的主流 JavaScript 框架都支持它们。你可以将 Web Components 视为使用开放 Web 技术创建的可重用的用户界面小部件,也许会是 Web 组件化的未来。

Web Components 由以下四种技术组成(尽管每种技术都可以独立使用):

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

在微前端的实践当中:

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

不同 App 模块之间如何交互?

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

微前端的页面优化与实例

多模块页面加载问题与优化建议

 

 

 

 

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

微前端在 AEM(CMS)项目的应用

我们在「三靠谱」(已和谐客户名称)的 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 “meta framework”

https://single-spa.surge.sh/

 

开源的single-spa自称为「元框架」,可以实现在一个页面将多个不同的框架整合,甚至在切换的时候都不需要刷新页面(支持 React、Vue、Angular 1、Angular 2、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();

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

优点

缺点

持续思考…

 

 

标签:Web,理念,前端,应用程序,模块,组件,前端开发,加载
来源: https://www.cnblogs.com/qiucunxin/p/15395731.html