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

前言与大纲

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

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

文章大纲

  • 微前端的可选实践方案(4 种 +)
    • 创建更小的 Apps(而不是 Components)
    • 如何组合微前端的 App 模块?
      • Option 1: 使用后端模板引擎插入 HTML
        • Option 1.1: 渐进式从后端进行加载
      • Option 2: 使用 IFrame 隔离运行时
      • Option 3: 客户端 JavaScript 异步加载
      • Option 4: WebComponents 整合所有功能模块
    • 不同 App 模块之间如何交互?
    • More Options…
  • 微前端的页面优化与实例
    • 多模块页面加载问题与优化建议
    • 微前端在 AEM(CMS)项目的应用
    • 现成解决方案:Single-SPA “meta framework”
  • 总结与思考:微前端的优缺点
    • 微前端的优点
    • 微前端的缺点
    • 持续思考…
  • 附:参考资料

微前端的可选实践方案(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,但与此同时却徒增了后端复杂度,并且又将前端的渲染控制权交回了后端服务器。而且我们的优化也取决于每个模块加载的速度,若是进行优化就必须按一定顺序进行加载。

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 来进行交互。

  • 优点
    • 最强大的是隔离了组件和应用程序部分的运行时环境,因此每个模块都可以独立开发,并且可以与其他部分的技术无关
    • 可以各自使用完全不同的前端框架,可以在 React 中开发一部分,在 Angular 中开发一部分,然后使用原生 JavaScript 开发其他部分或任何其他技术。
    • 只要每个 iframe 来自同一个来源,消息传递也就相当直接和强大。参考文档 Window.postMessageAPI
  • 缺点
    • Bundle 的大小非常明显,因为可能最终会多次发送相同的库,并且由于应用程序是分开的,所以在构建时也不能提取公共依赖关系。
    • 至于浏览器的支持,基本上不可能嵌套两层以上的 iframe(parent - > iframe - > iframe)。
  • 如果任何嵌套的框架需要能够滚动或具有 Form 表单域,那样的情况处理起来就会变得特别痛苦。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 由以下四种技术组成(尽管每种技术都可以独立使用):

  • 自定义元素(Custom Elements)对外提供组件的标签,实现自定义标签:可以创建自己的自定义 HTML 标签和元素。每个元素可以有自己的脚本和 CSS 样式。还包括生命周期回调,它们允许我们定义正在加载的组件特定行为。
  • HTML 模板(HTML <template>定义组件的 HTML 模板能力:一种用于保存客户端内容的机制,该内容在页面加载时不被渲染,但可以在运行时使用 JavaScript 进行实例化。可以将一个模板视为正在被存储以供随后在文档中使用的一个内容片段。
  • 影子 DOM(Shadow DOM)封装组件的内部结构,并且保持其独立性:允许我们在 Web 组件中封装 JavaScript,CSS 和 HTML。在组件内部时,这些东西与主文档的 DOM 分离。
  • HTML 导入(HTML Imports)解决组件组合和依赖加载:在微前端的上下文中,可以是包含我们要使用的组件在服务器上的远程位置。
# 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>

在微前端的实践当中:

  • 每个团队使用各自的技术栈创建他们的组件,并把它包装到自定义元素(Custom Element)中(如 <microfrontends-header></microfrontends-header>)。
  • Web 组件就是应用程序中包含的组件的本地实现,如菜单,表单,日期选择器等。每个组件都是独立开发的,主应用程序项目利用它们组装成最终的应用程序。
  • 特定元素(标签名称,属性和事件)的 DOM 规范还可以充当跨团队之间的契约或公共 API。
  • 创建可被导入到 Web 应用程序中的可重用组件,它们就像可以导入任何网页的用户界面小部件。
<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">
  • 优点
    • 代码的可读性变得非常清晰,组件资源内部高内聚,组件资源由自身加载控制,作用域独立。
    • 功能团队可以使用组件及其功能,而不必知道实现,他们只需要能够与 HTML DOM 进行交互。
    • 使用 PubSub 机制,组件可以发布消息,其他组件可以订阅特定的主题。幸运的是浏览器内置了这个功能。比如购物车可以在 window 订阅此事件并在应该刷新其数据时得到通知。
  • 缺点
    • 可惜的是,Web 组件规范跟服务器渲染无关。没有 JavaScript,就没有所谓的自定义元素。
      • 浏览器和框架的支持不够,需要更多的 polyfills 从而影响到用户页面的加载体验。
      • 我们需要在整个 Web 应用程序上做出改变,把它们全部转换成 Web Components。
      • 社区不够活跃,Web Components 还没有真正流行起来,也许永远也不会。

不同 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);
}
  • 得益于浏览器的原生 API,Custom Event 可以与其他任何技术和框架一起工作。比如,我们可以将消息从 Angular 组件发送到 React 组件。其实这也是现在 API 之间普遍使用 JSON 进行通信的原因,即使没有人使用 NodeJS 作为服务器端。
  • 但是,新的问题又出现了。我们该如何测试这种跨模块之间的交互?需要编写类似于后端微服务之间的 Contract Testing 或 Integration Testing 吗?并没有答案。

