# npm
# npm 的安装机制
npm 会优先将依赖包安装到项目目录。
这样做的好处是使不同项目的依赖各成体系,同时还减轻了包作者的 API 压力;缺点也比较明显,如果我们的 repo_a 和 repo_b 都有一个相同的依赖 pkg_c,那么这个公共依赖将在两个项目中各被安装一次。也就是说,同一个依赖可能在我们的电脑上多次安装。
# npm 的安装过程
npm 安装依赖有以下几个过程:
1. 检查配置
包括项目级、用户级、全局级、内置的 .npmrc 文件。
2. 确定依赖版本,构建依赖树
确定项目依赖版本有两个来源,一是 package.json 文件,一是 lockfile 文件,两个确认版本、构建依赖树的来源,互不可少、相辅相成。如果 package-lock.json 文件存在且符合 package.json 声明的的情况下,直接读取;否则重新确认依赖的版本。
3. 下载包资源
下载前先确认本地是否存在匹配的缓存版本,如果有就直接使用缓存文件,如果没有就下载并添加到缓存,然后将包按依赖树解压到 node_modules 目录。
4. 生成 lockfile 文件
lockfile 的存在(package-lock.json 文件),保证了项目依赖结构的确定性,保障了项目在多环境运行的稳定性。
因此,项目中一定要存在 lockfile 文件,且禁止手动修改,因为这是项目稳定性运行的保障。
补充
构建依赖树的过程中,版本确认需要结合 package.json 和 package-lock.json 两个文件。先确认 package-lock.json 安装版本,符合规则就以此为准,否则由 package.json 声明的版本范围重新确认。特别地,若是在开发中手动更改包信息,会导致 lockfile 版本信息异常,也可能由 package.json 确认。确认好的依赖树会存到 package-lock.json 文件中,这里跟 yarn.lock 存在差异。
同一个依赖,更高版本的包会安装到顶层目录,即 node_modules 目录;否则会分散在某些依赖的 node_modules 目录,如:node_modules/expect-jsx/node_modules/react 目录。
如果依赖升级,造成版本不兼容,需要多版本共存,那么仍然是将高版本安装到顶层,低版本分散到各级目录。
# yarn
# yarn 的安装机制
1. 确定性
通过 yarn.lock 等机制,保证了确定性,这里的确定性包括但不限于明确的依赖版本、明确的依赖安装结构等。即在任何机器和环境下,都可以以相同的方式被安装。
2. 模块扁平化安装
将依赖包的不同版本,按照一定策略,归结为单个版本,以避免创建多个副本造成冗余。(npm3 也有相同的优化)
3. 更好的网络性能
Yarn 采用了请求排队的理念,类似并发连接池,能够更好地利用网络资源;同时引入了更好的安装失败时的重试机制。(npm 较早的版本是顺序下载,当第一个包完全下载完成后,才会将下载控制权交给下一个包)
4. 引入缓存机制,实现离线策略(npm5 也有类似的优化)
# yarn.lock 文件解读
所有依赖,不管是项目声明的依赖,还是依赖的依赖,都是扁平化管理。
依赖的版本是由所有依赖的版本声明范围确定的,具备相同版本声明范围的依赖归结为一类,确定一个该范围下的依赖版本。如果同一个依赖多个版本共存,那么会并列归类。
每个依赖确定的版本中,是由以下几项构成:
多个依赖的声明版本,且符合 semver 规范;
确定的版本号 version 字段;
版本的完整性验证字段
依赖列表
相比 npm,Yarn 一个显著区别是 yarn.lock 中子依赖的版本号不是固定版本。也就是说单独一个 yarn.lock 确定不了 node_modules 目录结构,还需要和 package.json 文件进行配合。
如果 yarn.lock 在代码合并的过程中出现了问题,可以尝试使用 yarn install 解决问题。
# pnpm
pnpm (opens new window) 指 performant npm(高性能的 npm),它是快速的,节省磁盘空间的包管理工具,同时,它也较好地支持了 workspace 和 monorepos。
如果项目中,你使用了某个依赖项的多个版本,那么 pnpm 只会将有差异的文件添加到仓库。
如果某个依赖包有 100 个文件,而它的新版本只改变了其中 1 个文件。那么 pnpm update 时只会添加 1 个新文件,而不会复制整个新版本的所有包。
此外,所有文件都会存储在硬盘上的某一位置。当依赖包被被安装时,其中的文件会硬链接到这一位置,而不会占用额外的磁盘空间。同时,项目中允许共享同一版本的依赖。
pnpm 与 npm、yarn、yarn pnp 工具链效果对比可以查看 pnpm benchmarks (opens new window)。
# 硬链接
硬链接的概念来自于 Unix 操作系统,它是指将一个文件 A 指针复制到另一个文件 B 指针中,文件 B 就是文件 A 的硬链接。
通过硬链接,不会产生额外的磁盘占用,并且,两个文件都能找到相同的磁盘内容。
硬链接的数量没有限制,可以为同一个文件产生多个硬链接。
# 符号链接
符号链接又称为软连接,如果为某个文件或文件夹 A 创建符号链接 B,则 B 指向 A。符号链接类似于 windows 系统中的快捷方式。
# pnpm 的原理
pnpm 通过硬链接(hard links)和符号链接(symbolic links,软链接)的组合,实现了对相同版本的包的共享,以及对不同版本和全局安装的包的引用。这种方法既减少了磁盘空间的使用,又提高了安装和运行时的性能。
当多个项目依赖于相同版本的包时,pnpm 会使用硬链接来共享这些依赖项。通过硬链接,相同版本的包只需在磁盘上存储一份,而不是每个项目都复制一份。这大大减少了磁盘空间的占用。
当项目依赖于不同版本的同一个包,或者依赖于全局安装的包时,pnpm 使用符号链接。
# npm、yarn、pnpm 比较
性能比较:yarn 和 pnpm 通常在安装速度上优于 npm,尤其是在大型项目中。
版本锁定:所有三者都支持版本锁定,确保在不同环境中使用相同的依赖版本。
磁盘空间:pnpm 在磁盘空间的使用上更为高效。
并行安装:yarn 采用并行安装策略,pnpm 通过符号链接在全局位置共享依赖项来提高性能。
# 常用的 npm 脚本
# 内置脚本
下面所有的命令的效果都是一样的:
npm run-script test
npm run test
npm test
npm t
2
3
4
npm run-script start
npm run start
npm start
2
3
# 执行多个脚本
要依次运行多个脚本,可以使用 &&,例如:
npm run lint && npm test
要并行运行多个脚本,可以使用 &,例如:
npm run lint & npm test
# pre & post
我们可以为任何脚本创建 “pre” 和 “post” 脚本,npm 会自动按顺序运行它们。
唯一的要求是脚本的名称(后跟 “pre” 或 “post” 前缀)与主脚本匹配。例如:
{
"scripts": {
"prehello": "echo \"--Pre \"",
"hello": "echo \"Hello World\"",
"posthello": "echo \"--post\""
}
}
2
3
4
5
6
7
如果我们执行 npm run hello, npm 会按以下顺序执行脚本:prehello, hello, posthello。
# 错误
当脚本以非 0 退出码结束时,这意味着在运行脚本的时候发生了错误,并终止了执行。例如:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
2
3
如果想减少错误日志并非防止脚本抛出错误,可以使用下面的命令来 “静默” 处理, (比如在 ci 中,即使测试命令失败,也希望整个管道继续运行,就可以使用这个命令)
npm run <script> --silent
// 或者
npm run <script> -s
2
3
如果脚本名不存在时不想报错,可以使用下面的命令:
npm run <script> --if-present
# 日志等级
有不同的日志级别:
"silent", "error", "warn", "notice", "http", "timing", "info", "verbose", "silly"
默认值为 “notice”。日志级别确定哪些日志将显示在输出中。将显示比当前定义更高级别的任何日志。
我们可以使用 --loglevel
明确定义要在运行命令时使用的日志级别。
还可以使用一些简短的版本来简化命令:
-s, --silent, --loglevel silent
-q, --quiet, --loglevel warn
-d, --loglevel info
-dd, --verbose, --loglevel verbose
-ddd, --loglevel silly
2
3
4
5
要获得最高级别的详细信息,我们可以使用下面的命令:
npm run <script> -ddd
// 或
npm run <script> --loglevel silly
2
3
# 从文件中引用路径
如果脚本很复杂的话,在 package.json 中维护明显会越来越冗长,也越来越难维护,因此复杂的脚本我们一般会写在文件中,再从文件中执行脚本。例如:
{
"scripts": {
"hello:js": "node scripts/helloworld.js",
"hello:bash": "bash scripts/helloworld.sh",
"hello:cmd": "cd scripts && helloworld.cmd"
}
}
2
3
4
5
6
7
# 访问环境变量
在执行 npm 脚本时,npm 提供了一组我们可以使用的环境变量。
npm_config_<val> 或者 npm_package_<val>
例如:
{
"scripts": {
"config:loglevel": "echo \"Loglevel: $npm_config_loglevel\""
}
}
2
3
4
5
终端输出如下:
➜ xxx npm run config:loglevel
> xx@1.0.0 config:loglevel /Users/beidan/Desktop/xxx
> echo "Loglevel: $npm_config_loglevel"
Loglevel: notice
2
3
4
5
6
# 传递参数
在某些情况下,我们可能需要向脚本传递一些参数,可以使用命令末尾的 -- 来实现这一点。添加到脚本中的任何 -- 都会被转换成一个带有 npm 配置前缀的环境变量。
npm run <script>---<argument>="value"
例如:
{
"scripts": {
"ttt": "echo \"ttt $npm_config_firstname!\""
}
}
2
3
4
5
➜ xxx npm run ttt --firstname=234 // 传入
> xx@1.0.0 ttt /Users/beidan/Desktop/xxx
> echo "ttt $npm_config_firstname!"
ttt 234! // 输出的值
2
3
4
5
6
# 调试 npm 包神器:yalc
当我们开发了一个 npm 包之后,通常需要进行调试,看看它在实际项目中的表现如何。
一般来讲,调试方案有以下几种:
# 通过相对或绝对路径引用
将业务代码中的 import 和 require 所用到的地址,从在 node_modules 里检索改为真实的物理地址。
但是这种方式需要频繁改业务代码,这既麻烦又危险。
# 发布到 npm 源后再调试
将开发好的包发布到 npm 或私有 npm 上,然后在本地重新 install 后直接使用。
但是这种方式需要频繁走 npm 包的发布安装流程,效率也不高。
# 使用 npm link 或 yarn link 调试
使用 npm link 或 yarn link 等方式通过软链接的方式进行调试。这种方式的实现原理是:
在全局包路径下创建一个软连接指向该 npm 包;
在项目里通过软连接,将全局的软链接指向其 node_modules 下对应的包。
# 第一步,在开发好的 npm 包中执行:
npm link
# or
yarn link
# 第二步,在项目中执行:
npm link xxx
# or
yarn link xxx
2
3
4
5
6
7
8
9
但是这种方式也有缺点:
会影响 node_modules 中原本的包;
软链接和文件系统会引发其他各种奇怪的问题。
# 使用 yalc
yalc (opens new window) 主要解决了一些 npm link/yarn link 本身存在的缺陷,满足了包开发者的实际需求。
结合 yalc ,我们可以优化 npm 包调试的流程。
1. 在 npm 包中增加命令
"scripts": {
"build": "打包包的命令",
"async": "npm run build && yalc push",
"watch": "nodemon --ignore dist/ --ignore node_modules/ --watch src/ -C -e ts,tsx,scss --debug -x 'tnpm run async'", // 自动监听
},
2
3
4
5
2. 在项目中执行以下命令
yalc link 包名
npm run start
2
这样在 npm 包中修改,在项目中可以快速看到结果,快速验证了,并且不会出现 npm link 中各种奇奇怪怪的问题。
补充
nodemon 可以来监视文件更改并执行对应的命令。
nodemon
--ignore dist/ # 忽略目录
--ignore node_modules/
--watch projects # 观察目录
-C # 只在变更后执行,首次启动不执行命令
-e ts,html,less,scss # 监控指定后缀名的文件
--debug # 调试
-x "npm run build && yalc push" # 自定义命令
2
3
4
5
6
7
8
# 如何发布一个 npm 包
发布之前可以先到 npm 官网上查看包名是否有被使用过。
执行
npm login
登录 npm 账号,如果没有账号,需要上 npm 官网注册一个。执行
npm publish
发布包。发布完成后,隔一会就可以在 npm 官网上找到发布的包了。每次包有更新时,都需要先升级版本,再重新发布。升级版本的规则和命令如下:
升级补丁版本号:
npm version patch
升级小版本号:
npm version minor
升级大版本号:
npm version major
注意
执行 npm login
命令的时候,如果发现明明账号密码都是正确的,但就是登录不上,此时需要特别注意下 npm 源是否是官方源。
npm 会优先读取项目里的 .npmrc 文件,如果项目中没有改文件,则会去当前电脑登录用户下找 ~/.npmrc
。
因此,为了确保能够顺利发布,最好是在当前项目下创建一个 .npmrc 文件,并指定为 npm 官方源。
registry=https://registry.npmjs.org
配置全局的 npm 源:
// 配置官方源
npm config set registry https://registry.npmjs.org
// 配置淘宝镜像源
npm config set registry https://registry.npm.taobao.org
2
3
4
# npm 企业级部署私服原理
npm 中的源(registry),其实就是一个查询服务。以 npmjs.org 为例,它的查询服务网址是 https://registry.npmjs.org/ ,在这个网址后加上依赖的名字,就会得到一个 JSON 对象,里面包含了依赖所有的信息。例如:
https://registry.npmjs.org/react
https://registry.npm.taobao.org/react
2
我们可以通过 npm config set registry 命令来设置安装源。你知道公司为什么要部署私有的 npm 镜像吗?虽然 npm 并没有被屏蔽,但是下载第三方依赖包的速度依然较缓慢,这严重影响 CI/CD 流程或本地开发效率。通常我们认为部署 npm 私服具备以下优点:
确保高速、稳定的 npm 服务;
确保发布私有模块的安全性;
审核机制可以保障私服上 npm 模块质量和安全;
部署企业级私服,能够获得安全、稳定、高速的保障。
# node_modules 的包安全性问题如何解决
node_modules 中的包安全性是⼀个重要的问题,因为 Node.js 项⽬通常会依赖⼤量的第三⽅包。以下是⼀些解决 node_modules 包安全性问题的策略:
# 1. 使⽤可信的包
- 选择可靠的包
选择社区认可、活跃维护的包。查看包的下载量、更新频率、开放的问题和拉取请求等。
- 审查依赖
审查新添加的包及其⼦依赖,以确保它们来⾃可信的源且被良好维护。
# 2. 定期扫描安全漏洞
- 使⽤安全扫描⼯具
使⽤如 npm audit (opens new window),snyk (opens new window),yarn audit (opens new window) 等⼯具定期扫描 node_modules 以识别安全漏洞。
- 及时更新依赖
定期更新项⽬依赖,以获取最新的安全修复和更新。
# 3. 锁定依赖版本
- 使⽤锁⽂件
使⽤ package-lock.json(npm)或 yarn.lock(Yarn)来锁定依赖版本,确保环境⼀致性和依赖的可预测性。
# 4. 最⼩化依赖
- 减少不必要的依赖
仔细评估项⽬是否真正需要某个包。减少不必要的依赖可以降低安全⻛险。
# 5. 使⽤私有 npm 仓库
- 设置私有仓库
使⽤如 Nexus、Artifactory 或 npm Enterprise 的私有仓库,可以控制和审查使⽤的包。
# 6. 代码审查和测试
- 严格代码审查
对引⼊第三⽅包的更改实施代码审查流程,确保团队成员理解添加的包及其功能。
- 编写测试
为重要的依赖编写测试,确保它们的⾏为符合预期。
# 7. CI/CD 集成
- 集成到持续集成流程
在 CI/CD 流程中集成安全扫描和依赖更新步骤,以⾃动化安全检查。
# 8. 遵循最佳实践
- 遵循安全最佳实践
遵循 Node.js 和 npm 的安全最佳实践,如使⽤ HTTPS 获取依赖,不将敏感信息存储在代码中等。