🌈 微前端方案调研

1,337 阅读7分钟

需求背景

  • 公司各开放平台接入网联平台,网联平台逐渐变成“巨石”应用
  • 各平台接入后的开发方式,如何共享公共资源,如平台网页的头部、底部
  • 如何统一设计风格
  • 是否有必要统一技术栈

什么是微前端

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently.

译:与多个团队一起构建一个可以独立发布功能的现代web应用程序的技术、策略和诀窍。

The idea behind Micro Frontends is to think about a website or web app as a composition of features which are owned by independent teams. Each team has a distinct area of business or mission it cares about and specialises in. A team is cross functional and develops its features end-to-end, from database to user interface.

译:微前端背后的设计思想:网站或web应用由独立团队提供的特性组合而成。每个团队各自关注不同的业务或目标,每个团队跨职能从后端到前端完成特性开发。

核心思想:

  • 技术栈无关
  • 隔离团队代码
  • 确定团队前缀(即Namespace,减少命名冲突之类的问题)
  • 倾向于浏览器原生API而非自定义API(父子应用机制,不是自建发布/订阅系统)
  • 韧性,(即保持特性可用性,渐进式提升体验)

微前端方案介绍

  • qiankun

  • Single-spa

  • Ara-framework

  • webpack5 模块联邦

方案对比

因各个方案都涉及到publicPath的问题,即加载静态资源的路径,建议将静态资源都放到CDN管理。

Portal应用提供公共资源

优点:

  • 微应用接入无感
  • SPA,用户体验上是一个整体站点
  • 各微应用和portal应用独立部署
  • 父子关系的架构更容易理解,数据流转更清晰

缺点:

  • 需提前知道微应用的部署地址(nginx反向代理解决)
  • 各微应用需使用运行时publicPath或构建时指定静态资源位置
  • 父子应用样式会相互影响(qiankun有实验性的隔离配置,也可以利用Namespace)
  • 不能通过独立域名访问到子应用(保证有公共资源的情况下,或子应用在非微前端环境下做降级处理)

公共资源作为微应用

优点:

  • 公共资源独立发布,可以只更新公共资源
  • 各应用可以通过独立的域名访问
  • 各应用不需要处理publicPath的问题

缺点:

  • 公共资源作为独立站点发布,部署/维护麻烦(在咱们的CI/CD环境下还好)
  • 仓库较多,特别是公共资源较多的情况下
  • 应用开发麻烦,作为主应用,需要更多额外的编码
  • 与公共资源的通信变成子-->父,不好理解
  • 对公共资源(子应用)的样式影响可能性增加

模块联邦(Webpack 5)

优点:

  • 无需引入新框架,学习成本更小
  • 像引入第三方库一样方便
  • 无需维护额外的仓库,各个应用的资源都可以相互共享
  • 应用间松耦合,各应用平行的关系

缺点

  • 需升级到Webpack 5.x版本
  • 技术栈必须保持一致
  • 应用内必须实现降级方案(加载资源失败的场景),新应用接入需要修改实现
  • publicPath需为绝对路径

组件库+模块联邦

将组件库以服务的形式发布,以模块联邦的方式调用,实现统一的更新。公共资源以组件方式提供,需要考虑组件如何与应用进行通信。

Demo演示

以下内容基于qiankun框架

开发流程

image.png

部署方案

image.png

静态资源交互

image.png

其他

实践中遇到的问题

方案二:公共资源作为微应用

  • 状态问题:共用的一些信息,如用户状态,权限等,每个独立应用里都需要维护,代码冗余。如果还要再通信,会更麻烦,比如在一个微应用(用户信息维护)中修改用户头像,在另一个微应用(页面头部)需要更新头像,会带来额外的通信负担,在一个主应用里做通用功能会减少跨应用的通信。
  • 路由问题:如果是在单页应用内打开,共用页面的需要操控主应用的路由,且打开的微应用也需要在主应用内注册,不合理。在其他窗口内打开的话,需要把微应用做成一个“完整”的应用。
  • 仓库问题:要维护多个仓库,且会存有冗余代码。带来额外的维护难度。
  • umi.js 3.x支持微前端插件,而我们当前的版本是2.x。因umi.js的高度封装,对webpack的定制比较麻烦,升到3.x又可能引入其他问题。最方便的方案还是把当前的应用当作微应用。

管理平台portal应用架构设计

微前端下如何管理菜单的权限?在这种架构下,管理平台portal负责大板块和公共管理需求的权限控制,如为qingmobile管理员分配只有qingmobile管理权限+部分公共业务管理权限的角色,为qingai管理员分配只有qingai管理权限+部分公共业务管理权限的角色;由各微应用负责管理各自的权限控制,如控制具体的小程序审核权限,企业审核权限等。

image.png

微应用无法加载时页面展示优化

当前版本中,没有加载失败的处理方案,因此我通过自定义fetch,在失败时返回自定义的异常页面,这样来当作加载失败的降级方案。

  fetch(url: string) {
      const oldFecth = window.fetch;
      const appname = data.find((item: any) => item.entry === url);
      return new Promise((resolve, reject) => {
          oldFecth.apply(window, [url]).then((...res) => {
            resolve(...res);
          }).catch((e) => {
              // 异常时,模拟fetch响应,返回一个失败的页面
              resolve({
                  text: () => `
                      <script entry>
                      const render = (props) => {
                      var container = props.container;
                      container.innerHTML = '加载失败!';
                      return Promise.resolve();
                      };
                
                        (global => {
                          global["${appname.name}"] = {
                            bootstrap: () => {
                              return Promise.resolve();
                            },
                            mount: (props) => {
                              props.setLoading(false);
                              return render(props);
                            },
                            unmount: (props) => {
                              props.container.innerHTML = '';
                              return Promise.resolve();
                            }
                          }
                        })(window);
                      </script>
                      `
                  });
              });
          });
      }

