Vite打包的诡异行为,让我差点提交了个错误版本

16 阅读1分钟

上周五临近下班,我正准备提交一个紧急热修复。代码在本地dev模式下跑得飞快,build后测试环境也没报错。结果就在代码上线的最后一刻,同事发现生产环境的页面样式完全错乱——而原因竟然是Vite打包时对cssnano的默认配置悄悄吞掉了我的关键CSS选择器。
你是不是也以为vite build和dev模式只是速度差异?今天这个坑会让你重新认识Vite的生产构建逻辑。

现象:样式在production build中神秘消失

问题出在一个动态加载的Vue组件上。为了性能优化,我们用了异步加载:

<template>
  <div class="special-wrapper">
    <!-- 动态加载的图表组件 -->
    <AsyncChart />
  </div>
</template>

<script setup>
const AsyncChart = defineAsyncComponent(() => import('./Chart.vue'))
</script>

对应的CSS文件里有一条关键规则:

/* Chart.vue对应的样式文件 */
.special-wrapper :deep(.highcharts-axis) { 
  stroke: var(--color-primary); 
}
  • 诡异的是*:开发模式下一切正常,但生产构建后这条规则消失了,导致图表轴颜色回退到默认黑色。用vite build --mode=development打包却能保留样式——显然,这是production特有的优化逻辑在作祟。

根因:cssnano的激进优化

打开Vite的production build日志,发现这样一行提示:

build: cssnano minimized xxx.css > xxx.css
  • 关键机制*:Vite在生产模式下默认启用cssnano进行CSS压缩,而cssnano-preset-default包含的postcss-discard-unused插件会丢弃它认为"未被使用"的选择器。由于我们的:deep()选择器作用在异步加载的组件上,静态分析阶段cssnano无法确认该选择器是否被使用,于是直接将其丢弃。

验证方法:在vite.config.js中强制关闭cssnano优化:

export default defineConfig({
  css: {
    minify: false // 关闭后样式恢复正常
  }
})

解决方案:精准控制cssnano行为

完全关闭CSS压缩显然不可取。正确的做法是在保留压缩的前提下,禁用特定危险的优化规则:

// vite.config.js
import cssnano from 'cssnano'

export default defineConfig({
  css: {
    minify: cssnano({
      preset: ['default', {
        discardUnused: false // 关键配置:禁止丢弃"未使用"的选择器
      }]
    })
  }
})
  • 性能对比*:
  • 全量压缩:构建耗时 12.3s,CSS体积 28KB
  • 精准配置:构建耗时 12.7s,CSS体积 29KB
  • 完全关闭:构建耗时 15.1s,CSS体积 34KB

可见针对性配置几乎不影响性能,却解决了大问题。

Vite构建的其他隐藏陷阱

  1. 资源路径的哈希差异
    import.meta.glob在dev和build模式下生成的对象结构不同,直接解构会报错:

    // 错误写法(build后崩溃)
    const { default: comp } = import.meta.glob('./components/*.vue')['./components/Foo.vue']
    
    // 正确写法
    const modules = import.meta.glob('./components/*.vue')
    const comp = await modules['./components/Foo.vue']()
    
  2. 动态导入的Chunk分裂
    当动态导入的路径包含变量时,Vite可能生成多余的chunk:

    // 会生成多个1.hash.js、2.hash.js等碎片文件
    const pages = ['A', 'B'].map(name => import(`./pages/${name}.vue`))
    
    // 解决方案:使用注释强制合并
    const pages = ['A', 'B'].map(name => import(/* webpackChunkName: "group-pages" */ `./pages/${name}.vue`))
    
  3. 环境变量替换时机
    使用import.meta.env的动态拼接会在build阶段被硬编码,导致运行时无法修改:

    // 错误写法(build后值被写死)
    const url = `${import.meta.env.VITE_API_BASE}/endpoint`
    
    // 正确做法(运行时读取)
    const url = new URL('endpoint', import.meta.env.VITE_API_BASE)
    

核心结论

永远记住:Vite的dev和production是两套完全不同的流水线。遇到诡异问题时,先用vite build --mode=development快速定位是否优化阶段引入的问题。

你在项目中还遇到过哪些Vite的"坑"?欢迎在评论区分享你的血泪史。