我用 qiankun 把 React、Vue、原生 JS 整合到一起——从「一看就懂」到「一动就废」

0 阅读9分钟

我用 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 文件「提取」出来,在一个隔离的沙箱环境里执行,就好像这些代码是主应用自己的一部分一样。

整个过程大概是这样的:

  1. 主应用注册子应用,告诉 qiankun「这个子应用的入口地址是哪里」
  2. 当路由匹配时,qiankun 去请求子应用的 HTML 入口
  3. 从 HTML 里解析出需要加载的 JS 和 CSS 文件
  4. 在一个沙箱环境里执行这些 JS(防止全局变量污染)
  5. 把子应用渲染到主应用指定的 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.libraryoutput.libraryTarget 配置,Vite 的构建模式,子应用的 CORS 配置……每一项都可能让你的子应用加载失败,而且报错信息往往不够直白。

坑二:子应用要兼容两种运行场景

子应用需要同时支持两种情况:

  • 被 qiankun 加载时:通过生命周期钩子挂载和卸载
  • 独立访问时:直接在浏览器里打开,像普通应用一样运行

这意味着子应用的入口文件要做判断:

// 判断是否在 qiankun 环境中运行
if (!window.__POWERED_BY_QIANKUN__) {
  // 独立运行时,直接挂载
  render()
}

这个细节文档里有提,但你没有真正联调过一次,很难体会到它的重要性——尤其是当你发现子应用独立访问正常、但被主应用加载就空白的时候,这里往往就是问题所在。

坑三:生命周期的时序问题

bootstrapmountunmount 这三个钩子的执行时机,只有在你真正联调、打上断点、观察调用顺序之后,才会有真实的感受。

比如: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-appReact 18主力技术栈,Concurrent Mode 性能更优
app-userReact 18同上
app-orderReact 18同上
app-productVue 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)反而是最有意思的。因为没有框架帮你管理生命周期,你要完全手写 bootstrapmountunmount,这个过程让我对 qiankun 的生命周期机制理解最深。市面上几乎没有教程详细讲这块,所以我会单独用一篇文章来写它。

两周之后,当我第一次看到五个应用——三种技术栈——都被 shell-app 成功加载、路由切换流畅、shared/ 里的工具函数在各个子应用里都能直接用的时候,我的感受真的就是那四个字:「这么牛逼。」

而且让我惊喜的是,子应用改动真的很小,既可以独立运行,又能整合进主应用。这种「双模兼容」的设计,才是 qiankun 最让我折服的地方。


下一篇预告

第一篇到这里,我们理清楚了:

  • qiankun 的实际工作原理(不是 iframe)
  • 核心概念:主应用、子应用、沙箱、生命周期
  • 整个项目的结构:pnpm monorepo + shell-app + 四个子应用 + shared 共享包

下一篇,我们开始动手——搭建 shell-app 主应用骨架,完成 qiankun 安装、micro/ 目录配置,以及子应用注册,把整个架构的骨架先立起来。

→ [第 2 篇:主应用搭建 — qiankun 安装与子应用注册](即将发布)


觉得有帮助的话,欢迎点赞收藏,也欢迎去 GitHub 给项目点个 Star ⭐
项目地址:github.com/nacheal/erp…