likes
comments
collection
share

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

作者站长头像
站长
· 阅读数 10

前言

我们在 Element Plus 项目根目录下发现了一个名叫:.npmrc 文件,这个是 npm 的配置文件,它主要是供 npm、yarn 或者最新的 pnpm 这些包管理工具进行配置使用的。

前端工程是离不开 npm、yarn 或者最新的 pnpm 这些包管理工具的。通过这些包管理工具我们可以对项目进行依赖安装和维护,同时还可以通过 package.json 文件中的 script 设置进行串联各个工程化不同阶段需要运行的命令。这其中的原理是什么呢?

关于 npm 命令,有一道很出名的面试题:npm run xxx 发生了什么?今天我们就从终端命令说起,详细了解它的运行原理。

终端命令的执行流程

命令解析器

通过命令操控计算机在 DOS、Linux 系统中最常用的方式。命令操作的核心便是命令解析器(如 Linux 中的 Shell)。命令解析器实现接收命令字符串,解析命令并执行相应操作。

常见命令解析器:

  • shell:Unix操作系统下的命令解析器
  • bash:Linux操作系统下的命令解析器
  • cmd:Windows 命令解释器
  • PowerShell:一种跨平台的任务自动化和配置管理框架,由命令行管理程序和脚本语言组成。(win10 默认提供)

本质:对用户输入的命令进行解析,调用对应的执行程序。

命令执行流程

当用户在终端键入一个命令后,系统的执行流程如下:

判断命令路径

先判断该命令是否包含了路径,如果命令已经存在有路径,则会直接读取该路径下的命令文件以执行。这就是同过命令路径执行命令。

判断是内部还是外部命令

当用户在终端键入一个字符后,命令解析器就需要判断该字符是系统内部命令还是系统外部命令。所谓内部命令,就是这该命令常驻内存,直接执行即可(例如:cd、ls),外部命令就是指命令的代码在磁盘中,在执行时需要先把磁盘中的命令代码读入内存才能执行。

在 PATH 变量中查找命令

如果用户键入的命令不是内部命令,又没有包含命令路径的话,那么就需要去系统环境变量 PATH 中配置的目录中进行查找。我们平时很多软件需要配置 PATH 环境变量的原因也在于此。

其他

报错,输入的不是命令。

Node 安装及执行

我们以为 windows 环境为例,我们一般去 Node 官网下载node-v18.12.0-x64.msi 集成版的安装包,下载后进行安装之后,它就会自动给系统的 PATH 环境变量配置了一个 node.exe 文件所在的目录路径,也就是安装路径。这样我们在终端输入 node -v 命令,那么就会进行我们上面说到的那些步骤,最终会在 PATH 环境变量配置的目录中找到 node.exe 执行命令,然后进行相关的程序执行。

我们上面说到的 node-v18.12.0-x64.msi 集成版的安装包,安装的时候是自动给我们进行系统的 PATH 环境变量配置了一个 node.exe 文件所在的目录路径。此外我们也可以手动下载单独的 Node 版本,然后手动设置 node.exe 文件所在的目录路径到统的 PATH 环境变量上。

基于此原理我们也可以下载不同版本的 Node 文件,然后需要用到不同版本的 Node 时,就手动设置不同版本的 node.exe 文件所在的目录路径到系统的 PATH 环境变量上。从而也可以达到手动切换 Node 版本的目的。例如大名鼎鼎的 Node 版本管理工具 nvm 切换不同版本 Node 的原理也源于此。

npm run 发生了什么

安装 npm 命令

我们以为 windows 环境为例,上述例子中我们安装 Node 环境的时候,通过集成安装包安装的时候,默认除了安装 Node 命令之外,还默认安装了 npm 命令。在 node.exe 文件所在的目录下

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

我们可以看到在 node.exe 文件所在的目录下存在这两个文件,这两个文件就是 npm 的命令执行文件。同时我们也可以手动去 npm 源中下载对应的 npm 版本,然后把 npm 命令文件所在的目录设置到系统的 PATH 环境变量上。这样在命令终端输入 npm 的时候,就可以通过系统的 PATH 环境变量上配置的目录路径找到对应的 npm 命令文件了。然后系统就会把 npm 命令文件中的内容读取到内存中,就可以执行了。

