4、主应用搭建:创建React主应用、配置qiankun、注册子应用、全局状态管理

好,咱们进入实战环节。

前面聊了那么多微前端的概念和架构选型,说白了都是为了这一刻。今天我们就亲手搭一个主应用出来。我自己的习惯是,先把主应用跑起来,再慢慢往里填子应用。这样心里踏实。

4.1 创建React主应用

先别想太复杂。主应用本质上就是一个普通的React项目。我用的是Create React App,当然你用Vite也行,看团队习惯。

npx create-react-app micro-main-app
cd micro-main-app
npm start

嗯,这里要注意一点:主应用不需要太重的UI框架。它就是个容器,负责加载子应用、管理路由、做全局状态。你想想看,如果主应用本身又大又重,那微前端带来的性能优势就大打折扣了。

我个人习惯把主应用的代码控制在500行以内。超过这个数,就该考虑是不是把某些逻辑下沉到子应用里去了。

4.2 安装并配置qiankun

qiankun是蚂蚁开源的一个微前端框架。说实话,它把很多复杂的东西都封装好了。你只需要关心两件事:注册子应用生命周期管理

安装很简单:

npm install qiankun --save

然后在主应用的入口文件里,我们来做配置。我直接贴代码,你照着抄就行:

// src/index.js
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'react-app', // 子应用名称,唯一
    entry: '//localhost:3001', // 子应用入口地址
    container: '#sub-app-container', // 挂载点
    activeRule: '/react-app', // 路由匹配规则
  },
  {
    name: 'vue-app',
    entry: '//localhost:3002',
    container: '#sub-app-container',
    activeRule: '/vue-app',
  },
]);

start();

这里有个坑,我当年踩过。子应用的entry地址,不要写成相对路径。qiankun内部是通过fetch去拉取子应用的HTML的,相对路径在跨域场景下会出问题。我建议直接用完整的URL,包括端口号。

注意:子应用必须开启跨域支持。否则qiankun无法加载。在子应用的webpack配置里加上:
devServer: {
  headers: {
    'Access-Control-Allow-Origin': '*',
  },
}

4.3 注册子应用的生命周期

光注册还不够。子应用需要暴露三个生命周期钩子:bootstrapmountunmount。qiankun会在合适的时机调用它们。

以React子应用为例,你需要在子应用的入口文件里加上:

// 子应用的 src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

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

// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}

export async function bootstrap() {
  console.log('子应用启动');
}

export async function mount(props) {
  render(props);
}

export async function unmount(props) {
  const { container } = props;
  ReactDOM.unmountComponentAtNode(
    container ? container.querySelector('#root') : document.getElementById('root')
  );
}

为什么要有__POWERED_BY_QIANKUN__这个判断?因为子应用既要能独立运行,也要能嵌入主应用。这个变量就是qiankun注入的标识。我在项目中遇到过,有人忘了加这个判断,结果子应用独立运行时白屏了。排查了半天,发现是生命周期函数没被调用。

4.4 全局状态管理

微前端最头疼的问题之一:主应用和子应用怎么共享状态?

qiankun提供了initGlobalState方法。说白了,就是主应用维护一个全局的store,子应用通过props拿到它。

主应用里这样写:

import { initGlobalState } from 'qiankun';

const initialState = {
  user: { name: '张三', role: 'admin' },
  token: 'abc123',
};

const actions = initGlobalState(initialState);

// 监听状态变化
actions.onGlobalStateChange((state, prev) => {
  console.log('全局状态变了', state, prev);
});

// 更新状态
actions.setGlobalState({ user: { name: '李四' } });

子应用通过props接收:

export async function mount(props) {
  // props.onGlobalStateChange 监听
  // props.setGlobalState 更新
  render(props);
}
我的建议:全局状态不要放太多东西。只放那些真正需要跨应用共享的数据,比如用户信息、权限、主题配置。子应用内部的状态,还是用各自的状态管理工具(Redux、Vuex)去维护。否则全局状态一多,调试起来就是噩梦。

4.5 路由整合

主应用和子应用的路由怎么配合?这是另一个容易出问题的地方。

我常用的做法是:主应用用BrowserRouter,子应用用HashRouter。为什么?因为BrowserRouter需要服务端配合,子应用嵌入后路径容易冲突。HashRouter用#号隔离,省心。

主应用的路由配置:

// 主应用的路由
<BrowserRouter>
  <Switch>
    <Route path="/react-app" component={MicroAppWrapper} />
    <Route path="/vue-app" component={MicroAppWrapper} />
    <Route path="/" exact component={Home} />
  </Switch>
</BrowserRouter>

子应用的路由:

// 子应用的路由,用HashRouter
<HashRouter>
  <Switch>
    <Route path="/" exact component={Dashboard} />
    <Route path="/detail" component={Detail} />
  </Switch>
</HashRouter>

这样,主应用访问/react-app时,加载子应用。子应用内部跳转/detail,实际URL是/react-app#/detail。互不干扰。

4.6 避坑指南

最后,分享几个我踩过的坑:

  • 样式隔离:qiankun默认用Shadow DOM做样式隔离,但有些老项目不支持。我建议子应用用CSS Modules或BEM命名规范,从源头避免冲突。
  • 全局变量污染:子应用卸载后,要清理全局事件监听、定时器。否则内存泄漏。我曾在生产环境遇到过,子应用切走了,但setInterval还在跑,CPU直接飙到100%。
  • 子应用加载慢:第一次加载子应用时,qiankun会去fetch整个HTML。如果子应用很大,会有明显的白屏。解决方案是用prefetch预加载,或者加一个loading动画。

总结一下:

  • 主应用要轻量,只做容器和路由
  • qiankun配置时注意跨域和entry地址
  • 子应用必须暴露生命周期钩子
  • 全局状态只放共享数据,别滥用
  • 路由用HashRouter隔离,省心

好了,主应用搭建这块就聊到这。下一章我们讲子应用的开发和接入,到时候会手把手带你写一个React子应用和一个Vue子应用。有什么问题,欢迎在评论区交流。