深入前端包管理:npm、yarn和pnpm的背后
引言
当我第一次在实验室中上手第一个项目时,包管理着实给我上了一课,我花了整整一个晚上也没能把项目启动起来。只好第二天提着奶茶求助学长把他的node_modules
拷贝给我,至今难忘项目第一次跑起来的那份激动。言归正传,本文的目标是帮助小白和有一定经验的同学更加了解包管理工具的前世今生,能够在今后的工作运用的更加得心应手。欢迎大家的批评和鼓励,你的建议是我提升文章质量和持续产出的动力之源,好了废话不多说我们开始吧!
包管理是什么
什么是包?
在软件开发领域,"包"作为一个专业术语,通常是指一组相关的代码块、功能、资源的集合,他们被封装打包用于管理、共享和复用。类比到生活中,就像是简单的分类管理,你可以把日常用品分为一个“包”,可以把游戏机分为一个“包”。分类标准上并没有严格的定义,但往往一个好的分类方式能够受到大多数人的认可,也就慢慢地成为了一种标准。在软件开发中当然也不例外,聪明的先驱者们早已为百花齐放的“包”们制定了一套严格的标准,用于高效地管理、复用,这便是包管理。
包管理的定义
包管理是来自项目管理的产物,它的主要作用是管理和维护项目开发中所需的各种依赖项,保障项目构建、运行的高效、可靠。这样描述可能有点抽象,这里结合前端的npm
包、Java
的jar
包和C++
的动态依赖库来深入探讨一下包管理的概念吧~
通用特征
- 依赖管理机制: 前面提到包的分类标准百花齐放,因此会常常存在“站在巨人肩膀之上”的现象,大多数的包是在小包的基础上进行集成的,这些小包便成为了大包的依赖项。对于开发者来说,依赖管理机制就像是一份说明书,其描述了包与包之间的依赖关系以保证项目开发所需要的包都能够被正确地集成进来。
- 版本机制: 以文章开头的例子为例,时隔多月之后我才弄清楚那晚项目跑不起来的原因竟然是有两个子包的版本与学长的不一样所致...因此版本机制的主要作用便是避免开发者陷入不同包版本之间的不兼容问题,说人话就是只要锁定了相同版本的包,项目就一定是可以稳定运行的。
- 更新机制: 一个项目可能依赖了成千上万个包,如果都由开发者进行管理,这无疑是及其低效且不稳定的。(老虎也有打盹的时候)因此更新机制应运而生,开发者只需要管理最上层的应用大包,更新机制会自动解析大包所依赖的子包并进行下载、安装、更新、卸载等操作。
JS
中的npm
包
- 特点: 前端开发中使用
npm
包管理工具,它以js
库和工具的形式存在。包管理声明文件为package.json
,通过npm install
命令进行依赖安装 - 差异: 前端项目的一大特点是涉及大量的依赖,同时
js
社区非常活跃,上至AIGC
下至进程调度,无所不用其极,包版本的更新频率也是快的飞起,也更加松散。尽管npm
采用语义化版本控制进行包版本管理,但仍然存在解析依赖和版本冲突解决的挑战。
Java
中的jar
包
- 特点: 在
Java
开发中,Jar
(Java Archive
)包包含Java
类、资源和元数据。包管理通常通过构建工具(如Maven
)处理,通过中央仓库下载和管理依赖(严重怀疑pnpm
偷学了一手) - 差异:
Java
包管理工具更注重精确的版本管理,可以更好地处理版本冲突。Java
包的发布和分发通常以统一的标准格式(JAR
文件)进行。
C++
的动态依赖库
- 特点:
C++
项目通常使用动态链接库或共享库来贡献可执行代码或函数。包管理通常由操作系统或构建工具处理,以确保库文件在系统对应路径或项目指定位置。我印象最深刻的就是刷题中常用的#include<bits/stdc++.h>
。 - 差异: 正如
C++
的语言特性一样,C++
的包管理也需要开发者关注更多,包括依赖的路径和编译的过程,并且在跨平台时要考虑到不同操作系统的差异性。简而言之就是给了开发者更大的自由空间和更高的上手度。
包管理的历史进程
- 手工下载和管理:早期的前端开发,开发者需要手动下载和管理所有的依赖文件,包括
HTML
、CSS
、JavaScript
等。这种方式效率低下,容易出错。 - 独立工具出现:随着前端开发的发展,一些独立的包管理工具开始出现,如
Bower
。这些工具允许开发者通过命令行安装、更新和删除前端依赖,提高了开发效率。 - NPM的崛起:
NPM(Node Package Manager
)作为Node.js
的包管理工具,逐渐成为前端开发的主流工具。它不仅用于管理Node.js
的依赖,还用于管理前端依赖。NPM
的强大生态系统和易用性使其成为前端包管理的首选工具。 - Yarn的出现:
Yarn
是由Facebook
、Google
和Exponent
合作开发的前端包管理工具。它旨在改进NPM
的性能和稳定性,并引入了一些新特性,如离线安装和更快的下载速度。Yarn
迅速获得了广泛的认可,与NPM
之间形成了竞争关系。 - PNPM的兴起:
PNPM
是另一种前端包管理工具,与传统的NPM
和Yarn
不同,它采用符号链接和硬链接的方式来共享依赖项,节省了磁盘空间,并提供了快速的安装速度。PNPM
的出现为前端包管理带来了新的选择,尤其在大型项目中表现出色。 - 模块化和构建工具的崛起:随着前端开发越来越复杂,模块化和构建工具如
Webpack
、Rollup
、Vite
也变得至关重要。这些工具帮助开发者将前端代码拆分成模块,然后使用包管理工具来管理这些模块的依赖关系。 - 前端框架的普及:前端框架如
React
、Angular
和Vue.js
的兴起进一步推动了前端包管理工具的发展。这些框架通常有自己的生态系统和依赖关系,需要包管理工具来有效地管理它们。
本文主要介绍项目开发期的包管理工具,关于模块化、构建管理、集成部署、框架等等内容会在之后的内容中慢慢提到~
为什么要使用包管理
前端包管理工具在处理项目复杂性、版本控制、开发者协作和生态系统集成等方面发挥了关键作用。它有助于确保前端项目的稳定性、可维护性和可扩展性,同时提供了更高效的开发流程,是现代前端开发的不可或缺的一部分。
- 项目复杂性管理:前端项目的复杂性不断增加,包括多个页面、组件、第三方库和框架。包管理工具可以帮助开发者有效地管理这些复杂性。通过定义项目依赖和模块化组织代码,开发者可以更轻松地维护和扩展项目,确保代码的可维护性。
- 版本控制:包管理工具允许开发者明确指定项目依赖的版本。这对于版本控制非常重要,因为不同版本的依赖项可能存在兼容性问题。通过锁定依赖项的版本,开发者可以确保在不同开发环境和团队成员之间保持一致性,避免潜在的问题。
- 开发者协作:在团队协作中,多个开发者可能同时参与项目。包管理工具简化了依赖项的共享和安装过程。开发者可以共享项目的依赖配置文件(如
package.json
),并使用包管理工具轻松地安装所需的依赖项。这样,团队成员之间可以更好地协作,减少了配置和环境问题。 - 生态系统支持:前端生态系统不断演进,有大量的开源库、框架和工具可供选择。包管理工具提供了便捷的方式来访问和集成这些资源。开发者可以使用包管理工具来搜索、安装和管理这些第三方资源,加速开发过程,同时也能够受益于社区维护的库的稳定性和更新。
深入npm
npm
简介
npm
(Node Package Manager
)正如它的名字一样最初是Node
包管理工具,后来逐渐扩展到整个JavaScript
生态系统中。这里我们废话不多说,以一个demo
为例,讲述npm
的核心机制和开发者常遇到的应用场景。
安装和项目初始化
安装Node
在安装Node
后,会顺带安装好npm
(毕竟npm
是node
提供的包管理工具)。访问node
中文官网,下载最新稳定包即可。
控制台输入node -v
验证安装是否成功。
! 注意:部分老项目会限定Node
版本,这主要是因为Node
版本迭代飞快,大版本不兼容升级带来的。那么对于不同项目限定不同node
版本的情况有什么好的解决方案吗?那当然,nvm
应运而生。
nvm
安装
nvm
全称node version manager
,顾名思义是node
的版本管理器,它允许我们安装多个node
版本,并在某个作用域下(如某个项目工作区)锁定node
版本
我们直接访问nvm
的仓库,下载安装包
这一步是选择nvm
安装并链接node
的路径,我们选择已安装的node
路径后能一键把当前的node
版本和nvm
关联起来
重新打开控制台验证安装是否成功(要关掉之前的控制台重新打开哦)
运用
查看当前版本、已安装过的node
版本
# 查看当前版本
nvm current
# 查看所有版本包括当前版本
nvm list
设置镜像源,目的是提升下载的速度
# 设置node源
nvm node_mirror https://npm.taobao.org/mirrors/node/
# 设置npm源
nvm npm_mirror https://npm.taobao.org/mirrors/npm/
安装、卸载指定版本的node
# 这里的版本号自行指定即可
nvm install 14.0.0
# 卸载
nvm uninstall 14.0.0
在当前控制台指定node
版本,常用于在不同项目间指定node
版本
注意:这个命令在windows
和mac
上表现不一致,mac
只会在当前工作区生效,而windows
会全局生效,详情参考nvm官网
nvm use 14.0.0
全局指定默认node
版本(仅mac
,windows
无该命令)
# 这里的版本号自行指定即可
nvm alias default 18.15.0
包安装和更新
初始化项目:创建一个新的npm
项目,这将自动生成一个package.json
文件,其中包含项目的元数据和依赖项信息。
npm init
安装包:用于安装npm
包及其依赖项。你可以通过包名指定要安装的包。
npm install 包名
-g
标志:全局安装包,而不是项目特定的依赖项。--save
或-S
标志:将包保存到项目的dependencies
中。--save-dev
或-D
标志:将包保存到项目的devDependencies
中,通常用于开发和构建工具。
这里补充一下dependencies
和devDependencies
的区别,前者是项目必备依赖,会随项目源码一起打包;后者是项目开发环境所需依赖,不会在构建阶段打包进正式包。通常devDependencies
中包含的依赖有项目脚手架、代码检查工具、单测工具等
卸载包:从项目中卸载已安装的包。
npm uninstall 包名
--save
或-S
标志:同时从dependencies
中卸载。--save-dev
或-D
标志:同时从devDependencies
中卸载。
查看已安装的包:列出项目中已安装的所有npm
包及其版本。
npm ls
更新包:将已安装的包升级到其最新版本。
npm update 包名
-g
标志:全局更新已安装的包。
运行脚本:执行在package.json
中定义的脚本命令,可以自定义各种项目任务,如启动服务器、构建应用程序等。
# 脚本存放在 package.json 下的scripts
npm run 脚本名称
# package.json
{
"name": "fe-engineering",
"version": "1.0.0",
"description": "前端工程化",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wangyangzero/fe-engineering.git"
},
"author": "lemon橙汁",
"license": "ISC",
"bugs": {
"url": "https://github.com/wangyangzero/fe-engineering/issues"
},
"homepage": "https://github.com/wangyangzero/fe-engineering#readme",
"dependencies": {
"lodash": "^4.17.21"
}
}
依赖管理
依赖安装机制
npm
2.x
npm 2.x
时代,安装依赖的方式比较简单粗暴,会为每个npm
包安装其所需的依赖从而形成项目依赖树。这样做能够很好地实现依赖隔离,但是缺点也是显而易见的——相同的依赖被安装了多次,降低了开发效率和打包产物的包体积(影响性能)。(如下图)
---- packageA
|
|
-------- packageD@0.1.0
|
|
---- packageB
|
|
-------- packageD@0.1.0
|
|
---- packageC
|
|
-------- packageD@0.2.0
npm
3.x
// 假设依赖关系、包的排列顺序如下
---- packageA
|
|
-------- packageD@0.1.0
|
|
---- packageB
|
|
-------- packageD@0.1.0
|
|
---- packageC
|
|
-------- packageD@0.2.0
// 最终安装结构
---- packageA
|
|
---- packageB
|
|
---- packageC
|
|
-------- packageD@0.2.0
|
|
---- packageD@0.1.0
// 假设依赖关系、包的排列顺序如下
---- packageA
|
|
-------- packageD@0.1.0
|
|
---- packageB
|
|
-------- packageD@0.1.0
|
|
---- packageC
|
|
-------- packageD@0.2.0
|
|
---- packageD@0.3.0
// 最终安装结构,可以发现此时安装后的结构已经退化到2.x了
---- packageA
|
|
-------- packageD@0.1.0
|
|
---- packageB
|
|
-------- packageD@0.1.0
|
|
---- packageC
|
|
-------- packageD@0.2.0
|
|
---- packageD@0.3.0
package-lock.json
package-lock.json
的机制有点像DNS
解析的缓存机制,主要作用有两个:锁定包版本和记录包的下载地址。
- 锁包:锁包意味着只要使用同一份配置,无论开发者本地的开发环境如何,最终下载到的包依赖版本都是一致的。这样可以有效避免因为开发环境等原因造成的包依赖版本不一致的工程问题
- 记录包下载地址:最大的好处就是快!无需再去读取包的
package.json
文件,无需再去进行依赖分析、无需再去搜索下载依赖。其中integrity
是一个hash
值,主要作用是验证包的有效性。(每一个包版本都会生成一个验证其有效的hash
值)
{
"name": "fe-engineering",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}
}
}
dependencies
终于到了项目依赖声明这部分了,我的观点是抛开业务场景讲dependencies
都是耍流氓,因此下面是一些常备的业务场景。
业务开发场景
业务开发时,通常只会用到npm
包,常接触的有dependencies
、devDependencies
和resolutions
-
dependencies
:项目强依赖,会打包集成到最终产物中,是业务源码中不可获取的一部分。当然因为直属依赖包下又依赖很多子包,并非所有的子包都有必要打包到最终产物中(如前文安装造成的重复包等),打包阶段有一个tree shaking
的机制,这个机制将在后续打包文章中进行分享。 -
devDependencies
:本地开发环境依赖,不会打包集成到最终产物中,仅供开发环境使用。通常是一些稳定性检查、格式检查工具,如eslint
、prettier
、jest
等等 -
resolutions
:作用是锁定依赖版本。比如当前项目有A@2.0.0
、B@1.0.0
、C@2.0.0
,其中A
包依赖了C@1.0.0
,B
包依赖了A@1.0.0
。通常我们会取所有公共依赖中版本最高的那个,因此resolutions
配置如下,这样在装包时只会装resolutions
里面指定的版本,不会多装。{ "resolutions": { "A": "2.0.0", "C": "2.0.0" } }
公共包维护场景
相比于业务常用场景,额外增加了对peerDependencies
的使用。
peerDependencies
的作用是对于同一个npm
包在公共包依赖的包版本和项目依赖的包版本不一致是优先取项目里配置的依赖的版本,这个特性能在很大程度上增强公共包的适配性。当然遇到版本冲突时resolutions
始终是一个万能解决方案。
scripts
scripts
的主要目的有两个:一是配置在package.json
中,清晰明了;二是简化执行node
脚本的命令
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js"
}
version
、tag
npm
采用semver
语义化版本规范,以下是基本介绍。
版号格式
一个完整的版本号由三部分组成major.minor.patch
- 主版本号(
major
):做了不向前兼容的修改 - 次版本号(
minor
):做了向前兼容的修改,并且是稳定的新增功能 - 修订号(
patch
):做了向前兼容的修改,通常包含新功能特性和问题修复,但不保证版本的稳定性 - 先行版本号(
pre-release version
):通常作为发布正式版之前的版本,常用的规范是1.0.0-alpha.0
、1.0.0-beta.0
,表示是预发布的测试版本.
依赖中的版号规则
1.0.0
:安装指定版本^1.0.0
:可以安装1.0.0
及其以上版本>1.0.0
:安装1.0.0
以上版本,>
、<
、=
、>=
、<=
同理1.0.0 || 2.0.0
:逻辑或*
:通配符,表示可安装任意版本,通常会安装latest
tag
的版本1.x
:表示可以安装主版本号为1
的任意版本,1.0.x
同理
标签规范
-
latest
:当你使用npm publish
发布一个npm
包会自动给这个包版本打上latest
tag
,即这是最新版本的包 -
语义
tag
:这个跟项目诉求有关,通常要求tag
具有一定的语义,如beta
、alpha
表示测试包;release
表示稳定包。想要手动指定添加/移除某个tag
,参考下面命令。# 查看包有哪些tag npm dist-tag ls [<package-spec>] # 添加tag npm dist-tag add <package-spec (with version)> [<tag>] # 移除tag npm dist-tag rm <package-spec> <tag>
npm
、yarn
和pnpm
yarn
yarn
是一个用于管理JavaScript
包依赖关系的包管理工具,它是由 Facebook
开发的,并且与 npm
相似,但旨在提供更快、更可靠的性能。
yarn
vs npm
-
性能:
yarn
的并行安装和缓存机制通常使其在安装依赖项时更快。yarn
可以同时下载多个包,而npm
默认是串行下载。yarn
使用了更有效的算法来处理依赖解析,因此通常可以更快地生成依赖关系树。
-
可重复性:
yarn
使用yarn.lock
锁定文件来确保在不同的环境中安装相同的依赖项版本,从而减少了潜在的版本冲突。npm
通过package-lock.json
实现锁定功能,但在某些情况下可能不够可靠。
-
离线模式:
yarn
支持离线模式,可以在没有网络连接时安装缓存的依赖项,这对于在没有互联网连接的环境中工作非常有用。
-
工作区:
yarn
的工作区功能允许在一个存储库中管理多个相关项目(即monorepop
项目),这有助于更好地组织大型代码库。- 虽然
npm
也支持类似的功能,但在使用上可能更复杂。
pnpm
pnpm
是近些年比较火的包管理器,它最大的特点一个是快,另一个是高效利用了磁盘空间天然规避了幽灵依赖问题。
不同于yarn
和npm
扁平化的依赖解析机制,pnpm
做了如下几点创新。
- 项目的
node_modules
下只存放package.json
存放的依赖,有效地避免了扁平化导致的node_modules
一大堆,半天找不到直属依赖包的情况。并且还能顺带解决幽灵依赖问题,当你使用未在package.json
中声明的包是pnpm
会将错误信息返回给你 node_modules
下的npm
包的依赖则是放到了.pnpm
文件夹下,并且在.pnpm
中完整保留了依赖树结构,以兼容node require
的机制- 项目中的
.pnpm
文件夹下的npm
包其实是指向全局的.pnpm
文件夹里对应包的硬链接,这无疑对于磁盘空间友好度极高。 - 对比
yarn
,下载速度更快,占用磁盘空间更小,包的复用性更强,这对于monorepo
结构的项目来说是非常棒的特性。 pnpm
并非完美无缺,当同一个包的不同版本在不同项目中用到时必须在项目中锁定包版本;此外如果一个包的某个版本有问题时,因为硬链接的缘故,删除起来十分费事。(需要删除硬链接及源文件)
如何选择
讲真我也并不是很能严格区分应用场景,我这里只能分享在工作中的实际应用场景。
- 长期迭代的企业级应用项目:
yarn
仍然是稳定性的象征,旧项目实在是没有硬切换到pnpm
的必要。 - 重
monorepo
项目,如低代码项目:pnpm
收益巨大,强烈推荐。 - 1考古项目:保持
npm
结构就行,冒昧更新容易得不偿失。 - 新业务项目:都可以用,但需要和团队内部的风格一致。
发布一个npm
包
-
创建
npm
账户: 如果你还没有npm
账户,首先需要创建一个。你可以在www.npmjs.com/signup上注册一个npm
账户。 -
登录到
npm
: 使用以下命令在终端中登录到npm
npm login
-
准备你的包: 在发布之前,确保你的
npm
包项目包含一个有效的package.json
文件。你可以通过手动创建一个或使用npm init
命令来生成它。确保包含以下字段:name
: 包的名称version
: 包的版本description
: 包的描述main
: 入口文件scripts
: 可选,用于运行各种脚本任务keywords
: 可选,关键字列表author
: 包的作者信息license
: 包的许可证
-
发布包: 使用以下命令来发布你的
npm
包:npm publish
如果你的包之前没有被发布过,这将会创建一个新的包,并将其发布到
npm
仓库。如果包之前已经发布过,确保更新package.json
中的版本号,然后再次运行npm publish
来发布更新的版本。 -
验证发布: 在发布成功后,你可以访问www.npmjs.com/package/你的包名称来验证你的包是否已成功发布到
npm
仓库。
结语
感谢你能看到最后,这是前端工程化系列的第二篇文章,后续陆续会产出代码质量、构建工具、缓存管理、兼容性、模块化、性能优化、自动化测试等内容,如果对你有帮助的话点个👍和收藏吧❤️
参考文献
转载自:https://juejin.cn/post/7283832557502218277