npm的起源和发展
一.起源
-
在GitHub还没有兴起的时候,人们通过网址来共享代码,比如当你想使用JQ的时候,你可以去JQ官网下载链接使用JQ。当GitHub兴起之后,社区中也会有人使用GitHub的下载功能
-
当项目依赖的代码越来越多,你会发现一件很繁琐的事情
-
去JQ官网下JQ
-
去BootStorap官网下BootStarp的文件放到项目里面
-
...
-
-
nodejs出世后,npm 紧随其后诞生
- 当有困难发生时,总会有一位先行者出现 —— Isaac Z. Schlueter(npm创始人),其给出了一个解决方案:用一个工具把这些东西集中到一起来管理,这个工具就是npm,全称 Node Package Managerå
-
npm的思路:
-
建立一个代码仓库,里面存放了所有需要被共享的代码
-
通知JQ,BootStarp等的作者,让其把代码提交到仓库中,然后分别给他们取个名字,例:jQuery,BootStarp等
-
当有人想使用这些代码时,就可以使用npm来下载代码了
-
这些被使用的代码就叫做包[package],也是npm的名字由来
-
二.发展
-
当 Isaac Z. Schlueter 通知其他作者加入到 npm 时,作者们会答应吗? —— 这个就不一定了,但当社区里的人都使用 npm 的时候,作者们才会开始考虑加入到 npm
-
npm 的逆袭
-
这里就不得不提到 node.js 了,作者是 Ryan Dahl
-
npm 的发展和 node.js 的发展相辅相成 , node.js 诞生后当时缺少一个包管理工具,而 npm 又缺少用户量,于是他们一拍即合,最终node.js内置了npm
-
后来 node.js 火了,随着 node.js 的火爆,大家开始使用 npm 来共享 js 代码,于是JQ等的作者们也将自己的东西发布到了npm上,所以现在大家可以使用 npm install xxx 来下载 xxx 代码了
-
package.json
package.json是npm包管理最核心也是最重要的文件,他用于描述当前项目所有的 npm 相关信息,我们来简单的看一下它有哪些内容。
{
"private": true,// 是否私有
"name": "my_package",// 包名/项目名
"bin": {
"react-cli": "./bin/index.js"
},
"description": "this is test project",// 描述
"version": "1.0.0",// 版本
"scripts": {// 脚本
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {// 仓库地址
"type": "git",
"url": "https://github.com/monatheoctocat/my_package.git"
},
"keywords": ["test", "project", "didi"],// 搜索关键字
"author": "Barney Rubble <b@rubble.com> (http://barnyrubble.tumblr.com/)",// 作者npm用户
"license": "ISC",// 开源协议
"bugs": {// 项目问题跟踪器的 url 和/或应报告问题的电子邮件地址
"url": "https://github.com/owner/project/issues" ,
"email":"project@hostname.com"
},
"homepage": "https://github.com/monatheoctocat/my_package",// 主页
"dependencies": {// 生产依赖
"my_dep": "^1.0.0",
"another_dep": "~2.2.0"
},
"devDependencies": {// 开发依赖
"my_test_framework": "^3.1.0",
"another_dev_dep": "1.0.0 - 1.2.0"
}
}
上面的这些内容只是 package.json 的凤毛麟角,例如还有还有 engines(该package运行对node\npm的版本要求)、os(该package运行对操作系统的要求)、cpu(处理器要求)......
详见:docs.npmjs.com/cli/v8/conf…
语义化版本
说到 package.json 就不得不说到他的 语义化版本 管理。npm 对版本的描述可以是一个指定的版本(例如1.1.0),也可以是一个范围(例如>1.1.0)。
语义化版本表示方式 | 语义 |
---|---|
^1.2.3 | 1.x.x(第一位非0数字后取最新子版本) |
~1.2.3 | 1.2.x |
1.2.3 | 1.2.3 |
1.x.x | 1.x.x |
>1.0.0,>=1.0.0,<2.0.0,<=2.0.0,>= 3.0.0 | <2.0.0 | |
* | x.x.x |
… |
值得一提的是,^a.b.c 并不是指大版本 a 固定,其他子版本取最新的意思,而是指第一位非0数字右边的版本取最新的意思,也就是说,^0.1.1 其实是指 0.1.x(>= 0.1.1 && < 0.1.2) 而不是 0.x.x。
开发>发布一个npm包的基本流程
在大型大项目开发中,我们一般会将一些工具抽离为npm包,然后项目中通过安装这些 npm package 来使用他的功能。学会如何从一个 npm package 开发者的角度去使用 npm 也是非常重要。
-
首先需要到 npm 官网 注册一个 npm 账号
你也可以直接在本地执行
npm adduser
来创建账户 -
在本地通过运行
npm login
登陆你的 npm 账号npm 的账户管理是镜像维度的,所以当你切换镜像的时候用户也会跟着切换,也就是说如果你想把你的包发布到官方的 npm 上,那么你登陆时就需要将你的镜像设置为npm官方就像,如果你想发布到 taobao,那么你就需要切换为 taobao 镜像。
随便推荐一个 npm 镜像管理工具 nrm,可以像 nvm 切换 node 一样方便的切换 npm 镜像。
npm whoami
可以查看到当前登陆的用户名 -
初始化你的项目
npm init -y
可以在当前目录下快速初始化一个 package.json 文件
{ "name": "yuexi_utils", "description": "this is test project", "version": "1.0.0", "repository": { "type": "git", "url": "https://github.com/monatheoctocat/my_package.git" }, "keywords": ["test", "project", "yuexi"], "author": "yuexi <yuexi_email@163.com> (http://barnyrubble.tumblr.com/)", "license": "ISC", "bugs": { "url": "https://github.com/owner/project/issues" , "email": "project@hostname.com" }, "homepage": "https://github.com/monatheoctocat/my_package", "main": "index.js" }
-
初始化一个 README.md 文件
# yuexi_utils 这是一个测试的npm包
-
编写代码/修改代码
- 如果你是修改代码,那么你还需要修改 package.json 中的 version 来修改版本,或者也可以运行
npm version xxx
来智能的生成新版本号(命令参数详见官网或者npm version -h
)
- 如果你是修改代码,那么你还需要修改 package.json 中的 version 来修改版本,或者也可以运行
-
使用
npm publish
发布当前包到 npm 仓库注意你当前的镜像必须是 npm 官方镜像
发布属于某个scope或者组织下的包
在实际项目中,对于一些不能完全和项目或者框架解藕的npm包我们一般会将其发布到相应的命名空间或者组织下,例如 @vue/cli、 @vue/runtime-core、 @vue/composition-api...他们都是 vue 组织下的包,同理 @bable/xxx、@webpack/xxx 等也是一样的。当我们在开发一个公司内部的项目时一般也会搭建 npm 私服,然后在其中创建项目的scope,然后将项目的 npm 发布上去。
发布属于某个scope或者组织下的包需要满足的条件:
-
需要 name 用 @组织名 开头,例如 @vue/cli
{ "name": "@yuexi111/foo", "version": "1.0.4", "description": "", "main": "index.js", "keywords": ["test", "project", "yuexi"], "author": "yuexi", "license": "ISC", "dependencies": { "jquery": "^3.6.0", "lodash": "<4.2.0" } }
-
你的 npm 账户需要属于这个组织或者命名空间
-
如果发布的包属于某一个 scope 或者组织,如果是非 npm 官方镜像(一般就是指私有 npm 仓库),那么你还需要配置
publishConfig.registry
来指定镜像地址。// package.json { // ... "publishConfig": { "registry": "私有镜像地址" } // ... }
-
运行
npm publish --access public
发布 npm 包注意:一定要加上 --access 参数,否者会失败
调试/修改包
我们在开发一个 @yuexi111/foo 包时,经常会在本地先做一些测试或者修改。如果我们本地正好有一个项目 project1 在使用这个包时,使用这个项目来检验包的运行结果当然是不二之选,所以我就需要将我们修改的包给放入项目的 node_modules 中来替换线上的版本。这时候就需要使用到 npm link 命令了
- 在你的 @yuexi111/foo 目录下运行
npm link
命令,不加任何参数。这一步会在你全局的 node_modules 下创建一个名为 @yuexi111/foo 的链接(可以理解为快捷方式)链接到你的 @yuexi111/foo 代码所在目录。 - 在 project1 目录下运行
npm link @yuexi111/foo
,这一步就是在 project1 的 node_modules 中再创建一个 @yuexi111/foo 链接链接到全局的 node_modules/@yuexi111/foo。
这样当你修改 @yuexi111/foo 的代码时, project1 中的 @yuexi111/foo 代码也会同步。
如果你觉得每次修改后手动copy更方便就当我没说
千万不要直接在 node_modules 中写代码,一旦你运行 npm ci 或者不小心删除了 node_modules,那么就会【花谢了明年还是一样地开,美丽小鸟一去无影踪,你的代码小鸟一样不回来,你的代码小鸟一样不回来】
安装本地的包
上面说到 npm link 可以创建链接直接链接到本地对应包的代码目录,但是当我们运行 npm i 或者 node_modules 丢失之类的情况时,再次安装就会出现去 npm 官网下载包代码而不是创建链接,原因是 npm link 并不会在 package.json 存在记录。但是有的时候我们想开发一个私有包,不想发布到 npm 上,又要运行 npm install 能正常安装这个包,那么我们可以用下面这种方式
npm install 待安装包的相对路径
当运行 npm i ../bar
时,我们可以在 package.json 中看到相关信息
{
"name": "@yuexi111/foo",
"version": "1.0.4",
"description": "",
"main": "index.js",
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@yuexi111/bar": "file:../bar", // 这里
"jquery": "^3.6.0",
"lodash": "<4.2.0"
}
}
npm常用的命令如何工作的
npm run
npm run 是最常用的命令,他的作用是运行 pacgake.json 中指定的脚本
// package.josn
{
"name": "h5",
"version": "1.0.7",
"scripts": {
"serve": "vue-cli-service serve"
}
}
当我们运行 npm run serve
会查找 package.json 中 scripts 中 key 为 serve 对应的值来当作命令执行,也就是相当于执行了 vue-cli-service serve
这里有一个 npm 包:@yuexi111/hello_shell
,他给我们提供了一个 hello 命令,我们来在项目中使用一下它。
// package.json
{
"name": "demo1",
"scripts": {
"hello": "hello"
},
"dependencies": {
"@yuexi111/hello_shell": "^1.0.0"
}
}
当运行 npm run hello
时其实就相当于运行了 hello
那为什么我们要执行 npm run hello
而不直接执行 hello
呢?
hello
zsh: command not found: hello
为什么我们直接执行 hello
找不到命令,使用 npm run 来执行却可以?原因是 npm run 执行脚本时会先去 node_modules/.bin
中查找是否存在要运行的命令,如果不存在则查找 ../node_modules/.bin
,如果全都找不到才会去按系统的环境变量查找。
好在现在 node 给我们提供了 npx 命令来解决这个问题。运行 npx hello
即可运行 hello 命令。当然你也可以直接运行 node_modules/.bin/hello
npx 可以让命令的查找路径与 npm run 一致
那么 node_modules/.bin 中的文件从哪来的呢?npm i @yuexi111/hello_shell
时会将 @yuexi111/hello_shell
中的 package.json 中的 bin 指定的命令和文件链接到 node_modules/.bin
,也就是说 node_modules/.bin/hello
其实是 node_modules@yuexi111/hello_shell/bin/index.js
的快捷方式
// package.json
{
"name": "@yuexi111/hello_shell",
"version": "1.0.0",
"description": "一个描述",
"keywords": [],
"bin": {
"hello": "bin/hello_shell.js"// 指定运行 hello 命令时运行的文件
},
"license": "ISC"
}
// bin/index.js
#!/usr/bin/env node
console.log('Hello NPM')
当运行 npx hello
时自然就相当于运行了 @yuexi111/hello_shell/bin/index.js
小结:
- 当我们使用 npm install 安装包时,会将这个包中 package.json 中 bin 中指定的脚本软链接到项目的 node_modules/.bin 下,key 作为链接名字(也就是命令),value 作为命令运行时执行的文件
- 当我们通过npm run xxx 运行某个脚本时,会执行 package.json 中 scripts 中指定的脚步后的命令,会先去 node_modules/.bin 中查找这些命令,然后去 ../node_modules/.bin,...全都找不到才会去环境变量中查找。
npm install
假如我们从 github 上 conle 了一个项目,他的 package.json 是这样的:
{
"name": "demo2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@yuexi111/foo": "^1.0.4",
"@yuexi111/bar": "^1.0.1"
}
}
当我们去运行 npm install 的时候,会经过一下几步
执行工程自身 preinstall 钩子
npm 跟 git 一样都有完善的钩子机制散布在 npm 运行的各个阶段,当前 npm 工程如果定义了 preinstall 钩子此时会在执行 npm install 命令之前被执行。
// 如何定义钩子:直接在 scripts 中定义即可
// package.json
{
// ...
"scripts": {
"preinstall": "echo \"preinstall hook\"",
"install": "echo \"install hook\"",
"postinstall": "echo \"postinstall hook\""
// ...
}
// ...
}
获取 package.json 中依赖数据构建依赖树
首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install的其他参数)。
工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。
确定完首层依赖后,就开始获取各个依赖的模块信息,获取模块信息是一个递归的过程,分为以下几步:
-
获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 packaeg.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。
-
获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。
-
查找该模块依赖,如果有依赖则回到第1步,如果没有则停止。
最终会得到一个类似下图中的依赖树
如果项目中存在 npm 的 lock 文件(例如package-lock.json),则不会从头开始构建依赖树,而是对 lock 中依赖树中存储冲突的依赖进行调整即可
依赖树扁平化(dedupe)
上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 foo 模块依赖于 loadsh,bar 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,也就是方便在 foo 和 bar 的 node_modules 中各安装一份,因此会造成模块冗余。
从 npm3 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node_modules 的第一层。当发现有重复模块时,则将其丢弃。
经过优化后的依赖树就是变成了下面这样
而 lock 文件中存储的正是这颗被优化后的依赖树。
这里需要对重复模块进行一个定义,它指的是模块名相同且 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 的规则,二者不存在兼容版本。会将一个版本放在首层依赖中,另一个仍保留在其父项(foo或者bar)的依赖树里。
举个栗子🌰,假设一个依赖树原本是这样:
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
安装模块
这一步将会按照依赖树下载/解压包,并更新工程中的 node_modules
其中还有许多细节可以看这张图
npm ci
npm ci 命令可以完全安装 lock 文件描述的依赖树来安装依赖,可以用它来避免扁平化造成的 node_modules 结构不确定的问题。
npm ci
和 npm i
不仅仅是是否使用 package-lock.json 的区别,npm ci
会删除 node_modules 中所有的内容并且毫无二心的按照package-lock.json 的结构来安装和保存包,他的目的是为了保证任何情况下产生的node_modules结构都一致的。而 npm i
不会删除 node_modules(如果node_modules已经存在某个包就不会重新下载了)、并且安装过程中可能还会调整并修改 package-lock.json 的内容
实际项目中建议将 lock 也添加到 git 中,尽量使用
npm ci
来安装依赖,如果有依赖需要修改的,可以通过npm install xxx@xxx
来安装指定依赖的指定版本,这样只会调整 lock 文件中指定依赖的依赖树,不会修改其他依赖的依赖树。
npm有哪些问题
依赖结构不确定
假如项目依赖两个包 foo 和 bar,这两个包的依赖又是这样的:
那么 npm install
的时候,通过扁平化处理之后,究竟是这样
还是这样?
答案是: 都有可能。取决于 foo 和 bar 在 package.json
中的位置,如果 foo 声明在前面,那么就是前面的结构,否则是后面的结构。
这就是为什么会产生依赖结构的不确定
问题,也是 lock 文件
诞生的原因之一,无论是package-lock.json
(npm 5.x 才出现)还是yarn.lock
,都是为了保证 install 之后都产生确定的 node_modules
结构。
扁平化导致可以非法访问没有声明过依赖的包(幽灵依赖)
“幽灵依赖” 指的是项目代码中使用了一些没有被定义在其 package.json 文件中的包。
考虑下面的例子:
// package.json
{
"name": "demo4",
"main": "index.js",
"dependencies": {
"minimatch": "^3.0.4"
},
"devDependencies": {
"rimraf": "^2.6.2"
}
}
但假设代码是这样:
// index.js
var minimatch = require("minimatch")
var expand = require("brace-expansion"); // ???
var glob = require("glob") // ???
// (更多使用那些库的代码)
稍等一下下… 有两个库根本没有被作为依赖定义在 package.json 文件中。那这到底是怎么跑起来的呢?
原来 brace-expansion 是 minimatch 的依赖,而 glob 是 rimraf 的依赖。在安装的时候,NPM 会打平他们的文件夹到 node_modules 。NodeJS 的 require()
函数能够在依赖目录找到它们,因为 require()
在查找文件夹时 根本不会受 package.json 文件 影响。
这是很不安全的,当未来 minimatch 中不再依赖 brace-expansion 时将会导致项目报错,因为那时整个项目可能没有如何包依赖了 brace-expansion,也就不会在顶层依赖树中有 brace-expansion,所以项目一定会因为找不到 brace-expansion 这个包而报错。
又慢又大
分析依赖树
npm 在分析依赖树的时候会先并行发出项目顶级的依赖解析请求,当某一个请求回来时,在去请求起所有的子依赖,直到不存在依赖为止,由于每一个树都需要根节点的依赖解析请求后才能开始解析其子树,如果依赖树深度比较深就会导致等待时间过长
递归的分析依赖树需要非常大量的http请求,这也会导致依赖树构建时间过长
-
这里推荐一个分析依赖树的工具 npm-remote-ls
-
可视化依赖关系:npm.anvaka.com/ 下图是 webpack 的依赖树分析结果
大量文件下载/解压
因为 npm 下载的内容是一个个压缩包,解压后文件数量多,需要大量的IO操作(创建文件夹、创建文件、写入文件...),这也是导致 npm 慢的主要原因
依然可能存在大量重复包
扁平化只能会在首次遇到一个包时才会将其提升到顶部,如果项目中有A、B、C三个包分别依赖了D@1.0.0、D@2.0.0、D@2.0.0,那么可能会产生D@1.0.0被提升,D@2.0.0出现在B、C的node_modelus的情况。
pnpm 依赖管理
pnpm 的作者Zoltan Kochan
发现 npm/yarn 并没有打算去解决上述的这些问题,于是另起炉灶,写了全新的包管理器,开创了一套新的依赖管理机制,现在就让我们去一探究竟。
以安装 express
为例,我们新建一个目录,执行:
pnpm init -y
然后执行:
pnpm install express
我们再去看看node_modules
:
.pnpm
.modules.yaml
express
我们直接就看到了express
,但值得注意的是,这里仅仅只是一个软链接
,里面并没有 node_modules 目录,如果是真正的文件位置,那么根据 node 的包加载机制,它是找不到依赖的。那么它真正的位置在哪呢?
继续在 .pnpm 当中寻找:
▾ node_modules
▾ .pnpm
▸ accepts@1.3.7
▸ array-flatten@1.1.1
...
▾ express@4.17.1
▾ node_modules
▸ accepts
▸ array-flatten
▸ body-parser
▸ content-disposition
...
▸ etag
▾ express
▸ lib
History.md
index.js
LICENSE
package.json
Readme.md
.pnpm/express@4.17.1/node_modules/express
随便打开一个别的包:
也都是一样的规律,都是<package-name>@version/node_modules/<package-name>
这种目录结构。并且 express 的依赖都在.pnpm/express@4.17.1/node_modules
下面,这些依赖也全都是软链接。
再看看.pnpm
,.pnpm
目录下虽然呈现的是扁平的目录结构,但仔细想想,顺着软链接
慢慢展开,其实就是嵌套的结构!
▾ node_modules
▾ .pnpm
▸ accepts@1.3.7
▸ array-flatten@1.1.1
...
▾ express@4.17.1
▾ node_modules
▸ accepts -> ../accepts@1.3.7/node_modules/accepts
▸ array-flatten -> ../array-flatten@1.1.1/node_modules/array-flatten
...
▾ express
▸ lib
History.md
index.js
LICENSE
package.json
Readme.md
将 包本身
和 依赖
放在同一个node_module
下面,与原生 Node 完全兼容,又能将 package 与相关的依赖很好地组织到一起,设计十分精妙。
现在我们回过头来看,根目录下的 node_modules 下面不再是眼花缭乱的依赖,而是跟 package.json 声明的依赖基本保持一致。即使 pnpm 内部会有一些包会设置依赖提升,会被提升到根目录 node_modules 当中,但整体上,根目录的node_modules
比以前还是清晰和规范了许多。
pnpm 使用类似 maven 一样将所有的包都存放在一个 .pnpm 缓存目录中,然后在 node_modules 中创建一个软链接链接到缓存目录中对应的包上,解决了重复依赖的问题。而 .pnpm 中的文件又是通过硬链接来链接到一个全局的包存放地址中,也就是说同一个包的某个版本在你的电脑上只会出现一份代码,无论你有多少个项目使用了多少次这个包。因为每一个项目中的 .pnpm 中都只是通过一个硬链接指向同一份代码。
如何做到项目隔离?
因为 .pnpm 中都是通过硬链接来链接到同一份源码文件,当我们在某个项目中修改了这个包的文件时,所有项目中这个包都会被修改,这导致无法做到修改的项目隔离。
好在我们有 webstorm ,webstorm 以及对此作了优化,当你在修改其 node_modules 中的内容时,不会直接修改到这个硬链接到目标文件,而是将目标文件 copy 一份到当前项目下,然后对其进行修改,这样就不会影响到其他项目。
很遗憾 vscode 目前好像没有这功能。
听说,下雨天, pnpm 跟 webstorm 跟配哟~
再谈安全
pnpm 这种依赖管理的方式也很巧妙地规避了 幽灵依赖
的问题,也就是只要一个包未在 package.json 中声明依赖,那么在项目中是无法访问的。但在 npm/yarn 当中是做不到的
npm 也有想过去解决这个问题,指定 --global-style
参数即可禁止扁平化,但这样做相当于回到了当年嵌套依赖的时代,一夜回到解放前,前面提到的嵌套依赖的缺点仍然暴露无遗。
npm/yarn 本身去解决依赖提升的问题貌似很难完成,不过社区针对这个问题也已经有特定的解决方案: dependency-check,地址: github.com/dependency-…
但不可否认的是,pnpm 做的更加彻底,独创的一套依赖管理方式不仅解决了依赖提升的安全问题,还大大优化了时间和空间上的性能。
tnpm:比pnpm更快
蚂蚁集团 npm 工程师零弌在 SEE Conf 2022 支付宝体验科技大会上给分享了 一种秒级安装 npm 的方式:tnpm(蚂蚁集团鲁班奖)
1、使用多级缓存来将每个npm包不同版本的依赖树缓存起来,减少分析依赖树需要的时间
2、不解压文件,而是对多个压缩包进行拼接、最后直接使用tar中的内容(FUSE),IO操作由原来的对每个文件创建文件夹、创建文件、写入文件变成了整个依赖只有一次文件的拼接,大大减少了I/O操作。
随之而来的问题:用户不能直接打开node_modules读取和修改文件内容了,因为这是一个 tar 文件。
- 解决方案:实现了一个基于 tar 的文件系统中间层,直接接管操作系统对该目录的I/O操作,通过中间层提供文件的读操作
3、如何实现项目隔离?
修改操作在项目的work dir中执行,不会对最底层的总压缩文件做修改。文件系统读取文件时会先尝试在 work dir中读取,将 work dir和压缩包的内容融合作为读取结果。
目前该方案正在完善中,尽管现在没tnpm可用了,还是要respect一下。