原定的公司内部分享,先拿出来也问题不大
Semver细节
npm semantic version calculator (npmjs.com)
x. | y. | z. | m |
---|---|---|---|
1. | 2. | 0 | -beta.3 |
Major | Minor | Patch | Prelease tag |
版本通配符
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.
lockfile机制
生成与更新策略
- 若lock文件不存在,安装依赖并生存lock文件
- 若lock文件存在且与package.json版本范围匹配,则lock不变,不会检查是否有新版本
- 若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分为两部分
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下还有会有路径名过长的问题)
Flatten node_modules
后来的npmv3,还有yarn做了改进,对node_modules的结构做了拍平(flat
)处理。
因为node的require机制是从父级逐层往上找,所以可以把公共依赖尽量提升(hoist
)到上层,减少嵌套。像下图,可以把 v1.0的B包放到顶层。A包同样可以require到。
遗留痛点一:doppelgangers
由于公共依赖提升只能提升同一个包的一个版本,所以如果项目里对于一个包引用的版本比较杂,flat方案的收益就较小
遗留痛点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
pnpm的解决方案
pnpm很巧妙的通过symlink来避免了上面的两个痛点。以安装express为例
可以看到,只有显式声明的依赖(**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
Why Symlink / Hardlink ?
硬链接的文件,有着同样的 inode,可以理解为都指向同一个文件块,和源文件是完全相同的文件。只有删除了源文件和所有对应的硬链接文件,文件实体才会被删除;
而软链接的文件,可以简单理解为只是一个指向文件的快捷方式,源文件一旦被删除,则该软链接就会失效。
为什么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 tobar@1.0.0
, but in project B the same dependency offoo
might resolve tobar@1.1.0
; so, pnpm hard linksfoo@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.
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!