跳到主要内容

介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?

1. npm 模块安装机制:

  • 发出npm install命令
  • 查询node_modules目录之中是否已经存在指定模块
    • 若存在,不再重新安装
    • 若不存在
      • npm 向 registry 查询模块压缩包的网址
      • 下载压缩包,存放在根目录下的.npm目录里
      • 解压压缩包到当前项目的node_modules目录

2. npm 实现原理

输入 npm install 命令并敲下回车后,会经历如下几个阶段(以 npm 5.5.1 为例):

  1. 执行工程自身 preinstall

当前 npm 工程如果定义了 preinstall 钩子此时会被执行。

  1. 确定首层依赖模块

首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。

工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。

  1. 获取模块

获取模块是一个递归的过程,分为以下几步:

  • 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
  • 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
  • 查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。
  1. 模块扁平化(dedupe)

上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。

从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。

这里需要对重复模块进行一个定义,它指的是模块名相同semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。

比如 node-modules 下 foo 模块依赖 lodash@^1.0.0,bar 模块依赖 lodash@^1.1.0,则 ^1.1.0 为兼容版本。

而当 foo 依赖 lodash@^2.0.0,bar 依赖 lodash@^1.1.0,则依据 semver 的规则,二者不存在兼容版本。会将一个版本放在 node_modules 中,另一个仍保留在依赖树里。

举个例子,假设一个依赖树原本是这样:

node_modules

-- foo
---- lodash@version1

-- bar
---- lodash@version2

假设 version1 和 version2 是兼容版本,则经过 dedupe 会成为下面的形式:

node_modules

-- foo

-- bar

-- lodash(保留的版本为兼容版本)

假设 version1 和 version2 为非兼容版本,则后面的版本保留在依赖树中:

node_modules

-- foo
---- lodash@version1

-- bar
---- lodash@version2
  1. 安装模块

这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。

  1. 执行工程自身生命周期

当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。

最后一步是生成或更新版本描述文件,npm install 过程完成。

什么是pnpm?相比npm和yarn有什么优势

pnpm(pronounced as "pin-pm")是一种包管理工具,用于管理 JavaScript 项目中的依赖关系。它类似于 npm 和 Yarn,但有一些不同之处和优势。

以下是 pnpm 相对于 npm 和 Yarn 的一些优势:

  1. 空间效率:pnpm 使用一种称为 "透明硬链接" 的技术来共享依赖项。这意味着当多个项目使用相同的依赖项时,它们可以共享它们的实际文件,而不是复制多个副本。这显著减少了磁盘空间的使用量,尤其是对于大型项目或具有许多共享依赖项的项目。

  2. 安装速度:由于透明硬链接的使用,pnpm 在安装依赖项时更快。相比之下,npm 和 Yarn 在安装相同依赖项时需要进行复制和解压缩操作,导致更长的安装时间。pnpm 只需创建硬链接,因此安装速度更快。

  3. 更新效率:当需要更新项目中的依赖关系时,pnpm 仅需要更新被修改的依赖项,而不需要重新安装所有依赖项。这减少了更新所需的时间和网络流量。

  4. 并发安装:pnpm 具有并发安装的能力,可以在同一时间安装多个依赖项。这对于大型项目或具有大量依赖项的项目来说,可以显著加快安装速度。

  5. 锁文件:与 npm 和 Yarn 类似,pnpm 也使用锁文件来确保在不同环境中安装相同的依赖项版本。pnpm 的锁文件非常简洁,只包含真正需要的信息,因此更小且更易于维护。

需要注意的是,尽管 pnpm 具有许多优势,但它并不是适用于所有项目的最佳选择。如果你的项目已经使用 npm 或 Yarn,并且没有特别的需求,那么它们可能仍然是很好的选择。选择使用哪种包管理工具取决于项目的需求、团队的偏好以及特定的使用场景。

如何发布开发项目的特定文件夹为 Npm 包的根目录?

一般情况下,npm包的根目录时node_modules,可以使用package.json的directories属性里的directories.lib,更改 Npm 包的根目录。

如何发布一个支持 Tree Shaking 机制的 Npm 包?

