🗒️配置 npm

1. 下载和安装

要从 public npm registry 安装和发布 packages,就必须安装 Node.js 和 npm 命令行工具。强烈建议使用 Node version manager 来安装 Node.js and npm,而不建议用 Node installer(因为它会把 npm 安装在一个具有 local 权限的目录里,这会导致 permissions errors 当我们全局运行 npm 包时)。

Node version manager:

  • OSX 和 Linux

  • Windows

    • nodist

    • nvm-windows

node -v
npm -v

2. 目录结构

介绍 npm 使用的 folder structures。

npm 会在我们的电脑上放各种东西。当然,这正是它的工作。

2.1 不同模式

  1. local 安装(默认):会安装在 ./node_modules

  2. global 安装(-g):会安装在 prefix 配置的文件夹下

    • prefix 配置默认是 node 安装的位置

      • 大多数系统,/usr/local

      • Windows,%AppData%\npm

      • Unix,它是上一级(one level up),因为 node 通常安装在 {prefix}/bin/node 而不是 {prefix}/node.exe

    • prefix 没设置,则就用当前 package 或者当前工作目录的根目录

若想 require() 使用,就 local 安装。若想在 command line 里使用,就 global 安装。若想同时使用,就在两个地方都安装,或者使用 npm link

2.2 安装位置

资源
local 模式
global 模式

packages 或

node modules

./node_modules

  • Unix 系统 {prefix}/lib/node_modules/

  • Windows 系统 {prefix}/node_modules/

可执行文件 be linked to

./node_modules/.bin/

  • Unix 系统 {prefix}/bin/

  • Windows 系统 {prefix}/

操作手册 be linked to

不安装

{prefix}/share/man

cache 文件

  • Posix ~/.npm

  • Windows %AppData%/npm-cache

临时文件

默认是环境变量 TMPDIR, TMP, TEMP

  • Unix /tmp

  • Windows c:\windows\temp

补充说明:

  1. packages

    • scoped packages 的安装位置相同,只是它们又被分进了子目录

    • 若是 local 安装的

      • 可以使用 require("packagename") 去 load 它的 main module

      • 使用 require("packagename/lib/path/to/sub/module") 去 load 其它 modules

  2. 可执行文件

    • 对于 local 安装的,就能通过 npm run script 了

  3. cache 文件

    • 其路径可以由 cache config 参数控制的

    • 详见 npm cache

  4. 临时文件

    • 其路径可以由 tmp config 参数配置控制

    • 每次 run program 时,临时文件都会在此根目录下指定一个唯一的文件夹,并在成功退出时被删除

2.3 prefix 补充

当 local 安装时,npm 首先尝试找到合适的 prefix folder。这样的话,即便我们碰巧 cd 进入了其它目录,npm install xxx 也会安装到合理的根目录中。

  1. $PWD 开始,npm 将遍历 folder tree 以检查包含 package.json 文件或 node_modules 文件夹的 folder。

    • 如果找到了,那它就被视为有效的 current directory,以运行 npm commands

      • 这个逻辑受 git 的 .git-folder 的查找逻辑的启发。

    • 如果没有找到 package root,则使用 current folder。

  2. 当运行 npm install foo@1.2.3

    • package 被 loaded 到 cache 中,然后 unpacked 到 ./node_modules/foo

    • 接着,所有 foo 的依赖都被类似地 unpacked 到 ./node_modules/foo/node_modules/..

  3. 所有 bin 文件都 symlinked(符号链接)到 ./node_modules/.bin/

    • 以便在运行 npm scripts 时可以找到它们。

对于 global 安装,packages 的安装方式大致相同,只是使用的文件夹不同。

2.4 循环+冲突+文件夹优化

cycles, conflicts 和 folder parsimony(简约)

cycles 是使用 node 的 module system 的属性来处理的,它会在目录中查找 node_modules 文件夹。所以,在每个阶段,如果某个 package 已经安装在了其祖先的 node_modules 文件夹中,那么它就不在当前位置安装了。

比如 foo -> bar -> baz,如果再加一个 baz -> bar,就不用将 bar 的另一个副本再放到 .../baz/node_modules/ 里了,因为当 baz 调用 require("bar") 时会从 foo/node_modules/bar 里获得安装的副本。

