npm scripts的高级玩法:pre、post和--,你真的会用吗?
![]()
我们每天的开发,可能都是从一个npm run dev开始的。npm scripts对我们来说,天天用它,但很少去思考它。
不信,你看看你项目里的package.json,是不是长这样👇:
"scripts": {
"dev": "vite",
"build": "rm -rf dist && tsc && vite build", // 嘿,眼熟吗?
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest",
"test:watch": "vitest --watch",
"preview": "vite preview"
}
这能用吗?当然能用。
但这专业吗?在我看来,未必!
一个好的scripts,应该是原子化的、跨平台的。而上面这个,一个build命令就不行,而且rm -rf在Windows上还得装特定环境才能跑🤷♂️。
今天,我就来聊聊,如何用pre、post和--,把你的脚本,升级成专业的脚本。
pre 和 post:命令的生命周期钩子
pre和post,是npm内置的一种钩子机制。
它的规则很简单:
- 当你执行
npm run xyz时,npm会自动先去找,有没有一个叫prexyz的脚本,有就先执行它。 - 当
xyz执行成功后,npm会自动再去找,有没有一个叫postxyz的脚本,有就最后再执行它。
这个自动的特性,就是神一般的存在。
我们来改造那个前面👆提到的build脚本。
业余写法 (用&&手动编排)
"scripts": {
"clean": "rimraf dist", // rimraf 解决跨平台删除问题
"lint": "eslint .",
"build:tsc": "tsc",
"build:vite": "vite build",
"build": "npm run clean && npm run lint && npm run build:tsc && npm run build:vite"
}
你的build脚本,它必须记住所有的前置步骤。如果哪天你想在build前,再加一个test,你还得去修改build的定义。这违反了单一职责。
专业写法 (用pre自动触发)
"scripts": {
"clean": "rimraf dist",
"lint": "eslint .",
"test": "vitest run",
"build:tsc": "tsc",
"build:vite": "vite build",
// build的前置钩子
"prebuild": "npm run clean && npm run lint && npm run test",
// build的核心命令
"build": "npm run build:tsc && npm run build:vite",
// build的后置钩子
"postbuild": "echo 'Build complete! Check /dist folder.'"
}
看到区别了吗?
现在,当我只想构建时,我依然执行npm run build。
npm会自动帮我执行prebuild(清理、Lint、测试)👉 然后执行build(编译、打包)👉 最后执行postbuild(打印日志)。
我的build脚本,只关心构建这件事。而prebuild脚本,只关心前置检查这件事。
这就是单一职责和关注点分离。
你甚至可以利用这个特性,搞点骚操作😁:
"scripts": {
// 当你执行npm start时,它会自动先执行npm run build
"prestart": "npm run build",
"start": "node dist/server.js"
}
-- (双短线):脚本参数
--是我最爱的一个特性。它是一个参数分隔符。
它的作用是:告诉npm,我的npm参数到此为止了,后面所有的东西,都原封不动地,传给我要执行的那个底层命令。”
我们来看开头👆那个脚本:
"scripts": {
"test": "vitest",
"test:watch": "vitest --watch"
}
为了一个--watch参数,你复制了一个几乎一模一样的脚本。如果明天你还想要--coverage呢?再加一个test:coverage?这叫垃圾代码💩。
专业写法 (用--动态传参)
"scripts": {
"test": "vitest"
}
就这一行,够了。
等等,那我怎么跑watch和coverage?
答案,就是用--🤷♂️:
# 1. 只跑一次
$ npm run test -- --run
# 实际执行: vitest --run
# 2. 跑watch模式
$ npm run test -- --watch
# 实际执行: vitest --watch
# 3. 跑覆盖率
$ npm run test -- --coverage
# 实际执行: vitest --coverage
# 4. 跑某个特定文件
$ npm run test -- src/my-component.test.ts
# 实际执行: vitest src/my-component.test.ts
--就像一个参数隧道 ,它把你在命令行里,跟在--后面的所有参数,原封不动地扔给了vitest命令。
一个专业的CI/CD脚本
好了,我们把pre/post和--结合起来,看看一个专业的package.json是长什么样子👇。
"scripts": {
// 1. Lint
"lint": "eslint .",
"lint:fix": "eslint . --fix",
// 2. Test
"test": "vitest",
"pretest": "npm run lint", // 在test前,必须先lint
// 3. Build
"build": "tsc && vite build",
"prebuild": "npm run test -- --run", // 在build前,必须先test通过
// 4. Publish (发布的前置钩子)
// prepublishOnly 是一个npm内置的、比prepublish更安全的钩子
// 它只在 npm publish 时执行,而在 npm install 时不执行
"prepublishOnly": "npm run build" // 在发布前,必须先build
}
看看我们构建了怎样一条自动化脚本:
- 你兴高采烈地敲下
npm publish,准备发布。 -
npm一看,有个prepublishOnly,于是它先去执行npm run build。 -
npm一看,build有个prebuild,于是它又先去执行npm run test -- --run。 -
npm一看,test有个pretest,于是它又双叒叕先去执行npm run lint。
最终的执行流是:Lint -> Test -> Build -> Publish。
这些脚本,被pre钩子,自动地、强制地串联了起来。你作为开发者,根本没有机会犯错。你不可能发布一个连Lint都没过或者测试未通过的包😁。
npm scripts,它不是一个简单的脚本快捷方式。它是一个工作流(Workflow)的定义 。
pre和post,定义了你工作流的执行顺序和依赖,保证了代码检查等功能,而--是确保你工作流中的脚本参数。
现在,马上去打开你项目的package.json,看看它,是专业的,还是业余的呢?🤣