微前端入门

javascript/jquery

浏览数:42

2020-5-28

AD:资源代下载服务

最近在学习一些微前端的知识,转载一片个人觉得比较好的。

基本概念

微前端可以理解为,一个很大的前端应用,将此应用整体分解,分解成多个项目,每个项目交给一个团队,各团队可以独立开发、测试、部署和维护,其实用什么方法都没有明确规定,最终的目的是要达到代码隔离和团队自治即可。

微前端实现的一些方法

1. 后端模板集成

我们用一个非常传统的方式开始,将多个模板渲染到服务器上的HTML里。我们有一个index.html,其中包含所有常见的页面元素,然后使用 include 来引入其他模板:

<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Feed me</title>
  </head>
  <body>
    <h1> Feed me</h1>
    <!--# include file="$PAGE.html" -->
  </body>
</html>

然后配置 nginx

server {
    listen 8080;
    server_name localhost;

    root /usr/share/nginx/html;
    index index.html;
    ssi on;

    # 将 / 重定向到 /browse
    rewrite ^/$ http://localhost:8080/browse redirect;

    # 根据路径访问 html 
    location /browse {
      set $PAGE 'browse';
    }
    location /order {
      set $PAGE 'order';
    }
    location /profile {
      set $PAGE 'profile'
    }

    # 所有其他路径都渲染 /index.html
    error_page 404 /index.html;
}

这是相当标准的服务器端应用。我们之所以可以称其为微前端,是因为我们让每个页面独立,可由一个独立的团队交付。

为了获得更大的独立性,可以有一个单独的服务器负责渲染和服务每个微型前端,其中一个服务器位于前端,向其他服务器发出请求。通过缓存,可以把延迟降到最低。

2. 使用package集成,即将小应用打包成npm包

有人会用到的一种方法是将每个微前端发布为一个 node 包,并让容器应用程序将所有微前端应用作为依赖项。比如这个 package.json:

{
  "name": "@feed-me/container",
  "version": "1.0.0",
  "description": "A food delivery web app",
  "dependencies": {
    "@feed-me/browse-restaurants": "^1.2.3",
    "@feed-me/order-food": "^4.5.6",
    "@feed-me/user-profile": "^7.8.9"
  }
}

乍看似乎没什么问题,这种做法会产生一个可部署的包,我们可以轻松管理依赖项。
但是,这种方法意味着我们必须重新编译并发布每个微前端应用,才能发布我们对某个应用作出的更改。我们强烈不建议使用这种微前端方案。

3. iframe集成

iframe 是集成的最简单方式之一。本质上来说,iframe 里的页面是完全独立的,可以轻松构建。而且 iframe 还提供了很多的隔离机制。

<html>
  <head>
    <title>Feed me!</title>
  </head>
  <body>
    <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>

iframe 并不是一项新技术,所以上面代码也许看起来并不那么令人兴奋。
但是,如果我们重新审视先前列出的微前端的主要优势,只要我们谨慎地划分微应用和组建团队的方式,iframe便很适合。
我们经常看到很多人不愿意选择iframe。因为 iframe有点令人讨厌,但 iframe 实际上还是有它的优点的。上面提到的容易隔离确实会使iframe不够灵活。它会使路由、历史记录和深层链接变得更加复杂,并且很难做成响应式页面。

4. 使用js集成

这种方式可能是最灵活的一种,也是被采用频率最高的一种方法。每个微前端都对应一个 <script> 标签,并且在加载时导出一个全局变量。然后,容器应用程序确定应该安装哪些微应用,并调用相关函数以告知微应用何时以及在何处进行渲染。

<html>
  <head>
    <title>Feed me!</title>
  </head>
  <body>
    <h1>Welcome to Feed me!</h1>

    <!-- 这些脚本不会马上渲染应用 -->
    <!-- 而是分别暴露全局变量 -->
    <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];

      // 渲染第一个微应用
      renderFunction('micro-frontend-root');
    </script>
  </body>
</html>

上面是一个很基本的例子,演示了 JS 集成的大体思路。
与 package 集成不同,我们可以用不同的bundle.js独立部署每个应用。
与 iframe 集成不同的是,我们具有完全的灵活性,你可以用 JS 控制什么时候下载每个应用,以及渲染应用时额外传参数。
这种方法的灵活性和独立性使其成为最常用的方案。当我们展示完整的示例时,会有更详细的探讨。

5. 通过 Web Component 集成

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

<html>
  <head>
    <title>Feed me!</title>
  </head>
  <body>
    <h1>Welcome to Feed me!</h1>

     <!-- 这些脚本不会马上渲染应用 -->
    <!-- 而是分别提供自定义标签 -->
    <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');
      const webComponent = document.createElement(webComponentType);
      root.appendChild(webComponent);
    </script>
  </body>
</html>

主要区别在于使用 Web Component 代替全局变量。如果你喜欢 Web Component 规范,那么这是一个不错的选择。如果你希望在容器应用程序和微应用之间定义自己的接口,那么你可能更喜欢前面的示例。

可能遇到的问题

1. 样式

CSS 没有模块系统、命名空间和封装。就算有,也通常缺乏浏览器支持。在微前端环境中,这些问题会更严重。

例如,如果一个团队的微前端的样式表为 h2 { color: black; },而另一个团队的则为 h2 { color: blue; },而这两个选择器都附加在同一页面上,就会冲突!

这不是一个新问题,但由于这些选择器是由不同的团队在不同的时间编写的,并且代码可能分散在不同的库中,因此更难避免。

多年来,有许多方法可以让 CSS 变得更易于管理。有些人选择使用严格的命名约定,例如 BEM,以确保选择器的范围是足够小的。其他一些人则使用预处理器,例如 SASS,其选择器嵌套可以用作命名空间。一种较新的方法是通过 CSS 模块 或各种 CSS-in-JS 库,以编程的方式写 CSS。某些开发者还会使用 shadow DOM 来隔离样式。

