微前端架构实战:qiankun子应用隔离与跨应用通信方案

微前端将大型单体前端应用拆分为可独立开发、部署的子应用,每个子应用可以使用不同的技术栈。qiankun基于single-spa封装,通过HTML Entry加载子应用、JS沙箱隔离运行环境、CSS样式隔离防止冲突。微前端的工程挑战集中在子应用隔离、通信机制和公共依赖处理三个方向。

主应用注册与子应用加载

主应用通过registerMicroApps注册子应用列表,qiankun在路由匹配时自动加载对应子应用的HTML、CSS和JS资源。子应用导出生命周期函数供主应用调用。

主应用配置:

// main-app/src/micro-apps.ts
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun';

const microApps = [
  {
    name: 'user-app',
    entry: '//localhost:7101',
    container: '#sub-app-container',
    activeRule: '/user',
    props: { token: localStorage.getItem('token'), apiBaseUrl: '/api' }
  },
  {
    name: 'order-app',
    entry: '//localhost:7102',
    container: '#sub-app-container',
    activeRule: '/order',
    props: { token: localStorage.getItem('token'), apiBaseUrl: '/api' }
  }
];

registerMicroApps(microApps, {
  beforeLoad: [(app) => { console.log(`[主应用] ${app.name} 开始加载`); return Promise.resolve(); }],
  beforeMount: [(app) => { console.log(`[主应用] ${app.name} 开始挂载`); return Promise.resolve(); }],
  afterUnmount: [(app) => { console.log(`[主应用] ${app.name} 已卸载`); return Promise.resolve(); }]
});

setDefaultMountApp('/user');
start({ prefetch: true, sandbox: { strictStyleIsolation: false, experimentalStyleIsolation: true } });

子应用生命周期适配

子应用需要导出bootstrap、mount、unmount三个生命周期函数。以React子应用为例:

// user-app/src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

let root: ReactDOM.Root | null = null;

function render(props: any) {
  const { container } = props;
  const mountNode = container ? container.querySelector('#root') : document.getElementById('root');
  root = ReactDOM.createRoot(mountNode);
  root.render(<App {...props} />);
}

export async function bootstrap() { console.log('[user-app] bootstrap'); }

export async function mount(props: any) {
  console.log('[user-app] mount', props);
  render(props);
}

export async function unmount(props: any) {
  console.log('[user-app] unmount');
  if (root) { root.unmount(); root = null; }
}

if (!(window as any).__POWERED_BY_QIANKUN__) { render({}); }

子应用Webpack配置需要设置打包格式为UMD,并允许跨域:

module.exports = {
  output: { library: 'user-app', libraryTarget: 'umd', chunkLoadingGlobal: 'webpackJsonp_user_app' },
  devServer: { headers: { 'Access-Control-Allow-Origin': '*' } }
};

JS沙箱与样式隔离机制

qiankun的JS沙箱有两种模式:LegacySandbox(单实例,通过Proxy劫持window对象)和ProxySandbox(多实例,每个子应用有独立的fakeWindow)。样式隔离通过experimentalStyleIsolation在子应用所有样式选择器前添加属性前缀:

/* 隔离前的子应用样式 */
.container { padding: 20px; }
.title { color: #333; }

/* 开启experimentalStyleIsolation后的编译结果 */
div[data-qiankun="user-app"] .container { padding: 20px; }
div[data-qiankun="user-app"] .title { color: #333; }

注意:experimentalStyleIsolation无法隔离通过JavaScript动态插入的样式(如styled-components运行时注入的style标签)。此类场景需要配合strictStyleIsolation使用Shadow DOM,但Shadow DOM会导致第三方组件库的弹窗appendToBody行为异常,需要逐个排查。

跨应用通信:initGlobalState

qiankun提供initGlobalState实现主应用与子应用的全局状态共享。通信基于发布订阅模式,主应用初始化GlobalState后通过props传递给子应用:

// 主应用 - 初始化全局状态
import { initGlobalState } from 'qiankun';

const actions = initGlobalState({ user: null, theme: 'light', locale: 'zh-CN' });

actions.onGlobalStateChange((state, prev) => {
  console.log('[主应用] 全局状态变更:', state);
});

actions.setGlobalState({ user: { id: 1, name: '张三' }, theme: 'dark' });
// 子应用 - 接收并监听全局状态
export async function mount(props: any) {
  props.onGlobalStateChange((state, prev) => {
    console.log('[user-app] 收到全局状态:', state);
    if (state.user) {
      root?.render(<App user={state.user} theme={state.theme} />);
    }
  }, true);

  props.setGlobalState({ theme: 'dark' });
  render(props);
}

公共依赖提取与共享方案

多个子应用可能依赖相同的库(React、antd、lodash等),重复加载浪费带宽和内存。通过Webpack Module Federation或外部CDN加载实现共享:

// 方案一:externals + CDN
module.exports = {
  externals: { 'react': 'React', 'react-dom': 'ReactDOM', 'antd': 'antd' }
};

// 主应用index.html引入CDN
<script src="https://cdn.example.com/react/18.2.0/react.production.min.js"></script>

// 方案二:Module Federation(Webpack 5+)
// 主应用 - Host
new ModuleFederationPlugin({
  name: 'host',
  shared: {
    react: { singleton: true, requiredVersion: '^18.2.0' },
    'react-dom': { singleton: true, requiredVersion: '^18.2.0' }
  }
})

路由冲突与子应用路由适配

子应用使用BrowserRouter时,basename需要与主应用的activeRule一致,避免路由冲突:

import { BrowserRouter } from 'react-router-dom';

function App({ basename }: { basename?: string }) {
  return (
    <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/user' : '/'}>
      <Routes>
        <Route path="/" element={<UserList />} />
        <Route path="/detail/:id" element={<UserDetail />} />
        <Route path="/settings" element={<UserSettings />} />
      </Routes>
    </BrowserRouter>
  );
}

资源预加载与加载性能优化

qiankun默认开启prefetch,在浏览器空闲时间预加载未激活的子应用资源。对于体积较大的子应用,可以手动控制预加载策略:

start({
  prefetch: (apps) => apps.filter(app => app.name === 'user-app'),
  excludeAssetFilter: (assetUrl) => assetUrl.includes('.woff') || assetUrl.includes('.ttf')
});

// 按需加载
import { loadMicroApp } from 'qiankun';
const microApp = loadMicroApp({
  name: 'dashboard-widget',
  entry: '//localhost:7103',
  container: '#widget-container',
  props: { widgetId: 'sales-overview' }
});
microApp.unmount();

常见问题排查

子应用加载白屏:检查子应用entry是否可访问,CORS是否配置。浏览器Network面板查看子应用HTML和JS是否加载成功。若子应用资源路径错误,在Webpack配置output.publicPath为子应用完整URL。

样式被主应用覆盖:开启experimentalStyleIsolation。若使用antd等组件库,检查是否使用了CSS-in-JS方案导致样式未被子应用容器包裹。

子应用卸载后内存泄漏:确保unmount中清理了所有事件监听器、定时器和全局变量。检查React组件的useEffect cleanup是否完整执行。

微前端的拆分边界应基于业务域而非技术层。过细的拆分(按页面拆)会增加通信成本和加载开销。实际项目中,按业务模块拆分3-5个子应用是比较合理的粒度,配合公共依赖共享和路由预加载,可以兼顾开发效率和用户体验。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/wei-qian-duan-jia-gou-shi-zhan-qiankun-zi-ying-yong-ge-li/

(0)
小编小编
上一篇 8小时前
下一篇 8小时前

相关推荐