全局命令与局部命令

我们以 typescript 为例,我们进行全局安装:

npm install typescript -g

执行以上命令之后,我们就获得了一个全局命令 tsc。我们就可以在终端输入 tsc 开头的命令。这是因为我们执行上面的命令代码之后会在系统变量 PATH 中设置的 node 命令所在的目录上写入了以下命令文件:

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

这样我们在终端输入 tsc -v 命令之后,命令解析器就可以通过系统变量 PATH 中设置目录路径找到对应的 tsc 命令。

如果我们进行局部安装:

npm install typescript -D

那么前面上面看到的那三个命令文件就会出现在当前目录的 ./node_modules/.bin 文件目录下:

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

这个时候我们是不能直接通过终端执行 tsc 命令的,因为此时 tsc 命令文件所在的目录并没有在系统变量 PATH 中进行设置。

在上述文章中,我们知道命令还可以通过路径进行执行,所以我们可以以下方式执行:

./node_modules/.bin/tsc -v

执行结果:

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

还可以通过绝对路径执行:

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

这种方式相信大家都不会觉得陌生,我们通常在使用代码检测工具 ESLint 的时候,会先安装:

npm install eslint --save-dev

然后在根目录下执行以下命令:

./node_modules/.bin/eslint --init

所以经过上面的解析,大家是不是豁然开朗了,原来是这么回事。本质就是通过指定命令路径执行命令。

这里顺便还说一个题外话。我们上述 ESLint 问题,还可以通过 npx 方式执行。

npx eslint --init

这是因为 npx 可以直接运行 node_modules/.bin 文件下的命令。npx 可以自动检查命令是否在 node_modules/.bin 目录中或者是否在系统环境变量 PATH 配置的目录路径中。npx 另外一个更实用的特点是,它在执行相关模块命令时会先进行依赖安装,但会在安装成功并执行完相关命令代码后便删除此依赖,从而避免了全局安装带来的问题。

npm scripts 的本质

我们知道我们可以在 package.json 文件的 scripts 选项中进行自定义脚本,然后通过 npm run xxx 来执行。原理就是 npm run 会创建一个 shell 脚本,package.json 文件的 scripts 选项中自定义的脚本内容就会在这个新创建的 shell 脚本中运行。

所以我们上述局部安装 typescript 的命令还可以通过 package.json 文件的 scripts 选项中自定义的脚本内容方式进行执行。package.json 代码设置如下:

"scripts": {
	"tsctest": "tsc -v"
}

然后执行 npm run tsctest。运行结果如下:

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

此外 package.json 文件的 scripts 选项也可以运行全局命令。

"scripts": {
	"tsctest": "node -v"
}

上述代码中 node 是一个全局安装的命令,也可以在 scripts 中执行。执行结果如下:

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

小结

我们通常全局安装一个命令工具,那么我们就可以直接在命令终端输入对应的命令名称就可以执行了,但如果是项目中安装,那么就要通过 package.json 文件的 scripts 选项配置脚本命令来执行。

通过上文我们就可以知道 npm run 发生了什么。首先是 npm scripts 本质是一个 Shell 脚本,原理就是执行 npm run 的时候会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令,同时会把当前目录下的 node_modules/.bin 目录路径添加到系统环境变量 PATH 中,这样 Shell 脚本的命令解析器就可以查找当前目录中的 node_modules/.bin 目录中的命令了,执行结束后,再将 PATH 变量恢复原样 。然后再通过命令执行对应的应用程序,然后输出结果。重点:Shell 在寻找命令的时候是按照系统环境变量 PATH 中配置的目录去查找的,如果找到了就执行对应的命令,若找不到就报错

另外只要是 Shell 脚本可以运行的命令,都可以作为 npm scripts 脚本。因此它不一定是 Node 脚本,任何可执行文件都可以写在里面。比如如果系统安装了 ffmpeg ,也可以将 ffmpeg 脚本作为 npm scripts。npm 脚本的退出码也遵守 Shell 脚本规则。如果退出码不是 0,npm 就认为这个脚本执行失败。

