最近在写一个npm包。写的过程中遇到的最大问题是:如何在 npm install <package> 之前能够先执行一个脚本呢?。那么本篇文章将带着这个问题去了解   npm 脚本。读完本篇文章你可以了解:
- 什么是 npm脚本?
- 如何使用 npm脚本
- npm脚本相关的生命周期(重点)
什么是 npm 脚本 ?
npm    脚本是 package.json 文件 scripts  字段存储的脚本命令。存储的脚本命令可以是:
- 预设的生命周期脚本 
- 自定义的任何脚本 
npm    脚本的目的是提供一种简单的方法来执行重复的任务。比如:
- 启动项目
- 打包项目
- 执行单元测试
- …
当我们要定义一个 npm 脚本时, 我们需要做的就是设置它的名称,并且在 package.json 文件 scripts 添加该名称和脚本。如:
| 1 | "scripts": { | 
以上是 package.json文件的一个片段,可看出 scripts 字段是一个对象, 它每一个属性名称对应着一个脚本。如 build 对应的脚本是 node index.js。
可以使用 npm run <event>    或者 npm run-script  <event> 来执行相应的命令。 
| 1 | $ npm run build | 
想要查看当前的所有的 npm 脚本命令。可以使用不带任何参数的 npm run 来查看。
| 1 | $ npm run | 
执行原理
npm 脚本的原理是每当执行 npm run  时,就会自动新建一个shell,然后在这个  shell  里面执行指定的脚本命令。因此,只要是 shell可以运行的命令,就可以写在 npm 脚本里面。
上面所说的
shell一般是指Bash
但是有一点需要注意, npm run  新建 shell的时候,  会把当前的目录的 node_modules/.bin  子目录加入PATH 变量。 执行完之后再将 PATH 变量恢复原样。
PATH变量即PATH环境变量,一般是指在操作系统中用来指定系统运行环境的一些参数,如: 临时文件夹和系统文件夹位置等。windows和iOS操作系统中的PATH环境变量,当要求系统运行一个程序而没有告诉告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,如果找不到, 可以到PATH中执行的路径去找。
由于这一特点,所以当前目录的node_modules/.bin子目录里面的所有脚本,都可以直接用脚本名调用 ,而不必加上路径。如:
| 1 | "scripts": { | 
而不用写成这样:
| 1 | "scripts": { | 
npm 脚本唯一要求就是可以在 shell执行,因此它不一定是 NODE 脚本,任何可执行的文件都可以可以写在里面。
npm  脚本的退出码,也遵守 Shell 脚本规则,如果退出码不是 0, npm  就认为这个脚本执行失败。
通配符
因为 npm 脚本就是 shell 脚本,所以是可以使用shell 通配符。
| 1 | "scripts": { | 
上面代码表示 * 表示任意文件名,**表示任意一层子目录。
生命周期脚本
文章一开始我们就有一个问题: 如何在 npm install <packge> 之前先执行一个脚本呢? 其实这就用到了 npm 的生命周期脚本了。即官方自己的命令属性。
| 1 | "scripts": { | 
如上这个package.json字段,其中build 是用户自己自定义的命令属性,而prepublish则是npm自带的生命周期钩子,表示在npm publish 之前执行该脚本。
那么下面的内容我们就主要来介绍生命周期脚本。在这之前, 我们先初始化一个例子以供我们之后演示。
例子准备
我们先做个前期的例子准备, 之后用例子一一介绍相关的钩子
- 创建文件夹
| 1 | $ mkdir shuliqi-npm-demo & cd shuliqi-npm-demo | 
- 初始化
| 1 | $ npm init | 
- 创建 index.js文件
| 1 | $ touch index.js | 
| 1 | // index.js | 
那么我们现在已经有了一个基础的例子。
pre[event 和 post[event]
当我们执行任意的 npm run <event> 脚本的时候, 回依次自动触发pre<event>, post<event>的生命周期。
假如有一个命令叫 build。那么当执行npm run build时:
- prebuild
- build
- postbuild
例子:
packge.json:
| 1 | { | 
如上代码我们执行 npm run build 的时候,结果如下:
 
pre<event>或则post<event>中的event可以是我们自定义的,也可以是系统自带的, 如何是我们自定义的, 我们需要把<event>脚本也写上:如上是系统的, 是不需要写的<event>脚本。具体可以看下面的内容。 如上的例子是自定义的<event>
本例子如需代码可点击: npm-demo
npm publish
该命令是对包进行发布。当执行 npm publish  会执行如下的顺序脚本:
- prepublishOnly
- prepack
- postpack
- publish
- postpublish
例子:
packge.json:
| 1 | { | 
结果:
 
本例子如需代码可点击: npm publish
npm pack
该命令是对当前目录下任何可安装的内容(软件包,tarball,tarball url,name@tag,name@version,名称或者作用域名称)提取到缓存中。然后将 tarball 复制到当前工作环目录,名为<name-version>.tgz。
当执行 npm pack的时候会执行如下的脚本顺序:
-  prepack
-  postpack
例子:
packge.json:
| 1 | { | 
结果:
 
本例子如需代码可点击: npm pack
npm install
该命令是用来安装依赖的。具体操作是当发出npm install <packge>的命令时会发生如下的情况:
- npm向- registry查询模块压缩包的网址。
- 下载压缩包,存放在 ~/.npm目录。
- 解压压缩包到当前目录的 node_modules目录。
当执行 npm install <packge>的时候也会有生命周期,具体执行的脚本顺序如下:
- preinstall
- install
- postinstall
例子:
packge.json:
| 1 | { | 
表示在安装依赖之前先执行preinstall脚本。之后执行postinstall脚本。
到这我们的代码已经发成了, 现在我们需要把这个包发布,最后来 npm install该包看看结果。发布只需要在项目根目录执行npm publish 即可。
结果:
我们执行 npm install shuliqi-npm-demo@1.0.18 --loglevel info 来下载我们这个 npm 包。 其中 --loglevel info 表示我们需要在控制台显示 info级别的信息。
关于 –logleve 详情
 
从图上可看出: preinstall 脚本在 postinstall脚本之前。
这个生命周期也是我们文章开头想要实现的功能。解决!!!!
本例子如需代码可点击: npm install
npm ci
该命令跟 npm install类似。但是它主要是用于自动化环境中,如:测试平台,持续集成和部署,或者任何你希望确保对依赖项进行全新安装的情况。
npm ci 在以下这种情况会明显更快:
- 有 package-lock.json或者npm-shrinkwrap.json文件。
- node_modules文件夹丢失或为空。
和 npm install  的主要区别 npm ci是:
- 项目必须有 - package-lock.json或者- npm-shrinkwrap.json文件。
- 如果 - package-lock.json或- npm-shrinkwrap.json中的依赖项- package.json不匹配,- npm ci将退出并出现报错,而不是更新- package-lock.json或- npm-shrinkwrap.json。
- npm ci只能一次安装整个项目,不能使用该命令添加某个依赖项。
- 如果 - node_modules已经存在,它将在- npm ci开始安装之前自动删除。
- 永远不会写入 - packge.json或者任何包锁,安装基本上被冻结。
当执行 npm ci 的时候也会有生命周期,具体执行的脚本顺序如下:
- preinstall
- install
- postinstall
例子:
package.json:
| 1 | { | 
然后进行该包的发布:npm publish 。
在当前目录下创建一个npm_ci_demo 用于测试:
| 1 | $ mkdir npm_ci_demo | 
在目录 npm_ci_demo  执行: npm ci --loglevel info  结果如下:
 
本例子如需代码可点击: npm ci
npm rebuild
该命令表示重建构建软件包。使用的场景如:在一个项目使用了npm install之后,当升级,降级了Node 版本或者复制该项目到其他电脑(其他电脑Node可能跟当前的不一致)可使用该命令重新构建,如果重新构建,那么将使用新的Node(二进制文件)重新编译所有的 C++插件。
当执行该命令的时候,具体执行的生命周期脚本顺序如下:
- preinstall
- install
- postinstall
例子:
package.json:
| 1 | { | 
之后我们在该目录新建一个npm_rebuild目录用于测试,然后在npm_rebuild 安装依赖:npm i shuliqi-npm-demo@1.0.23 --save。
使用命令:npm rebuild --loglevel info 来重新构建。其中 --loglevel info 表示我们需要在控制台显示 info级别的信息。
 
本例子如需代码可点击: npm rebuild
npm restart
该命令表示将重启一个新的项目。相当于执行了npm run-script restart。
如果在package.json定义了restart脚本, 那么执行 npm restar 之后的生命周期顺序如下:
- prerestart
- restart
- postrestart
如果在 package.json 没有定义了 restart 脚本。但是有stop 或 start 脚本。 那么执行 npm restar 之后的生命周期顺序如下:
- prerestart
- prestop
- stop
- poststop
- prestart
- start
- poststart
- postrestart
如果以上的场景都不符合, 那么执行 npm restart将会直接报错。
例子1:
package.json:
| 1 | { | 
执行npm restart结果如下:
 
本例子如需代码可点击: npm restart 第一种场景
例子2:
package.json:
| 1 | { | 
执行nnpm restart结果如下:
 
本例子如需代码可点击: npm restart 第二种场景
npm start
该命令将尝试执行 package.json 定义的 start  脚本。分为以下这两种情况:
如果 package.json 没有start 脚本。 那么将会执行 node server.js :
 
如果 package.json 有start 脚本,那么执行生命周期如下:
- prestart
- start
- poststart
例子
package.json:
| 1 | { | 
结果:
 
npm stop
该命令将尝试执行 package.json 定义的 stop  脚本。分为以下情况:
如果 package.json 有定义stop 脚本,那么执行生命周期如下:
- prestop
- stop
- poststop
 
如果 package.json 没有定义stop 脚本,它则不会像 npm start 一样去执行默认的脚本。而是直接报错
npm test
该命令将尝试执行 package.json 定义的 test  脚本。如果没有定义将会直接报错。 如果package.json 定义有 test   脚本。 那么执行的生命周期顺序如下:
- pretest
- test
- posttest
缺少 npm unstall 说明
最后来说明关于缺少npm unstall的原因。在npm v6是有 uninstall 生命周期脚本。但 npm v7 没有。去掉的原因是:没有明确的方法可以为脚本提供足够的上下文以使其有用。
因为删除包的方式很多:
- 用户直接卸载了这个包
- 用户卸载了依赖包,因此正在卸载此依赖项
- 用户卸载了一个依赖包,但另一个包也依赖于这个版本
- 此版本已作为副本与另一个版本合并
- 等等
由于缺乏必要的上下文,uninstall生命周期脚本没有实现并且无法运行。
