# 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
1
2
3
4
npm run-script start
npm run start
npm start
1
2
3

# 执行多个脚本

依次运行多个脚本,可以使用 &&,例如:

npm run lint && npm test
1

并行运行多个脚本,可以使用 &,例如:

npm run lint & npm test
1

# pre & post

我们可以为任何脚本创建 “pre” 和 “post” 脚本,npm 会自动按顺序运行它们。

唯一的要求是脚本的名称(后跟 “pre” 或 “post” 前缀)与主脚本匹配。例如:

{
  "scripts": {
    "prehello": "echo \"--Pre \"",
    "hello": "echo \"Hello World\"",
    "posthello": "echo \"--post\""
  }
}
1
2
3
4
5
6
7

如果我们执行 npm run hello, npm 会按以下顺序执行脚本:prehello, hello, posthello。

# 错误

当脚本以非 0 退出码结束时,这意味着在运行脚本的时候发生了错误,并终止了执行。例如:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
}
1
2
3

如果想减少错误日志并非防止脚本抛出错误,可以使用下面的命令来 “静默” 处理, (比如在 ci 中,即使测试命令失败,也希望整个管道继续运行,就可以使用这个命令)

npm run <script> --silent
// 或者
npm run <script> -s
1
2
3

如果脚本名不存在时不想报错,可以使用下面的命令:

npm run <script> --if-present
1

# 日志等级

有不同的日志级别:

"silent", "error", "warn", "notice", "http", "timing", "info", "verbose", "silly"
1

默认值为 “notice”。日志级别确定哪些日志将显示在输出中。将显示比当前定义更高级别的任何日志。

我们可以使用 --loglevel 明确定义要在运行命令时使用的日志级别。

还可以使用一些简短的版本来简化命令:

-s, --silent, --loglevel silent
-q, --quiet, --loglevel warn
-d, --loglevel info
-dd, --verbose, --loglevel verbose
-ddd, --loglevel silly
1
2
3
4
5

要获得最高级别的详细信息,我们可以使用下面的命令:

npm run <script> -ddd
// 或
npm run <script> --loglevel silly
1
2
3

# 从文件中引用路径

如果脚本很复杂的话,在 package.json 中维护明显会越来越冗长,也越来越难维护,因此复杂的脚本我们一般会写在文件中,再从文件中执行脚本。例如:

{
  "scripts": {
    "hello:js": "node scripts/helloworld.js",
    "hello:bash": "bash scripts/helloworld.sh",
    "hello:cmd": "cd scripts && helloworld.cmd"
  }
}
1
2
3
4
5
6
7

# 访问环境变量

在执行 npm 脚本时,npm 提供了一组我们可以使用的环境变量。

npm_config_<val> 或者 npm_package_<val>
1

例如:

{
  "scripts": {
    "config:loglevel": "echo \"Loglevel: $npm_config_loglevel\""
  }
}
1
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
1
2
3
4
5
6

# 传递参数

在某些情况下,我们可能需要向脚本传递一些参数,可以使用命令末尾的 -- 来实现这一点。添加到脚本中的任何 -- 都会被转换成一个带有 npm 配置前缀的环境变量。

npm run <script>---<argument>="value"
1

例如:

{
    "scripts": {
        "ttt": "echo \"ttt $npm_config_firstname!\""
    }
}
1
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! // 输出的值
1
2
3
4
5
6

# 调试 npm 包神器:yalc

当我们开发了一个 npm 包之后,通常需要进行调试,看看它在实际项目中的表现如何。

一般来讲,调试方案有以下几种:

# 通过相对或绝对路径引用

将业务代码中的 import 和 require 所用到的地址,从在 node_modules 里检索改为真实的物理地址。

但是这种方式需要频繁改业务代码,这既麻烦又危险。

# 发布到 npm 源后再调试

将开发好的包发布到 npm 或私有 npm 上,然后在本地重新 install 后直接使用。

但是这种方式需要频繁走 npm 包的发布安装流程,效率也不高。

使用 npm link 或 yarn link 等方式通过软链接的方式进行调试。这种方式的实现原理是:

  • 在全局包路径下创建一个软连接指向该 npm 包;

  • 在项目里通过软连接,将全局的软链接指向其 node_modules 下对应的包。

# 第一步,在开发好的 npm 包中执行:
npm link
# or
yarn link

# 第二步,在项目中执行:
npm link xxx
# or
yarn link xxx
1
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'", // 自动监听
},
1
2
3
4
5

2. 在项目中执行以下命令

yalc link 包名
npm run start
1
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" # 自定义命令
1
2
3
4
5
6
7
8

# 如何发布一个 npm 包

  1. 发布之前可以先到 npm 官网上查看包名是否有被使用过。

  2. 执行 npm login 登录 npm 账号,如果没有账号,需要上 npm 官网注册一个。

  3. 执行 npm publish 发布包。发布完成后,隔一会就可以在 npm 官网上找到发布的包了。

  4. 每次包有更新时,都需要先升级版本,再重新发布。升级版本的规则和命令如下:

  • 升级补丁版本号:npm version patch

  • 升级小版本号:npm version minor

  • 升级大版本号:npm version major

注意

执行 npm login 命令的时候,如果发现明明账号密码都是正确的,但就是登录不上,此时需要特别注意下 npm 源是否是官方源。
npm 会优先读取项目里的 .npmrc 文件,如果项目中没有改文件,则会去当前电脑登录用户下找 ~/.npmrc
因此,为了确保能够顺利发布,最好是在当前项目下创建一个 .npmrc 文件,并指定为 npm 官方源。

registry=https://registry.npmjs.org
1

配置全局的 npm 源:

// 配置官方源
npm config set registry https://registry.npmjs.org
// 配置淘宝镜像源
npm config set registry https://registry.npm.taobao.org
1
2
3
4

# npm 企业级部署私服原理

npm 中的源(registry),其实就是一个查询服务。以 npmjs.org 为例,它的查询服务网址是 https://registry.npmjs.org/ ,在这个网址后加上依赖的名字,就会得到一个 JSON 对象,里面包含了依赖所有的信息。例如:

https://registry.npmjs.org/react
https://registry.npm.taobao.org/react
1
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 获取依赖,不将敏感信息存储在代码中等。

上次更新时间: 2023年12月24日 00:19:28