Node Cli 命令运行原理

创建 Node CLI 命令

我们先创建一个目录:npm-link-test,然后在根目录下进行 Node 项目初始化操作。即运行以下命令:

npm ini -y

然后新建目录bin,在bin目录下新建一个index.js文件,这个文件到时是作为一个shell文件进行执行的,需要在文件开头加一行:

#!/usr/bin/env node

这段代码的意思是指定脚本解析器类型,本来 Shell 环境只能执行 B-shell 文件,如果要执行 JavaScript 文件则需要指定解析器为 node。

配置 package.json 文件:

{
  "bin": {
    "npm-link-test": "./bin/index.js"
  },
}

这个配置的意思是说,这个指令名称是 npm-link-test  运行之后执行的是 "./bin/index.js" 这个文件的代码。

因为我们只是为了理解 Node Cli 命令的运行原理,我们只需要在终端输入 npm-link-test 命令能运行起来即可。所以我们在 "./bin/index.js" 文件中加一行能体现我们命令运行成功的代码:

#!/usr/bin/env node
console.log('这是测试命令')

所以只要在终端输入 npm-link-test 然后能打印:'这是测试命令' 即可。

利用 npm link 进行本地调试

全局安装调试

我们开发了一个库包或者一个 Node CLI 工具,是不适合发布到线上进行调试的。所以我们可以利用 npm link 高效地进行本地调试。

进行全局虚拟安装,在项目根目录下执行以下命令:

npm link

这样就会在系统变量 PATH 设置的 Node 命令安装的目录下创建了以下可执行的命令文件:

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

这样我们就可以在命令终端输入 npm-link-test 命令了。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

我们可以看到成功打印了我们上面设置的代码。

其实我们执行 npm link 命令相当于执行了全局安装,它会把我们的项目安装到全局的 node_modules 目录文件下。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

我们打开上面的任意一个命令文件看一下:

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

所以我们通过查看全局安装的命令文件可以得知,当在终端执行 npm-link-test 命令的时候,实际执行的是当前目录下的 node_modules/npm-link-test/bin/index.js 文件,也就是我们上面写的 bin 文件。

当然别忘了,调试结束后执行 npm unlink 命令取消安装关联。

局部安装调试

我们上面是通过全局安装进行调试,但我们有可能需要进行具体项目的局部安装,也就是安装到具体的项目中。我们可以进入具体项目的根目录下执行:

npm link npm-link-test

但此种方式有可能会报错,我们还可以通过以下方式安装:

npx link L:\work2022\t\npm-link-test

也就是 npx link <package-path> 方式。这样就可以在具体的项目中的 node_modules/bin 目录中安装了相关命令文件。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

这个时候我们删掉全局安装的命令文件,再在命令终端输入 npm-link-test 发现报错了。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

也就是局部安装的命令无法通过终端命令进行运行。但我们可以通过 package.json 文件中的 scripts 选项进行自定义脚本来进行局部安装的命令。

"scripts": {
	"test": "npm-link-test"
}

这个时候我们在终端键入 npm run test,运行结果如下:

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

小结

Node CLI 命令就是通过在 JavaScript 文件头部添加 Shell 脚本声明,然后通过 package.json 文件的 bin 选项定义 Node CLI 命令名称及 Shell 脚本文件的位置。然后安装的时候就会根据 package.json 文件的 bin 选项声明的 Node CLI 命令名称生成不同平台的命令文件,命令文件的主要内容就执行 Node CLI 命令文件的内容。通常安装的时候随着项目安装到对应的 node_modules 目录下,局部安装的时候 Node CLI 命令文件是在 node_modules/bin 目录中,全局安装的时候 Node CLI 命令文件则在系统环境变量 PATH 设置的 node 命令所在的目录中。

npm install 发生了什么

npm run 主要是执行 package.json 中 scripts 选项定义的脚本,而 npm install 则是用来安装项目依赖。

执行 npm install 命令之后,当前项目如果定义了 preinstall 钩子此时会被执行。之后会获取 npm 配置,即 .npmrc 文件。

优先级为:项目级的 .npmrc 文件 > 用户级的 .npmrc 文件 > 全局的 .npmrc 文件 > npm 内置的 .npmrc 文件。

然后检查项目根目录中有没有 package-lock.json 文件,如果有 package-lock.json 文件,则检查 package-lock.json 文件和 package.json 文件中声明的版本是否一致。

一致,直接使用 package-lock.json 文件中的信息,从缓存或从网络仓库中加载依赖。

不一致,则根据 npm 版本进行处理。

  • npm v5.0x:根据 package-lock.json 下载。
  • npm v5.10 - v5.4.2:当 package.json 声明的依赖版本规范有符合的更新版本时,忽略 package-lock.json,按照 package.json 安装,并更新 package-lock.json
  • npm v5.4.2 以上: 当 package.json 声明的依赖版本规范与 package-lock.json 安装版本兼容时,则根据 package-lock.json 安装;如果 package.json 声明的依赖版本规范与 package-lock.json 安装版本不兼容,则按照 package.json 安装,并更新 package-lock.json。

如果没有 package-lock.json 文件,则根据 package.json 文件递归构建依赖树,然后按照构建好的依赖树下载完整的依赖资源,在下载时会检查是否有相关缓存。

有,则将缓存内容解压到 node_modules 目录中。

没有,则先从 npm 远程仓库下载包资源,检查包的完整性,并将其添加到缓存,同时解压到 node_modules 目录中。

最后生成 package-lock.json 文件。

项目如果定义了 postinstall 钩子此时会被执行。

构建依赖树时,首先将项目根目录的 package.json 文件中 dependencies 和 devDependencies 选项的依赖按照首字母(@排最前)进行排序,排好序后 npm 会开启多进程从每个首层依赖模块向下递归获取子依赖。这样便获得一棵完整的依赖树,其中可能包含大量重复依赖。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成依赖冗余。 从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有依赖,将每个依赖 安装在根目录的 node_modules 文件夹中,当发现有重复依赖时,则将其丢弃,不重复则在新模块的 node_modules 目录下放置该依赖。这就是所谓依赖扁平化结构处理。

再次执行 npm install 时,会根据 package-lock.json 中的 integrity、version、name 信息生成一个唯一的 key,再根据这个 key 就可以找到对应的缓存包了,再把对应的缓存安装到 node_modules 下面即可,这样便省去了网络下载资源的开销。

关于依赖我们还需要特别了解一下 package.json 文件中 dependencies 和 devDependencies 选项,dependencies 表示项目依赖,这些依赖都会成为线上生产环境中的代码组成部分。比如我们在项目中安装 Element Plus 组件时,Element Plus 组件库中的 dependencies 选项中的依赖就同时被下载。devDependencies 选项的依赖则不会被下载。因为 devDependencies 选项依赖一般只在开发环境中被使用到。但如果我们从 GitHub 上克隆 Element Plus 组件库源码项目下来,进入到 Element Plus 项目中进行 npm install 时则 dependencies 和 devDependencies 选项都会被下载,因为此时是在开发 Element Plus 组件库阶段了。

这里还需要说明一点的是,并不是 dependencies 选项中的依赖才会被一起打包,而 devDependencies 选项中的依赖就一定不会被打包。事实上,dependencies 和 devDependencies 选项中的依赖是否被打包,只取决于项目中是否引入了改依赖。dependencies 和 devDependencies 选项在业务中更多起到的是规范作用。

幽灵依赖的前世今生及 pnpm

Element Plus 项目在 .npmrc 文件中主要配置了两项内容:shamefully-hoist=truestrict-peer-dependencies=false

package.json 中的 peerDependencies 选项表示同版依赖,简单来说就是,如果你安装我,那么你最好也安装我对应的依赖。老版本的 npm 会自动安装,npm3 之后就不会了,而是报个错给你看,让你自己选择处理方式。成熟的开发者需不需要提醒安装 peerDependencies,本身就应该知道要安装那些依赖,比如说我们 Element Plus 组件库,肯定是要安装 Vue3 的,这个时候 package.json 的 peerDependencies 选项更多的像一个文档说明。所以我们会在 .npmrc 配置文件中添加 strict-peer-dependencies=false ,这意味着将关闭严格的对等依赖模式。

