Vite 静态资源打包踩坑指南:从 base 配置到 CDN 部署全解析
我前段时间把一个老项目从 webpack 迁移到 Vite开发环境爽得飞起结果一打包部署到测试服务器页面直接白屏。控制台一片红全是静态资源 404。折腾了几个小时最后发现就是base路径没配。那之后我又在静态资源这块踩了不少坑从图片、字体、CSS 引用到 CDN 部署基本把能踩的都踩了一遍。这篇文章就把这些坑好好梳理一下里面有具体的报错信息、排查思路和最终的解决方案希望能帮你绕开我走过的弯路。1. Vite 静态资源体系的底层设定1.1 base 是 Vite 静态资源的根决定打包后路径是否正确很多人从 webpack 转过来第一个没意识到的点就是base。webpack 里对应的是publicPath默认情况下 Vite 的base默认值是/意味着打包后的资源路径都是绝对路径比如/assets/index-xxx.js。如果你的应用是部署在域名根目录那没问题。但如果是部署在子路径下比如https://example.com/my-app/那么/assets/index-xxx.js就会去请求https://example.com/assets/index-xxx.js直接 404页面自然白屏。我当时就是这种情况测试环境把前端资源放在 Nginx 的/test-vite/目录下但没配base。解决方案很简单在vite.config.ts里设置// vite.config.ts export default defineConfig({ base: /test-vite/, // 或者在构建时用环境变量 })如果你不知道部署的路径是什么也可以用相对路径base: ./。但这里有个坑base: ./虽然能自适应子路径但会导致动态路由下的懒加载资源路径出问题尤其是使用createWebHistory的时候。所以我的建议是能用绝对子路径就用绝对子路径别偷懒用./否则后面路由和资源配在一起容易出幺蛾子。还有一点base不仅影响打包后的资源路径还会影响index.html里引用的 JS、CSS 路径。你打包后打开dist/index.html如果路径不对先看base是不是配错了。我见过不少同事排查半天最后发现只是base少了个结尾的/。base: /test-vite和base: /test-vite/是有区别的前者拼接出来的路径可能是/test-viteassets/xxx.js这种坑极其隐蔽因为单独看配置看不出问题。1.2 public 和 src/assets 的边界不是所有静态资源都归 Vite 管Vite 对静态资源的处理分为两大类public目录和src/assets目录严格来说是项目中 import 的资源。这俩的定位完全不同很多人混着用结果打包后路径找不到文件或者文件没被压缩处理。public目录下的文件Vite 在打包时原封不动地复制到dist根目录不会做内容处理也不会加 hash。引用时直接用根路径比如public/favicon.ico代码里写/favicon.ico。这类资源适合放那些不需要经过打包构建的静态文件比如 favicon、robots.txt、sitemap.xml、第三方瞎搞的 JS 插件。src/assets下以及从代码里 import 的资源会经过 Vite 的构建管道包括压缩、指纹命名、小体积 base64 内联。比如script setup import logo from /assets/logo.png /script template img :srclogo altlogo / /template这种情况下logo变量在开发环境下是一个路径在构建后可能变成一个 base64 字符串如果小于assetsInlineLimit或者一个带 hash 的文件路径。很多人踩坑在于把图片放在public目录下然后在 CSS 或模板里写相对路径../public/img/xxx.png。这是错的前面说了public目录内容会被复制到dist根目录所以正确引用方式是绝对路径/img/xxx.png。但如果你用了base配置那么这里的路径也应该带前缀更保险的做法是用import.meta.env.BASE_URL拼const imgUrl ${import.meta.env.BASE_URL}img/xxx.png另外public目录不要放需要 Vite 处理的东西比如需要压缩的图片、需要转译的 TS因为它不会经过任何处理。有人为了省事把大图扔到public里结果页面加载慢图片体积一点没压缩这是典型的误用。2. 高频踩坑场景与排查思路2.1 图片打包后不显示相对路径、动态拼接路径是重灾区图片不显示应该是 Vite 打包静态资源最常见的坑了。出现这种情况优先排查三个方向使用方式、路径写法、构建产物路径。第一个坑是直接给src写死相对路径。比如在组件里写img src../../assets/img/logo.png。开发环境可能能跑但打包后组件代码往往会被编译到不同的目录层级这个相对路径就会计算错导致 404。正确做法是使用import导入或者new URL(..., import.meta.url)动态构造// 动态路径场景 const imgUrl new URL(../assets/img/${props.name}.png, import.meta.url).href用new URL这种方式Vite 在打包时会识别import.meta.url并分析动态路径前提是路径模板部分越完整越好。如果变量太动态比如new URL(${baseUrl}/${name}.png, import.meta.url)Vite 没法静态分析会直接把整个目录都打包进去资源体积可能暴涨。第二个坑是 CSS 里的背景图路径。有些开发者习惯在 CSS 里写background: url(../assets/img/bg.png)。这个写法在 webpack 里很常见但在 Vite 里要注意CSS 中的相对路径是相对于 CSS 文件的而不是相对于项目根目录的。如果 CSS 文件在src/styles下图片在src/assets/img下那么应该写url(../assets/img/bg.png)还是url(./assets/img/bg.png)取决于你 CSS 文件的位置和项目的目录结构。我自己为了避免这种心智负担统一用别名来写.bg { background: url(/assets/img/bg.png); }这样无论 CSS 文件放在哪个目录路径都是相对于项目根的不会算错。但你需要在vite.config.ts里给配置别名CSS 文件里的解析是靠 Vite 内部处理的它会在打包时把别名改写为正确的相对路径。第三个坑是打包后图片的路径带了 hash但你不知道具体路径。比如logo.png会被编译成logo-abc123.png如果你在 JS 或者别的文件里写死了assets/logo.png那肯定找不到。所以不要手动拼接资源路径一定让 Vite 处理资源引用import 或 url()让它在构建时帮你改写路径。2.2 字体文件跨域和 404 问题字体文件是另一个重灾区。开发环境下字体加载是 Vite Dev Server 转发的同源没问题。但打包部署后字体请求会变成跨域请求。如果你的服务器没配 CORS浏览器会在控制台报错字体就加载不出来界面上的图标字体全变成方块或者豆腐块。另外字体文件路径拼接如果带了不正确的base或publicPath也会 404。比如你用 element-plus 之类的组件库它自带的字体文件路径是在 CSS 里通过url()引用的Vite 会尝试改写这些路径。如果你手动覆盖了组件库的字体变量比如$--font-path改成一个相对路径那打包时可能会出错。我的做法是字体文件统一放在src/assets/fonts下通过 CSSfont-face引用时用/assets/fonts/xxx.woff2font-face { font-family: MyFont; src: url(/assets/fonts/my-font.woff2) format(woff2); font-display: swap; }打包后 Vite 会自动把字体文件复制到dist/assets下并加上 hash也会自动改写 CSS 里的路径。此时只需要确认服务器的 MIME 类型配置正确.woff、.woff2、.ttf等通常没问题以及如果资源部署在 CDNCDN 的 CORS 配置要允许字体跨域。字体请求本身是一种 CORS 请求服务器响应头必须包含Access-Control-Allow-Origin否则即便文件存在浏览器也会拦截。这里还有个细节很多人用了 iconfont 或者是 Alibaba 的字体图标下载下来的文件里带着//at.alicdn.com开头的绝对地址这种是走的外部 CDN不受 Vite 管控。好处是不占打包体积坏处是如果外部资源挂了图标就全没了。建议把字体文件和 CSS 文件下载到本地统一走项目内资源方便自己管控。2.3 资源没打进 dist 包或路径带错指纹如果你在src里 import 的资源构建后没有出现在dist/assets下那你得检查是不是这个资源被打进了 base64 内联还是根本没被 Vite 识别到。内联的情况很常见Vite 默认assetsInlineLimit是 4096 字节4KB小于这个值的资源会被转为 base64 字符串直接内嵌到代码里不会生成独立文件。如果你希望某些小图标不要内联可以把assetsInlineLimit调小或者设成 0。但我不建议全局设 0因为小资源内联可以减少 HTTP 请求对性能是好事。真正需要控制的是那些体积接近 4KB、但压缩后可能会超出的资源比如很多 PNG 图标3.8KB 左右开发时看着是文件打包后变成一段长字符串有时候会让 JS 体积莫名增加不少。这时你可以手动设置// vite.config.ts export default defineConfig({ build: { assetsInlineLimit: 2048, // 只对 2KB 以下的内联 }, })还有一个坑是build.assetsDir。默认是assets也就是说所有打包出来的图片、字体等静态资源会放在dist/assets下。如果你改成了别的目录比如static那要注意代码里引用路径是否跟随变化。正常情况下 Vite 会处理但如果你在public下手动引用了/assets/xxx.png那打包后你的手写路径不会跟着变就会 404。所以public下的文件路径基本上只能你自己维护尽量别在代码里引用public下的带 hash 文件因为根本没有 hash。3. 实操案例一次完整的问题排查与修复3.1 问题现场部署后白屏和 404 日志有一次同事的项目打包部署后Nginx 访问首页能看到 HTML但页面全白。打开控制台发现报错集中在index-xxx.js加载失败、一堆图片 404。我那时候已经对 Vite 有点经验了大致猜到是基础路径的问题。但同事坚持说之前 webpack 没遇到过webpack 默认自动拼路径。这里要说一下Vite 的base不会自动根据部署目录推断必须显式配置。而 webpack 有些模板里publicPath用的是相对路径或者自动的所以给人感觉“不用配”。排查流程我给整理成了一张速查表症状大概率原因处理方式首页白屏控制台 JS 404base路径错误检查vite.config.ts中base页面能打开但图片 404相对路径写死改用 import 或new URL方式引用图片能打开但字体图标变方块字体文件跨域或路径错误检查 CORS 与font-face路径部分 JS 文件 404动态 import 路径问题或者路由懒加载检查rollupOptions.output.entryFileNames或分包配置页面刷新 404回到首页却正常路由 history 模式没有服务端回退配置 Nginxtry_files那一次就是表格里第一行改完base之后页面还是有一堆图片 404。后来发现这些图片是放在src/assets里但代码里写的是相对路径类似于../assets/img/xxx.png打包后组件被编译到了某个 JS chunk 里相对路径的基准变了于是图片 404。把图片引用改成/assets/img/xxx.png之后就一切正常了。3.2 从配置根源上消除基础路径问题为了不再被这类问题反复折腾我后来在自己的项目模板里做了一个强制约定构建时base必须从环境变量读取不允许默认值跑生产环境。// vite.config.ts export default defineConfig(({ mode }) { const isProd mode production return { base: isProd ? (process.env.VITE_PUBLIC_PATH || /) : /, } })然后在.env.production里配置VITE_PUBLIC_PATH/test-vite/这样不同环境的部署路径可以各自配置代码仓库里不会因为环境切换频繁改动。如果你们是固定部署在子目录那直接在环境变量里写死就行。如果是纯相对路径布局可以用./但前提是你们没有使用 history 路由或者路由部署时会统一加前缀。我平时最蠢的做法就是给base写死常量然后每个环境去手动改代码简直是自找麻烦。另外如果你在index.html里手动引了public下的脚本建议用% BASE_URL %或者import.meta.env.BASE_URL来拼因为base变化时这些手写路径也要跟着变。Vite 的 HTML 环境变量替换支持%VITE_XXX%和%- VITE_XXX %这样的插值但BASE_URL这种比较特殊官方推荐用import.meta.env.BASE_URL在 HTML 里可以这么写script const assetUrl ${import.meta.env.BASE_URL}some-path/ /script构建时 Vite 会替换掉import.meta.env.BASE_URL但要注意这个变量替换只发生在被 Vite 处理的模块和 HTML 里如果你直接在 CDN 上放一个没被构建的.html文件那就不会被替换。3.3 验证构建产物dist 目录到底长什么样排查 Vite 打包问题最快的办法是直接看dist目录结构。在项目根目录执行npm run build然后看输出dist/ ├── assets/ │ ├── index-abc123.js │ ├── index-abc123.css │ └── logo-def456.png └── index.html打开dist/index.html看里面的script src...和link href...检查路径前缀是否正确。如果你发现路径是/assets/index-xxx.js而你部署的目录是https://xxx.cn/my-app/那肯定 404。重新改base再构建一遍直到index.html里路径变成/my-app/assets/index-xxx.js为止。这里有个工具很好用可以直接在本地起一个静态服务器来看打包产物效果npm run previewVite 内置的 preview 会基于base配置启动一个本地服务器能大概模拟生产环境。但要注意preview默认还是在根路径起服务如果你配的base是/my-app/访问http://localhost:4173/my-app/才能看到页面。很多人在这里又懵了以为 preview 挂了其实是你路径没带前缀。4. 进阶配置与优化建议4.1 小图片内联和手动分包别让静态资源反噬性能之前提到assetsInlineLimit默认 4KB小于它的资源会被内联成 base64。这个机制本意是减请求数但如果你图片多且每个都接近 4KB那它们会被全部内联进 JS导致首屏加载的 JS 文件巨大白屏时间反而变长。这种情况适合调低assetsInlineLimit比如改成 1024让大部分图片生成独立文件利用浏览器并行下载。另一个性能问题是 chunk 体积过大。Vite 基于 Rollup默认会把所有代码打成一个 JS 文件和一个 CSS 文件对中等以上项目来说这个 JS 可能很大。手动分包是解决手段之一尤其是第三方库的体积。常见的做法是把vue、vue-router、pinia、element-plus这些大块头拆出来利用浏览器缓存策略让用户升级代码时不用重新下载框架部分// vite.config.ts export default defineConfig({ build: { rollupOptions: { output: { manualChunks(id) { if (id.includes(node_modules)) { if (id.includes(vue) || id.includes(vue-router) || id.includes(pinia)) { return vendor-vue } if (id.includes(element-plus) || id.includes(element-plus)) { return vendor-element } return vendor } } } } } })分包之后静态资源数量会变多但都带 hash浏览器只要内容不变请求会走缓存对回访用户非常友好。注意这里id.includes(vue)可能把vueuse、vue-i18n之类的也拆到vendor-vue里问题不大但如果你要精确控制可以写更严格的正则或路径判断。分包不是越多越好拆得太碎会带来额外的请求开销建议保持 3~6 个左右的公共 chunk 就行。4.2 大文件处理图片压缩与 SVG 统一管理Vite 不会自动帮你压缩图片它只负责搬运和指纹命名。但实际部署场景中图片体积太大是一个很常见的问题。一个 5MB 的 PNG 图片Vite 打包照样原样复制到 dist。解决思路有两个方向一是提前在源文件层面压缩二是借助插件在构建时自动压缩。我比较推荐在构建时用vite-plugin-imagemin或者vite-plugin-image-optimizer这类插件可以在 CI/CD 流程中自动压缩图片减少人工干预。配置大概长这样// vite.config.ts import imageOptimizer from vite-plugin-image-optimizer export default defineConfig({ plugins: [ imageOptimizer({ png: { quality: 80 }, jpeg: { quality: 80 }, jpg: { quality: 80 }, }), ], })但要注意这类插件在构建时会对图片做二次编码构建时间会变长。如果你图片数量非常多比如上千张建议分场景处理少量大图手动压缩大量小图才用插件自动化。另外SVG 文件一般不推荐拆分成独立文件可以通过vite-plugin-svg-icons这类插件把所有 SVG 合并成雪碧图在代码里用 symbol id 引用。这样有几个好处减少请求次数、SVG 可以继承当前颜色fill: currentColor、图标管理方便。但前提是你得统一图标的风格比如都设置相同 viewBox 和 fill 属性否则合并后的表现会很乱。4.3 部署到 CDN 或 Nginx 时的常见联动问题把 Vite 打包产物部署到 CDN 时一个经常踩的坑是 CDN 域名和业务域名不是一个域。你的index.html部署在主域名下资源文件在 CDN 域名下这时如果资源请求带了 cookie或者跨域没配置就会出问题。我的建议是如果静态资源走 CDN那么把base直接配置成 CDN 的完整地址比如base: https://cdn.example.com/my-app/这样打包后所有资源路径都是完整 URL浏览器直接请求 CDN不存在相对路径计算问题。需要注意的是CDN 上文件的目录结构要和dist里的结构保持一致也就是说assets目录整体同步到 CDN 对应目录下即可。如果部署在 Nginx除了base要配好还建议加上静态资源缓存策略。Vite 默认给打包后的文件名带 hash意味着文件名变了才是内容变了不变可以直接走缓存。在 Nginx 配置里可以对assets目录做强缓存location /assets/ { expires 1y; add_header Cache-Control public, immutable; }但对index.html千万别加immutable。因为它需要根据文件名变化重新拉取更新的资源。如果index.html被 CDN 或浏览器缓存了用户就会一直拿到旧的页面引用导致打完包上线后用户还是旧版资源这个坑我见过不少团队踩。建议对index.html设置Cache-Control: no-cache或max-age0每次都回源校验一次。5. 常见问题排查速查表5.1 问题现象与根因对照这里我把实际工作中遇到最多、群里问得最多的几类问题整理成一个速查表报错/现象根因修复建议白屏控制台Failed to load module scriptbase或部署子路径不匹配检查base确认index.html中路径前缀图片 404本地开发正常代码中使用了相对路径改用 import 或/别名图片 404且打包产物无该文件资源被assetsInlineLimit内联或压根没 import确认使用方式小图内联属正常字体图标显示为方块CORS 或字体路径错误确认Access-Control-Allow-Origin与font-face路径路由刷新 404history 路由没有服务端回退Nginx 配置try_files $uri $uri/ /index.html打包产物巨大代码全部打进一个 chunk使用manualChunks分包图片压缩磁盘占用异常大重复的图片资源或未压缩大图构建插件压缩、精简资源、删除无用文件页面加载慢控制台有大量请求阻塞静态资源请求太多适当加大assetsInlineLimit或使用雪碧图/字体图标这张表不是我凭空写的每条都是从实际案例里总结的。你可以按“现象→根因→修复”的流程去套能省不少排查时间。尤其是前两行几乎占了 Vite 静态资源问题的七八成。5.2 我踩坑后形成的资源管理规范经过好几轮折腾我给自己项目定了几条硬性规则也算是一种防止问题复发的工程约束所有组件内引用的图片、字体等资源一律通过 import 或new URL禁止裸写相对路径。public目录只放favicon.ico、robots.txt、sitemap.xml等不需要构建处理的文件不放业务资源。全局统一在vite.config.ts配置别名CSS、JS 里引用资源都走别名不猜相对位置。静态资源命名用语义化前缀比如logo-、bg-、icon-方便在上线后的日志或控制台观察加载情况。所有部署类路径信息通过import.meta.env.BASE_URL或其他环境变量控制不允许在代码里硬编码部署前缀。这些规范不一定适合所有团队但对我来说确实把资源相关的问题率降到了很低。其实 Vite 官方文档对静态资源处理写得很清楚只是很多人遇到问题不去翻文档喜欢直接在网上搜碎片化的答案。我的经验是先把官方文档的“静态资源处理”这一节通读一遍再结合项目的实际部署架构很多坑都能提前避开。6. 总结之外的真心话写到最后说几句实在的。Vite 的静态资源处理范式跟 webpack 确实有差异但它本质上就是把资源的引用、构建、指纹、输出这些环节整合到了一套更简洁的机制里。你用 webpack 时要写各种 loader、手动配publicPathVite 里很多时候默认行为已经够用。麻烦的从来不是工具本身而是历史的路径假设、脏数据、手写字符串这些在工程里长期积累下来的坏味道。如果你现在正被 Vite 打包的静态资源问题困扰我建议按这个顺序排查先看base再看资源引用方式再看index.html产物路径最后检查服务器缓存和跨域配置。大多数问题跑不出这个范围。实在不行把dist目录结构发给你身边的同事看一眼有时候你自己盯着看半天的地方没问题漏掉的恰恰在别人一眼能看到的角落。我自己现在看到白屏第一反应就是去翻dist/index.html路径对了一切都好说。希望这篇文章也能帮你有这种条件反射。