这种 shortcut(快捷方式)只使用在多个嵌套的 node_modules 文件夹中需要安装 exact same version 的 package 时。当 a 的版本不一样时还是有可能出现 a/node_modules/b/node_modules/a 的。然而,如果不多次重复 exact same package,将始终 prevent(阻止/避免)无限 regress(倒退)。

还可以做个优化,就是在尽可能高的级别下安装 dependencies,在 localized "target" folder (hoisting, 提升)。从版本 3 开始,npm 默认 hoist(提升)dependencies。

比如如下 dependency graph:

foo
+-- blerg@1.2.5
+-- bar@1.2.3
|   +-- blerg@1.x (latest=1.3.7)
|   +-- baz@2.x
|   |   +-- quux@3.x
|   |       +-- bar@1.2.3 (cycle)
|   +-- asdf@*
+-- baz@1.2.3
    +-- quux@3.x
        +-- bar@1.2.3

第一:先处理第一层的依赖,即 foo 的依赖项

如下:

`foo`
+-- node_modules
    +-- `blerg@1.2.5`
    +-- `bar@1.2.3`
    +-- `baz@1.2.3`

第二:再依次处理第二层的依赖

  1. 对于 blerg@1.2.5,它没有依赖项。可以考虑 hoist 了,但因为其直接父已经是 root 了所以忽略

  2. 对于 bar@1.2.3,它有三个依赖项

    1. blerg@1.x 不用安了,因为其祖先已经有了,且版本匹配

    2. baz@2.x 需要安,因为它和祖先的 baz@1.2.3 版本不匹配

    3. asdf@* 需要安

  3. 对于 baz@1.2.3,它有一个依赖项

    1. quux@3.x 需要安

结果如下:

foo
+-- node_modules
    +-- `blerg@1.2.5`
    +-- `bar@1.2.3`
    |    +-- node_modules
    |    |   +-- `baz@2.x`
    |    |   +-- `asdf@*`
    +-- `baz@1.2.3`
    |    +-- node_modules
    |    |   +-- `quux@3.x`

第三:再依次处理第三次依赖

  1. bar@1.2.3 -> baz@2.x,它有一个依赖

    1. quux@3.x 不用安,因为祖先里已经有了

    2. 此时 bar@1.2.3 -> baz@2.x 是个叶子节点,但因为 root 下已经有另外一个版本的了所以不提升

  2. bar@1.2.3 -> asdf@*,它没有依赖。可以考虑 hoist,将其提升到 root

  3. baz@1.2.3 -> quux@3.x,它有一个依赖

    1. bar@1.2.3 不用安,因为祖先里已经有了

    2. 此时 baz@1.2.3 -> quux@3.x 是个叶子节点,所以提升到 root

结果如下:

foo
+-- node_modules
    +-- blerg@1.2.5
    +-- bar@1.2.3
    |    +-- node_modules
    |    |   +-- `baz@2.x`
    +-- `asdf@*`
    +-- baz@1.2.3
    +-- `quux@3.x`

所以,最终的 folder structure 如上,我们把所有 dependencies 都 hoist 到尽可能高的级别。当然,真实项目里的版本都是符合条件的明确版本。

我们可以使用 npm ls 命令查看安装的 graphical breakdown(细分)。

2.5 发布

一旦发布,npm 将会在 node_modules 文件夹中查找。只要是不在 bundleDependencies 数组中的 items,就不会被包含在 package tarball(压缩包)中。

这允许 package 维护者在本地安装他们所有的 dependencies(和 dev dependencies),但只 re-publish 那些在别处找不到的 items。相关详情可查阅 package.json。

3. .npmrc

npm 配置文件。npm 从 command line, environment variables 和 npmrc files 获取它的 config settings。

四个相关文件是:

  1. 每个项目的配置文件 /path/to/my/project/.npmrc

  2. 每个用户的配置文件 ~/.npmrc

  3. 全局配置文件 $PREFIX/etc/npmrc

  4. npm 内置配置文件 /path/to/npm/npmrc

npm config 命令可用来更新和编辑 user 和 global npmrc files 的内容。

所有的 npm 配置文件都是 ini 格式的 key = value 参数对列表。注释是以 ;# 开头的行。

4. package.json

package.json 必须是实际的 JSON,而不仅仅是一个 JavaScript 对象字面量。

