原定的公司内部分享,先拿出来也问题不大

Semver细节

npm semantic version calculator (npmjs.com)

x. y. z. m
1. 2. 0 -beta.3
Major Minor Patch Prelease tag

版本通配符

img

Prerelease

发新版本之前,给alpha/beta包打上tag

可用tag

  • alpha
  • beta
  • rc

If a version has a prerelease tag (for example, 1.2.3-alpha.3) then it will only be allowed to satisfy comparator sets if at least one comparator with the same [major, minor, patch] tuple also has a prerelease tag.

semver | npm Docs (npmjs.com)

lockfile机制

生成与更新策略

  1. 若lock文件不存在,安装依赖并生存lock文件
  2. 若lock文件存在且与package.json版本范围匹配,则lock不变,不会检查是否有新版本
  3. 若lock文件存在,且不满足package.json的版本范围,则会查找最新的满足版本范围的包安装,并更新lock文件

CI环境

为了确保生产环境安装的版本是开发者想要的,可以使用 npm ci,其会先删除本地的node_modules,并检查lock文件与package.json文件是否可以匹配。若不匹配则强制报错。

pnpm原理

Frequently Asked Questions | pnpm

pnpm的两大特性,其实可以用 hard link + sym link 来去概括。

硬链+全局store = 快速安装+空间节省

全局安装其实很常见,像pip、maven、cargo等,都是安装到全局的。相同版本的package只会在电脑上存在一份。

而npm比较特别,它每次install,都会把依赖装到当前目录。导致安装速度慢、占用空间大。

而pnpm其实也就是借鉴了pip它们的思路。把依赖装在了用户的全局目录中 (~/.pnpm-store),然后通过hard link,在项目的 node_modules 中把对应依赖链接过来。

全局store结构

全局store分为两部分

img

metadata部分

存放package的元信息(名字,依赖,maintainer,readme,各版本的发布时间。。。)

https://github.dev/pnpm/pnpm/blob/6cc1aa2c00f81361cea7c0f591a142d192b603b8/packages/npm-resolver/

files部分

会存放代码源文件,文件被哈希处理了,通过哈希的前两位来做一级索引。

为什么这么设计?主要是考虑到如果两个文件内容相同,可以链接到一块,让安装更快。

参考这个Issue:Content-addressable storage · Issue #2470 · pnpm/pnpm (github.com)

存放路径长下面这样

files/f3/012e9f98ee989e17d10a2ea30306e2650bf2e870f0e86aab15388755f9e7ebf0253ff707b8bee46301fa604af1d678e332e4481d292cd253bbe32caf8f1c0b

软链+非平铺结构 = 避免重复依赖和幽灵依赖

参考@杨健的知乎文章【建议熟读】:node_modules 困境 - 知乎 (zhihu.com)

最早期的node_modules结构

最早的npm版本(<=v3)采用的其实也是嵌套结构,每个package依赖什么子package,也递归的安装到该package的node_modules/下面。很容易造成重复安装,这样会导致依赖项非常的大(在Windows下还有会有路径名过长的问题)

img

Flatten node_modules

后来的npmv3,还有yarn做了改进,对node_modules的结构做了拍平(flat)处理。

因为node的require机制是从父级逐层往上找,所以可以把公共依赖尽量提升(hoist)到上层,减少嵌套。像下图,可以把 v1.0的B包放到顶层。A包同样可以require到。

img

遗留痛点一:doppelgangers

由于公共依赖提升只能提升同一个包的一个版本,所以如果项目里对于一个包引用的版本比较杂,flat方案的收益就较小

img

遗留痛点2:Phantom dependency

继续以下面这张图为例,该场景下,dependcy只有两个:A和C。但在代码里去直接require B包却是完全可以运行的。

// package.json
{
    "dependencies": {
        "A": "^1.0.0",
        "C": "^1.0.0"
    }
}


// index.js
const B = require('B') // OK

img

pnpm的解决方案

pnpm很巧妙的通过symlink来避免了上面的两个痛点。以安装express为例

img

可以看到,只有显式声明的依赖(**express**)被放到了node_modules下面,并symlink到了.pnpm目录下。

.pnpm 是所有npm包的集中营(virtual store),安装的所有package都会平铺的存放在这里,在这里处理package的子依赖,并同样通过symlink链接到 .pnpm 目录。真正的npm包会hard link到全局store中(见上一章节)

假使我们在代码里使用了 require('exprss') ,则其的查找路径是这样的:

node_modules/express –[symlink]–>

.pnpm/express/4.0.0/node_modules/express –[hard link]–> 全局store

express包里的调用的 require('debug') 的查找路径则为这样:

.pnpm/express/4.0.0/node_modules/debug` --[symlink]--> `.pnpm/debug/1.0.0/node_modules/debug` --[hard link] --> `全局store

硬链接的文件,有着同样的 inode,可以理解为都指向同一个文件块,和源文件是完全相同的文件。只有删除了源文件和所有对应的硬链接文件,文件实体才会被删除;

而软链接的文件,可以简单理解为只是一个指向文件的快捷方式,源文件一旦被删除,则该软链接就会失效。

img

为什么node_modules内部不使用硬链

硬链无法对文件夹生效,需要对底下每个文件都执行hard link操作

为什么全局store不使用软链

个人的想法是,如果把全局store删了,就会影响项目内的node_modules,这不make sense。

正式的原因官网有写,不过我没理解 Frequently Asked Questions | pnpm

One package can have different sets of dependencies on one machine.

In project A foo@1.0.0 can have a dependency resolved to bar@1.0.0, but in project B the same dependency of foo might resolve to bar@1.1.0; so, pnpm hard links foo@1.0.0 to every project where it is used, in order to create different sets of dependencies for it.

Direct symlinking to the global store would work with Node’s --preserve-symlinks flag, however, that approach comes with a plethora of its own issues, so we decided to stick with hard links. For more details about why this decision was made, see this issue.



经验分享      JS 包管理

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!