什么是微前端?

微前端 (micro-frontends) 术语在 2016 年在 TECHNOLOGY RADAR 中被提及。

该网站叫做 thoughtworks,有个叫 雷达 (radar) 的技术期刊,用于持续追踪有趣的技术是如何发展的,每种技术被称之为 条目

该条目提到了 微服务 中,多服务可 独立部署 并易于 扩展交付 的特性。而随着前端 单页面应用 (SPA) 的流行,构建大型应用需要更为合理的前端架构来管理 多开发人员 多团队 开发带来的复杂性。

微前端的含义

微前端应该有两方面的含义:

  1. 从架构来说, 微前端 采用微服务的一些设计理念,是实现组合多应用的一种技术架构;
  2. 每个微 (子) 应用,我们可以称之为 微前端

优势

  • 技术栈无关
    主框架不限制接入应用的技术栈,微应用具备完全自主权

  • 独立开发、独立部署
    与微服务一样,微前端的独立部署能力是关键。这会缩小任何给定部署的范围,从而降低相关风险。无论您的前端代码以何种方式或在哪里托管,每个 微应用 都应该有自己的 持续交付管道 ,用于 构建 测试 并将其部署到 生产环境

    micro-frontends.svg

  • 团队自治
    我们的团队将围绕业务功能的垂直切片组建,而不是围绕技术能力。每个微应用都是封装的单个页面,并由单个团队端到端维护,这带来了团队更高的凝聚力。

    micro-frontends-1.svg

  • 增量升级
    在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略。

  • 独立运行时
    每个微应用之间状态隔离,运行时状态不共享。

微前端架构旨在解决 单体应用 在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个 巨石应用 (Frontend Monolith) 后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

为什么不用 iframe

这里 给出了解释,主要有以下几点:

  1. URL 状态不同步。iframe 的页面 url 中的状态信息并不能同步到父窗口,无法使用浏览器的前进后退功能。
  2. DOM 结构不共享。iframe 的页面布局只针对于 iframe 窗口 (例如:全局弹框无法给出合理布局) 。
  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的微应用中实现免登效果。
  4. 慢。每次微应用进入都是一次浏览器上下文重建、资源重新加载的过程。

集成方式

Iframe 集成

在浏览器中组合应用程序的最简单方法之一是不起眼的 iframe。就其性质而言,iframe 可以轻松地从独立的子页面构建页面。它们还在样式和全局变量方面提供了很好的隔离度,不会相互干扰。

虽然不推荐,但是这里仍然展示其基本使用方式:

<html>
    <title>Feed me!</title>
  </head>
    <h1>Welcome to Feed me!</h1>
    <iframe id="micro-frontend-container"></iframe>
    <script type="text/javascript">
      const microFrontendsByRoute = {
        '/': 'https://browse.example.com/index.html',
        '/order-food': 'https://order.example.com/index.html',
        '/user-profile': 'https://profile.example.com/index.html',
      const iframe = document.getElementById('micro-frontend-container');
      iframe.src = microFrontendsByRoute[window.location.pathname];
    </script>
  </body>
</html>

Javascript 集成

这种方法可能是最灵活的方法,也是我们看到团队最常采用的方法。每个 微应用 都使用 <script> 标签包含在页面上,并在加载时公开一个 全局函数 作为其 入口 点。然后 容器应用程序 确定应该挂载哪个 微应用 ,并调用相关函数来告诉微前端何时何地渲染自己。

下面是一个比较粗糙的实现,不过它演示了基本技术:

<html>
    <title>Feed me!</title>
  </head>
    <h1>Welcome to Feed me!</h1>
    <!-- 这些 bundle 不会立即渲染任何东西 -->
    <!-- 每一个 boudle 提供一个“入口函数”添加到 window -->
    <script src="https://browse.example.com/bundle.js"></script>
    <script src="https://order.example.com/bundle.js"></script>
    <script src="https://profile.example.com/bundle.js"></script>
    <div id="micro-frontend-root"></div>
    <script type="text/javascript">
      const microFrontendsByRoute = {
        '/': window.renderBrowseRestaurants,
        '/order-food': window.renderOrderFood,
        '/user-profile': window.renderUserProfile,
      // 路由其中一个入口函数
      const renderFunction = microFrontendsByRoute[window.location.pathname];
      // 提供一个元素 ID ,作为渲染容器
      renderFunction('micro-frontend-root');
    </script>
  </body>
</html>

Web Components 集成

这是前一种方法的一个 变体 是为每个微前端定义一个 HTML 自定义元素供容器实例化,而不是定义一个全局函数供容器调用。

<html>
    <title>Feed me!</title>
  </head>
    <h1>Welcome to Feed me!</h1>
    <!-- 这些 bundle 不会立即渲染任何东西 -->
    <!-- 每一个 bundle 都自定义了一个元素类型 -->
    <script src="https://browse.example.com/bundle.js"></script>
    <script src="https://order.example.com/bundle.js"></script>
    <script src="https://profile.example.com/bundle.js"></script>
    <div id="micro-frontend-root"></div>
    <script type="text/javascript">
      const webComponentsByRoute = {
        '/': 'micro-frontend-browse-restaurants',
        '/order-food': 'micro-frontend-order-food',
        '/user-profile': 'micro-frontend-user-profile',
      // 路由对应组件
      const webComponentType = webComponentsByRoute[window.location.pathname];
      // 把组件挂载到容器上。
      const root = document.getElementById('micro-frontend-root');