More Options…

  • 组件库 – 根据主 App 的技术栈,不同的组件和 App 模块拆分作为库的形式提供给主App,所以主 App 是由不同组件组成的。但是组件库的升级将成为一个大麻烦,比如对 Header 组件进行了更改,那么如果已经有 50 个页面使用了 Header 组件该怎么办?必须要求每一页都升级它的 Header,而且升级过程中用户还会在整个网站不同页面上看到不一致的标题。并且,在两边还必须都使用相同的技术,比如 Header 组件中使用了 ClojureScript,而 Content 组件中又用了 Elm,那么该怎么办?构建工具就必须在编译时处理不同的语言。
  • 将 App 模块作为 React 黑盒组件分发给消费者模块 – 应用程序的状态完全包含在组件中,API 只是通过 props 暴露出来。这种方式其实增加了应用程序之间的耦合,因为它迫使每个人都使用 React,甚至会使用相同版本的 React,但是这似乎也是一个比较好的折衷。
  • Edge Side Includes(ESI)/Server Side Includes(SSI) – 通过特殊的文件后缀 (shtml,inc) 或简单的标记语言来对那些可以加速和不能加速的网页中的内容片断进行描述,将每个网页划分成不同的小部分分别赋予不同的缓存控制策略。SSI / ESI 方法的缺点是,最慢的片段决定了整个页面的响应时间。

微前端的页面优化与实例

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

  • 使用 skeleton screen 响应式布局:如上图 LinkedIn 所做的那样,首先展现给用户一个页面的空白版本,然后在这个页面中逐渐加载和填充相应的信息。否则中间的信息流部分的内容最初是空白的,然后在 JavaScript 被加载和执行过后,信息流就会因为需要占用更多的空间而推动整个页面的布局。虽然我们可以控制页面来固定中间部分的高度,但在响应式网站上,确定一个确切的高度往往很难,而且不同的屏幕尺寸可能会有所不同。但更重要的问题是,这种高度尺寸的约定会让不同团队之间产生紧密的联系,从而违背了微前端的初衷。
  • 使用浏览器异步加载加快初始渲染:对于加载成本高且难以缓存的碎片,将其从初始渲染中排除是一个好主意。比如说 LinkedIn 首页的信息流就是一个很好的例子。
  • 共享 UI 组件库保证视觉体验一致:在前端设计中,必须向用户呈现外观和感觉一致的用户界面。建议可以建立一个共享组件库(包含 CSS、字体和 JavaScript)。将这些资源托管在 CDN,每个微前端就可以在其 HTML 输出中引用它们的位置。每个组件库的版本都正确地对资源进行版本控制,而每个微前端都指定要使用的组件库的版本和显式更新依赖关系。
  • 使用集中式服务(Router)来管理 URL:可以理解为前端的 Gateway,不同的 URL 对应不同应用程序所包含的内容。建议通过一个集中式的 URLs Router 来为应用程序提供一个 API 来注册他们自己的 URL,Router 将会位于 Web 应用程序的前面,根据不同的用户请求指向不同的 App 模块组合。
  • 提取共同依赖作为 externals 加载:虽然说不同 App 模块之间不能直接共享相同的第三方模块,当我们依然可以将常用的依赖比如 lodashmoment.js等公共库,或者跨多个团队共同使用的 reactreact-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>

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

我们在「三靠谱」(已和谐客户名称)的 Marketplace 项目当中也曾经探索过 AEM + React 混合开发的解决方案,其中就涉及到如何在 AEM 当中嵌入 React 组件,甚至将 AEM 组件又强行转化为 React 组件进行嵌套。现在回过头来其实也算是微前端的一种实践:

  • AEM 仅仅包含网页内容,不包含 domain 相关的结构化数据。
  • React 组件被托管在 AEM 组件当中,再经由 AEM 传递给组件所需要的属性,比如 IDs 或 APIs 的 URL 等等
  • 后端微服务则包含 domain 结构化数据,由对应的 React 组件通过 Ajax 进行数据查询。
  <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”

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

  • 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).

请看示例代码,所提供的 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 会是值得研究一番的技术,以确认它将对你产生何种影响,你应该投入一些精力来确定它是否会对你所在的组织产生影响。

摘自技术雷达:

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

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

微前端的优点

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

微前端的缺点

  • 开发与部署环境分离
    • 本地需要一个更为复杂的开发环境。
    • 每个 App 模块有一个孤立的部署周期。
    • 最终应用程序需要在同一个孤立的环境中运行。
  • 复杂的集成
    • 需要考虑隔离 JS,避免 CSS 冲突,并考虑按需加载资源
    • 处理数据获取并考虑用户的初始化加载状态
    • 如何有效测试,微前端模块之间的 Contract Testing?
  • 第三方模块重叠
    • 依赖冗余增加了管理的复杂性
    • 在团队之间共享公共资源的机制
  • 影响最终用户的体验
    • 初始 Loading 时间可能会增加
    • HTML 会需要服务器端的渲染

持续思考…

  • 变幻莫测)前端的技术选型?
    • 前端 JavaScript 框架工具穷出不穷,过几个月就要重写前端项目?比如最近又出来了声称要取代 Webpack(Parcel)和 Yarn(Turbo)的工具。伴随着前端框架的更新换代,如果整个项目一起升级/重构的话压力大、风险高,那不如拆分微前端直接支持多 framework,或者同一 framework 的不同版本?`
  • 在 Mobile/Mobile Web 上的悖论
    • 受限于 Mobile 尺寸大小,单一页面所能展现的内容本就有限。
    • 既然已经分出了不同的子页面,那何不如直接 Route 即可?
  • 合理划分的边界:DDD(领域驱动开发)
    • 最大的挑战是搞清楚如何合理拆分应用程序。
    • 糟糕的设计可能成为开发和维护的噩梦。
  • Don’t use any of this if you don’t need it
    • Do not use the ideas described here until it is needed, it will make things more complex.
    • If you are in a big company, those ideas could help you.
  • 软件架构到底在解决什么问题?—— 跨团队沟通的问题
    • 在正常情况下,每个团队拥有开发和维护其特性所需的一切,都应该有自己的能力来完成自己的特性,并最大限度地减少团队要求其他部门获得许可和/或帮助。
    • 当引入 library 或 framework 时的好处是只需要少数人讨论,而不用涉及超过 100 人的决策和他们的各种需求。这样一场大讨论不仅会耗费时间和精力,而且会迫使我们采用最不起眼的方法来选择 library,而不是选择专门针对每个 team 的问题领域的方案。

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

附:参考资料

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


更多精彩洞见,请关注微信公众号:思特沃克

Share

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

前言

在 ThoughtWorks 正式发布的最新一期技术雷达当中,「微前端(Micro Fontends)」已经进入到试验阶段,而试验环所列出的技术是我们认为值得去追求的。理解如何建立这种能力对你所在的组织十分重要,现在就可以尝试在一个低风险的项目上试点和实践这项技术,帮助你真正了解这门技术。

摘自最新一期技术雷达:

我们已经从引入微服务架构中获得了明显的好处,微服务架构可以让团队裁剪出独立部署的交付物以及可维护的服务。不幸的是,我们还看到许多团队在后端服务之上创建了前端单体——一个单一、庞大和杂乱无绪的浏览器应用。我们首选的(经过验证的)方法是将基于浏览器的代码拆分成微前端。在这种方法中,Web 应用程序被分解为多个特性,每个特性都由不同的前后端团队拥有。这确保每个特性都独立于其他特性开发,测试和部署。这样可以使用多种技术来重新组合特性——有时候是页面,有时候是组件——最终整合成一个内聚的用户体验。

文章大纲:

  • 微前端的缘由:单体应用与微服务架构
    • 微服务架构带来了哪些好处?
    • 那么前端的现状呢? —— 臃肿的前端
  • 微前端的定义 – 将微服务理念扩展到前端开发
    • 微前端的核心思想
    • 拆分微前端所带来的好处

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

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

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

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

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

  • 复杂性:服务可以更好地分离,每一个服务都足够小,能够完成完整的定义清晰的职责;
  • 扩展性:每一个服务可以独立横向扩展以满足业务伸缩性,并减少资源的不必要消耗;
  • 灵活性:每一个服务可以独立失败,允许每个团队自主选择最适合他们的技术和基础架构;
  • 敏捷性:每一个服务都可以独立开发,测试和部署,并允许团队独立扩展和维护各自的部署服务。

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

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

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

(图片来自:http://t.cn/R8biudo

在前端,往往由一个前端团队创建并维护一个 Web 应用程序,使用 REST API 从后端服务获取数据。这样的做法能够提供优秀的用户体验,但会导致单页面应用(SPA)不能很好地扩展和部署。在一个大公司里,单前端团队可能成为一个发展瓶颈。随着时间的推移,由一个独立团队所开发的前端层往往会越来越难以维护。特别是一个特性丰富、功能强大的前端 Web 应用程序,却位于后端微服务架构之上时。随着业务的发展,前端会变得越来越臃肿,一个项目可能会有 90% 的前端代码,却只有非常薄的后端,这种情况在 Serverless 架构的背景下还会愈演愈烈。

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

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

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

微前端的核心思想

  • Be Technology Agnostic:每个团队都应该能够选择并升级他们的技术栈,而不必与其他团队协调。自定义元素(后面会具体提到)是隐藏实现细节的好方法,同时为其他人提供公共接口。
  • Isolate Team Code:即使所有团队使用相同的框架,也不要共享运行时。构建独立的应用程序。不要依赖共享状态或全局变量。
  • Establish Team Prefixes:相互约定命名隔离。为 CSS、浏览器事件、Local Storage 和 Cookies 制定命名空间,以避免冲突,明确其所有权。
  • Favor Native Browser Features over Custom APIs:使用浏览器事件进行通信,而不是构建全局的 PubSub 系统。如果确实需要构建跨团队 API,请尽量保持简单。(与框架无关,可使用 CustomEvent)
  • Build a Resilient Site:即使 JavaScript 失败或尚未执行,Web 应用程序的功能仍应有效。可以使用通用渲染和渐进增强来提高用户的感知性能。

拆分微前端所带来的好处

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

  • Web 应用程序被分解成独立的特征,并且每个特征都由不同的团队拥有,前端到后端。这确保了每个功能都是独立于其他功能开发、测试和部署的。
  • 将网站或 Web 应用程序视为由独立团队拥有的功能组合。每个团队都有一个独特的业务或关注点确定的任务。
  • 每一个团队是跨职能的,从数据库到用户界面端到端地开发其功能/特性。
  • 所有前端功能(身份验证,库存,购物车等)都是 Web 应用程序的一部分,并与后端(大部分时间通过 HTTP)进行通信,并将其分解为微服务。
  • 可以同时拥有后端、前端、数据访问层和数据库,即一个服务子域所需的所有内容。
  • 查找线上 bug、测试、框架迭代,甚至语言、代码隔离与责任和其他事情变得更容易处理。
  • 我们不得不付出的代价是部署,但是容器(Docker 和 Rocket)以及不可变服务器使得这种情况也得到了极大的改善。

续:微前端的实践方案

本文的下篇即技术雷达之「微前端」- 将微服务理念扩展到前端开发(实战篇)将逐一介绍微前端实战中超过 4 种的可选实践方案,并对多模块页面加载可能出现的问题与优化给出建议,最后对微前端的优缺点进行总结并提出了一些新的思考。


更多精彩洞见,请关注微信公众号:思特沃克

Share

浏览器通讯新标准——WebRTC

What is WebRTC?

WebRTC是Web Real-Time Communication的简称,它是谷歌的一个开源项目,其目的是通过一系列的协议和规范来让浏览器提供支持实时通讯功能的API接口,这样在浏览器中通过简单的接口调用即可实现本地音频、视频等资源的实时共享。

早在 2009 年,Google的一名员工就提出了该想法,随后便有几位对此想法有兴趣的人开始投入精力开发,不久后关于获取本地资源的差异性问题都已经解决,唯一的难点就是解决实时通讯。与此同时,随着Chrome浏览器的推广, Google开始对此想法投入大量的精力,在2011年收购了当时拥有实时通讯所需低级组件的Gips公司后,实时通讯的难题也逐渐得到解决,随后WebRTC便应运而生。

(图片来自:http://t.cn/RQ3FnsC

Why WebRTC ?

在没有WebRTC之前,如果要在浏览器中实现实时通讯只有两种方式:

  • Flash: Flash顾名思义是通过Flash技术来实现本地音、视频资源共享。而使用Flash最大的问题在于Flash只能提供质量较差的视频及音频资源,而且运行时还需要服务器许可证。
  • Plug-in: Plug-in是通过给浏览器安装支持访问本地资源的插件来实现对本地音、视频资源的共享。而使用Plug-in最大的问题在于维护成本太高,因为不同的浏览器,不同的系统需要提供不同版本的插件,这样则导致一个功能需要针对不同的平台去做开发。

通过比较,很明显可以发现,WebRTC仅仅通过浏览器提供的同样的API接口,就可以实现实时通讯,而在开发过程中不用去关心平台和兼容性甚至安全性问题,那么实时通讯的实现成本就会降低很多。因此,很多网站已经开始使用WebRTC技术来实现实时通讯功能。

Why ASSESS ?

WebRTC在解决Web实时通讯问题中可以说是首选方案,但为什么在我司的技术雷达中仍然处于“评估”呢?我觉的目前最主要的一个问题是浏览器支持程度。这里是WebRTC对浏览器最新的支持情况,明显可以看出,WebRTC目前是不支持任何IOS设备的,这将使 WebRTC的适用性大大降低。其次,出于安全性考虑,所有使用WebRTC的站点必须使用 HTTPS协议,这又大大的限制了WebRTC的适用范围。

虽然如此,WebRTC依然是目前在浏览器实现AR/VR技术最简单易用的流媒体平台,加之Apple已经明确表示在未来的Safari中将支持WebRTC,不知道在IOS设备支持WebRTC及浏览器中AR/VR技术普遍采用WebRTC后,WebRTC是否会迎来突飞猛进的发展呢?


点击这里查看最新版技术雷达

Share

phantomJs之殇,chrome-headless之生

技术雷达快讯:自2017年中以来,Chrome用户可以选择以headless模式运行浏览器。此功能非常适合运行前端浏览器测试,而无需在屏幕上显示操作过程。在此之前,这主要是PhantomJS的领地,但Headless Chrome正在迅速取代这个由JavaScript驱动的WebKit方法。Headless Chrome浏览器的测试运行速度要快得多,而且行为上更像一个真正的浏览器,虽然我们的团队发现它比PhantomJS使用更多的内存。有了这些优势,用于前端测试的Headless Chrome很可能成为事实上的标准。

随着Google在Chrome 59版本放出了headless模式,Ariya Hidayat决定放弃对Phantom.js的维护,这也标示着Phantom.js 统治fully functional headless browser的时代将被chrome-headless代替。

Headless Browser

也许很多人对无头浏览器还是很陌生,我们先来看看维基百科的解释:

A headless browser is a web browser without a graphical user interface.

Headless browsers provide automated control of a web page in an environment similar to popular web browsers, but are executed via a command-line interface or using network communication.

对,就是没有页面的浏览器。多用于测试web、截图、图像对比、测试前端代码、爬虫(虽然很慢)、监控网站性能等。

为什么要使用headless测试?

headless broswer可以给测试带来显著好处:

  1. 对于UI自动化测试,少了真实浏览器加载css,js以及渲染页面的工作。无头测试要比真实浏览器快的多。
  2. 可以在无界面的服务器或CI上运行测试,减少了外界的干扰,使自动化测试更稳定。
  3. 在一台机器上可以模拟运行多个无头浏览器,方便进行并发测试。

headless browser有什么缺陷?

以phantomjs为例

  1. 虽然Phantom.js 是fully functional headless browser,但是它和真正的浏览器还是有很大的差别,并不能完全模拟真实的用户操作。很多时候,我们在Phantom.js发现一些问题,但是调试了半天发现是Phantom.js自己的问题。
  2. 将近2k的issue,仍然需要人去修复。
  3. Javascript天生单线程的弱点,需要用异步方式来模拟多线程,随之而来的callback地狱,对于新手而言非常痛苦,不过随着es6的广泛应用,我们可以用promise来解决多重嵌套回调函数的问题。
  4. 虽然webdriver支持htmlunit与phantomjs,但由于没有任何界面,当我们需要进行调试或复现问题时,就非常麻烦。

那么Headless Chrome与上面提到fully functional headless browser又有什么不同呢?

什么是Headless Chrome?

Headless Chrome 是 Chrome 浏览器的无界面形态,可以在不打开浏览器的前提下,使用所有Chrome支持的特性,在命令行中运行你的脚本。相比于其他浏览器,Headless Chrome 能够更加便捷的运行web自动化测试、编写爬虫、截取图等功能。

有的人肯定会问:看起来它的作用和phantomjs没什么具体的差别?

对,是的,Headless Chrome 发布就是来代替phantomjs。

我们凭什么换用Headless Chrome?

  1. 我爸是Google,那么就意味不会出现phantomjs近2k问题没人维护的尴尬局面。 比phantomjs有更快更好的性能。
  2. 有人已经做过实验,同一任务,Headless Chrome要比现phantomjs更加快速的完成任务,且占用内存更少。https://hackernoon.com/benchmark-headless-chrome-vs-phantomjs-e7f44c6956c
  3. chrome对ECMAScript 2017 (ES8)支持,同样headless随着chrome更新,意味着我们也可以使用最新的js语法来编写的脚本,例如async,await等。
  4. 完全真实的浏览器操作,chrome headless支持所有chrome特性。
  5. 更加便利的调试,我们只需要在命令行中加入–remote-debugging-port=9222,再打开浏览器输入localhost:9222(ip为实际运行命令的ip地址)就能进入调试界面。

能带给QA以及项目什么好处?

前端测试改进

以目前的项目来说,之前的前端单元测试以及组件测试是用karma在phantomjs运行的,非常不稳定,在远端CI上运行时经常会莫名其妙的挂掉,也找不出来具体的原因,自从Headless Chrome推出后,我们将phantomjs切换成Headless Chrome,再也没有出现过异常情况,切换也非常简单,只需要把karma.conf.js文件中的配置改下就OK了。如下

customLaunchers: { myChrome: { base: 'ChromeHeadless', flags: ['--no-sandbox', '--disable-gpu', '--remote-debugging-port=9222'] } },

browsers: ['myChrome'],

UI功能测试改进

原因一,Chrome-headless能够完全像真实浏览器一样完成用户所有操作,再也不用担心跑测试时,浏览器受到干扰,造成测试失败

原因二,之前如果我们像要在CI上运行UI自动化测试,非常麻烦。必须使用Xvfb帮助才能在无界面的Linux上 运行UI自动化测试。(Xvfb是一个实现了X11显示服务协议的显示服务器。 不同于其他显示服务器,Xvfb在内存中执行所有的图形操作,不需要借助任何显示设备。)现在也只需要在webdriver启动时,设置一下chrome option即可,以capybara为例:

Capybara.register_driver :selenium_chrome do |app|
Capybara::Selenium::Driver.new(app, browser: :chrome,
                               desired_capabilities: {
                                   "chromeOptions" => {
                                       "args" => [ "--incognito",
                                                   "--allow-running-insecure-content",
                                                   "--headless",
                                                   "--disable-gpu"
                                   ]}
                               })
end

无缝切换,只需更改下配置,就可以提高运行速度与稳定性,何乐而不为。

Google终极大招

Google 最近放出了终极大招——Puppeteer(Puppeteer is a Node library which provides a high-level API to control headless Chrome over the DevTools Protocol. It can also be configured to use full (non-headless) Chrome.)

类似于webdriver的高级别的api,去帮助我们通过DevTools协议控制无界面Chrome。

在puppteteer之前,我们要控制chrome headless需要使用chrome-remote-interface来实现,但是它比 Puppeteer API 更接近低层次实现,无论是阅读还是编写都要比puppteteer更复杂。也没有具体的dom操作,尤其是我们要模拟一下click事件,input事件等,就显得力不从心了。

我们用同样2段代码来对比一下2个库的区别。

首先来看看 chrome-remote-interface

const chromeLauncher = require('chrome-launcher');
const CDP = require('chrome-remote-interface');
const fs = require('fs');
function launchChrome(headless=true) {
return chromeLauncher.launch({
// port: 9222, // Uncomment to force a specific port of your choice.
 chromeFlags: [
'--window-size=412,732',
'--disable-gpu',
   headless ? '--headless' : ''
]
});
}
(async function() {
  const chrome = await launchChrome();
  const protocol = await CDP({port: chrome.port});
  const {Page, Runtime} = protocol;
  await Promise.all([Page.enable(), Runtime.enable()]);
  Page.navigate({url: 'https://www.github.com/'});
  await Page.loadEventFired(
      console.log("start")
  );
  const {data} = await Page.captureScreenshot();
  fs.writeFileSync('example.png', Buffer.from(data, 'base64'));
  // Wait for window.onload before doing stuff.  
   protocol.close();
   chrome.kill(); // Kill Chrome.

再来看看 puppeteer

const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://www.github.com');
await page.screenshot({path: 'example.png'});
await browser.close();
})();

对,就是这么简短明了,更接近自然语言。没有callback,几行代码就能搞定我们所需的一切。

总结

目前Headless Chrome仍然存在一些问题,还需要不断完善,我们应该拥抱变化,适应它,让它给我们的工作带来更多帮助。


更多精彩洞见,请关注微信公众号:思特沃克

Share

技术雷达——科技宏观趋势

ThoughtWorks每年都会出品两期技术雷达,这是一份关于科技行业技术趋势的报告。是ThoughtWorks对工具、技术、编程语言和平台的详细解读,我们通常会引入一百余个技术条目。编写技术雷达需要与来自ThoughtWorks全球各个办公室的资深技术专家进行深入沟通,在讨论个别现象的过程中,我们也会谈及宏观趋势。本文汇集了我们眼中当前科技行业的大趋势,以飨读者。

区块链不仅仅是炒作

在本文编写之时,一枚比特币的市值已经突破一万美元大关,从年初至今已经翻了十倍。而埃隆·马斯克明确否认自己是中本聪本尊,中本聪是比特币的神秘发行人。比特币炒作带火了加密货币这个混乱的市场,同时名人效应带来的ICO投机也是风生水起,这引发了加密货币存在“巨大泡沫”的担忧。不过在这种过山车式的疯狂炒作下,也孕育了一些很有价值的技术。

我们的许多客户都在试图建立一个运用区块链的分布式账本和智能合约,一些雷达条目显示,区块链相关技术已经趋于成熟,使用多种技术和编程语言实施智能合约的有效方法越来越多。区块链会解决两大问题。首先,这种技术可以让我们摆脱对“大家共同信赖”中间人(如银行或者股票交易所)的依赖,建立分布式信任机制;其次,区块链可以让我们创建一个共享式、不可更改的的可信的账本——是对事实的记录。如今,我们已经见证了基于这两个核心理念的组织的诞生。其中,我们认为以太坊智能合约和Corda分布式账本技术值得持续关注。

企业内部署(on-premise)软件风光不再?

谈及基础设施和部署,暂且把我们的沟通对象变成我们的每一个客户。在组织开始考虑配置服务器、安装软件,并且对软件进行后续打补丁和维护等动作时,第一个问题是“有我可以购买的定制服务吗?”,然后是“我可以从云服务供应商买什么来构建我的云服务?”这个决策流程可以总结为“最后考虑企业内部署(on-premise)软件”。曾几何时,人们在使用云服务时会研究多时;而今使用on-premise式服务时人们才会非常谨慎。过去一年来,云端托管已经成为大家非常感兴趣的话题。

雷达报告中再次印证了这个趋势——本文中谈及的许多工具、技术和平台要么是云服务辅助,要么支持云端服务。我们切实见证了许多组织“默认上云”的趋势,我们这里提到“企业内部署”,但是重点不是服务器在哪里,而是高效获得一项服务或功能,并长期保证其运行和维护所需要的工作量。

虚拟化的“长尾效应”

早在1999年我们开始使用Vmware的虚拟机时,并没有预料到虚拟化将会给软件带来全方位的变革。虚拟机如今已成为软件行业各个环节的必选,无论是开发者工作站还是谷歌这个体量的数据中心,而且虚拟机也是许多系统的“扩展单元”(除非你是谷歌,在谷歌数据中心本身就是扩展单元!)。Docker、Kubernetes以及当前所有重量级云技术都是基于虚拟化来实现的。

虚拟化促成了云服务的繁荣,我们认为,在NIST定义中的云极具价值。NIST的五个“基本特征”中,我们认为两个特征——按需自助服务和弹性——是云服务能够获得宠爱的绝对关键要素。选择云服务时,还有三个特征,而这些优势正是许多“私有云”产品所无法比拟的。

同等特性(feature parity)的误导

我们发现目前科技行业呈现出一种不良趋势,即在实施云迁移、遗留系统升级或产品再开发时以“同等特性(feature parity)”为目标。将一套运行时间达十年或十五年的老系统单纯用新技术重新部署,且不论程序缺陷等等,这绝非好主意。常用的借口是“我们不想给企业带来困扰”,或是担心改变流程或计算,但结果常常是交付遥遥无期、进展缓慢、一次性交付,还潜藏各种风险。在发现项目延期、预算大幅超支且不能给企业带来任何新的利益时,利益相关者往往大失所望。

这些教训值得我们反思。我们认为IT领导者(和企业)应当大胆质疑十年前编写的逻辑能否代表当今企业的运行方式,要相信用户有能力采纳(整体更强大的)新系统。企业应当深入研究自己真正需要的功能,而不是在新平台上重建一套功能完备的特性集。关于如何为云服务重写敏捷项目管理工具Mingle本期技术雷达进行了更多深入的探讨。

中国正在开源世界中崛起

我们发现中国的开源项目在数量和质量上均呈跳跃式增长。百度和阿里巴巴等大企业已经发布自己的开源代码,令全球为之瞩目。在过去的几年里,中国公司对开源代码的认知悄然转变。以前出于保护知识产权的忧虑,不愿意开源。而现在他们看到了Docker、Kubernetes和OpenStack等大型项目的影响力,认识到建立一套生态系统是比闭关更好的选择。只要保持对开源社区的影响力,他们就可以掌握其IP的控制权,同时享受开源的福利。

另外一个因素是中国与发达国家的市场有很大不同,具有独特的文化和视角,由此产生的期望与要求也有所不同,所以中国企业并不一定需要亦步亦趋地追随西方企业的脚步。中国市场的体量巨大,中国企业正在创建、分享开源代码,开发自己特有的软件和生态系统,从而解决中国特有的问题。

本期技术雷达中,我们重点介绍了阿里巴巴的两大项目AtlasBeehive,可以更好地实现应用程序模块化,有助于分布式或者远程团队协作。借此你可以动态地将物理隔离模块统一装配到单个应用程序中,其具体设计显然考虑到了中国软件市场的情况。

值得注意的是,中国的开源代码首先是为中国编写的,因此不用走出国门就能取得巨大成功。文档将使用中文撰写,如果一个项目进行得足够顺利,后续可能创建翻译版本。中国涌现了一些质量很高的软件,而且非常实用,但需要注意的是其主要受众是中国市场。

Kubernetes统领容器管理生态

一年前,身在ThoughtWorks的我们曾被问道“你们偏爱哪一种容器管理平台,Kubernetes还是Mesos?”如今,这个问题的答案已经不言而喻。Kubernetes俨然已是事实上的默认标准。这是为什么呢?我们认为是各种因素作用下的综合结果。

容器化趋势已经建立了一套生态系统,我们所有的工具都可以在该生态系统内与容器协作(而且经常需要容器),Docker在这一点上尤为突出。在某种程度上,容器就是新POSIX、新通用接口。IT行业在创建软件组件上付出了多年的努力,看来容器可能是目前最好的标准化方式。(然而,因为一个容器里可以插入任何内容,所以目前尚无法保证组件可以很好地共同运行。)微服务、演化架构、默认云等其他重要科技趋势与容器的协作极好,因此也存在自然的共生关系。

几年前,科技行业主要参与者还在探讨GIFFEE——谷歌提供的针对其他所有人的基础架构。“GIFEE”的话题才刚开始,Kubernetes基本已经成了所有人都能用的谷歌式基础架构。谷歌努力推进项目,投入了大量资源,希望把人们吸引到谷歌云产品上。随着时间的推移,Kubernetes已经成了我们与供应商和云提供商打交道的默认容器平台。

除此之外,Kubernetes还进化得更易于大规模运行。经过对Kubernetes核心软件的改进,借助更好的工具和高度活跃的生态系统,运行弹性生产集群的学习曲线已经不再那么陡峭。现在所有主要云提供商都提供基于Kubernetes的托管,所以进入门槛很低。

数据流即是标准

本期技术雷达中,我们探讨了一系列与Kafka相关的问题:Kafka、Kafka Streams、Kafka作为正确数据之源、Kafka作为轻量级ESB。然而我们为什么要强调数据流?

全世界都渴望实时分析。事实上,设计系统时我们必须做出调整适应。我们喜欢基于事件的流式架构所带来的福利——松散耦合、自主组件、高性能和高扩展性——但分析要求推动了对数据流的要求。离开数据流便无法实现实时分析。

与数据流兴起相关的是事件驱动架构的成熟度。人们对这些系统已然司空见惯,也很好理解了。有些新技术还在涌现,例如用数据流作为企业事实/状态的持久化存储。我们并非百分百确定所有这些技术都是好主意(CQRS已经坑了许多不设戒备心的人),但数据流已深入人心,这一点毋庸置疑。

Share