Skip to content

qiankun 与 Vite 的兼容性问题 ​

在微前端架构中,qiankun 是一个非常流行的解决方案,它允许我们将多个微应用整合到一个主应用中。然而,当我们尝试将 qiankun 与 Vite 一起使用时,可能会遇到一些问题。本文将详细探讨这些问题的原因、原理,并提供解决方案。

1. runtime publicPath 的缺失 ​

publicPath:

  • 在 Webpack 中,publicPath 的作用是告诉 Webpack 在构建时生成的资源(如 JS、CSS、图片等)应该从哪里加载。通常情况下,publicPath 是在构建时就确定的,但在微前端架构中,微应用可能会被嵌入到不同的主应用中,导致其资源路径需要在运行时动态确定。
  • 在 Webpack 中,publicPath 通常通过以下方式设置
js
// webpack.config.js
module.exports = {
  output: {
    publicPath: 'http://localhost:3000/', // 这里是静态资源的基础路径
  },
};
  • 在微应用加载时,主应用可以通过 JavaScript 代码将 publicPath 设置为一个动态值__webpack_public_path__ 。这样就可以动态调整 runtime publicPath,从而实现在运行时动态加载资源。
ts
// main.js
if (window.__POWERED_BY_QIANKUN__) {
  // 如果是 qiankun 环境,动态设置 publicPath
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  • 而 Vite 2.x 版本并不支持这一特性。这导致了 qiankun 无法正确加载子应用的资源。

原因:

  • Vite 的构建方式与 Webpack 不同,Vite 使用原生 ES 模块,而 Webpack 则使用打包后的文件。
  • 由于缺乏 runtime publicPath,qiankun 无法在运行时确定资源的加载路径。

2. ESM 模块导致的沙箱失效问题 ​

1. 问题原理 ​

1.1 qiankun 沙箱机制 ​

qiankun 使用 JavaScript 沙箱来隔离微应用的全局作用域,主要通过以下方式实现:

javascript
// qiankun 源码简化版
class ProxySandbox {
  /** window 对象的代理 */
  private proxy: WindowProxy;
  
  /** 记录被修改的属性 */
  private updateValueMap = new Map<PropertyKey, any>();

  constructor() {
    const fakeWindow = Object.create(null);
    const proxy = new Proxy(fakeWindow, {
      set: (target: typeof window, p: PropertyKey, value: any) => {
        // 记录被修改的属性
        this.updateValueMap.set(p, value);
        return true;
      },
      get: (target: typeof window, p: PropertyKey) => {
        // 优先从修改记录中取值
        if (this.updateValueMap.has(p)) {
          return this.updateValueMap.get(p);
        }
        // 否则从真实 window 对象中取值
        const value = (window as any)[p];
        return value;
      },
    });

    this.proxy = proxy;
  }
}

1.2 ESM 模块特性 ​

ESM(ECMAScript Modules)模块具有以下特性:

  • 具有独立的作用域
  • 模块代码在严格模式下运行
  • 模块内的顶级 this 是 undefined
  • 模块会被 JavaScript 引擎静态分析,import/export 语句会被提升
javascript
// ESM 模块示例
import { someFunction } from './module';

// 这里的 window 是全局的 window,而不是 qiankun 的代理 window
window.someGlobalVar = 'value';

// 模块内的代码在严格模式下运行
const strictModeExample = () => {
  'use strict';
  // 已经在严格模式下
};

2. 沙箱失效的具体原因 ​

当 Vite 使用 ESM 模块时,会出现以下问题:

  1. 直接访问全局对象:
javascript
// ESM 模块中
window.someValue = 'test'; // 直接访问了原始 window,绕过了 qiankun 的代理

// qiankun 期望的方式
const qiankunWindow = window.__POWERED_BY_QIANKUN__ ? window : window.proxy;
qiankunWindow.someValue = 'test';
  1. 模块缓存问题:
javascript
// module.js
export const globalState = {
  value: window.someValue // 这里的 window 是原始 window
};

// 即使后续更新了 proxy window,该模块已经被缓存,不会重新执行

3. 解决方案 ​

3.1 使用 vite-plugin-qiankun 插件 ​

typescript
// vite.config.ts
import qiankun from 'vite-plugin-qiankun';

export default {
  plugins: [
    qiankun('microApp', {
      useDevMode: true
    })
  ]
};

这个插件通过以下方式解决问题:

  1. 注入运行时代码,处理 window 对象的访问
  2. 处理资源加载路径
  3. 提供辅助函数处理生命周期

3.2 手动处理 window 对象 ​

typescript
// window-proxy.ts
export const getGlobalWindow = () => {
  return window.__POWERED_BY_QIANKUN__ ? window : window.proxy;
};

// 在需要访问 window 的地方
import { getGlobalWindow } from './window-proxy';

const globalWindow = getGlobalWindow();
globalWindow.someValue = 'test';

3.3 使用运行时公共路径 ​

typescript
// public-path.ts
if (window.__POWERED_BY_QIANKUN__) {
  // 动态设置 publicPath
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  
  // 处理 window 对象
  const rawWindow = window;
  const proxyWindow = new Proxy(rawWindow, {
    get(target, key) {
      if (key === 'window' || key === 'self' || key === 'globalThis') {
        return proxyWindow;
      }
      return target[key];
    },
    set(target, key, value) {
      target[key] = value;
      return true;
    }
  });
  
  // 替换全局 window
  window = proxyWindow;
}

4. 最佳实践 ​

  1. 使用统一的 window 访问方式:
typescript
// utils/global-window.ts
export const getWindow = () => {
  if (window.__POWERED_BY_QIANKUN__) {
    return window.proxy || window;
  }
  return window;
};

// 在组件中使用
import { getWindow } from '@/utils/global-window';

const globalWindow = getWindow();
globalWindow.addEventListener('resize', handleResize);
  1. 避免直接在模块顶级作用域访问 window:
typescript
// ❌ 错误示例
const globalValue = window.someValue;

// ✅ 正确示例
function getValue() {
  const globalWindow = getWindow();
  return globalWindow.someValue;
}
  1. 使用生命周期钩子处理全局状态:
typescript
// main.ts
import { renderWithQiankun } from 'vite-plugin-qiankun/dist/helper';

renderWithQiankun({
  mount(props) {
    // 在这里初始化全局状态
    const globalWindow = getWindow();
    globalWindow.microAppState = props.initialState;
  },
  unmount() {
    // 清理全局状态
    const globalWindow = getWindow();
    delete globalWindow.microAppState;
  }
});

通过以上方案,我们可以有效解决 ESM 模块导致的沙箱失效问题,确保微应用在 qiankun 框架下正常运行。

3. 最终解决方案 ​

1. 修改 Vite.config.js ​

我们需要在 Vite 的配置文件中进行一些修改,以确保 qiankun 能够正常工作。以下是一个示例配置:

javascript
// vite.config.js
import { UserConfig } from 'vite';
import reactRefresh from '@vitejs/plugin-react-refresh';
import path from 'path';
import qiankun from 'vite-plugin-qiankun';

const useDevMode = true;

// https://vitejs.dev/config/
const baseConfig: UserConfig = {
  plugins: [
    ...(useDevMode ? [] : [reactRefresh()]),
    qiankun('qaSystem', { useDevMode }),
  ],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    fsServe: { // 设置静态文件目录
      root: path.join(process.cwd(), '../../'),
    },
    port: 5173,
    cors: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  define: { // 定义全局变量
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
    '__webpack_public_path__': 'window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__',
  },
  build: {
    assetsDir: 'assets',
    rollupOptions: {
      output: {
        assetFileNames: 'assets/[name].[hash][extname]',
        chunkFileNames: 'assets/[name].[hash].js',
        entryFileNames: 'assets/[name].[hash].js',
      },
    },
  },
};

export default ({ mode }) => {
  if (mode === 'development') {
    baseConfig.base = 'http://localhost:5173/';  // 改回5173端口
  } else {
    baseConfig.base = '/qaSystem/';  // 使用实际的部署路径
  }
  return baseConfig;
};

2. 修改子应用的 main.ts ​

在子应用中,我们需要确保生命周期函数(如 mount、bootstrap、unmount)通过 renderWithQiankun 函数暴露出来。以下是一个示例:

typescript
// main.ts
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper';

interface QiankunProps {
  container?: HTMLElement;
  initQuery?: string;
  modelType?: string;
}

let initProps: QiankunProps = {};

function render(props: QiankunProps = {}) {
  const { container } = props;
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    container ? container.querySelector("#root") : document.getElementById("root")
  );
}

