
接手过 qiankun 微前端项目的同学多半都撞过这堵墙子应用单独跑得好好的CSS 里引用的 logo、背景图、字体文件全都加载正常一旦挂到主应用下就齐刷刷 404Network 面板里一片红。控制台报错还不明显只说某个图片资源找不到页面却像被泼了油漆一样错位、背景缺失、图标全变成占位符。我这次踩的坑就是典型的“css 里的 url 路径在打包后没跟着 qiankun 的加载方式走”。子应用是 Vue2 webpack 构建接入 qiankun 后 CSS 文件能加载但 CSS 内部background-image引用的图片全部请求到了主应用的域名下路径变成了http://主应用地址/static/img/banner.xxxx.png结果当然是找不到资源。这篇文章就把这个问题的根因、排查过程和最终修复方案完整记录下来顺带附上几种不同场景下的避坑配置希望对正要接微前端或已经遇到 404 的同学有帮助。1. 先看问题现场独立运行正常接入 qiankun 后图片集体 404我负责的子应用是一个运营后台技术栈 Vue2 vue-cli 4 webpack 4里面有不少 CSS 背景图比如登录页的背景、侧边栏的 logo、空状态插画。子应用本身部署在内网某台服务器的/operation/路径下主应用部署在另一个容器的/main/路径下。一开始我把子应用单独部署通过 nginx 指到http://子应用域名/operation/去访问页面显示完全正常。然后按 qiankun 的标准套路在主应用注册子应用registerMicroApps([ { name: operation, entry: //operation.example.com/operation/, container: #subapp-container, activeRule: /operation, }, ])启动主应用进入对应路由子应用的 DOM 和样式确实都渲染出来了JS 逻辑也正常但浏览器控制台刷刷刷出现一堆红色报错GET http://main.example.com/static/img/bg_login.9f3c2a.png 404 (Not Found) GET http://main.example.com/static/img/empty.6b8d1e.png 404 (Not Found)注意看这个请求地址图片路径里的static/img前面没有任何子应用前缀且域名变成了主应用main.example.com。这说明问题根本不是 qiankun 没把样式加载进来而是样式中的资源路径被浏览器解析到了错误的基准地址上。当时我用 Chrome DevTools 的 Elements 面板去查子应用的样式发现 qiankun 把子应用的 CSS 以style标签的方式注入到了主应用页面里。CSS 里的规则是.login-page { background-image: url(/static/img/bg_login.9f3c2a.png); }问题一下就清楚了浏览器遇到style标签里的相对路径或根路径会以当前页面也就是主应用页面的 URL 为基准去拼接。/static/img/...这种以根开头的路径自然就拼到http://main.example.com/static/...去了。为了确认不是偶发现象我又检查了 Network 面板发现请求的是main.example.com/static/img/...而子应用真正的图片地址在operation.example.com/operation/static/img/...。两边差了整整一段/operation前缀404 几乎是注定的。2. 根因拆解qiankun 加载子应用 CSS 的路径机制2.1 浏览器解析 CSS url 的规则要彻底搞懂这个坑得先弄明白一个基础规则浏览器解析 CSS 里url()的时候基准是什么。如果样式是外链link relstylesheet href...引入的浏览器会以这个 CSS 文件自身的完整 URL 作为基准来解析里面的相对路径。比如 CSS 文件地址是http://sub.example.com/operation/css/app.abc.css里面写了url(../img/banner.png)浏览器最终请求的是http://sub.example.com/operation/img/banner.png。如果样式是被塞进style标签里的内联样式那么没有外部 CSS 文件作参考浏览器就只能拿当前页面文档的 URL 来当基准。qiankun 默认的样式加载方式恰恰就是后者它会把子应用 HTML 里提取到的 CSS 内容注入到主应用的 DOM 中很多构建场景下会变成style或动态生成的样式标签。所以只要 CSS 里的图片路径不是绝对完整 URL也不是data:base64在 qiankun 的加载机制下就很容易解析到主应用域名下404 就是这么来的。2.2 qiankun 的 HTML Entry 与样式注入qiankun 的官方定位是“微前端解决方案”但它并不是把子应用当作 iframe 塞进去而是通过 HTML Entry 的方式加载子应用的 HTML。qiankun 会 fetch 子应用的index.html解析出里面的 JS 和 CSS拉取后注入主应用。当子应用使用了 webpack 或 Vite 这类现代构建工具时产物里通常会有一个或多个 CSS 文件。qiankun 加载这些 CSS 后为了做样式隔离或作用域处理会把它们插入到主应用的head或者目标容器里。如果你开启了sandbox和样式隔离qiankun 还会对 CSS 选择器做处理比如给每个选择器加[data-qiankunxxxx]前缀。这种注入方式导致样式表失去了“自己的 URL”。你可以简单理解成原本子应用的 CSS 文件带着自己的出身地址现在却像被“剥了壳”一样换成内联身份寄宿在别人家里。浏览器看它就是一个style所有相对路径都只能按主应用的文档地址去算。这就是为什么很多人在子应用里明明是好的一进主应用就稀碎。2.3 webpack 和 Vite 是怎么把图片路径写进 CSS 的这里得再往前走一步看看构建工具到底怎么生成 CSS 里的图片路径。webpack 的处理逻辑是当 CSS 里出现background-image: url()时交由 css-loader 和对应的资源 loader 处理。默认情况下如果图片小于某个阈值可能被转成 base64 内联如果超过阈值则会被输出到构建目录并替换成一个新的资源路径。这个新路径由output.publicPath、filename、资源目录共同决定。以 Vue CLI 为例vue.config.js里的publicPath会影响所有静态资源的对外路径。生产构建如果设成publicPath: /webpack 产出的 CSS 里大图就会是background-image: url(/static/img/bg_login.9f3c2a.png);如果设成publicPath: ./很多时候产物里会变成相对路径background-image: url(static/img/bg_login.9f3c2a.png);这两种形式在子应用单独部署时都能正常工作但在 qiankun 下就都比较危险。前者会拼到主应用根域名后者在被转成style标签后同样会被浏览器按主应用页面位置解析。Vite 的逻辑类似。Vite 的base配置决定了生产构建后的资源引用前缀默认是/。用 Vite 构建的子应用CSS 里的图片路径一样依赖这个base。如果base没设置为子应用的真实部署路径接进 qiankun 就会重蹈覆辙。2.4 一张表看清不同产物路径的差异构建配置CSS 内图片路径示例独立部署访问效果qiankun 注入后实际请求是否可能 404publicPath: /url(/static/img/a.png)子应用域名根目录下正常主应用域名根目录下缺少子应用前缀大概率 404publicPath: /operation/url(/operation/static/img/a.png)子应用/operation/下正常如果是根路径绝对地址仍请求主应用但有/operation前缀若主应用中也有该路径可正常部分场景可正常publicPath: ./url(static/img/a.png)以 CSS 文件地址为基准正常内联style会以主应用页面为基准路径错误极高概率 404publicPath: //cdn.example.com/assets/url(//cdn.example.com/assets/a.png)CDN 上正常完整 URL不依赖主应用域名不会 404资源内联 base64url(data:image/png;base64,...)无需请求正常无需请求正常不会 404这张表基本概括了所有常见组合。结论也很直观你想让 qiankun 模式下 CSS 图片稳定可用要么让 CSS 里的图片路径是完整 URL要么直接不发起请求也就是转成 base64。3. 让人能“抄作业”的四种修复方案3.1 方案 A构建时把 publicPath 改成子应用的绝对部署路径这个方案从根上解决“路径基准”问题。既然子应用最终要部署到某个独立路径下那 webpack 或 Vite 产出的 CSS 资源路径就直接带上这个前缀写成/operation/static/img/a.png这种根绝对路径。对 Vue CLI 项目来说在vue.config.js里配置const { defineConfig } require(vue/cli-service) module.exports defineConfig({ publicPath: process.env.NODE_ENV production ? /operation/ : /, // 其他配置... })对原生 webpack 项目在webpack.prod.js里配置module.exports { output: { publicPath: /operation/, }, }对 Vite 项目在vite.config.js里配置import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ base: /operation/, plugins: [vue()], })这样构建后CSS 里的路径会变成background-image: url(/operation/static/img/bg_login.9f3c2a.png);但这里有一个很多人忽略的细节这个路径是“根绝对路径”浏览器看到它会直接在当前域名下找/operation/...。如果子应用资源部署在另一个域名比如operation.example.com而主应用运行在main.example.com那 CSS 注入后请求的还是http://main.example.com/operation/static/...仍然可能 404。所以方案 A 的正确使用姿势还要配合部署架构来定。如果子应用和主应用同域部署比如都在main.example.com下子应用放/operation/那这个方案就完美。如果子应用是独立域名就要用下面的方案 C或者在公共网关层做转发。3.2 方案 B小图片直接内联为 base64从源头消灭请求遇到这种问题最省心的处理方法是把图片转成 base64尤其是 logo、小图标、几 KB 的背景纹理。转成 base64 后CSS 里的内容长这样background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ...);base64 本身是完整的数据不依赖任何域名和路径无论在子应用单独运行、还是 qiankun 注入到主应用都不可能 404。webpack 5 使用内置的 asset module 来处理可以调整内联阈值module.exports { module: { rules: [ { test: /\.(png|jpe?g|gif|svg|webp)$/i, type: asset, parser: { dataUrlCondition: { maxSize: 4 * 1024, // 4KB 以下内联 }, }, }, ], }, }Vue CLI 项目可以在vue.config.js里通过 chainWebpack 配置chainWebpack(config) { config.module .rule(images) .set(type, asset) .tap(args ({ ...args, parser: { dataUrlCondition: { maxSize: 8 * 1024 } } })) }提示内联阈值不是设得越大越好。转成 base64 会让 CSS 文件体积膨胀本来一张 200KB 的图转成 base64 后可能变成 260KB 左右。如果全站都是大背景图这种方案会导致首屏加载变慢建议只对 20KB 以下的小图使用大图走正常文件加载。我用这个方案解决了项目的侧边栏 logo 和几个空状态小插画立竿见影不用动部署架构。3.3 方案 C大图资源使用 CDN 完整地址对于超过内联阈值、又是页面设计重要组成部分的大图最稳妥的是直接托管到 CDN并在代码里写完整 URL。CSS 里可以这么写.banner { background-image: url(https://cdn.example.com/assets/banner.202506.png); }也可以保留相对路径但在构建配置里把 publicPath 指向 CDNpublicPath: process.env.NODE_ENV production ? https://cdn.example.com/operation/ : /,这种写法下webpack 产出的 CSS 图片路径会自动变成完整 URLbackground-image: url(https://cdn.example.com/operation/static/img/bg_login.9f3c2a.png);独立运行时图片来自 CDN接入 qiankun 后浏览器看到完整 URL 就直接加载 CDN 资源完全不经过主应用域名的路径拼接自然不会有 404 问题。这个方案适合已经有了 CDN 或对象存储的项目改动也不大。需要注意 CDN 的跨域配置如果图片资源设置了 CORS 限制部分浏览器可能会拦截一般给图片资源加Access-Control-Allow-Origin响应头即可。3.4 方案 D运行时设置__webpack_public_path__解决 JS 和动态资源路径qiankun 官方其实提供了一个运行时 publicPath 方案qiankun 在加载子应用时会注入window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__告诉子应用入口文件当前运行时环境的资源路径。我们可以在子应用入口 JS 的最顶部设置 webpack 的 public path// main.js 第一行 if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef __webpack_public_path__ window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ }这个方案对 webpack 运行时动态加载的 JS chunk、异步路由组件里的图片、import 引入的资源等非常有效。因为 webpack 在运行时加载文件都会拼上__webpack_public_path__作为前缀。但要注意一个边界这个变量主要影响的是“运行时通过 JS 产生的资源请求”不一定能改变已经编译进 CSS 文件里的url()路径。我在实际项目中把动态路由 chunk 的资源 404 问题用这个方案修好了但 CSS 背景图依旧 404最后还是配合方案 A 和 B 才彻底解决。所以方案 D 适合作为补充不要当成唯一解。4. 实操记录从 404 到稳定的完整修复过程4.1 明确部署规划和路径前缀动手改配置之前先把部署结构看清楚。我这次的子应用最终部署在内外网统一网关上通过域名/operation/访问。主应用通过/main/访问两者同域但路径不同。基于这个前提我选定方案 A 为主方案 B 为辅构建配置里把publicPath设为/operation/登录页背景这类 30KB 左右的图片适度提高内联阈值直接转 base64大型 banner 和插画保留文件引用但路径带上/operation/前缀4.2 修改 vue.config.js我的子应用是 Vue CLI 4因此只需要调整vue.config.jsconst { defineConfig } require(vue/cli-service) module.exports defineConfig({ publicPath: process.env.NODE_ENV production ? /operation/ : /, outputDir: dist, assetsDir: static, productionSourceMap: false, chainWebpack(config) { config.module .rule(images) .set(type, asset) .tap(args ({ ...args, parser: { dataUrlCondition: { maxSize: 12 * 1024, }, }, })) }, })这里解释两个关键点。第一publicPath为什么只在 production 改成/operation/因为开发环境一般主应用和子应用都是localhost通过 webpack-dev-server 和 qiankun 的代理访问路径前缀不同会导致 dev 场景下资源解析不一致。开发和生产的 publicPath 分开能减少本地调试时的认知负担。第二chainWebpack改动 images 规则本质是让 webpack 按 asset 模块处理图片并提高内联阈值到 12KB。这样小图直接变 base64小于 12KB 的都不再生成文件也不存在路径问题大于 12KB 的继续输出到static/img路径前缀则由publicPath控制。4.3 重新构建并检查产物执行构建npm run build构建完成后打开dist/css/app.xxx.css看到的路径应该是background-image: url(/operation/static/img/bg_login.9f3c2a.png);小图则已经变成background-image: url(data:image/png;base64,...);这说明构建层已经正确。4.4 在 qiankun 主应用中验证把构建产物部署到/operation/路径下回到主应用刷新页面。这时 CSS 会被 qiankun 注入为style标签里面的图片路径是/operation/static/img/...。浏览器看到根绝对路径会向主应用域名发起请求如http://main.example.com/operation/static/img/bg_login.png。由于网关已经把/operation/转发到子应用服务所以资源能正常返回404 消失页面图片恢复。4.5 验证动态加载的资源除了 CSS 静态图片项目里还有路由懒加载的页面和动态 import 的图片。这类资源走的是 JS 运行时的__webpack_public_path__。我在入口文件顶部加了运行时设置// main.js if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef __webpack_public_path__ window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ }之后异步 chunk 的请求地址也会自动带上/operation/不再出现动态路由页面图片 404 的情况。提示__webpack_public_path__必须在入口文件的最顶部设置而且要在任何业务模块 import 之前执行。否则 webpack 运行时组件已经在加载部分资源的 publicPath 已经定了后面再改已经晚了。5. 常见问题与排查技巧实录5.1 子应用独立访问正常进入 qiankun 就 404这是最常见的一种。优先检查 CSS 中图片路径的形态。打开主应用的 DevTools找到子应用注入的style标签看url(...)是相对路径、根路径还是完整 URL。如果是url(/static/...)而没有子应用前缀说明publicPath没配置到位。解决方案就是按上面方案 A 设置成/子应用路径/。5.2 配置了 publicPath 还是 404而且请求到了主应用的另一个目录这种一般是 publicPath 配成了/operation/但子应用资源实际部署它自己的独立域名下导致 CSS 内背景图请求/operation/static/...落到主应用而主应用根本没有这个目录。这时候有两个选择在网关层把/operation/反向代理到子应用资源服务改用方案 C把 publicPath 指向子应用的完整域名或 CDN生产环境的域名拆分情况各不相同需要结合具体部署来接。5.3 CSS 里已经是url(https://cdn.example.com/...)本地跑主应用时还是 404如果 CSS 里是完整 CDN 地址一般不会 404。万一出现大概率是图片本身在 CDN 上不存在或者本地开发环境被 CSP 或代理拦截。先在浏览器里直接访问这张图片的完整 URL看能否打开。打不开就是 CDN 上传或缓存问题跟 qiankun 没直接关系。5.4 子应用动态加载的 JS chunk 正常但里面动态创建的 img 元素请求 404这属于运行时资源路径问题对应方案 D。检查入口文件是否在业务代码前设置了__webpack_public_path__以及 qiankun 传入的window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__是否是正确的 HTML Entry 地址。有时候 qiankun 的 entry 地址带了路径比如//sub.example.com/operation/那注入的 publicPath 就是//sub.example.com/operation/JS 内部请求的 img 也会基于这个地址。你可以在子应用入口打印一下console.log(window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__)如果值为空可能是手动加载子应用但没走 qiankun 的完整流程比如测试时直接访问子应用地址那自然没有注入变量就按独立部署方式判断。5.5 图片请求返回 200但内容是一段 HTML不是图片这种情况不是路径 404而是开发服务器或网关的路由回退。比如子应用是 SPA设置了 history 路由所有不存在的资源路径都被回退到index.html结果图片的响应是 HTML导致图片渲染失败控制台会提示“resource interpret as Image but transferred with MIME type text/html”。排查方法是看响应头的Content-Type如果是text/html基本就是服务端路由回退。解决方向是让静态资源目录优先匹配真实文件而不是回退到 index.html。5.6 base64 资源越来越多CSS 首屏体积过大提高内联阈值虽然方便但别贪心。我在实际项目里一开始为了省事把 100KB 以下的图片全内联结果构建后 CSS 从 60KB 涨到接近 500KB首屏加载明显变慢。后来把阈值降到 12KB大背景图改用 CDN 方案体积和路径问题都控制住了。提示内联阈值要根据页面首屏的实际资源和网络环境来定。通常 8KB-12KB 是相对平衡的选择既照顾到小图标也不至于让 CSS 变成一个巨大的数据包。5.7 排查工具别只用眼睛看遇到资源 404我最常用的一套排查顺序DevTools 的 Elements 面板选中目标元素查看 Computed Style 里的background-image确认实际渲染路径Network 面板筛选img或stylesheets看失败请求的完整 URL在浏览器新标签里直接打开这个完整 URL确认是 404 还是回退到了 index.html如果 URL 正确但 404去部署服务器或 CDN 上确认文件是否存在如果 URL 本身不对回构建配置里查publicPath和产物 CSS 文件这套流程基本能覆盖绝大多数路径问题比反复改代码猜测原因高效得多。6. 从源头上避免这类问题的小建议如果你所在团队刚好要重新搭建微前端项目建议在工程规范阶段就把资源路径定好。子应用统一部署路径前缀、统一 CDN 域名构建配置里通过环境变量区分本地和生产环境。一个常见的做法是在 webpack 或 Vite 配置里读环境变量publicPath: process.env.VUE_APP_PUBLIC_PATH || /sub-app/这样不同子应用可以维护各自的构建环境文件互不干扰。同理qiankun 主应用注册子应用时entry 的路径要和子应用 publicPath 保持匹配。比如子应用 publicPath 是/operation/entry 就应该写成//网关地址/operation/否则即便 CSS 路径正确HTML 和 JS 也容易先异常。我个人在经历这轮 404 之后还养成了一个习惯每次构建完子应用都会先解压产物看一下 CSS 里的url()和 JS 里的资源请求前缀。这个检查不到一分钟但能提前发现路径配置错误避免等接入 qiankun 后黑压压一片的 404 再回头改。项目里所有子应用构建物的资源路径规范也慢慢沉淀成了一份团队文档后来新同学接微前端时照着配置基本不会再踩同样的坑。