记录一次组内npm&pnpm分享

3,492 阅读20分钟

npm的起源和发展

一.起源

  1. 在GitHub还没有兴起的时候,人们通过网址来共享代码,比如当你想使用JQ的时候,你可以去JQ官网下载链接使用JQ。当GitHub兴起之后,社区中也会有人使用GitHub的下载功能

  2. 当项目依赖的代码越来越多,你会发现一件很繁琐的事情

    • 去JQ官网下JQ

    • 去BootStorap官网下BootStarp的文件放到项目里面

    • ...

  3. nodejs出世后,npm 紧随其后诞生

    • 当有困难发生时,总会有一位先行者出现 —— Isaac Z. Schlueter(npm创始人),其给出了一个解决方案:用一个工具把这些东西集中到一起来管理,这个工具就是npm,全称 Node Package Managerå
  4. npm的思路:

    • 建立一个代码仓库,里面存放了所有需要被共享的代码

    • 通知JQ,BootStarp等的作者,让其把代码提交到仓库中,然后分别给他们取个名字,例:jQuery,BootStarp等

    • 当有人想使用这些代码时,就可以使用npm来下载代码了

    • 这些被使用的代码就叫做包[package],也是npm的名字由来

二.发展

  1. 当 Isaac Z. Schlueter 通知其他作者加入到 npm 时,作者们会答应吗? —— 这个就不一定了,但当社区里的人都使用 npm 的时候,作者们才会开始考虑加入到 npm

  2. 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.31.x.x(第一位非0数字后取最新子版本)
~1.2.31.2.x
1.2.31.2.3
1.x.x1.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 也是非常重要。

  1. 首先需要到 npm 官网 注册一个 npm 账号

    你也可以直接在本地执行 npm adduser 来创建账户

  2. 在本地通过运行 npm login 登陆你的 npm 账号

    npm 的账户管理是镜像维度的,所以当你切换镜像的时候用户也会跟着切换,也就是说如果你想把你的包发布到官方的 npm 上,那么你登陆时就需要将你的镜像设置为npm官方就像,如果你想发布到 taobao,那么你就需要切换为 taobao 镜像。

    随便推荐一个 npm 镜像管理工具 nrm,可以像 nvm 切换 node 一样方便的切换 npm 镜像。

    npm whoami 可以查看到当前登陆的用户名

  3. 初始化你的项目

    • 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包
      
  4. 编写代码/修改代码

    • 如果你是修改代码,那么你还需要修改 package.json 中的 version 来修改版本,或者也可以运行 npm version xxx 来智能的生成新版本号(命令参数详见官网或者 npm version -h
  5. 使用 npm publish 发布当前包到 npm 仓库

    注意你当前的镜像必须是 npm 官方镜像

发布属于某个scope或者组织下的包

在实际项目中,对于一些不能完全和项目或者框架解藕的npm包我们一般会将其发布到相应的命名空间或者组织下,例如 @vue/cli、 @vue/runtime-core、 @vue/composition-api...他们都是 vue 组织下的包,同理 @bable/xxx、@webpack/xxx 等也是一样的。当我们在开发一个公司内部的项目时一般也会搭建 npm 私服,然后在其中创建项目的scope,然后将项目的 npm 发布上去。

发布属于某个scope或者组织下的包需要满足的条件:

  1. 需要 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"
      }
    }
    
  2. 你的 npm 账户需要属于这个组织或者命名空间

  3. 如果发布的包属于某一个 scope 或者组织,如果是非 npm 官方镜像(一般就是指私有 npm 仓库),那么你还需要配置 publishConfig.registry 来指定镜像地址。

       // package.json
     {
       // ...
       "publishConfig": {
         "registry": "私有镜像地址"
       }
       // ...
     }
    
  4. 运行 npm publish --access public 发布 npm 包

    注意:一定要加上 --access 参数,否者会失败

调试/修改包

我们在开发一个 @yuexi111/foo 包时,经常会在本地先做一些测试或者修改。如果我们本地正好有一个项目 project1 在使用这个包时,使用这个项目来检验包的运行结果当然是不二之选,所以我就需要将我们修改的包给放入项目的 node_modules 中来替换线上的版本。这时候就需要使用到 npm link 命令了

  1. 在你的 @yuexi111/foo 目录下运行 npm link 命令,不加任何参数。这一步会在你全局的 node_modules 下创建一个名为 @yuexi111/foo 的链接(可以理解为快捷方式)链接到你的 @yuexi111/foo 代码所在目录。
  2. 在 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_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步,如果没有则停止。

最终会得到一个类似下图中的依赖树

demo2

如果项目中存在 npm 的 lock 文件(例如package-lock.json),则不会从头开始构建依赖树,而是对 lock 中依赖树中存储冲突的依赖进行调整即可

依赖树扁平化(dedupe)

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

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

经过优化后的依赖树就是变成了下面这样

2-3233906.png

而 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 install 详细流程

npm ci

npm ci 命令可以完全安装 lock 文件描述的依赖树来安装依赖,可以用它来避免扁平化造成的 node_modules 结构不确定的问题。

npm cinpm 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 的时候,通过扁平化处理之后,究竟是这样

扁平化后的依赖树1

还是这样?

扁平化后的依赖树1

答案是: 都有可能。取决于 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 的依赖树分析结果

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.7array-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

随便打开一个别的包:

.pnpm

也都是一样的规律,都是<package-name>@version/node_modules/<package-name>这种目录结构。并且 express 的依赖都在.pnpm/express@4.17.1/node_modules下面,这些依赖也全都是软链接

再看看.pnpm.pnpm目录下虽然呈现的是扁平的目录结构,但仔细想想,顺着软链接慢慢展开,其实就是嵌套的结构!

▾ node_modules
  ▾ .pnpm
    ▸ accepts@1.3.7array-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 与相关的依赖很好地组织到一起,设计十分精妙。

pnpm的目录结构

现在我们回过头来看,根目录下的 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和压缩包的内容融合作为读取结果。

work dir

目前该方案正在完善中,尽管现在没tnpm可用了,还是要respect一下。