如果你做 qiankun / 微前端,遇到过下面任意一种情况,可以往下读:
- 一用
useState、useEffect就报 Invalid hook call - 控制台
React is not defined,或antd is not defined - 子应用单独跑没问题,挂到基座里就白屏、Hooks 全炸
- 不确定 webpack 的 externals 和基座的 window 注入 到底啥关系
PS: 我们项目(qiankun 基座 + 多个子应用 monorepo)已经按这套方案跑通了
你照这个思路配新子应用、或排查报错,应该是可以用上哒
🤔 首先呢,你先搞懂:到底出啥事了?
微前端 = 一个主应用(基座) + 好几个独立小应用(子应用),比如会话一个包、地图一个包、监控一个包,用 qiankun 拼在同一个页面里。
每个子应用代码里都会写:
typescript
import React from 'react'
import { Button } from 'antd'
问题来了:如果每个子应用打包时,都把 React 打进了自己的 js 文件 ,浏览器里就会同时存在 好几份 React。
打个比方(很重要)
React 有点像 全班共用的订书机。
-
正确做法:教室门口放 一台 订书机(基座加载一份 React),每个同学(子应用)用完放回去,大家用的是 同一台。
-
错误做法:每个同学书包里各带一台(每个子应用 bundle 里各一份 React)。
看起来都能用,但 React 的规则是:全页面只能有一台「正版」 。
你用它自带的 Hooks(useState之类),它发现「这不是我这台机器上的调用」,就报错:Invalid hook call. Hooks can only be called inside of the body of a function component.
页面可能还能渲染一点,一用 Hooks 就炸。这就是开头说的那个坑。
「window.React 和子应用里的 React 不是同一个源头」是啥意思?
浏览器里有个全局对象 window,可以挂东西,比如 window.React。
- 基座 先加载,把真正的 React 挂上去:
window.React = React - 子应用 如果打包时又 import 了一份 React,它用的是 自己 bundle 里那份 ,不是
window上那份
两份 React 互不认识 → Hooks 报错。
DevTools 里你能看到 window.React 有值,但组件里用的可能是另一份。
💡 解法就两板斧(缺一刀都不行)
| 步骤 | 谁做 | 干啥 | 人话 |
|---|---|---|---|
| 第一斧 | 基座(主应用) | registerGlobal() |
把 React、antd 等 放到 window 上,全站共用这一份 |
| 第二斧 | 每个子应用 | webpack externals |
打包时 别再打 React 进 bundle ,要用就去 window.React 拿 |
缺第一斧:子应用 externals 配置了 'React',但 window 上没有 → React is not defined
缺第二斧:window 上有 React,子应用还是自己打了一份 → 双 React,Invalid hook call
两板斧一起上,全站只有一份 React,大家都用这一份。
🎬 完整流程(按时间顺序看一遍)
1. 用户打开网站
2. 基座 主应用 的 js 先执行
3. registerGlobal() 运行 → window.React = React,window.antd = antd ...
4. qiankun start,按需加载子应用 A 的 js
5. 子应用 A 里 import React
→ 因为 externals,不会打包 React
→ 实际读的是 window.React(和基座同一份)
6. 子应用 A 里 useState() → 正常,因为用的是正版那一台订书机
第一斧:基座 registerGlobal(把菜炒好放窗口)
基座代码里 主动 import ,再 挂到 window:
typescript
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import * as antd from 'antd'
const registerGlobal = () => {
// 挂到 window 上,键名下面 externals 要对上
window.React = React
window.ReactDOM = ReactDOM
window.antd = antd
// mapboxgl、axios、moment 等同理
}
// ⚠️ 必须在 qiankun start() 之前调用!
registerGlobal()
start()
为啥叫 inject(注入)?
就是「基座先把公共库塞进 window,子应用来取」。
npm 包名 vs window 上的名字(要对表)
子应用 webpack 里写的是 window 上的名字,不是 npm 包名:
| 你 import 的包 | window 上挂的名字 | externals 里写 |
|---|---|---|
react |
React |
'React' |
react-dom |
ReactDOM |
'ReactDOM' |
antd |
antd |
'antd' |
mapbox-gl |
mapboxgl |
'mapboxgl' |
三个字对不齐,子应用就找不到 React。
第二斧:子应用 externals(打包时别带 React)
子应用 webpack.config.js:
javascript
module.exports = {
externals: {
react: 'React', // import react → 用 window.React
'react-dom': 'ReactDOM',
antd: 'antd',
},
}
意思是:构建子应用 js 时,遇到 import ... from 'react',不要打进 bundle,运行时用全局变量 React。
打包成功后,子应用的 js 体积会小很多 ,里面也 不应该 再有一整份 react.production.min.js。
🎭 还是食堂:串起来记
- 基座 = 食堂,做好一大锅 React,放在取餐口(
window) - 子应用 = 各班组,只带自己的菜(业务代码) ,React 这种公共的 去窗口打
- externals = 子应用打包规则:「React 别自己带,去窗口拿」
你既不在窗口放饭(没 registerGlobal),又规定去窗口拿(externals)→ 饿肚子。
窗口有饭,子应用又自己带一份 → 两台订书机,Hooks 报错。
🤯 进阶:为啥 antd 有时不能 external?
大多数页面:import { Button } from 'antd' → 用 window.antd,OK。
但某个 内部表单库 自己打包时已经塞进了一份 antd,你再 external,会出现 两份 antd,按钮样式、弹层行为可能对不上。
所以有的项目 externals 写成 函数 :在特定包 context 里 不让 antd external ,让它自己带;其它业务代码仍用 window.antd。
这是踩坑后的补丁,第一版先搞懂 React 双实例再说 antd。
🔍 确认配置是否正确
- Network:基座加载了 react 相关 js;子应用 chunk 明显更小
- 报错没了 :
useState不再 Invalid hook call - 构建产物:搜一下子应用打包结果,不应出现完整 react 生产环境源码字符串
🕳️ 报错对照
| 报错 | 通常是因为 |
|---|---|
React is not defined |
基座没 registerGlobal,或子应用比基座先跑 |
Invalid hook call |
双 React------externals 没生效,子应用仍打了自己的 React |
antd is not defined |
window 上没挂 antd,或名字写错 |
🎬 最后说两句
搞懂「一份 React 全站共用」,后面改 webpack、升级 React 版本才有底