要发布一个支持 Tree Shaking 机制的 Npm 包,需要进行以下步骤:

  1. 使用 ES6 模块语法导出: 在你的源代码中,使用 ES6 模块语法来导出你的模块。确保使用 export 关键字来明确导出需要暴露的功能。例如:

    // myModule.js
    export function foo() {
    // ...
    }

    export function bar() {
    // ...
    }
  2. 配置 package.json: 在 package.json 文件中,确保以下字段被正确设置:

    {
    "name": "your-package-name",
    "version": "1.0.0",
    "main": "dist/index.js",
    "module": "dist/index.esm.js",
    "sideEffects": false
    }
    • "name":设置你的包名。
    • "version":设置你的包的版本号。
    • "main":指定入口文件的路径,这应该是打包后的 CommonJS 格式的代码。
    • "module":指定入口文件的路径,这应该是打包后的 ES6 模块格式的代码。
    • "sideEffects":设置为 false,表示你的模块没有副作用,可以进行 Tree Shaking。这个字段通常与打包工具(如 Webpack)一起使用,以允许进行无用代码的消除。
  3. 配置打包工具: 如果你使用打包工具(如 Webpack)来构建你的包,确保进行以下配置:

    • 使用 mode"production": 在打包配置中将 mode 设置为 "production",以启用优化和 Tree Shaking。

    • 配置 output: 在打包配置中,设置 output 字段的 libraryTarget"umd",以支持在多种环境(如 CommonJS、AMD、全局变量等)中使用你的包。

    • 配置 Babel: 如果你使用 Babel 进行转译,确保启用了 @babel/preset-env 插件,并配置 modules 选项为 false,以保留 ES6 模块语法。

  4. 构建项目: 运行相应的构建命令或脚本,将你的源代码打包为发布版本。

  5. 发布到 Npm: 运行以下命令将你的项目发布到 Npm:

    npm publish

通过以上步骤,你可以发布一个支持 Tree Shaking 机制的 Npm 包。在你的包被使用时,Tree Shaking 可以通过消除未使用的代码来减小包的大小,提高性能,并确保只有被使用的代码被包含在最终的构建结果中。

Npm 包中 peerDependencies 的作用是什么?

在 Npm 包中,peerDependencies 字段用于声明你的包依赖的其他包的版本范围。它的作用是告知使用者,你的包在使用时需要依赖某些特定版本的其他包。

当一个包被安装时,Npm 会检查该包的 peerDependencies 字段,并确保安装了与这些依赖相匹配的版本。然而,与普通依赖不同,peerDependencies 并不会自动被安装,而是依赖于使用者来手动安装这些依赖。

peerDependencies 的主要作用是解决包之间的版本冲突问题。当多个包都依赖于同一个包的不同版本时,通过在 peerDependencies 中指定所需的版本范围,可以确保使用者在安装这些包时,安装的是兼容的版本,从而避免冲突和不一致的问题。

使用 peerDependencies 的示例:

{
"name": "your-package-name",
"version": "1.0.0",
"peerDependencies": {
"other-package": "^2.0.0"
}
}

在上述示例中,your-package-name 声明它依赖于 other-package 的版本大于等于 2.0.0,但小于下一个主版本。这样,当其他开发者使用 your-package-name 时,Npm 会提示他们安装兼容的 other-package,但不会自动安装。

需要注意的是,peerDependencies 字段并不会自动安装依赖项,使用者需要手动安装符合要求的依赖版本。因此,在编写包时,应该在文档或使用说明中明确指出所需的 peer dependencies,以便使用者正确安装和使用你的包。

如何优雅的调试需要发布的 Npm 包?

在调试需要发布的 Npm 包时,可以采取以下优雅的调试方法:

  1. 使用本地链接(npm link): 使用本地链接可以将你的开发环境中的包链接到测试项目中,以便实时进行调试和修改。以下是使用本地链接的步骤:

    • 在要调试的包的根目录中运行 npm link 命令,将包注册到全局的 Npm 链接目录中。
    • 在测试项目的根目录中运行 npm link your-package-name 命令,将测试项目与你的包建立链接。

    这样,任何对包的修改都会立即反映在测试项目中,方便进行调试和验证。

  2. 使用调试器(Debugger): 配置调试器可以让你在调试过程中逐步执行代码,观察变量的值和程序的执行流程。可以使用 Node.js 内置的调试器或调试器插件,如 VS Code 的调试器插件。

    • 配置调试器的启动参数,指定入口文件、断点等。
    • 在需要调试的代码处设置断点。
    • 启动调试器,运行测试项目,以及触发需要调试的代码路径。

    这样,调试器将会在断点处中断执行,允许你逐步调试代码,观察变量和执行路径,定位问题并进行修复。

  3. 添加日志输出: 在关键代码路径中添加适当的日志输出语句,以便在运行时查看某些值或执行状态。你可以使用 console.log 或专门的日志库(如 debug)来输出日志。

    • 在需要观察的代码路径中添加日志输出语句。
    • 运行测试项目,观察日志输出。

    通过查看日志输出,你可以了解代码的执行情况、变量的值以及可能的错误或异常信息。

  4. 使用单元测试: 编写单元测试可以帮助你对包的各个部分进行 isolated 测试,以验证其功能和逻辑。单元测试框架如 Mocha、Jest 等可以帮助你编写和运行测试用例。

    • 编写针对包的各个函数和模块的单元测试用例。
    • 运行测试用例,观察测试结果和输出。

    单元测试可以帮助你在不依赖外部环境的情况下测试代码,快速发现问题并进行修复。

通过以上方法,你可以以优雅的方式进行调试需要发布的 Npm 包,确保代码的正确性和可靠性。同时,记得在发布前移除或注释掉调试相关的代码和日志输出,以确保最终发布的包是干净的和不包含调试相关的内容。