其中 shamefully-hoist = true 选项是因为幽灵依赖问题而设置的,接下来我们进行详细了解一下幽灵依赖的来龙去脉。“幽灵依赖” 指的是 项目中使用了一些没有被定义在项目中的 package.json 文件中的包。我们首先要搞清清楚幽灵依赖是怎么来的,要从 npm 的包管理器的历史说起,其实我们上面也有说到了一些,下面我们将从实践中去更详细地了解。

手动设置 node 版本

我们先通过 nvm 的 node 版本管理工具把 node 版本回退到 node4.0版本:

nvm install 4.0.0

然后在安装对应版本的 npm 包的时候,报错了。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

然后我们通过 npm 的淘宝源的镜像地址进行手动下载安装。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

npm 的淘宝源的镜像地址传送门。同时在这个地址上也可以手动下载 node 其他版本的包,然后进行手动设置。

然后对下载好的 npm 包进行解压。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

然后复制到对应的 nvm 目录中对应的 node 包目录下。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

可以看到我的 nvm 目录下存在很多过版本的 node 目录了,因为我安装了很多个不同版本的 node,那么我们现在需要把刚刚下载解压之后的 npm 包复制到 v4.0.0 的目录中。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

这个时候我们就可以看到 node4 的版本目录 v4.0.0 中就存在了一个 npm.cmd 文件,这个是就 npm 的命令文件。

至此我们的 Node4 的版本环境就设置完毕了,我们通过 nvm ls 命令查看所有的 node 版本的时候,我们可以看到出现 4.0.0 版本了。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

然后我们通过 nvm use 4.0.0 命令进行切换 Node4 的版本。

我们再次通过 nvm ls 命令查看所有的 node 版本的时候,我们可以看到出现 4.0.0 版本是当前正在使用的版本了。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

我们通过 node -v 查看。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

我们通过 npm -v 查看。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

我们可以看到我们的环境已经成功设置为 Node 版本为 4.0, npm 版本为 1.4.9 版本了。

注意:我上述设置是在 windows 环境下设置的,mac 的同学也可以进行相同的设置。

嵌套结构的 node_modules

这个时候我们随便找一个目录进行 package 初始化: npm init -y ,接着安装一个 Vue1,npm install vue@1.0.0

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

我们可以到首先项目本身有一个 node_modules 目录,然后 Vue1 也有一个 node_modules 目录,然后 Vue1 里的一些包的目录底下也存在 node_modules 目录,就是一个不断套娃的状况。

这样回导致如果多个包之间存在相同的依赖的话,那么即便是同一个依赖包也要进行多次复制存储起来,造成磁盘空间浪费。而且嵌套过深的话,同时会造成路径过长,而 windows 的文件路径是有长度限制的,所以嵌套过深目录路径可能会超出限制。

什么是幽灵依赖

在 npm 升级到版本 3 的时候,就参考了社区的 yarn 包管理器的解决方案。将所有的依赖进行平铺,不再一层一层进行嵌套了,这样重复的依赖也只剩一个了,同时也不会存在路径过长的问题。

这样就不会造成大量包的重复安装,依赖的层级也不会太深,解决了依赖地狱问题,但也形成了新的问题。就是同时又带来了本章节要进行介绍的幽灵依赖的问题。因为所有的依赖都进行平铺了,所以依赖的依赖,也可以进行引用使用了。也就是一些没有被定义在项目中的 package.json 文件中的包,也可以进行导入使用了。但又因为没有显式在 packages 中进行声明引用,将来有一天你引用的那个主包不再使用了,而你的项目中还引用着它包中的依赖,自然就会出错了。这个就是幽灵依赖会导致的问题。

下面我们进行实操一下,我们先把我们的 node 环境通过 nvm 管理工具切换成现在常用的版本,node12 以后。我们安装一下我们常用的 echarts 包。

npm install echarts

然后我们再去看看 node_modules 目录。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