{
  "name": "hello-world",  // *
  "version": "1.0.0",     // *

  // `npm search`
  "description": "",      // 会在 npm 网站上显示
  "keywords": "",

  // 二选一,提示用户是依赖于 Node.js modules 还是 primitives
  "main": "index.js",     // 程序的 primary entry point
  "browser": "",          // 如果该 module 打算是在 client-side 使用,那么更应该用此字段

  // 脚本,是一个包含 script commands 的 dictionary
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    // default some values based on package contents.
    "start": "node server.js",  
    "install": "node-gyp rebuild", 
  },
  "config": {},   // 设置 configuration parameters,用在升级过程中持续存在的 package scripts

  // 依赖
  "dependencies": {},
  "devDependencies": {},
  "peerDependencies": {},
  "peerDependenciesMeta": {},
  "bundleDependencies": {},
  "optionalDependencies": {},
  "overrides": {},  // 对依赖的依赖做特定修改

  // people 相关
  "author": "one person",
  "contributors": "an array of people",  // ~ `AUTHORS` file 
  "person": {
    "name": "",
    "email": "",
    "url": ""
  },

  // 项目相关
  "homepage": "",                // project homepage
  "bugs": "https://.../issues",  // 也可以是个对象
  "repository": {                // 代码放置的地方
    "type": "git",
    "url": "https://github.com/npm/cli.git"
  },

  // 要求相关
  "engines": {}, // 指定 node 和 npm 版本
  "os": {},      // 指定 operating systems
  "cup": {},

  // 更多
  "license": "ISC",
  "private": true,      // npm will refuse to publish it
  "publishConfig": {},  // 在 publish-time 使用的 config values

  "workspaces": [],  // file patterns 数组
  "files": "",       // file patterns 数组,描述当该 package 作为依赖项安装时要包含的 entries 的

  "bin": "",         // executable files, 可执行文件
  "man": "",         // `man` 命令要查找的文件
  "directories": {}, // CommonJS Packages spec
}

更多信息详见 npm CLI / Configuring npm / package.json

这里介绍的许多行为都受 npm CLI / Using npm / Config 配置设置的影响。

5. lockfile

5.1 package-lock.json

当 npm 修改 node_modules 树或修改 package.json 时,就会自动生成 package-lock.jsonpackage-lock.json 描述了生成的 exact tree,以便后续的 install 能生成相同的树,而不管中间 dependency 的更新。

{
    "name": "",    // 同 package.json
    "version": "", // 同 package.json

    "lockfileVersion": "",  // integer, 从 1 开始

    "packages": {},
    "dependencies": {}
}

5.1.1 应提交 repo

package-lock.json 应该提交到 source repository,目的有:

  • 确保后续的 install 能生成完全相同的依赖关系

  • 不用提交 node_modules 目录本身

  • 提高 tree changes 的可见性,通过可读的 source control diffs

  • 优化安装过程,因为 npm 可以跳过重复的 metadata resolution

  • 从 npm v7 开始,lockfiles 包含了足够的信息以获取 package tree 的完整 picture,减少读取 package.json 文件的需要,允许显著的性能提升

5.1.2 隐藏的 lockfiles

为了避免重复处理 node_modules 文件夹,从 npm v7 开始使用一个 hidden lockfile node_modules/.package-lock.json。它包含 tree 的相关信息,用于代替读取整个 node_modules hierarchy(层次结构),前提是满足以下条件:

  • 它引用的所有 package folders 都存在于 node_modules 层次结构中

  • 不存在 package folders 是在 node_modules 层次结构中却没在 lockfile 中列出

  • file 的修改时间至少和其引用的所有 package folders 一样新

npm 的旧版本会忽略该 hidden lockfile。

5.2 npm-shrinkwrap.json

可发布的 lockfile(锁文件)。

该文件是由 npm shrinkwrap 命令创建的。它等价于 package-lock.json,不同的是 npm-shrinkwrap.json 可能会在发布 package 时包含。

5.3 对比

package-lock.json vs npm-shrinkwrap.json

  • 相同点:格式相同,在项目的根目录下发挥着相似的功能

  • 不同点:

    • package-lock.json 不能发布,并且在项目的根目录以外的任何地方该文件都会被忽略

    • npm-shrinkwrap.json 允许发布,并且从 point 定义 dependency tree。除非部署 CLI 工具或以其它方式使用发布过程来生成 production packages,否则不建议这样做

如果 package 根目录中同时有这两个文件,那么 npm-shrinkwrap.json 会优先于 package-lock.json 文件。

Last updated