微前端入门

最近在学习一些微前端的知识,转载一片我的以为比较好的。javascript

基本概念

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

微前端实现的一些方法

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>

而后配置 nginxjava

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

这是至关标准的服务器端应用。咱们之因此能够称其为微前端,是由于咱们让每一个页面独立,可由一个独立的团队交付。node

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

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

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

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

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

3. iframe集成

iframe 是集成的最简单方式之一。本质上来讲,iframe 里的页面是彻底独立的,能够轻松构建。并且 iframe 还提供了不少的隔离机制。git

<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不够灵活。它会使路由、历史记录和深层连接变得更加复杂,而且很难作成响应式页面。github

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... 来源:掘金

相关文章
相关标签/搜索