如何将 grunt-contrib-copy 替换为 vite-static-copy

解读

在国内前端工程化面试中,“老项目从 Grunt 迁移到 Vite” 是高频考点。面试官真正想考察的是:

  1. 你是否能准确识别两套工具链的插件映射关系
  2. 能否无痛迁移存量配置,保证上线节奏不被打断;
  3. 是否理解拷贝行为差异(Grunt 的 IO 同步 vs Vite 的内存+Rollup 插件钩子),避免上线后出现 404、缓存穿透等事故;
  4. Monorepo、微前端、CDN 路径等国内典型场景的适配经验。

一句话:不是简单“换个插件”,而是在持续交付压力下完成构建层换血

知识点

  1. grunt-contrib-copy 核心配置项

    • src / dest 支持 minimatch 通配
    • expand:true 开启动态文件对象
    • flatten、rename、filter 函数可二次加工路径
  2. vite-static-copy 运行原理

    • 基于 Rollup 的 generateBundle 钩子,在 Vite 生产打包阶段把文件写进 dist
    • dev 阶段默认不拷贝,除非额外写 server.middlewares.use 做代理
    • targets 数组每一项等价于 Grunt 的一个文件对象,但不支持函数式 filter/ rename,只能用 transformPath、transform 做字符串级处理
  3. 路径差异

    • Grunt 以 Gruntfile.js 所在目录为 CWD;Vite 以 root(默认项目根) 为基准
    • 国内项目常把静态资源放 public,但public 目录会被 Vite 整体伺服,vite-static-copy 的目标文件若与 public 重名会覆盖且不会告警
  4. 性能与缓存

    • Grunt 每次全量 IO;Vite 仅在 rollup 生成阶段写盘,dev 时无 IO 收益
    • 若拷贝体积 > 50 MB(常见政务、可视化大屏项目),需用 vite-plugin-static-copy 的 parallel 选项 开启多线程,否则 CI 时长翻倍
  5. 灰度与回滚

    • 国内上线常走 “蓝绿 + CDN 刷新”;务必保证拷贝后文件名带 contenthash,并在 vite.config 里配置 rollupOptions.assetFileNames,否则回滚时 CDN 边缘节点缓存无法及时失效

答案

步骤一:卸载旧插件
npm remove grunt-contrib-copy
npm remove grunt --save-dev

步骤二:安装新插件
npm i -D vite-plugin-static-copy

步骤三:把 Gruntfile 中的 copy 子任务映射成 vite.config.js 配置
原 Grunt 片段:

copy: {
  libs: {
    expand: true,
    cwd: 'node_modules/@company/lib/dist/',
    src: ['**/*'],
    dest: 'dist/assets/lib/'
  },
  locales: {
    expand: true,
    cwd: 'src/locales',
    src: ['**/*.json'],
    dest: 'dist/locales',
    rename: function(dest, src) {
      return dest + '/' + src.replace('zh-CN', 'zh');
    }
  }
}

Vite 等效配置:

import { defineConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';

export default defineConfig({
  plugins: [
    viteStaticCopy({
      targets: [
        {
          src: 'node_modules/@company/lib/dist/**/*',
          dest: 'assets/lib'
        },
        {
          src: 'src/locales/**/*.json',
          dest: 'locales',
          transformPath: (path) => path.replace('zh-CN', 'zh')
        }
      ],
      // 国内服务器 IO 慢,开启并行
      parallel: true
    })
  ],
  // 保证回滚安全
  build: {
    rollupOptions: {
      output: {
        assetFileNames: 'assets/[name]-[hash][extname]'
      }
    }
  }
});

步骤四:验证

  1. npm run build 后检查 dist 目录结构是否与 Grunt 时代完全一致
  2. 跑一遍 Jest 快照测试Playwright 视觉回归,防止路径 404
  3. GitLab CI 里对比产物体积,若增长 > 5%,排查是否重复拷贝了 node_modules 源码图

步骤五:清理
删除 Gruntfile.js、grunt 目录及所有 grunt-* 插件,更新 README 中的 “构建命令” 章节,避免新成员误用 grunt dev

拓展思考

  1. 混合迁移场景
    若公司仍保留 Grunt 做遗留 JSP 项目,而新模块用 Vite,可通过 npm-run-all 把 grunt & vite 串行跑,但需统一 dist 输出目录,防止运维脚本找不到包。此时建议把 vite-static-copy 的 dest 设成绝对路径,指向 Grunt 的 dist,实现**“双构建、单产物”**。

  2. 微前端基座隔离
    在 qiankun 基座里,子应用静态资源需带 micro_app_name 前缀。可在 transformPath 里注入子应用名,或写自定义插件在 generateBundle 阶段统一加前缀,避免手动维护。

  3. CDN 上传闭环
    国内云厂商(阿里云 OSS、腾讯云 COS)插件只认本地磁盘文件。vite-static-copy 执行时机在 generateBundle,之后立即走 upload-cdn-plugin,可保证拷贝文件被一并上传;若用 dev 阶段的代理方案,则 CDN 不会命中,导致首次 404。务必关闭 dev 代理,强制走 build。

  4. 未来可替换为 import.meta.globEager
    若拷贝的是本地化 JSON 配置,其实可以直接改写成 ES Module,通过 globEager 动态导入,利用 Vite 的 HTTP 304 缓存,节省一次拷贝。面试时可主动提及,体现**“不仅迁移,而且优化”** 的思维。