renderWithQiankun({
  mount(props) {
    initProps = props;
    render(props);
  },
  bootstrap() {
    console.log("bootstrap");
  },
  unmount(props) {
    const { container } = props;
    ReactDOM.unmountComponentAtNode(container ? container.querySelector("#root") : document.getElementById("root"));
  },
  update(props) {
    initProps = props;
    render(props);
  },
});

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

3. 解决 public-path 问题 ​

在子应用中,我们需要确保 public-path 的兼容性。以下是一个示例:

typescript
// public-path.ts
// 兼容 webpack 和 vite
if ((window as any).__POWERED_BY_QIANKUN__) {
  // 声明 __webpack_public_path__ 
  declare const __webpack_public_path__: string;
  
  if (process.env.NODE_ENV === 'development') {
    // 开发环境 - 使用实际的微应用地址
    (window as any).__webpack_public_path__ = 'http://localhost:5173/';
  } else {
    // 生产环境 - 使用主应用注入的地址
    (window as any).__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }
}

/**
 * 获取资源的完整路径
 * @param path 资源路径,支持 @ 开头的别名路径
 * @returns 处理后的完整路径
 */
export function getAssetPath(path: string): string {
  // 处理 @ 开头的路径
  const processedPath = path.startsWith('@/') 
    ? path.replace('@/', '/src/') 
    : path;

  if ((window as any).__POWERED_BY_QIANKUN__) {
    // 如果是 qiankun 环境,添加 publicPath
    return `${(window as any).__webpack_public_path__}${processedPath.startsWith('/') ? processedPath.slice(1) : processedPath}`;
  }
  
  // 非 qiankun 环境,保持原路径
  return processedPath;
}

/**
 * 获取静态资源的完整路径
 * @param path 静态资源路径(相对于 public 目录)
 * @returns 处理后的完整路径
 */
export function getPublicPath(path: string): string {
  const processedPath = path.startsWith('/') ? path : `/${path}`;
  
  if ((window as any).__POWERED_BY_QIANKUN__) {
    return `${(window as any).__webpack_public_path__}${processedPath.slice(1)}`;
  }
  
  return processedPath;
} 


// 使用 getAssetPath 处理静态资源路径
const successIcon = getAssetPath('@/assets/toast/success.svg');

总结 ​

在将 Vite 与 qiankun 结合使用时,主要面临 runtime publicPath 和 ESM 模块导致的沙箱失效问题。通过适当的配置和使用社区提供的插件,我们可以有效地解决这些问题,从而实现微前端架构的顺利运行。