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 模块时,会出现以下问题:
- 直接访问全局对象:
javascript
// ESM 模块中
window.someValue = 'test'; // 直接访问了原始 window,绕过了 qiankun 的代理
// qiankun 期望的方式
const qiankunWindow = window.__POWERED_BY_QIANKUN__ ? window : window.proxy;
qiankunWindow.someValue = 'test';- 模块缓存问题:
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
})
]
};这个插件通过以下方式解决问题:
- 注入运行时代码,处理 window 对象的访问
- 处理资源加载路径
- 提供辅助函数处理生命周期
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. 最佳实践
- 使用统一的 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);- 避免直接在模块顶级作用域访问 window:
typescript
// ❌ 错误示例
const globalValue = window.someValue;
// ✅ 正确示例
function getValue() {
const globalWindow = getWindow();
return globalWindow.someValue;
}- 使用生命周期钩子处理全局状态:
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 模块导致的沙箱失效问题。通过适当的配置和使用社区提供的插件,我们可以有效地解决这些问题,从而实现微前端架构的顺利运行。