只要你选择一种能确保开发人员的样式互不影响的方案即可。

2. 共享组件库

上面我们提到,视觉一致性很重要,一种解决方法是应用间共享可重用的 UI 组件库。

说起来容易,做起来难。创建这样一个库的主要好处是减少工作量。此外,你的组件库可以充当样式指南,作为开发人员和设计师之间进行协作的重要桥梁。

第一个容易出错的点,就是过早地创建了太多组件。比如你试图创建一个囊括所有常见 UI 组件的组件库。但是,经验告诉我们,在实际使用组件之前,我们很难猜测组件的 API 应该是什么样的,强行做组件会导致早期的混乱。因此,我们宁愿让团队根据需求创建自己的组件,即使这最初会导致某些重复。

让 API 自然出现,一旦组件的 API 变得显而易见,就可以将重复的代码整合到共享库中。

与任何共享内部库一样,库的所有权和治理权很难分配。一种人认为,所有开发成员都拥有库的所有权,实际上这意味着没有人拥有库的所有权。如果没有明确的约定或技术远见,共享组件库很快就会成为不一致代码的大杂烩。如果取另一个极端,即完全集中式的开发共享库,后果就是创建组件的人与使用这些组件的人之间将存在很大的脱节。

我们见过的最好的合作方式是,任何人都可以为库贡献代码,但是有一个 托管者(一个人或一个团队)负责确保这些代码的质量、一致性和有效性。

维护共享库的人需要技术很强,同时沟通能力也要很强。

3. 跨微应用通信

关于微前端的最常见问题之一是如何让应用彼此通信。我们建议应该尽可能少地通信,因为这通常会引入不必要的耦合。

不过跨应用通信的需求还是存在的。

  1. 使用自定义事件通信,是降低耦合的一种好方法。不过这样做会使微应用之间的接口变得模糊。
  2. 可以考虑 React 应用中常见的机制:自上而下传递回调和数据。
  3. 第三种选择是使用地址栏作为通信桥梁,我们将在后面详细探讨 。

如果你使用的是 Redux,那么通常你会为整个应用创建一个全局状态。但如果每个微应用是独立的,那么每个微应用就都应该有自己的 Redux 和全局状态。

无论选择哪种方法,我们都希望我们的微应用通过消息或事件进行通信,并避免任何共享状态,以避免耦合。

你还应该考虑如何自动验证集成没有中断。功能测试是解法之一,但是由于实现和维护成本,我们倾向于只做一部分功能测试。或者,你可以实施消费者驱动接口,让每个微应用指定它对其他微应用的要求,这样你就不用实际将它们全部集成在一起并在浏览器中测试。

4. 后端通讯

如果我们有独立的团队独立处理前端应用,那么后端开发又是怎样的呢?

我们坚信全栈团队的价值,从界面代码一直到后台 API 开发,再到数据库和网站架构。

我们推荐的模式是 Backends For Frontends 模式,其中每个前端应用程序都有一个相应的后端,后端的目的仅仅是为了满足该前端的需求。BFF模式起初的粒度可能是每个前端平台(PC页面、手机页面等)对应一个后端应用,但最终会变为每个微应用对应一个后端应用。

这里要说明一下,一个后端应用可能有独立业务逻辑和数据库的,也可能只是下游服务的聚合器。 如果微前端应用只有一个与之通信的API,并且该API相当稳定,那么为它单独构建一个后台可能根本没有太大价值。指导原则是:构建微前端应用的团队不必等待其他团队为其构建什么事物。

因此,如果一个微前端用到的新功能需要后端接口的变更,那么这一前一后两个地方就应该交给一个团队来开发。

另一个常见的问题是,如何做身份验证和鉴权?

显然用户只需要进行一次身份验证,因此身份验证应该放在容器应用里。容器可能具有某种登录形式,通过该登录形式我们可以获得某种令牌。该令牌将归容器所有,并可以在初始化时注入到每个微前端中。最后,微前端可以将令牌发送到服务器,然后服务器进行验证。

5. 共享内容

虽然我们希望每个团队和微应用尽可能独立,但是有些事情还是会共享的。

上面提过共享组件库,但是对于这个小型应用而言,组件库会显得过大。因此,我们有一个小 的公共内容库,其中包括图像、JSON数据和CSS,这些内容被所有其他微应用共享。

还有一个重要的东西需要共享:依赖库。重复的依赖项是微前端的一个常见缺点。即使在应用程序之间共享这些依赖也非常困难,我们来讨论如何实现依赖库的共享。

第一步是选择要共享的依赖项。对我们已编译代码的分析表明,大约50%的代码是由 react 和 react-dom 贡献。这两个库是我们最核心的依赖项,因此如果把这两个库单独提取出来作为共享库,会非常有效。最后,它们是非常稳定和成熟的库,升级也很慎重,所以升级工作应该不会太困难。

至于如何提取,我们需要做的就是在 webpack 配置中将库标记为外部库(externals):

module.exports = (config, env) => {
  config.externals = {
    react: 'React',
    'react-dom': 'ReactDOM'
  }
  return config;
};

然后,用 script 向每个index.html 文件添加几个标签,以从共享内容服务器中获取这两个库:

<body>
  <div id="root"></div>
  <script src="%REACT_APP_CONTENT_HOST%/react.prod-16.8.6.min.js"></script>
  <script src="%REACT_APP_CONTENT_HOST%/react-dom.prod-16.8.6.min.js"></script>
</body>

作者:方应杭
链接:https://juejin.im/post/5d8adb…
来源:掘金

作者:上帝遗忘之子