把一个大型 Vue 3 单体应用拆成多个微前端子应用之后,最先撞上的问题往往不是线上部署,而是本地开发体验。子应用通常需要注册到主应用里才能渲染,开发者只想调试自己负责的那一个模块,却不得不把主应用和其他五六个子应用全部拉起来,启动一次要等好几分钟。更麻烦的是,改一行代码后热更新时不时失效,浏览器页面纹丝不动,只能手动刷新。这篇文章就围绕这两个痛点,讲清楚子应用独立运行的配置思路,以及热更新失效的排查与修复方法。

子应用如何做到既能被加载又能独立运行
实现独立运行的核心思路是环境判断加条件挂载。子应用在主应用环境下以 qiankun 或其他微前端框架约定的生命周期函数形式导出,而在本地独立启动时,则直接走常规的 createApp 挂载流程。两者的差异只需要在入口文件里用一个环境变量区分开。
以 qiankun 为例,主应用加载子应用时会向 window 上注入 __POWERED_BY_QIANKUN__ 标记,这个标记天然就是一个判断依据。独立运行时这个变量不存在,子应用就走自己的挂载逻辑:
import { createApp } from 'vue'
import App from './App.vue'
import { createRouter, createWebHistory } from 'vue-router'
import routes from './router/routes'
let app = null
let router = null
function render(props = {}) {
const { container } = props
// 独立运行时使用 /,被主应用加载时挂载到传入的容器节点上
const history = window.__POWERED_BY_QIANKUN__
? createWebHistory(window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__)
: createWebHistory('/')
router = createRouter({ history, routes })
app = createApp(App)
app.use(router)
app.mount(container ? container.querySelector('#app') : '#app')
}
// 独立运行时直接渲染,qiankun 环境下由生命周期函数触发
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
export async function bootstrap() {}
export async function mount(props) {
render(props)
}
export async function unmount() {
app.unmount()
app = null
router = null
}</code>这段代码有几个细节值得注意。路由的 base 在两种模式下不同:被主应用加载时应该以主应用分配的路径前缀为基准,独立运行时则是根路径。如果不做区分,独立调试时访问任何路由都会 404。另外 unmount 里必须彻底清理实例,否则主应用来回切换子应用时会残留旧的 Vue 实例和全局事件监听。
如果你用的是 Vite,还需要在 vite.config.js里关掉默认的 HTML 入口依赖,因为 qiankun 抓取的是子应用导出的 JS,而不是完整的 HTML 页面。通常的做法是配合 vite-plugin-qiankun 插件,它会在开发模式下处理生命周期导出问题,独立运行时又不影响正常开发服务。
publicPath 与端口约定:热更新失效的头号元凶
热更新在微前端场景下失效,九成以上和 publicPath 有关。原因是这样的:子应用被主应用加载后,页面的域名是主应用的开发服务器,而子应用的资源(包括热更新客户端连接的 WebSocket)指向的是子应用自己的开发服务器。如果 publicPath 没有动态设置为子应用的绝对地址,浏览器会去主应用域名下请求热更新相关的资源,请求自然失败,热更新就"静默死亡",控制台里只会看到几个不明显的网络报错。
Webpack 项目里最标准的做法是在入口文件顶部动态设置 publicPath:
// main.js 最顶部,必须位于其他 import 之前执行
if (window.__POWERED_BY_QIANKUN__) {
// qiankun 注入的运行时 publicPath
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}而在开发环境下,这个注入值应该指向子应用开发服务器的完整地址。主应用注册子应用时可以这样写:
registerMicroApps([
{
name: 'sub-app-order',
entry: '//localhost:7101',
activeRule: '/order',
props: {
// 把子应用的完整地址传进去,供其设置热更新基础路径
publicPath: '//localhost:7101/'
}
}
])子应用侧在 mount 里接收这个 props 并写入 window。同时子应用的 devServer 需要开启跨域支持,否则主应用域下的页面拿不到资源:
// vue.config.js 或 webpack.config.js
module.exports = {
devServer: {
port: 7101,
headers: {
'Access-Control-Allow-Origin': '*'
},
// 允许 WebSocket 跨域,热更新必需
ws: true
}
}Vite 项目对应的做法是在 vite.config.js 中配置 base 为完整的开发服务器地址,并确保 server.origin 也指向自身,这样热更新客户端才会连到正确的地址:
export default defineConfig({
base: process.env.QIANKUN ? '//localhost:7101/' : '/',
server: {
port: 7101,
origin: '//localhost:7101',
cors: true
}
})除了 publicPath,还要约定好每个子应用的固定端口并写进团队文档。端口一旦漂移,主应用里注册的 entry 地址失效,加载和热更新会一起出问题,而且报错信息很难直接看出是端口对不上。
沙箱、样式与状态:独立调试与集成环境的差异抹平
解决了运行和热更新,还有一类隐蔽的问题:子应用独立运行时一切正常,放进主应用就行为异常。这多半是沙箱和全局状态造成的。qiankun 默认开启 JS 沙箱,子应用对 window 的直接修改会被隔离或代理,独立运行时没有这层沙箱,代码行为就会不一致。典型的坑包括往 window 上挂全局事件监听、直接读取未声明的全局变量等。建议把需要跨应用共享的数据统一走主应用下发的 initGlobalState,不要依赖裸的 window 通信。
样式隔离是另一个高频问题。主应用和子应用如果都用了 Element Plus 这类组件库,样式互相覆盖会非常混乱。独立调试时看不到这些冲突,集成时才爆发。可以给子应用配置 experimentalStyleIsolation 或使用 Shadow DOM 隔离,同时团队内约定一套 CSS 前缀规范。验证方式很简单:独立运行时用浏览器检查关键组件的类名,确认前缀生效后再集成。
最后建议在 package.json 里准备两套启动脚本,例如 dev:standalone 设置 MODE=standalone 走独立挂载,dev:qiankun 走微前端模式。日常开发用独立模式享受完整热更新体验,联调阶段再切换到集成模式验证沙箱与样式问题。这套流程跑顺之后,多团队协作的等待成本会明显下降。