我用 qiankun 把 React、Vue、原生 JS 整合到一起——从「一看就懂」到「一动就废」
系列:《从零搭建 qiankun 微前端》第 1 篇
项目地址:erp-lite-microfrontends
前言:光看文档没用,得自己挨打
我相信很多前端开发者跟我有过一样的经历——打开 qiankun 的官方文档,读完觉得「哦,就这?挺简单的嘛」,然后合上文档,打开编辑器,敲了十分钟代码,页面一片空白。
这就是我最开始的状态。
微前端这个概念在前端圈已经不新鲜了。它能把一个庞大的前端工程拆分成多个独立的子应用,每个子应用可以用不同的技术栈、独立开发、独立部署,最终聚合在一个主应用里呈现给用户。理论上听起来相当优雅。
但「理论上」和「跑起来」之间,隔着一片雷区。
所以我决定自己做一个项目,把 qiankun 真正实操一遍。项目以一个轻量级 ERP 系统为背景,目标定得很明确:一个 React 主应用(shell-app),同时管理用户、商品、订单三个 React 子应用,一个 Vue 3 子应用,还有一个原生 JS 数据看板。三种技术栈,五个独立应用,跑通为止。
这个系列就是我踩坑两周后的真实记录。
我之前以为 qiankun 是什么
在动手之前,我对 qiankun 的认知大概是这样的:
「应该类似 iframe 吧?主应用给一个容器,子应用各自在自己的 URL 里独立运行,主应用把它们的页面嵌进来。」
这个理解有一半是对的——子应用确实可以独立运行。但「嵌入」的方式,完全不是我想的那样。
我以为主应用是直接用一个 URL,把子应用的页面挂载进来,就像 <iframe src="http://子应用地址"> 一样简单。
事实上,qiankun 做的事情要复杂得多,也精妙得多。
qiankun 到底是怎么工作的
这是我实操之后才真正理解的核心:
qiankun 不是把子应用的页面嵌进来,而是把子应用的 JS 和 CSS 文件「提取」出来,在一个隔离的沙箱环境里执行,就好像这些代码是主应用自己的一部分一样。
整个过程大概是这样的:
- 主应用注册子应用,告诉 qiankun「这个子应用的入口地址是哪里」
- 当路由匹配时,qiankun 去请求子应用的 HTML 入口
- 从 HTML 里解析出需要加载的 JS 和 CSS 文件
- 在一个沙箱环境里执行这些 JS(防止全局变量污染)
- 把子应用渲染到主应用指定的 DOM 容器里
这跟 iframe 有本质区别。iframe 里的子应用是完全独立的浏览上下文,跟主应用几乎没有办法直接通信,样式也完全隔离。而 qiankun 的子应用,本质上是跑在主应用的同一个页面上的,它们可以共享数据、可以通信,用户体验也更流畅。
当我真正理解这一点的时候,我的第一反应是:「我草,这么牛逼。」
然后第二个反应是:「难怪我一开始啥都跑不起来。」
为什么「一看就懂,一动就废」
理解了原理之后,很多坑就说得通了。
坑一:子应用必须用 UMD 格式导出
因为 qiankun 要在主应用的上下文里「执行」子应用的 JS,子应用的代码必须以特定方式打包——使用 UMD(Universal Module Definition)格式,并且暴露三个生命周期钩子:
export async function bootstrap() {
console.log('子应用初始化')
}
export async function mount(props) {
console.log('子应用挂载', props)
// 在这里渲染你的应用
}
export async function unmount(props) {
console.log('子应用卸载')
// 在这里销毁你的应用,清理副作用
}
光看文档,你会觉得「哦,加三个导出函数而已」。但真正动手的时候,Webpack 的 output.library 和 output.libraryTarget 配置,Vite 的构建模式,子应用的 CORS 配置……每一项都可能让你的子应用加载失败,而且报错信息往往不够直白。
坑二:子应用要兼容两种运行场景
子应用需要同时支持两种情况:
- 被 qiankun 加载时:通过生命周期钩子挂载和卸载
- 独立访问时:直接在浏览器里打开,像普通应用一样运行
这意味着子应用的入口文件要做判断:
// 判断是否在 qiankun 环境中运行
if (!window.__POWERED_BY_QIANKUN__) {
// 独立运行时,直接挂载
render()
}
这个细节文档里有提,但你没有真正联调过一次,很难体会到它的重要性——尤其是当你发现子应用独立访问正常、但被主应用加载就空白的时候,这里往往就是问题所在。
坑三:生命周期的时序问题
bootstrap、mount、unmount 这三个钩子的执行时机,只有在你真正联调、打上断点、观察调用顺序之后,才会有真实的感受。
比如:mount 里你要确保 DOM 容器已经准备好了才能渲染;unmount 里你必须清理掉所有副作用,否则切换子应用的时候会产生内存泄漏,或者上一个子应用的事件监听还在"鬼魂"一样跑着。
这些都是「一看就懂、一动就废」的典型例子。
核心概念速览
在进入实际代码之前,先把几个核心概念对齐。
主应用(Base App)
整个微前端架构的骨架。负责:
- 提供公共的顶部导航、侧边栏等 UI 框架
- 注册并管理所有子应用
- 根据路由决定激活哪个子应用
- 提供全局状态,供子应用共享
本系列的主应用用 React 搭建。
子应用(Micro App)
每个独立的业务模块。特点:
- 可以独立开发、独立部署
- 可以用任意前端框架
- 必须暴露 qiankun 要求的生命周期钩子
本系列有四个子应用:三个 React 子应用(用户管理、商品管理、订单管理)、Vue 3 子应用(商品管理演示多技术栈)、原生 JS 子应用(数据看板)。
沙箱(Sandbox)
qiankun 给每个子应用提供的隔离环境,防止:
- 子应用的全局变量(
window.xxx)污染主应用 - 多个子应用之间的全局变量互相干扰
沙箱的原理我们会在系列后面的文章里深入讲解,现在只需要知道它的存在。
生命周期(Lifecycle)
qiankun 在加载、挂载、卸载子应用时会调用对应的钩子函数:
| 钩子 | 触发时机 | 通常做什么 |
|---|---|---|
bootstrap | 子应用第一次加载时,只触发一次 | 初始化操作(加载资源等) |
mount | 每次子应用被激活时触发 | 渲染应用到 DOM |
unmount | 每次子应用被切走时触发 | 销毁应用,清理副作用 |
项目结构预览
我最终搭出来的项目是一个用 pnpm monorepo 管理的工程,整体结构如下:
erp-lite-microfrontends/
├── packages/ # 所有应用
│ ├── shell-app/ # 主应用,React 18(端口 3000)
│ │ └── src/
│ │ ├── components/ # 公共 UI 组件
│ │ ├── pages/ # 页面骨架
│ │ ├── router/ # 路由配置
│ │ ├── micro/ # qiankun 微前端配置 ← 核心
│ │ └── styles/ # 全局样式
│ ├── app-user/ # 用户管理,React 18(端口 3001)
│ ├── app-product/ # 商品管理,Vue 3(端口 3002)
│ ├── app-order/ # 订单管理,React 18(端口 3003)
│ └── app-dashboard/ # 数据看板,原生 JS(端口 3004)
└── shared/ # 跨应用共享包
├── types/ # TypeScript 类型定义
└── utils/ # 纯函数工具
这个结构有几个值得注意的设计决策:
为什么用 monorepo? 所有应用在同一个仓库里,shared/ 目录下的类型定义和工具函数可以通过 pnpm workspace 直接被任意子应用 import,不需要发布 npm 包,改完立刻生效。在实际项目里,这能减少大量的重复代码。
micro/ 目录的作用是什么? 这是 shell-app 里专门存放 qiankun 配置的目录——子应用的注册信息、激活规则、生命周期配置全部集中在这里,而不是散落在路由或者入口文件里。后续文章会详细展开这块。
技术栈为什么这样分配?
| 应用 | 技术栈 | 选择理由 |
|---|---|---|
| shell-app | React 18 | 主力技术栈,Concurrent Mode 性能更优 |
| app-user | React 18 | 同上 |
| app-order | React 18 | 同上 |
| app-product | Vue 3 | 演示微前端多技术栈共存场景 |
| app-dashboard | 原生 JS | 演示无框架子应用接入,适合纯展示型应用 |
整个架构的运行方式是这样的:
用户浏览器
│
▼
shell-app(React 18 主应用,端口 3000)
│ ↕ initGlobalState 全局状态同步
├── /user → app-user(React 18,端口 3001)
├── /product → app-product(Vue 3,端口 3002)
├── /order → app-order(React 18,端口 3003)
└── /dashboard → app-dashboard(原生 JS,端口 3004)
五个服务同时跑,每个都可以独立访问,也可以通过主应用统一加载。
花了两周,踩了多少坑
说实话,从零跑通这个项目花了我大约两周时间。
这两周里:
第一周大概有三天在搞环境和配置。pnpm monorepo 的工作区配置、每个子应用的 Webpack output.library 打包设置、CORS 跨域(五个端口同时跑,每个都要配)、路由 base 路径……每一个单独来看都不难,但它们叠在一起,报错的时候你不知道从哪里开始排查。
app-product(Vue 3)相对顺一点,但 Vue Router 在微前端环境下的 base 配置有一个细节坑,后面的文章会详细说。
app-dashboard(原生 JS)反而是最有意思的。因为没有框架帮你管理生命周期,你要完全手写 bootstrap、mount、unmount,这个过程让我对 qiankun 的生命周期机制理解最深。市面上几乎没有教程详细讲这块,所以我会单独用一篇文章来写它。
两周之后,当我第一次看到五个应用——三种技术栈——都被 shell-app 成功加载、路由切换流畅、shared/ 里的工具函数在各个子应用里都能直接用的时候,我的感受真的就是那四个字:「这么牛逼。」
而且让我惊喜的是,子应用改动真的很小,既可以独立运行,又能整合进主应用。这种「双模兼容」的设计,才是 qiankun 最让我折服的地方。
下一篇预告
第一篇到这里,我们理清楚了:
- qiankun 的实际工作原理(不是 iframe)
- 核心概念:主应用、子应用、沙箱、生命周期
- 整个项目的结构:pnpm monorepo + shell-app + 四个子应用 + shared 共享包
下一篇,我们开始动手——搭建 shell-app 主应用骨架,完成 qiankun 安装、micro/ 目录配置,以及子应用注册,把整个架构的骨架先立起来。
→ [第 2 篇:主应用搭建 — qiankun 安装与子应用注册](即将发布)
觉得有帮助的话,欢迎点赞收藏,也欢迎去 GitHub 给项目点个 Star ⭐
项目地址:github.com/nacheal/erp…