在 Vue 3 项目里,构建产物除了 Vite 打包出来的 js 和 css,往往还包含大量不需要参与打包的静态资源,比如第三方字体、离线配置文件、大的图片素材、CNAME 记录文件等。这些文件的搬运工作,通常交给 Unix 的 cp 命令完成。cp 虽然是几十年前的老命令,但在现代前端工程化里依然是最轻量、最快速的资源复制方案。本文从 cp 的基础用法讲起,一步步展开它在 Vue 3 构建流程中的工程化实践。

一、cp 命令的核心参数与工作原理
cp 的基本语法很简单:cp [选项] 源 目标。单文件复制时,目标是文件名或目录;多文件复制时,目标必须是已存在的目录。真正决定 cp 行为的是那些参数,工程化场景中最常用的有以下几个。
-r 或 -R 表示递归复制,用于整个目录的拷贝。注意这个参数只递归目录,遇到符号链接时默认行为是把链接本身复制过去,而不是它指向的文件,除非加上 -L 强制解引用。-p 会保留文件的修改时间、访问时间和权限位,这在希望构建产物保持原始文件指纹时很有用。-a 相当于 -dR --preserve=all 的组合,是做完整归档复制的快捷方式。-f 表示强制覆盖,-u 则只在源文件比目标文件新时才复制,可以实现增量更新,避免每次全量拷贝拖慢构建。
还有一个容易被忽略的行为差异:当源路径末尾是否带斜杠,在某些 shell 与 rsync 的组合中语义不同,但对 cp 来说,cp -r src dest 与 cp -r src/ dest 在 GNU coreutils 下结果一致,都是把 src 整个目录复制为 dest/src。如果希望把 src 的内容展开到 dest 下而不是嵌套一层,需要写 cp -r src/. dest。这个细节在写构建脚本时经常导致目录层级多了一层,排查起来很费时间。
二、在 Vue 3 构建流程中接入 cp 命令
最直接的接入方式是 npm scripts。Vite 项目默认的打包命令是 vite build,产物输出到 dist 目录。我们可以在 package.json 中定义一个组合命令,在打包完成后把 public 之外的特殊资源复制进去。下面是一个典型的配置示例,同时区分了 Linux/macOS 与 Windows 环境。
{
"name": "vue3-app",
"version": "1.0.0",
"scripts": {
"build": "vite build",
"copy:assets": "cp -r -p src/assets/vendor/fonts/ dist/assets/fonts/ && cp -p public/CNAME dist/",
"build:all": "npm run build && npm run copy:assets"
}
}
上面用 -p 保留权限和时间戳,用 && 保证前一步成功后才执行复制。如果团队里有人用 Windows,原生 cmd 并没有 cp 命令,可以考虑两个方案:一是要求统一在 Git Bash 或 WSL 里执行脚本;二是引入跨平台的 copy公共方法,例如用 shx 这个 npm 包,把命令改写成 shx cp -r ...,在 Windows 下也能得到一致的行为。对于需要在不同环境差异复制的情况,比如开发环境要复制 mock 数据、生产环境要复制监控配置,可以借助环境变量做分支判断:
# deploy.sh 构建后处理脚本 #!/bin/bash set -e npm run build if [ "$NODE_ENV" = "production" ]; then # 生产环境:复制监控探针与 CNAME,增量更新静态资源 cp -u -r src/static-prod/. dist/ cp -p public/CNAME dist/ else # 开发环境:复制本地 mock 数据 cp -r src/mock-data/. dist/mock/ fi echo "资源复制完成"
这个脚本里的 set -e 很关键,它让任何一条 cp 失败时整个脚本立刻退出,避免构建产物处于半成品状态被部署上线。cp -u 则保证只有更新过的文件才会被覆盖,配合 CI 缓存可以明显缩短流水线时间。
三、cp 与 Node.js 方案的对比及常见坑点
有人会问:既然是 Vue 3 工程,为什么不直接用 Node.js 的 fs.cp 或者写个 Vite 插件来做复制?其实各有适用场景。Node 从 16.7 版本开始提供了实验性的 fs.cp API,可以完全脱离 shell 实现,优点是跨平台零依赖(Node 22 起已稳定),缺点是参数行为与 cp 命令略有差异,且需要额外写脚本文件。Vite 插件方案则适合需要在打包过程中精细控制时机(比如 generateBundle 阶段)的场景。三者的对比如下:
| 方案 | 优点 | 缺点 |
|---|---|---|
| cp 命令 + scripts | 零依赖、执行速度最快、语法直观 | Windows 原生环境不可用,需 shx 辅助 |
| Node fs.cp | 跨平台、可用 JS 精细控制逻辑 | 需要写额外脚本,老版本 Node 不支持 |
| Vite 插件 | 与构建生命周期深度集成 | 开发成本高,简单复制任务属于杀鸡用牛刀 |
使用 cp 时还有几个高频踩坑点值得记录。第一,目标目录不存在时,cp -r src dest 会把 dest 当作复制后的目录名;目标目录已存在时,则会在 dest 下再建一层 src。这个不确定性可以通过 mkdir -p dest 先创建目录来消除。第二,通配符展开是由 shell 完成的,不是 cp 本身,所以 cp *.png dist/ 在没有任何 png 文件时,某些 shell 会把字面量星号传给 cp 导致报错,稳妥写法是开启 shell 的 nullglob 或先判断文件存在。第三,符号链接默认不被追踪,如果静态资源里含有指向 node_modules 的软链(比如复制的主题文件),要么加 -L 解引用,要么改用 -a 保留链接结构,否则部署到服务器后会出现断链。第四,权限问题:-p 保留权限在跨用户复制时可能产生无法读取的文件,普通资源复制建议不带 -p,只在确有需要时使用。
总结一下,cp 命令在 Vue 3 工程化中的定位是轻量级的构建后处理工具:简单场景直接写进 npm scripts,复杂分支逻辑放进 bash 脚本并配合环境变量,跨平台需求用 shx 或 Node fs.cp 兜底。掌握它的参数细节与目录复制语义,能让构建脚本更短、更快、也更不容易出错。把基础命令用对,往往比引入一堆插件更能体现工程化的功力。