然后我们发现除了我们安装的 echarts 包之外,还多了两个包:tslib、zrender。

  • tslib 这是的运行时库,其中包含所有 TypeScript 辅助函数。
  • zrender.js 是可视化框架 Echarts.js 的 2D 绘制引擎,支持 canvas\svg\vml 等多种渲染方式。

那么就意味着我们不需要再手动 npm install 进行安装,就可以引入使用了。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

那么有一天当我们不再需要使用 echarts 这个包的时候,我们从项目的 package.json 文件中依赖声明中删除之后,那么我们如果在项目中引用了 zrender.js 就会报错,因为 zrender 包会随着主包 echarts 的删除而删除了。

幽灵依赖如何解决

幽灵依赖的这个问题,随着最近火热的 pnpm 新一代的包管理器的出现而得到解决。 我们删除刚刚安装 echarts 包出现的 node_modules 目录,然后重新使用 pnpm 进行安装。

pnpm install echarts

然后我们再看看项目中的 node_modules 目录结构。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

我们发现只剩下一个我们手动安装的 echarts 包的目录和一个 .pnpm 的目录,而刚才通过 npm 安装出现的 tslib、zrender 包则不见了。

然后我们展开 .pnpm 目录中 node_modules 目录,则发现了 tslib、zrender 包。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

这样一来我们就清楚了,项目的 package.json 文件中显示声明的依赖则会平铺在 node_modules 根目录下,而依赖中依赖则放在 node_modules 根目录下的 .pnpm 的目录中 node_modules 目录下。这样由于 tslib、zrender 包没有直接暴露在 node_modules 根目录下,则项目中就不能再进行引用使用了,这样也就解决了幽灵依赖的问题。

我们发现已经不能像之前那样不安装 zrender 包,也能进行引用使用了,这样一来就可以确保不会产生幽灵依赖的问题了。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

pnpm 的一个使用技巧

我们来看一下 Element Plus 项目根目录下 npm 配置文件 .npmrc 文件,里面的内容则是:

shamefully-hoist = true

那么这个配置的作用是什么呢?我们在 pnpm 的官网中找到了答案了。

我们在 pnpm 的官网配置 .npmrc 的选项中找到了相关的说明。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

意思是:默认情况下,pnpm 创建半严格的 node_modules,这意味着在项目中可以访问未声明的依赖,但 node_modules 之外的模块不能访问。通过这种设置,大多数包都可以正常工作。但是,如果某些工具包需要将所有的依赖提升到位于 node_modules 的根目录中才起作用时,则可以将设置 shamefully-hoist = true 来提升所有的依赖。

另外我们也在 pnpm 的官网推荐的博客中也找到了相关介绍。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

意思就说有一些工具包在 pnpm 的默认设置下是无法运行的,必须把虚拟仓库中的依赖进行提升到 node_modules 根目录下。

我们按照官方的指引在项目根目录下添加一个 .npmrc 的文件,然后设置内容为 shamefully-hoist = true,然后再重新 pnpm install 安装依赖,然后我们发现 node_modules 根目录下又出现了 tslib、zrender 包目录。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

然后又可以像 npm 安装那样进行引用未在 package.json 里面进行声明的依赖了。

Element Plus 组件库相关技术揭秘:5. 从终端命令解析器说起谈谈 npm 包管理工具的运行原理

幽灵依赖部分的内容到此就告一段落了,通过对幽灵依赖的来龙去脉的了解,我们更清楚地了解了 npm 包管理工具的发展和演变。

总结

我们首先从终端命令的执行流程开始了解 npm 命令的执行原理,Node 安装的过程主要就是设置系统环境变量 PATH 指向 node 执行命令文件所在的目录路径。

npm run 的时候会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令,同时会把当前目录下的 node_modules/.bin 目录路径添加到系统环境变量 PATH 中,这样 Shell 脚本的命令解析器就可以查找当前目录中的 node_modules/.bin 目录中的命令了,执行结束后,再将 PATH 变量恢复原样 。

最后我们在了解 npm install 的执行过程引出幽灵依赖的问题,通过对幽灵依赖的来龙去脉的了解,我们更清楚地了解了 npm 包管理工具的发展和演变。

欢迎关注本专栏,了解更多 Element Plus 组件库知识

本专栏文章: