前言:npm在前端开发流程中提供了非常完善的自动化工具链,已成为每个前端开发者必备的工具。但是同样由于其强大性导致很多前端开发者只会简单的使用它。本文总结了日常开发中所需要的npm知识点,以便开发者们更好的运行在实际开发中。
为什么需要包管理工具?
每种主流编程语言都有包管理工具,比如 java
的 Maven
、Gradle
,Python
的 pip
,nodejs
的 npm
、yarn
、pnpm
等。
包管理工具的主要作用是管理第三方依赖,也可以看成一个“轮子”工厂,每个人都可以上传自己造的"轮子"和下载使用别人的"轮子"。包管理工具顾名思义就是统一管理这些轮子的软件或工具,它以多种方式自动处理项目依赖关系、提供了命令行工具(CLI
)、支持跟踪依赖项和版本等功能。除此之外还可以安装、卸载、更新和升级包,配置项目设置,运行脚本等等。
有了包管理工具,我们可以很简单地构建一个项目或者引入和管理一个库,留给我们的则是愉快地编码。
1. npm install 机制
1-1、嵌套安装
在 npm 2.x
时代,安装依赖方式比较简单直接,以递归的方式,按照包依赖的树形结构下载填充本地目录结构,也就是说每个包都会将该包的依赖安装到当前包所在的 node_modules
目录中。
执行 npm install
后,项目App的 node_modules
会变成如下目录结构:
├── node_modules
│ ├── A@1.0.0
│ │ └── node_modules
│ │ │ └── D@1.0.0
│ ├── B@1.0.0
│ │ └── node_modules
│ │ │ └── D@2.0.0
│ └── C@1.0.0
│ │ └── node_modules
│ │ │ └── D@2.0.0
很明显这样的依赖组织结构,有如下优点:
- 层级结构明显
- 简单的实现了多版本兼容
- 保证了对依赖项无论是安装还是删除都会有统一的行为和结构。
但是缺点也一样很明显:
- 可能会造成目录结构嵌套比较深的问题
- 可能会造成相同模块大量冗余的问题
1-2、扁平化安装
从npm 3.x
开始就采用了扁平化的方式来安装模块。npm install
时,首先将 package.json
里的依赖按照首字母(@排在最前面)进行排序,然后将排序后的依赖包按照广度优先遍历的算法进行安装,最先被安装到的模块将会被优先安装在一级 node_modules
目录下。
所谓广度优先遍历的安装方式,就是优先将同一层级的模块包及其依赖安装好,而不是优先将一个模块及其所有的子模块安装好。
node_modules 路径查找机制:模块在找对应的依赖包时,nodejs 会尝试从当前模块所在目录开始,尝试在它的 node_modules 文件夹里加载相应模块以寻找想要的依赖包,如果没有找到,那么就再向上一级目录移动,直到全局安装路径中的
node_modules
1-3、package-lock.json
从npm 5.x
开始,执行npm install
时会自动生成一个package-lock.json 文件。
npm 为了让开发者在安全的前提下使用最新的依赖包,在 package.json
中通常做了锁定大版本的操作,这样在每次npm install
的时候都会拉取大版本下的最新的版本,这种机制最大的问题就是当有依赖包下的小版本更新时,可能会出现协同开发者的依赖包不一致问题。
package-lock.json
文件精确描述了node_modules
目录下所有的包的树状依赖结构,每个包的版本号都是完全精确的。
package-lock.json
的详细描述主要由version
、resolved
、integrity
、dev
、requires
、dependencies
这几个字段构成:
version
:包唯一的版本号resolved
:安装源integrity
:表明包完整性的hash值(验证包是否已失效)dev
:如果为true,则此依赖关系仅是顶级模块的开发依赖关系或者是一个的传递依赖关系requires
:依赖包所需要的所有依赖项,对应依赖包package.json
里dependencies
中的依赖项dependencies
:依赖包node_modules
中依赖的包,与顶层的dependencies
一样的结构
package-lock.json
文件和node_modules
目录结构是一一对应的,即项目目录下存在package-lock.json
可以让每次安装生成的依赖目录结构保持相同。
在开发一个应用时,建议把package-lock.json
文件提交到代码版本仓库,从而让你的团队成员、运维部署人员或CI
系统可以在执行npm install
时安装的依赖版本都是一致的。
但是在开发一个库时,则不应把package-lock.json
文件发布到仓库中。实际上,npm
也默认不会把package-lock.json
文件发布出去。之所以这么做,是因为库项目一般是被其他项目依赖的,在不写死的情况下,就可以复用主项目已经加载过的包,而一旦库依赖的是精确的版本号那么可能会造成包的冗余。
3. npm scripts脚本
env 环境变量
在执行 npm run
脚本时,npm
会设置一些特殊的env
环境变量,其中package.json
中的所有变量都会被设置为以npm_package_
开头的环境变量。比如package.json
中有如下字段内容:
{
"name": "sh",
"version": "1.1.1",
"description": "shenhao",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+ssh://git@gitlab.com/xxxx/sh.git"
}
}
可以通过process.env.npm_package_name
可以获取到package.json
中name
字段的值sh
,也可以通过process.env.npm_package_repository_type
获取到嵌套属性type
的值git
。
同时,npm
相关的所有配置也会被设置为以npm_config_
开头的环境变量。
此外,还会设置一个比较特殊的环境变量npm_lifecycle_event
,表示正在运行的脚本名称。比如执行npm run serve
的时候,process.env.npm_lifecycle_event
值为serve
,通过判断这个变量,可以将一个脚本使用在不同的npm scripts
中。
4. 熟悉nvm、nrm
4-1、介绍
nrm 和 nvm 都是管理 npm 的工具。
- nvm 负责管理 npm、node的版本,有时项目对node的版本有要求,这时我们可以通过 nvm控制node 的版本
- nrm 负责管理 npm 的镜像资源,国外镜像资源较慢时,可以在多个源之前切换
- **安装注意:**windows系统中对于nvm、node、nrm三者的安装顺序,首先安装nvm,再通过nvm安装node,安装成功后再安装nrm。如果在安装nvm之前就已经安装好node,需要先卸载干净node,不然nvm安装不成功。
nvm version // 查看 nvm的版本号,出现版本号即为安装成功
nvm ls // 查看所有已安装的node版本,带*表示正在使用的node版本号
nvm list available // 查看网络上可以安装的版本
nvm install // 安装最新版本nvm 或者直接跟上版本号安装指定版本
nvm install <version> // 安装指定node版本
nvm use <version> // 切换使用指定的版本node
安装nrm 镜像管理工具
npm install nrm -g // 全局安装nrm
nrm ls // 查看所有的镜像
nrm current // 查看当前所使用的镜像
nrm use taobao // 切换为淘宝镜像
4-2、卸载nvm
1)删除nvm相关的安装文件
2)nvm安装时有可能自己设置了系统环境变量,我们需要同时删除它的系统环境变量。
我的电脑 - 右键选择“属性" - 高级系统设置 - 环境变量 - 在用户变量中找到
NVM_HOME;NVM_SYMLINK;
删除,同时在系统变量的PATH中找到 NVM 删除
5. 了解 npm、yarn、pnpm、cnpm,并对比其优劣势
https://juejin.cn/post/6844903870578032647#heading-4
npm是node.js自带的包管理工具
npm3+和yarn存在的问题
幽灵依赖(幻影依赖)
依赖包再依赖的东西,即某个包没有在package.json 被依赖,但是用户却能够引用到这个包。
pnpm了解
pnpm 全称是 “Performant NPM”,即高性能的 npm。它结合软硬链接与新的依赖组织方式,大大提升了包管理的效率,也同时解决了 “幻影依赖” 的问题,让包管理更加规范,减少潜在风险发生的可能性。
软链接:
指向源文件的指针,是一个仅仅只有几个字节的单独文件,拥有自己独立的inode。软链接只是对源文件的引用,前者有变化不会影响后者,但后者有变化会影响前者。(可粗暴理解成’桌面快捷方式’)
硬链接:
与源文件指向同一个物理地址,与源文件共享数据,两者共同拥有一个inode. 删除源文件,硬链接文件不会被删除,可以用来防止文件被误删。(可粗暴理解成’文件另一入口’)
tips: 可通过 ln -s创建一个软链接, ln 创建一个硬链接
节省磁盘空间并提升安装速度
当使用npm或yarn时,如果你有 100 个项目,并且所有项目都有一个相同的依赖包,那么, 你在硬盘上就需要保存 100 份该相同依赖包的副本。然而,如果是使用 pnpm,依赖包将被存放在你电脑上的一个全局存储store
中,从store
创建一个硬链接而不是复制。对于模块的每个版本,磁盘中只保存一个副本,因此:
- 如果你对同一依赖包需要使用不同的版本,则仅有 版本之间不同的文件会被存储起来。例如,如果某个依赖包包含 100 个文件,其发布了一个新 版本,并且新版本中只有一个文件有修改,则
pnpm update
只需要添加一个 新文件到存储中,而不会因为一个文件的修改而保存依赖包的 所有文件。 - 所有文件都保存在硬盘上的统一的位置。当安装软件包时, 其包含的所有文件都会硬链接自此位置,而不会占用 额外的硬盘空间。这让你可以在项目之间方便地共享相同版本的 依赖包。
最终结果就是以项目和依赖包的比例来看,你节省了大量的硬盘空间, 并且安装速度也大大提高了!
创建非扁平的 node_modules 目录
当使用 npm 或 Yarn Classic 安装依赖包时,所有软件包都将被提升到 node_modules 的 根目录下。其结果是,源码可以访问 本不属于当前项目所设定的依赖包。
默认情况下,pnpm 则是通过使用符号链接的方式仅将项目的直接依赖项添加到 node_modules 的根目录下。
对比
1)包管理工具安装
因为node 预装了 npm,所以安装node后,不需要手动安装 npm。相反的,yarn和pnpm需要手动安装,建议全局安装。
2)安装项目依赖
npm 具有以下缺点:
- 下载速度慢
- 安装速度慢
- 下载版本不一致。但是
npm 5
开始,使用了package-lock.json
,也支持了包的一致性 - 下载虽然有本地缓存,但是必须联网才能从缓存中获取
- 安装项目依赖时,依赖项是顺序安装,并且终端会输出很多的警告日志,导致覆盖报错日志,从而难以排查问题
yarn 针对npm的更改:
- 通过并行安装依赖项,提高下载速度
- 通过yarn.lock来保存依赖关系,下一次安装更快
- 通过yarn.lock来保存包之间的依赖关系,保证包的版本一致
- 支持离线下载。只要以前装过的包,可以在没有网络链接的情况下进行。
- yarn 具有重试机制,单个包安装失败不会导致整个安装失败
yarn 1
中的日志比较简洁干净,是以树形的形式显示,但是在yarn 2
和yarn 3
中日志发生了变化,并不像以前直观
pnpm:
- 下载速度甚至超过了 yarn 和 npm
- 同时继承了yarn的优点,支持离线下载
- 保证包的版本一致
- 节约空间
3)安全性
npm
最不好的缺点之一就是安全性,曾经的版本发生过几个严重的安全漏洞, npm 6
开始则是在安装之前会检查安全漏洞,并且支持使用 npm audit
手动检查安装包的安全性,如果发现安全问题,可以运行 npm audit fix
修复漏洞。
因为npm/yarn
是扁平化依赖结构,有个非常严重的问题就是可以非法访问未声明的包,而 pnpm
是将依赖通过 link
的形式链接,避免了非法访问依赖的问题,如果没在 package.json
声明的话,是无法访问的。
yarn
和 pnpm
同样也支持 yarn/pnpm audit
手动检查安装包的安全性。
yarn
和 npm
都是使用 hash加密算法
确保包的完整性。
4)lock文件
在 package.json 跟踪的依赖项和版本总是不准确的,因为 ~ ^ * 等前缀表示依赖更新时对应的版本范围。
范围版本可以在更新依赖时自动升级依赖到兼容性的次要版本或者补丁版本,让软件包支持最新的功能或者修复最近的错误。
所以,为了避免不同设备安装依赖时的版本不匹配的问题,在 lock 文件中定义了精确的安装版本。在每次新装(更新)依赖时,npm 和 yarn 会分别创建(更新) package-lock.json 和 yarn.lock 文件。这样就能保证其他设备安装完全相同的包。
在 pnpm 中,则是使用 pnpm-lock.yaml 文件定义依赖包的精确版本。
5)性能对比
根据上面的测试结果我们可以看出,首次执行 npm install 安装依赖时 pnpm 比 npm 和 yarn 大约快了 3 倍左右(在此处查看基准,上图相关),在有缓存和已安装过依赖的情况,比 npm 也快了不少,yarn 则是更快,其他场景 pnpm 也是占了很大优势。
6)常用命令
对比 | yarn | npm | pnpm | cnpm |
---|---|---|---|---|
来源 | Facebook于2016年发布的替代npm的包管理工具,还可以作为项目管理工具,定位是快速、可靠、安全的依赖管理工具 | npm 是2010年发布的nodejs 依赖管理工具。在此之前,前端的依赖管理都是手动下载和管理的。 | 2017 年发布的一款替代 npm 包管理工具,具有速度快、节省磁盘空间的特点。 | |
初始化 | yarn init | npm init | 利用硬链接和符号链接来避免复制所有本地缓存源文件 | |
安装依赖 | yarn install 或者 yarn | npm install | pnpm install | |
新增依赖 | yarn add element-ui | npm install element-ui --save | pnpm i element-ui | |
删除依赖 | yarn remove element-ui | npm uninstall element-ui --save | … | |
删除devDependencies | npm uninstall vite-plugin-lcons --save-dev | … | ||
更新依赖 | yarn upgrade | npm update | pnpm update | |
全局安装或删除 | yarn global remove vue-cli | npm uninstall vue-cli -g | … | |
同时下载多个 | yarn add axios vue-axios | npm install ==save axios vue-axios | … | |
6. 如何解决幻影(幽灵)依赖?
幻影依赖,是指项目代码中引用的某个包没有直接定义在package.json
中,而是作为子依赖被某个包顺带安装了。代码里幻影依赖最大的隐患是,对包的语义化控制不能穿透到其子包。也就是包a@patch
的改动可能意味着其子依赖包b@major
级别的Break Change(包冲突)。
包冲突是指在一个项目的不同部分开发时调用的代码库、资源包的版本号不一致。 包冲突的实质是不同部分同一个功能实现用的代码和资源不一致。
举例说明:在一个Android项目中,假设主工程是A ,它调用(依赖)代码库B版本为2.0,简称B2.0,同时依赖代码库C,而C又依赖库B1.0,可以看到项目不同部分依赖的B出现了版本不一致,这就叫包依赖冲突。在扁平化安装依赖时期,只能存在一个版本的依赖。因此,包a@patch
改动,不能引起子依赖包b@major
的改动,就有可能造成一些问题。
解决:
pnpm三层寻址的设计,很好的解决了这个问题。
第一层处理直接依赖关系。第一层寻找依赖是 nodejs
或 webpack
等运行环境/打包工具进行的,他们在 node_modules
文件夹寻找依赖,并遵循就近原则。所以第一层依赖文件势必写在node_modules/package-a下,一方面遵循依赖寻找路径,一方面没有将依赖都拎到上级目录,也没有将依赖打平,目的就是还原最语义化的 package.json
。同时每个包的子依赖也从该包内寻找,解决了多版本管理的问题,同时也使 node_modules
拥有一个稳定的结构,即该目录组织算法仅与 package.json
定义有关,而与包安装顺序无关。
第二层处理符号链接依赖项。解决 npm@2.x
设计带来的问题,主要是包复用的问题。利用软链接解决了代码重复引用的问题。相比 npm@3
将包打平的设计,软链接可以保持包结构的稳定,同时用文件指针解决重复占用硬盘空间的问题。
(bar 将被符号链接到 foo@1.0.0/node_modules 文件夹)
node_modules
└── .pnpm
├── bar@1.0.0
│ └── node_modules
│ └── bar -> <store>/bar
└── foo@1.0.0
└── node_modules
├── foo -> <store>/foo
└── bar -> ../../bar@1.0.0/node_modules/bar
第三层是硬链接寻址,脱离当前项目路径指向一个全局统一管理路径,这正是跨项目复用的必然选择,解决了多个项目对于同一个包的多份拷贝过于浪费问题。
但还有一种更难以解决的幻影依赖问题,即用户在 Monorepo 项目根目录安装了某个包,这个包可能被某个子 Package 内的代码寻址到,要彻底解决这个问题,需要配合使用 Rush,在工程上通过依赖问题检测来彻底解决。