npm原理
1. 怎样算个npm包
只要符合以下 a) 到 g) 其中之一条件,就是一个 package:
| 说明 | # | 例子 |
|---|---|---|
| 一个包含了程序和描述该程序的 package.json 文件 的 文件夹 | a) | ./local-module/ |
| 一个包含了 (a) 的 gzip 压缩文件 | b) | ./module.tar.gz |
| 一个可以下载得到 (b) 资源的 url (通常是 http(s) url) | c) | registry.npmjs.org/webpack/-/w… |
一个格式为 <name>@<version> 的字符串,可指向 npm 源(通常是官方源 npmjs.org)上已发布的可访问 url,且该 url 满足条件 (c) | d) | webpack@4.1.0 |
一个格式为 <name>@<tag> 的字符串,在 npm 源上该<tag>指向某 <version> 得到 <name>@<version>,后者满足条件 (d) | e) | webpack@latest |
一个格式为 <name> 的字符串,默认添加 latest 标签所得到的 <name>@latest 满足条件 (e) | f) | webpack |
| 一个 git url, 该 url 所指向的代码库满足条件 (a) | g) | git@github.com:webpack/webpack.git |
上面表格的定义意味着,我们在共享依赖包时,并不是非要将包发表到 npm 源上才可以提供给使用者来安装。这对于私有的不方便 publish 到远程源(即使是私有源),或者需要对某官方源进行改造,但依然需要把包共享出去的场景来说非常实用。
2. npm的几种依赖:
- dependencies 项目依赖
- devDependencies 开发依赖
- peerDependencies 同版本的依赖(宿主依赖)
- bundledDependencies 捆绑依赖
- optionalDependencies 可选依赖(依赖安装失败都不会对整个安装有影响,最好勿用)
它们起到的作用和声明意义是各不相同的。下面我们来具体介绍一下:
- dependencies 表示项目依赖,这些依赖都会成为你的线上生产环境中的代码组成的部分。当 它关联到 npm 包被下载的时候,
dependencies下的模块也会作为依赖, 一起被下载。 devDependencies表示开发依赖, 不会被自动下载的。因为 devDependencies 一般是用于开发阶段起作用或是只能用于开发环境中被用到的。 比如说我们用到的Webpack,预处理器babel-loader、scss-loader,测试工具E2E等, 这些都相当于是辅助的工具包, 无需在生产环境被使用到的。
并不是只有在dependencies中的模块才会被一起打包, 而是在 devDependencies 中的依赖一定不会被打包的。 实际上, 依赖是否是被打包,完全是取决你的项目里的是否是被引入了该模块。
- peerDependencies 表示同版本的依赖, 简单一点说就是: 如果你已经安装我了, 那么你最好也安装我对应的依赖。 举个小例子: 加入我们需要开发一个react-ui 就是一个基于
react开发的UI组件库, 它本身是会需要一个宿主环境去运行的, 这个宿主环境还需要指定的react版本来搭配使用的, 所以需要我们去package.json中去配置:
{
"name": "react-ui",
"version": "1.3.5",
"peerDependencies": {
"react": "2.x"
}
}
bundledDependencies:数组
{
"name": "awesome-web-framework",
"version": "1.0.0",
"bundledDependencies": [
"renderized", "super-streams"
]
}
如果你需要在本地保存npm包,或者通过单个文件下载来获得这些包,你可以通过在bundledDependencies数组中指定包名并执行npm pack,将包捆绑在一个tarball文件中。
假定上述是我们配置的包依赖信息。执行npm pack会将renderized, super-streams两个包一起打包到awesome-web-framework中,当下次在新项目中执行npm i awesome-web-framework时会将这两个包一起下载下来。
3. lock文件
package.lock.json文件,如果项目中没有lock文件。那么将在执行npm install时自动生成与package.json同级的文件。
它的作用主要用来锁默认的依赖版本,最好让别人的依赖和自己的依赖保持统一,这样的错误率最小。
"devDependencies": {
"@commitlint/cli": "^13.2.1",
"@commitlint/config-conventional": "^13.2.0",
"commitizen": "^4.2.4",
"cpx": "^1.5.0",
"cz-customizable": "^6.3.0",
"husky": "^7.0.2"
}
一个小知识:
~会匹配最近的小版本依赖包,比如~1.2.3会匹配所有1.2.x版本,但是不包括1.3.0^会匹配最新的大版本依赖包,比如^1.2.3会匹配所有1.x.x的包,包括1.3.0,但是不包括2.0.0*会安装最新版本的依赖包
4. npm的扁平化优化:
旧版本的npm安装依赖会存在依赖地狱的问题,类比js中的回调地狱。
{
"name": "my-app",
"dependencies": {
"buffer": "^5.4.3",
"ignore": "^5.1.4",
}
}
{
"name": "buffer",
"dependencies": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
}
一层一层依赖下去,照成无止境的依赖关联。
- 当遇到相同模块时,判断已放置在依赖树的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的
node_modules下放置该模块。
5. 缓存机制:
在执行 npm install 或 npm update命令下载依赖后,除了将依赖包安装在node_modules 目录下外,还会在本地的缓存目录缓存一份。
-
查看npm本地缓存的命令:
npm config get cache // ~\AppData\Local\npm-cache_cacache其实你也看到了
_cacache的目录有三个文件:- content-v2(存储
tar包的缓存,二进制的文件,具体资源) - index-v5( 存储
tar包的hash,content-v2 文件的索引) - tmp
- content-v2(存储
-
当
npm下载依赖的时候, 先下载到缓存当中,再解压到我们的项目的node_modules中。 其实pacote是依赖npm-registry-fetch来下载包, npm-registry-fetch 可以通过设置 cache 字段进行相关的缓存工作。
-
!!! !!! 紧接着呢, 我们在每次去安装资源的时候,会根据
package-lock.json中的integrity、verison、name 相关信息会生成一个唯一的key;这个key 就能够对应上 index-v5 目录下的缓存记录;
如果发现有缓存资源,就会去找到 tar 包对应的
hash值. 根据hash再去找缓存中的tar包,然后再次通过
pacote将二进制文件解压缩进我们项目的node_modules目录中,这样就省去了资源下载的网络开销。
5. package.json 的不足之处
某些依赖项自上次安装以来,可能已发布了新版本 。会导致代码拉下来之后相关依赖升级,带来一些依赖版本不同的bug。
解决办法:
在 npm 5.0 版本后,npm install 后都会自动生成一个 package-lock.json 文件 ,当包中有 package-lock.json 文件时,npm install 执行时,如果 package.json 和 package-lock.json 中的版本兼容,会根据 package-lock.json 中的版本下载;如果不兼容,将会根据 package.json 的版本,更新 package-lock.json 中的版本,已保证 package-lock.json 中的版本兼容 package.json。
6. 整体过程:
-
检查
.npmrc文件:优先级为:项目级的.npmrc文件 > 用户级的.npmrc文件> 全局级的.npmrc文件 > npm 内置的.npmrc文件 -
检查项目中有无
lock文件。 -
无 lock
文件:
-
从
npm远程仓库获取包信息 -
根据
package.json构建依赖树,构建过程:
- 构建依赖树时,不管其是直接依赖还是子依赖的依赖,优先将其放置在
node_modules根目录。 - 当遇到相同模块时,判断已放置在依赖树的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的
node_modules下放置该模块。 - 注意这一步只是确定逻辑上的依赖树,并非真正的安装,后面会根据这个依赖结构去下载或拿到缓存中的依赖包
- 构建依赖树时,不管其是直接依赖还是子依赖的依赖,优先将其放置在
-
在缓存中依次查找依赖树中的每个包
-
不存在缓存:
-
从
npm远程仓库下载包 -
校验包的完整性
-
校验不通过:
- 重新下载
-
校验通过:
- 将下载的包复制到
npm缓存目录 - 将下载的包按照依赖结构解压到
node_modules
- 将下载的包复制到
-
存在缓存:将缓存按照依赖结构解压到
node_modules -
-
将包解压到
node_modules -
生成
lock文件
-
有 lock 文件:
- 检查
package.json中的依赖版本是否和package-lock.json中的依赖有冲突。 - 如果没有冲突,直接跳过获取包信息、构建依赖树过程,开始在缓存中查找包信息,后续过程相同
npm script
每当执行npm run,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令。因此,只要是 Shell(一般是 Bash)可以运行的命令,就可以写在 npm 脚本里面。
比较特别的是,npm run新建的这个 Shell,会将当前目录的node_modules/.bin子目录加入PATH变量,执行结束后,再将PATH变量恢复原样。
这意味着,当前目录的node_modules/.bin子目录里面的所有脚本,都可以直接用脚本名调用,而不必加上路径。比如,当前项目的依赖里面有 Mocha,只要直接写mocha test就可以了。
也可以使用npx调用项目安装的模块。npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
通配符
*表示任意文件名,**表示任意一层子目录。
"lint": "jshint *.js"
"lint": "jshint **/*.js"
执行顺序
如果是并行执行(即同时的平行执行),可以使用&符号连接。
继发执行(即只有前一个任务成功,才执行下一个任务),可以使用&&符号。
钩子
npm 脚本有pre和post两个钩子。举例来说,build脚本命令的钩子就是prebuild和postbuild。
npm 默认提供下面这些钩子。
prepublish,postpublish
preinstall,postinstall
preuninstall,postuninstall
preversion,postversion
pretest,posttest
prestop,poststop
prestart,poststart
prerestart,postrestart
参数
向 npm 脚本传入参数,要使用--标明。
参考目录: