likes
comments
collection
share

Vite + Qiankun + Module Federation,微前端项目升级🚀

作者站长头像
站长
· 阅读数 14

起源

原有vue-cli + qiankun微前端项目,分为一个主应用,10多个子应用。由于历史原因存在vue2、vue3多个版本,并通过vue-cli构建,由于qiankun的沙箱隔离,每个子应用存在重复的依赖构建。所以将原有项目通过webpack5的module federation来解决,同时项目本地构建需要几分钟的漫长时间,所以采用vite来达到秒级构建。

注意

  1. 我们为了防止重复依赖构建,其实可以用monorepo的pnpm来简单处理,好处是安包快省磁盘,解决了幽灵和分身依赖问题,方便统一管理和依赖公用,缺点就是无法独立管理。用federation其实也方便了不用发到npm库。
  2. 如果qiankun和mf(module federation)的结合, 由于子应用需要构建到umd,就无法使用shared来共享依赖,那么暴露出去的必要依赖vue无法使用,那么mf将失效。如果不使用qiankun,我们可以在src下创建index.js引用main.js,防止main.js提前加载。
// src/index.js
import("./main.js");

// vue.config.js
module.exports = {
  pages: {
    index: {
      entry: "./src/index.js",
    },
  },
}

host(vite) + remote(vite) + qiankun + Federation(亲测有效)

相关

vite由于在dev模式下不支持chunk代码,在开发环境无法静态产出remoteEntry.js, 所以在host可以dev模式开发,remote端的只能通过vite build -w, 但这种更新是很缓慢的。

所以一般在host端可以vite、webpack等构建,在remote端尽量不要使用vite构建。

vite + qiankun

在vite与qiankun的结合,我们可以用vite-plugin-qiankun

// vite.config.ts

import qiankun from 'vite-plugin-qiankun';

export default {
  // 这里的 'myMicroAppName' 是子应用名,主应用注册时AppName需保持一致
  plugins: [qiankun('myMicroAppName')],
  // 生产环境需要指定运行域名作为base
  base: 'http://xxx.com/'
}

// main.ts
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper';

// some code
renderWithQiankun({
  mount(props) {
    console.log('mount');
    render(props);
  },
  bootstrap() {
    console.log('bootstrap');
  },
  unmount(props: any) {
    console.log('unmount');
    const { container } = props;
    const mountRoot = container?.querySelector('#root');
    ReactDOM.unmountComponentAtNode(
      mountRoot || document.querySelector('#root'),
    );
  },
});

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render({});
}

vue-cli + qiankun 老项目构建到vite + qiankun

  1. 安装vite的依赖后,并配置基础的vite.config.js后,并修改script脚本,vite build后我们会发现页面是空白,这个时候原因是public的index.html需要迁移到根目录。
  2. 由于老项目,会存在大量require, vite是只支持esm,我们可以使用 vite-plugin-commonjs插件写入pulgin。
  3. 启动项目后,我们会发现图片资源丢失,我们可以通过server.origin指定当前服务的地址即可解决。
  4. 修改vite的环境变量
  5. 后续结合mf后,我们将修改devServer的静态目录

@originjs/vite-plugin-federation 结合module Federation

host 生产端

// vite.config.js
import federation from "@originjs/vite-plugin-federation";
export default {
    plugins: [
        federation({
            name: 'remote-app',
            filename: 'remoteEntry.js',
            // Modules to expose
            exposes: {
                './Button': './src/Button.vue',
            },
            shared: ['vue']
        })
    ]
}

remote 消费端

// vite.config.js
import federation from "@originjs/vite-plugin-federation";
export default {
    plugins: [
        federation({
            name: 'host-app',
            remotes: {
                remote_app: "http://localhost:5001/assets/remoteEntry.js",
            },
            shared: ['vue']
        })
    ]
}

注意assets是默认vite的打包后的目录, host通过请求访问remoteEntry。所以只要确保生产端能访问remoteEntry即可。在webpack运行在本地时可以chunk代码,并全量构建bundle.js,所以我们只要刷新页面更新。Vite Dev模式是bundless的,所以我们只能通过vite build --watch更新。

静态导入与动态导入

// dynamic import
const myButton = defineAsyncComponent(() => import('remote/myButton'));
app.component('my-button' , myButton);
// or
export default {
  name: 'App',
  components: {
    myButton: () => import('remote/myButton'),
  }
}

// static import
import myButton from 'remote/myButton';
app.component('my-button' , myButton);
// or
export default {
  name: 'App',
  components: {
    myButton: myButton
  }
}

如果静态导入,我们需要安装vite-plugin-top-level-await来保证import导入完毕,在某些情况下使用动态导入也不一定保证能请求到。

import topLevelAwait from "vite-plugin-top-level-await";

export default defineConfig({
  plugins: [
    topLevelAwait({
      // The export name of top-level await promise for each chunk module
      promiseExportName: "__tla",
      // The function to generate import names of top-level await promise in each chunk module
      promiseImportName: i => `__tla_${i}`
    })
  ]
});

总结

如果项目使用了qiankun,目前尽量别使用module federation, 不过我们可以通过vite的生态来快速解决。我们也可以只用module federatoin来实现微前端,但是我们需要重新实现沙箱隔离。