SpringCloud我爱编程程序员

「微前端」- 将微服务理念扩展到前端开发(实践篇)

2018-02-02  本文已影响601人  ThoughtWorks

前言与大纲

本文分为理论和实战上下两篇。本篇为微前端的实战篇,共计约 5k 字,预计阅读时间 10 mins。

技术雷达之「微前端」- 将微服务理念扩展到前端开发(上:理论篇)中,我们介绍了微前端在单体应用和微服务的架构演进中所产生的缘由,将微服务理念运用到前端开发就是为了解决臃肿前端的当前现状。与此同时,合理拆分微前端也给我们的应用开发带来显而易见的好处,在本篇当中我们将逐一介绍微前端的实践方案与可能遇到的问题和对应的优化建议。

文章大纲

微前端的可选实践方案(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[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 数据而需要更多时间。通常情况下我们希望尽快将网页显示给用户,而在这种情况下后台加载时间就会变得更长。

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

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

image.png

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 加载顺序的问题,建议将其修改成类似于 Facebook bigpipe 的解决方案,返回一个 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...

微前端的页面优化与实例

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

image.png

<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)项目的应用

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

image.png

开源的 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();

值得一提的是,single-spa 已经进入到最新一期技术雷达的评估阶段。这意味着 single-spa 会是值得研究一番的技术,以确认它将对你产生何种影响,你应该投入一些精力来确定它是否会对你所在的组织产生影响。

image.png

摘自技术雷达:

SINGLE-SPA是一个 JavaScript 元框架,它允许我们使用不同的框架构建微前端,而这些框架可以共存于单个应用中。一般来说,我们不建议在单个应用中使用多个框架,但有时却不得不这么做。例如当你在开发遗留系统时,你希望使用现有框架的新版本或完全不同的框架来开发新功能,single-spa 就能派上用场了。鉴于很多 JavaScript框架 都昙花一现,我们需要一个解决方案来应对未来框架的变化,以及在不影响整个应用的前提下进行局部尝试。在这个方向上,single-spa 是一个不错的开始。

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

微前端的优点

微前端的缺点

持续思考…

image.png

所谓架构,其实是解决人的问题;所谓敏捷,其实是解决沟通的问题;

附:参考资料

本次技术雷达「微前端」主题的宣讲 Slides 可以在我的博客找到:[「技术雷达」之 Micro Frontends:微前端 - 将微服务理念扩展到前端开发 - 吕立青的博客]


image.png
上一篇 下一篇

猜你喜欢

热点阅读