1. 问题现象与背景如果你在做微前端改造大概率会撞上这个坑qiankun主应用把子应用加载进来页面正常渲染功能也正常唯独子应用css里通过背景图、伪元素或者某个图标引用到的图片要么不显示、要么直接在Network面板里出现一个刺眼的404。我第一次碰到这问题是在接一个老项目时——那个子应用是vue2 vue-cli3改造前图片一切正常改造后页面里一堆背景图全挂了。打开控制台请求路径五花八门有的是static/img/xxx.png挂在主应用域名下有的是http://cdn.xxx.com/static/img/xxx.png带了完整域名但目录不对还有的直接报Failed to load resource: the server responded with a status of 404 ()。排查一圈后发现问题本质不是图片丢了而是子应用打包后css里的资源路径在qiankun的加载机制下指向了错误的位置。这玩意儿表面看是个路径问题实际上牵扯到webpack的publicPath、css-loader对url的处理、qiankun的沙箱加载方式这几个环节任何一个配合不到位图片就会404。这篇文章就围绕这个具体场景从根因讲到底层原理再把几种可行的修复方案和实操配置都列出来最后给你一份排查清单。不管你是用vue-cli、react-scripts还是webpack自定义配置思路都是一样的照着改就行。2. 为什么打包后css里的图片会4042.1 一个容易被忽略的角色publicPath要理解这个404怎么来的得先看webpack打包产物里的css是怎么引用图片的。默认情况下webpack处理css里的url()时会根据配置和文件大小决定是把图片打进cssbase64还是抽成独立文件。如果是独立文件css-loader会把这个地址解析成一个相对路径或者绝对路径写入生成的css里。比如你源文件里写的是background: url(./img/logo.png)打包后可能会变成background: url(static/img/logo.abc1234.png);注意这个路径是相对于当前css文件的它不是从根域名开始算的。当你的代码部署到服务器根目录时浏览器访问/static/img/logo.abc1234.png没问题因为服务器根目录下正好有static目录。但问题来了qiankun的子应用不是直接通过域名访问的它是被主应用拉进来主应用用import-html-entry去请求子应用的入口html然后把html、js、css都塞进主应用的运行环境里。这里就出现了一个核心矛盾css文件的host是主应用提供的比如https://main.example.comcss里写的url(static/img/logo.abc1234.png)是相对路径相对路径的基准是css文件的地址而这个css文件在主应用看来它的基准路径往往是主应用的域名而不是子应用静态资源服务器的路径于是浏览器去https://main.example.com/static/img/logo.abc1234.png请求图片子应用静态资源服务器上哪有这个路径404就这么来了。2.2 相对路径和绝对路径的微妙区别有人会问如果我打包时把css里的图片路径配成绝对路径不就好了比如https://child.example.com/static/img/logo.png这样总该没问题了吧还真不一定。这里有三个细节第一打包产物里不一定全是绝对路径。webpack的publicPath是一个全局配置但css-loader在实际处理时会根据output.publicPath来拼接url。如果你只改了output.publicPath却没改css-loader的publicPath你会发现css里的图片路径跟js里资源的路径并不完全一致。第二qiankun会重写子应用的html入口。import-html-entry在处理子应用的入口html时会把html里写死的script和link标签的地址做一次解析转成绝对URL然后再插入到主应用的dom里。这个过程对css里 url() 的路径是无感知的——它只处理标签的src和hrefcss内部的东西它不帮你改。第三沙箱执行时资源加载发生在沙箱上下文之外。qiankun的js沙箱处理的是js代码运行时的window隔离但css里url()的请求是浏览器发的真实网络请求沙箱管不到。也就是说不管沙箱怎么隔离css里的路径如果不对浏览器照样请求404。2.3 开发环境没炸、生产环境炸了的原因还有个有意思的现象很多人在开发环境跑qiankun一切正常一旦打包部署就出问题。原因也很直白开发环境下子应用是webpack-dev-server实时编译的资源都挂在dev server上路径天然就是http://localhost:8081/static/img/xxx.png这种带完整域名的绝对地址。css-loader在dev模式下默认也会拼上完整的dev server地址所以图片能正常请求到。生产环境就不一样了。打包后css文件被提取成独立文件MiniCssExtractPlugin资源路径要么变成相对路径要么变成你配置的publicPath拼接路径。哪一步没配好图片就炸了。这也提醒了我一个关键点排查问题的时候先分清楚是dev下能跑生产下挂还是两端都挂。两种情况对应的排查方案完全不同。3. 几种可行的修复方案讲完原理我看下实际能落下地的方案。按优先级从高到低排。3.1 方案一配置webpack publicPath为绝对路径最推荐核心思路在构建子应用时把output.publicPath配成一个能真实访问到子应用静态资源的路径。如果你子应用部署在独立域名下比如https://child.example.com/那配置就是这样// vue.config.js 里 module.exports { publicPath: process.env.NODE_ENV production ? https://child.example.com/ : http://localhost:8081/, outputDir: dist, assetsDir: static, productionSourceMap: false, configureWebpack: { output: { library: childApp, libraryTarget: umd, jsonpFunction: webpackJsonpChildApp, }, }, };如果你的子应用没有独立域名是跟主应用共享域名只是不同路径区分那配置应该是主应用域名下的子应用路径publicPath: /child/比如你的子应用部署在https://main.example.com/child/下就这么配。这样打包出的css里图片路径就是/child/static/img/logo.abc1234.png浏览器在主应用域名下请求这个路径能命中山nginx的静态资源映射。这里有几个细节要注意vue-cli3/4的publicPath配置会同时影响webpack的output.publicPath和MiniCssExtractPlugin的publicPath所以css里的url会被正确改写。react-scripts则要通过环境变量PUBLIC_URL来控制或者在package.json的build脚本里临时指定。自定义webpack配置的话需要手动保证output.publicPath和MiniCssExtractPlugin的publicPath保持一致否则会犯我在2.2里说的错误。顺带说一句qiankun官方推荐子应用用umd格式导出并且建议把publicPath配成window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__这个变量是qiankun注入的会在运行时替换成子应用实际加载的路径。但如果你的打包产物是静态资源你更希望它指向固定的cdn或子应用域名那按我说的直接配死绝对路径也完全没问题。3.2 方案二在入口处动态设置publicPath适用于固定域名不明确的场景某些场景下子应用的部署域名不是固定的比如一套代码同时部署到多个环境或者同一个子应用被多个主应用拉取。这时候把publicPath写死在构建配置里就不好办了每次换环境都要重新打包。qiankun提供了运行时publicPath的方案。在子应用的index.html或者入口js里塞这么一段逻辑if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef __webpack_public_path__ window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }这段代码的作用是在运行时告诉webpack模块加载器后续加载的异步chunk和直接请求的资源路径全部基于qiankun注入的路径来拼接。注意这段代码必须在webpack运行时代码执行之前生效也就是要放在入口js的最顶部或者干脆放在html的head里内联执行。这个方案对js里的动态import很管用但对css里静态打包进去的url就没有直接作用了——因为css里的url是构建时根据publicPath生成的运行时改__webpack_public_path__不会重写css里的url。所以这个方案适合的场景是你的css是运行时异步加载的或者你大部分资源是通过import动态引入的。如果404的是静态css里的背景图还是老老实实用方案一或者在css里手动改路径。3.3 方案三把图片转成base64内联小图片专属如果404的图片都是小图标、小背景图最省事的办法就是让webpack把它们全打成base64。base64是直接嵌在css里的不发起网络请求自然不存在路径404的问题。配置方式// vue.config.js const imageUrlLoaderOptions { limit: 1024 * 10, // 小于10kb的图片转base64 // 主要是记得把这个loader的publicPath也改成你想要的 publicPath: /child/, }; module.exports { chainWebpack: config { config.module .rule(images) .use(url-loader) .loader(url-loader) .tap(options Object.assign({}, options, imageUrlLoaderOptions)); }, };但这个方法有明显的天花板图片超过几十kb后转成base64会让css文件膨胀得非常厉害浏览器解析起来也费劲。所以这个方案适合兜底不适合当作核心解决方案。比如一些项目的iconfont、小图标是合理的但大背景图肯定不能用这个。3.4 方案四用自定义loader改写css里的url终极武器前面几个方案都是在webpack层面想办法。如果项目历史包袱太重css里的图片引用方式乱七八糟或者你不想为了qiankun去改动子应用的构建配置那可以写一个自定义loader在css打包完成后把css里的url()自动改写成正确的绝对路径。比如这样写一个css-url-fix-loader.jsconst { urlToRequest } require(loader-utils); module.exports function(source) { if (this.cacheable) { this.cacheable(); } // 只处理url()匹配相对路径的 const reg /url\(([]?)(\.{1,2}\/[^)])\1\)/g; return source.replace(reg, (match, quote, urlPath) { // 把相对路径转成你想要的前缀 return url(${quote}/child/static/static-resolved/${urlPath.replace(/^\.\.\//, ).replace(/^\.\//, )}${quote}); }); };这个方案的好处是灵活坏处是维护成本高而且容易误伤绝对路径的资源。一般用不到我建议你先试方案一不行再考虑这个。4. 实操按步骤修复一个真实案例光讲配置和使用场景不如直接走一遍完整流程。我拿一个典型的vue2子应用来演一遍。4.1 确认问题细节先确认两件事子应用的配置是啥样的vue-cli版本、有没有自定义webpack。404的图片是css里哪个位置引用的路径长什么样。用浏览器的Network面板过滤img或者直接看404请求的URL记录下来。我之前碰到的案例404的URL长这样https://main.example.com/static/img/banner.abc1234.png但子应用实际部署在https://main.example.com/child/所以正确的路径应该是https://main.example.com/child/static/img/banner.abc1234.png确认后就是标准的publicPath没配好。4.2 调整子应用的构建配置子应用的vue.config.js 修改如下const { name } require(./package.json); module.exports { transpileDependencies: true, productionSourceMap: false, // 关键publicPath 改成子应用在服务器上的目录 publicPath: window.__POWERED_BY_QIANKUN__ ? /child/ : /, outputDir: dist/child, assetsDir: static, configureWebpack: { output: { library: ${name}-[name], libraryTarget: umd, chunkLoadingGlobal: webpackJsonp_${name}, }, }, devServer: { port: 8081, headers: { Access-Control-Allow-Origin: *, }, historyApiFallback: true, }, };注意这里我用了window.__POWERED_BY_QIANKUN__做判断因为我在开发环境不想被qiankun干扰所以开发时publicPath是/走dev server自己解决生产环境单独访问时可设置为空或子应用路径。但这里有个坑qiankun模式下运行时window上才存在__POWERED_BY_QIANKUN__而webpack解析publicPath是在构建时不是运行时。vue-cli的publicPath接受一个函数但它是在构建时执行的这个函数内部的window是构建机器的全局对象并不是浏览器里qiankun注入的那个。可能很多人已经踩过这个坑了。正确的做法是在public/index.html的head里加一段内联脚本把__webpack_public_path__提前设置好!DOCTYPE html html langzh-CN head meta charsetutf-8 meta http-equivX-UA-Compatible contentIEedge meta nameviewport contentwidthdevice-width,initial-scale1.0 titlechild-app/title script if (window.__POWERED_BY_QIANKUN__) { // 这里你按实际部署路径来写死 window.__webpack_public_path__ /child/; } /script /head body div idapp/div /body /html加上这段之后webpack在浏览器端解析模块路径时就会把/child/拼上去css里的图片路径也得以正确加载。4.3 处理css文件中已经被写死的相对路径如果你的项目里有些css不是webpack从js中提取的而是直接从publicPath下通过link标签引入的静态css文件比如放在public/目录下、或者放在CDN上这类css内部的图片路径往往是相对路径webpack根本管不到。之前我就遇过一种场景子应用的入口html里有一个静态的style.css是通过全局引入的里面有个背景图指向../img/bg.jpg。这个文件不在webpack打包范围里所以无论我怎么调publicPath它都不受影响因为浏览器加载这个css文件时相对路径的基准是css文件的url而css文件的url是从主应用侧发起的基准就乱了。解决这种静态css有两个方向。一是把它也放进webpack的构建链路里改成import ./style.css;让css-loader处理。二是手动把静态css里的相对路径改成绝对路径写死成子应用部署路径。第二点虽然丑但简单有效。我自己的习惯是子应用内尽量别出现webpack管不到的静态css、静态html、静态js都交给构建系统处理这样路径问题就能统一收敛到publicPath这一个变量上。4.4 验证修复效果修改配置后重新打包子应用传到服务器上部署。然后在主应用里刷新页面看Network里图片请求的状态码。这里我再强调一次排查顺序看404的URL记录完整的请求路径。判断这个路径与子应用静态资源实际部署路径的差异。如果是路径缺失直接调publicPath。如果是路径多了一段检查webpackoutput.publicPath和 css-loader的publicPath是否一致。如果路径完全正确但依然404那就不是路径问题是服务器配置问题比如nginx没有配置静态资源目录映射。我之前还犯过一个低级错误publicPath配对了但nginx只配置了location /child/代理到子应用的index.html没有配置location /child/static/的静态资源指向导致图片请求到了后端拿到404页面。这属于服务器配置问题和webpack无关但也同样表现出“css里的图片404”现象排查时别忘了这一层。5. 其他资源404的高发区字体、异步chunk图片404解决了不代表万事大吉。qiankun 子应用打包的场景里还有两类资源也经常404趁这个机会一并说一下。5.1 字体文件404css里通常还有字体文件比如font-face。字体文件和图片一样如果webpack打包时没有正确处理publicPathfont-face也会炸。处理逻辑和图片完全一样不需要单独配置只要publicPath正确字体路径也能正确解析。有一种特殊情况是有些团队用font-class方式的图标库比如iconfont的css是外置的引用了一大堆.ttf、.woff这些文件放在CDN上。那么css里url()的路径最好直接用CDN的绝对地址不用webpack的publicPath。5.2 异步chunk加载404qiankun中还有个常见坑是子应用路由懒加载。子应用的路由组件用了import()动态导入打包后会生成独立的js chunk。在主应用里跳转路由network里出现xxx.chunk.js 404。这种情况跟图片404原因类似都是相对路径基准错了。解决方法也一致保证webpackoutput.chunkFilename和output.publicPath配合好同时运行时设置__webpack_public_path__来兜底。比如我之前的实战项目子应用路由懒加载生成的chunk路径是/child/static/js/xxx.chunk.js配合publicPath/child/就能正常加载。如果没设qiankun加载主应用的js模块时会找不到这些异步chunk。5.3 一个图文并茂的排查表为了让你更方便地上手我把排查问题时的关键信息整理成一张速查表现象可能原因检查点解决方向css里背景图404js正常css-loader的publicPath和output.publicPath不一致检查css文件中url的实际前缀统一publicPath配置子应用js能加载图片404子应用publicPath未配置或配置错误查看body里子应用css的加载address在入口html设置runtime publicPathdev正常生产404dev server的绝对路径掩盖了问题看生产构建后css里的url是相对还是绝对生产环境显式配置绝对路径路径对了但依旧404服务器静态资源映射错误curl 一下图片URL看返回状态检查nginx或CDN映射字体404font-face路径错误查看css中font-face的src同图片404处理逻辑异步chunk js 404懒加载chunk路径不对network里看chunk请求地址设置__webpack_public_path__6. 踩过的坑和一些经验总结最后聊几个我实际踩过、也是朋友圈里被反复问到的坑。第一个坑是改了publicPath以为就万事大吉结果css-loader单独有publicPath配置。webpack4以前css-loader有个publicPath选项如果你用了MiniCssExtractPlugin它的publicPath也需要同步改。我一度只在output.publicPath配了绝对路径css里的url还是相对路径排查半天才发现在某个chainWebpack的段码里我做了一次调用之后没把修改后的url-loader公网路径同步过去。第二个坑是有些同事会把publicPath配置成./或空字符串美其名曰“相对路径更安全”。在普通单页应用里./配合路由的hash模式也许能凑合但在qiankun下这种配置基本就是必定404。因为css的基准路径会被qiankun入口转换打乱相对路径就彻底废了。所以在微前端体系里我强烈建议全部路径都用绝对路径写死域名或/child/这种根相对路径。第三个坑是缓存带来的假404。你可能修好后刷新还看到404控制台显示请求的是旧chunk名字。这是浏览器或CDN缓存了旧的css、js或者html里引用的文件指纹是旧的。遇到这种情况在部署后强制刷新、清掉CDN缓存即可别以为自己没修好又回到配置里瞎折腾。第四个坑是子应用独立访问正常被qiankun加载就不行。很多人验证修复方案时习惯先单独打开子应用看看图片是不是好的。这当然要做但要注意子应用独立访问时window.__POWERED_BY_QIANKUN__是undefined运行时设置的publicPath可能没生效此时图片正常说明不了什么只有放在主应用下computed runtime的__webpack_public_path__生效时图片才真正算修复成功。我的建议是修复后至少要在三种状态下各验证一遍子应用独立在生产地址访问子应用挂在主应用开发环境下访问子应用挂在主应用生产环境下访问这三种状态对资源路径的解析都不一样我之前修复好第一种和第二种却在第三种状态翻车的次数不止一次。另外如果你的子应用是react-scriptsCRA想设置publicPath得更粗暴一点把PUBLIC_URL环境变量设为部署路径或者直接在入口文件顶部写死__webpack_public_path__ /child/;。CRA默认publicPath是/而且不像vue-cli那样好改很多react渲染下子应用图片404都是因为没改这个变量。收个尾自己动手改配置时的心态说实话qiankun子应用的资源路径问题是微前端改造里最不性感、但最容易卡人的一类坑。它不涉及什么复杂的分布式理论也不牵扯状态管理、权限隔离就是一组webpack路径配置的组合问题。但正因为涉及的配置点多、不同构建工具起作用的配置名还不一样导致很多人卡在这里越调越乱。我个人在实际操作中的体会是遇到这种问题先别急着改代码先静下来把当前子应用的构建链路梳理一遍——从html入口怎么被主应用加载到css文件以什么形式插入页面再到css内url以什么路径去请求资源。把这三个环节搞清楚404的答案基本就在眼前。另外再分享一个小习惯我会在子应用的构建输出目录里直接打开打包后的css文件看看里面的url到底长什么样。这一步比在浏览器里看Network更直观能直接定位到底是相对路径炸了还是绝对路径配错了。每个团队都该把这一步写进微前端的排查手册里。