多项目融合方案

Aditya2024-01-29微前端Qiankun

引子

  • iframe标签嵌套,把n(n >=2)个项目部署在不同服务器上,切换 url 或者子父嵌套即可。
  • Nginx转发页面,变成 "多页面" 应用。难度中等,需要在外搭建一层,然后配置 Nginx 转发。
  • 微前端,利用single-spa、qiankun等微前端库。

iframe VS 微前端

使用iframe方式还是外跳的方式,都会有一段白屏加载时间,用户体验非常不好,而换成微前端的方式会发现跳转是非常丝滑的。

外跳方式:

外跳方式

微前端方式:

外跳-demo

iframe的优点:

  1. 完全隔离了css和js,避免了各个系统之间的样式和js污染。
  2. 可以在子系统完全不修改的情况下嵌入进来。

iframe的缺点

  1. 页面加载问题: 影响主页面加载,阻塞onload事件,本身加载也很慢,页面缓存过多会导致电脑卡顿。
  2. 布局问题:iframe必须给一个指定的高度,否则会塌陷。解决办法:子系统实时计算高度并通过postMessage发送给主页面,主页面动态设置高度,修改子系统或者代理插入脚本。有些情况会出现多个滚动条,用户体验不佳。

微前端的优点

  1. 加载快,可以将所有系统共用的模块提取出来,实现按需加载,一次加载,其他的复用。
  2. 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染。
  3. http请求少,服务器压力小。

微前端的缺点

  1. css和js需要制定规范,进行隔离。否则容易造成全局污染,尤其是vue的全局组件,全局钩子。
  2. 子系统需少量改动,是不影响子系统独立开发部署及其功能。

微前端

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontendsopen in new window

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端架构具备以下几个核心价值:

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

  • 独立开发、独立部署 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

  • 增量升级

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

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

qiankun

qiankun 是一个基于 single-spaopen in new window微前端open in new window实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

安装

yarn add qiankun # 或者 npm i qiankun -S

官网例子

主应用注册微应用并启动

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'reactApp',
    entry: '//localhost:3000',
    container: '#container',
    activeRule: '/app-react',
  },
  {
    name: 'vueApp',
    entry: '//localhost:8080',
    container: '#container',
    activeRule: '/app-vue',
  },
  {
    name: 'angularApp',
    entry: '//localhost:4200',
    container: '#container',
    activeRule: '/app-angular',
  },
]);
// 启动 qiankun
start();

微应用open in new window 改造

nuxt作为微应用改造

由于nuxt框架没有入口文件支持改造,引入一个第三方库进行改造Nuxt-Micro-Front-End

  1. 添加 @femessage/nuxt-micro-frontend 依赖到你的项目中

    yarn add @femessage/nuxt-micro-frontend -D 
    
    # or npm install @femessage/nuxt-micro-frontend
    
  2. 添加 @femessage/nuxt-micro-frontendnuxt.config.jsmodules 属性中

    {
      modules: [
        // 一般使用
        '@femessage/nuxt-micro-frontend',
    
        // 带上模块参数
        ['@femessage/nuxt-micro-frontend', { /* module options */ }]
      ]
    }
    
  3. 在项目根目录创建入口文件 mef.js

    export function bootstrap () {
    console.log('nuxt app bootstraped')
    }
    
    export async function mount (render, props) {
    props.onGlobalStateChange((state, prev) => {
    console.log('state change from nuxt', state)
    })
    await render()
    }
    
    export function mounted (vm) {
    console.log('mounted', vm)
    }
    
    export function update (vm, props) {
    console.log(props)
    }
    
    export function beforeUnmount (vm, props) {
    console.log(vm)
    }
    
    export function unmount () {
    console.log('nuxt app unmount')
    }
    
  4. 配置nuxt.config.js

    const isMicro = process.env.MODE === 'MICRO'
    
    export default { 
      ....
      // 入口文件配置
      MFE: isMicro ? {
        path: resolve(__dirname, './mfe.js'),
        force: true
      }: null,
      router: {
        extendRoutes(routes, resolve) {
          if(!isMicro) {
            return
          }
          routes.forEach(route => {
            // 添加路由前缀 与主应用的activeRule一致
            route.path = '/micro_nuxt' + route.path
          })
        }
    	},
      ....
      // 添加模块
      modules: [].concat(isMicro ? ['@femessage/nuxt-micro-frontend']: []),
      ....
    }
    
    

微应用跨域问题

由于 qiankun 是通过 fetch 去获取微应用的引入的静态资源的,所以必须要求这些静态资源支持跨域open in new window

官网给的例子是修改webpack

const { name } = require('./package');
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};

我这边直接在主应用代理了微应用请求。

nuxt下script脚本报错问题

// nuxt config
head: {
    title,
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
    ],
    link: [
      // { rel: 'icon', type: 'image/x-icon', href: '/DeepWise_16x16.png' }
    ],
    script: [
      { src: `/js/mathjax/MathJax.js?config=TeX-MML-AM_CHTML`},
    ]
  },

子应用对脚本是相对路径引用,那么到主应用路径匹配就会有问题

[import-html-entry]: error occurs while executing normal script http://localhost:8080/js/mathjax/MathJax.js?config=TeX-MML-AM_CHTML