一些静态资源如何处理

css会被qiankun提取出来,以style标签的方式插入到文档里,所以css文件不会有这样的问题 常用的如图片、字体等,在构建之后无法通过运行时publicPath修改路径,需要使用CDN。

CDN的方案在当前部署场景下难以满足需求,主要体现在两点:

  1. CDN服务器及其资源管理。我们可以申请BOS作为CDN服务器,在构建时将静态资源上传到BOS。然而每次都上传,必然会占用更多的资源或者覆盖之前的上传,这会带来额外的资源管理负担。如果采用覆盖上传的方案,多个环境(feature/dev/master)下如何保证资源一致性,这也是一个问题。
  2. 环境迁移时,CDN静态资源是否需要迁移。当前的发布系统在环境发布时,不会再有构建的过程。这就需要在发布时将CDN资源也迁移到对应的环境,如从sit环境迁移到perf环境。这会增加发布系统的工作。

既然CDN是利用域名区分资源,我们能否给各个微应用分配一个固定前缀来解决静态资源的问题?

方案如下:

假设有微应用A和主应用P:

  1. 为微应用A分配路由前缀:/assets-A,A在构建时,将output.publicPath设置为/assets-A,这样构建出来的所有静态资源引用便以‘/assets-A’开头。
  2. 微应用A部署时,通过nginx rewrite将路由中的/assets-A移除,这样微应用A的静态资源即可正常访问。
  3. 在集成到主应用P中时,P中根据分配出去的路由前缀生成nginx配置,将请求转发到对应的微前端上。再经过微应用的路由重写,即可访问到正确的资源。
  4. 同样,开发环境也在devServer中配置对应的路由重写和转发规则即可。
  5. 环境迁移时,通过环境变量,动态修改nginx配置。

微应用配置规则

  1. 主应用为父应用分配APPName,如‘sub’
  2. 子应用路由以‘/sub’开始,在nacos中配置时应该严格按照此规则
  3. 子应用构建时,以'/assets-sub'作为前缀(即配置webpack publicPath=‘/assets-sub’),并在nginx配置中将/assets-sub请求重写为/
  4. 父应用在nginx配置中,将命中'/assets-sub'的请求转发到对应的服务器(通过环境变量动态修改)

微应用接入注意事项

微应用静态资源需支持跨域

微前端框架会跨域加载微应用的html,js等,所以需支持跨域访问。如果接入微应用开发环境进行调试,则开发环境也需要支持跨域

nginx配置如下:

 location / {
 # * 需要在生产环境修改为具体域名
 add_header Access-Control-Allow-Origin *;
 add_header Access-Control-Allow-Methods 'GET, OPTIONS';
 add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

 if ($request_method = 'OPTIONS') {
 return 204;
 }

 try_files $uri $uri/ /index.html;
 }

静态资源访问

开发环境

开发环境通过运行时publicPath,在根目录下添加public-path.js,并在入口文件(一般是main.js)中顶部(第一行)导入。public-path.js内容如下:

// public-path.js

if (window.__POWERED_BY_QIANKUN__) {
    if (process.env.NODE_ENV === "development") {
    // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
    // @ts-ignore
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
}
生产环境

因为静态资源的问题,故在生产环境不采用运行时publicPath的方案,而是通过给微应用分配前缀的方式,通过路由重写和反向代理,来解决资源加载的问题。

  • 在构建时,配置publicPath,代码示例如下:
  // 示例为create-react-app中,通过@rescripts/cli来修改webpack配置,其他环境参考修改

    const { name } = require('./package');
    module.exports = {
    webpack: config => {
    // publicPath为主应用分配的固定前缀,为防止冲突,注意命名
    config.output.publicPath = '/assets-micro-app1/';
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    config.output.globalObject = 'window';
    return config;
    },
    ...
}

此时,打包后的文件引用已带上前缀/assets-micro-app1前缀;

  • nginx配置路由重写,将静态资源再次指向微应用服务器。示例代码如下:
  location ^~/assets-micro-app1/ {
  proxy_set_header  Host             $host;
  proxy_set_header  X-Real-IP        $remote_addr;
  proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
  proxy_set_header  X-NginX-Proxy    true;

  add_header Access-Control-Allow-Origin  *;
  add_header Access-Control-Allow-Methods 'GET, OPTIONS';
  add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

  if ($request_method = 'OPTIONS') {
  return 204;
  }
  rewrite ^/assets-qing-card/(.*)$ /$1 break;
  }

后端API访问

如果微应用内不写死域名,通过反向代理的方式转发到对应的域名,在挂载到主应用之后,请求会发往主应用所在域名,如果主应用没有配置反向代理到对应的服务器,则会导致请求404。即子应用没法正确访问资源。

两种方式:

  1. 写死域名配置,所有请求添加域名作为前缀。
  2. 同静态资源访问的方案,以主应用分配的固定前缀作为请求前缀,这样主应用会将该请求转发到微应用,微应用再通过路由重写,即可正确反向代理对应的服务器。

相关链接🔗