按照老办法就是主应用搞代理。

这种方式会让脚本资源正确引用,但也会引用两个不存在的资源,导致子应用报错。

问题定位了好久都没定位到为什么会去请求,如果资源链接换成CDN也会去请求,但相应的有对应资源不会报错。

后来想到了微前端数据共享,挂载在主应用的window上也可以,所以就在主应用去加载脚本去解决该问题。

// umi config
headScripts: [`/researchPage/js/mathjax/MathJax.js?config=TeX-MML-AM_CHTML`]

微应用打包之后 css 中的字体文件和图片加载 404

原因是 qiankun 将外链样式改成了内联样式,但是字体文件和背景图片的加载路径是相对路径。

css 文件一旦打包完成,就无法通过动态修改 publicPath 来修正其中的字体文件和背景图片的路径。

主要有以下几个解决方案:

  1. 所有图片等静态资源上传至 cdncss 中直接引用 cdn 地址(推荐
  2. 借助 webpackurl-loader 将字体文件和图片打包成 base64(适用于字体文件和图片体积小的项目)(推荐
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|webp|woff2?|eot|ttf|otf)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {},
          },
        ],
      },
    ],
  },
};
  1. 借助 webpackfile-loader ,在打包时给其注入完整路径(适用于字体文件和图片体积比较大的项目)

    // nuxt config
    const publicPath =  isDev ? '_nuxt/' : 'researchPage/public/'
    ...
    build: {
        vendor: ['jquery'],
        transpile: [/^element-ui/],
        /*
        ** You can extend webpack config here
        */
        publicPath: isDev ? '' : publicPath,
        extend (config, ctx) {
          if(!isDev){ // 微前端子应用字体路径
            const rules = config?.module?.rules || []
            for(let i=0, len = rules.length; i < len; i++){
              if(String(rules[i].test) == String(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i)){
                config.module.rules[i].use[0].options.publicPath =  '/' + publicPath
              }
            }
          }
        }
    }
    
    
    
    

目前在项目中利用第三种方式,注入完整的路径。

Application died in status NOT_MOUNTED: Target container with #container not existed while xxx mounting!

这个报错通常出现在主应用为 vue 时,容器写在了路由页面并且使用了路由过渡效果,一些特殊的过渡效果会导致微应用在 mounting 的过程中容器不存在,解决办法就是换成其他的过渡效果,或者去掉路由过渡。

官方给出的错误信息,路由跳转的过渡效果会导致微应用在 mounting 的过程中容器不存在。

造成这样的原因是从全院到专病的时候有几个接口请求过慢导致的loading。

addGlobalUncaughtErrorHandler(event => {
  console.log(event);
  // Target container with #Research not existed while research mounting!
  // Ignore ResizeObserver error
  if (/Research/.test(event.message)) {
    window.location.reload();
  }
});

我这边的处理方案就是捕捉一下子应用错误,匹配相应的错误然后reload,体验不太好。

登录Token问题

跟后端商讨的登录问题是request中携带token,主应用登录后通过sessionStorage保存token,子应用读取。

首先遇到一个问题就是子应用拿到了token,但是请求却没携带token,经过定位,是nginx转发问题。

underscores_in_headers,这个参数默认值为:off,即默认忽略带下划线的 header。

server {
  ...
  underscores_in_headers on;
  ...
}

修改了nginx后,发现请求还是不携带,后来想到ingress层是否也需要配置一下, 最后让运维同学在ingress也加一下这个配置,最终请求携带token了。

别问后端为什么不直接从cookie里拿,理由很多。

第二个问题就是sessionStorage和localStorage的问题了。

一直以来,我所以为的 sessionStorageopen in new window 的生命周期是这样的:在 sessionStorage 中存储的数据会在当前浏览器的同一网站的多个标签页中共享,并在此网站的最后一个标签页被关闭后清除。注意:这是错误的。

MDN 查了一下,上面是这么说的:

...data stored in sessionStorage gets cleared when the page session ends...Opening a page in a new tab or window will cause a new session to be initiated, which differs from how session cookies work.

通过点击链接(或者用了 window.open)打开的新标签页之间是属于同一个 session 的,但新开一个标签页总是会初始化一个新的 session,即使网站是一样的,它们也不属于同一个 session。

微前端部署

此次开发微前端过程中,最麻烦的一点在于没想好部署方案,导致后续来来回回去折腾。

最开始的部署方案是服务器部署,pm2去接管主应用的node服务,静态资源代理都写在了node层,子应用的nginx配置微调。

后来又改成了K8s部署,第一版的方案是打一个单镜像,包含了所有项目,经过了漫长的折腾,终于处理好了所有资源请求问题,也成功部署在了K8s,但这种方案有一个很大弊端,任何一个应用有更新都要重新打镜像,而且没有打通cicd,导致我成了一个“人肉CICD”。

这一版开发过程中的收获是把所有资源代理基本捋清楚了,把原本在node层代理全部干掉了,为最终方案扫清了障碍。

我们最终方案是所有应用单独部署,在ingress写一层代理,这样更新的时候互不干涉。

微前端nginx

写在最后

完结散花

Last Updated 2024/12/27 11:36:49