前端架构(一)Pnpm + Lerna 搭建私仓

Pnpm

首先,简单的看一下pnpm是什么,pnpm 在 npm 前加了一个 p,即 performance npm,既然是高性能的 npm 包管理工具,高性能体现在哪里呢,在 npm 3.0 之后,优化了之前的 modules 结构,有依赖的包,不会重复的再挂载到引用的 package 中,但即使是这样,多个项目,每个项目存在自己的 node_modules,而这其中一些相同的包,也会重复的存在于磁盘。pnpm 本着解决这一基本问题来了,用官网的话语来说,就是构造一套可寻址的磁盘存储

这里也不在过多叙述 npm 及 yarn 的包差异,找到一篇讲的不错的文章,未知大佬的差异对比,非原创

Pnpm 官网


可寻址的存储

在看了几篇关于 pnpm 的文章,大部分都在说 pnpm 是基于硬链的,但也不尽然,下面这张官网的图,诠释了这一点,对于寻址的 package,是硬链,但是对于依赖的包,会创建软链

那么依赖到底被存放在哪里了,既然 package 会被放到一个可寻址并且顶层磁盘存储,那么就在磁盘顶层,在~目录下,我们会看到有两个 pnpm 的文件夹,分别是.pnpm-state 和.pnpm-store,前者是存放 lastUpdateCheck 的 JSON 文件,后者是存储所有的 packages

为什么要使用硬链呢,在其他文档中看到硬链式不要占用额外存储空间,这一点,我认为是错误的,当我 ln 创建了一个硬链接,发现 size 是一样的

img

软链与硬链

敲两行 shell,来看一下硬链与软链的区别

touch tt.txt
ln tt.txt hard.txt
ln -s tt.txt soft.txt

img

  • 我们改变 tt.txt,查看软链与硬链,会发现软链和硬链内容都变了,但是我们在查看文件的详情,会发现,软链的文件信息并没有更新记录,硬链与源文件同步

  • 占用内存:硬链与源文件一致

  • 删掉源文件,软链因为地址索引找不到而打不开,硬链依旧在

  • system: 硬链的存储数据和 inode 结构一致,可以达到快速检索,与正常文件一样需要占据存储空间;软链的 inode 结构一致,但是存储数据不一致,不能快速检索,但是内部存放符号链接地址 — (inode 我们可以使用 ls -l 查看 )

与 npm 差异

功能 pnpm Yarn npm
工作空间支持(monorepo) ✔️ ✔️ ✔️
隔离的 node_modules ✔️ - 默认 ✔️
提升的 node_modules ✔️ ✔️ ✔️ - 默认
Plug’n’Play ✔️ ✔️ - 默认
零安装 ✔️
修补依赖项 ✔️
管理 Node.js 版本 ✔️
有锁文件 ✔️ - pnpm-lock.yaml ✔️ - yarn.lock ✔️ - package-lock.json
支持覆盖 ✔️ ✔️ - 通过 resolutions ✔️
内容可寻址存储 ✔️
动态包执行 ✔️ - 通过 pnpm dlx ✔️ - 通过 yarn dlx ✔️ - 通过 npx

pnpm 优势与不足

优势

  • 管理更为严格,只能使用 package.json 中的 packages

  • 模块依赖会通过生成软链,减少内存占用

  • 安装 package 时,不会重复安装,会检索已经存在的,安装没有的文件

  • package 全局寻址,打包速度快

不足

  • 不能同时存在不同版本的 package
  • 别名问题不友好
  • workspace 协议被其他工具不支持,比如 lerna

Lerna

Lerna 也是一个 npm 包管理工具,跟严格的针对 github 注册表提交,当然,也可以通过配置忽略,但是不建议这么做

与 Pnpm 一样,可以减少开发和构建环境中大量重复包的时间和空间需求,Lerna 话不多。。。。

小实战

    1. 初始化项目,duanxl/libs,默认pnpm配置

      pnpm init -y
      

      pnpm-workspace.yaml

      packages:
        # all packages in subdirs of packages/ and components/
        - 'packages/**'
        # exclude packages that are inside test directories
        - '!**/tests/**'
      
    1. 在自己的服务器,搭建私仓,通过Pm2 + verdaccio 搭建并启动服务,具体配置请参考官网
    2. 新建packages目录,存放我们的package
    3. 新建.npmrc,将registry地址指向我们的私仓地址
    // .npmrc
    registry=http://127.0.0.1:4873/
    // 开启链接namespace link 配置
    link-workspace-packages = true
    // 生成公共的lockfile
    shared-workspace-lockfile = false
    
    1. 初始化lerna,lerna使用pnpm,通过设置independent独立包管理,并开启命名空间配置,为了配合pnpm

      // lerna.json
      {
        "npmClient": "pnpm",
        "packages": ["packages/*"],
        "version": "independent",
        "command": {
          "publish": {
            "conventionalCommits": true,
            "message": "[skip ci] chore: release"
          }
        },
        "useWorkspaces": true,
        "ignoreChanges": ["**/node_modules/**", "**/__snapshots__/**"]
      }
      
    1. 在package.json中,指定workspaces,指向我们的packages,作为我们的工作目录,指定publish仓库地址

      "workspaces": [
          "packages/*"
        ],
      "publishConfig": {
          "registry": "http://duanxl.com:4873/"
        },
      
    1. lerna create core 通过lerna创建package

    2. package添加scripts,使用microbundle开箱即用编译Ts,也会自动生成.d.ts

      {
        "name": "@duanxl/core",
        "version": "1.0.2",
        "description": "我是core,核心业务库  🍺",
        "author": "duanxinlei",
        "homepage": "",
        "license": "ISC",
        "main": "src/index.ts",
        "directories": {
          "lib": "src",
          "test": "test"
        },
        "files": [
          "lib"
        ],
        "publishConfig": {
          "registry": "http://duanxl.com:4873/"
        },
        "scripts": {
          "dev": "microbundle"
        }
      }
      
    1. 接下来,我们再create一个package《header》,让header依赖core

      // duanxl-libs/packages/header/src/index.ts
      import data from '@duanxl/core'
      
      console.log('Header use------:',data)
      
    1. 在header中引入core ==> import data from ‘@duanxl/core’,但是,我们发现包引用找不到;怎么办,除了发包core,也要考虑开发环境,这时候,我们就可以用到lerna add,将package core 引用到package header中,原理的话,就是一个软链接

      lerna add @duanxl/core packages/header
      

      接下来,会发现ESM生效了

    2. 但是,我们又发现一个新的问题,如果我们私仓存在了@duanxl/core的话,引用会优先找私仓

      为了解决上面的问题,我们需要删除掉header中的依赖,就会优先找本地了,这也属于lerna的弊端吧,毕竟不会轻易的publish,当然,如果依赖package变更,我们可以再次lerna add,这样的话,因为软链的原因,会找本地

    3. 再考虑一个问题,如果我们一个package要区分环境,而且要publish给其他团队用,其他团队dev、prod都可能使用,这个时候,我们就要package的别名

    4. 为了解决以上问题,我们需要用到npm 别名,例如 pnpm add prod@npm:@duanxl/core -filter @duanxl/header –registry http://duanxl.com:4873

    5. 我们通过别名,就可以解决多环境多package的问题,而不是搞多套代码(被依赖包),然后再看package header中的package.json

      "dependencies": {
          "@duanxl/core": "^1.0.2",
          "prod": "workspace:npm:@duanxl/core@^1.0.2"
        }
      
    1. 但是,我们在publish 的时候,又出现了上面的workspace的问题,这个时候再次删掉workspace,就可以publish,删掉workspace并不影响代码的使用,因为我们的目的就是利用pnpm来实现别名,我们可以写个脚本,在pnpm解决别名后,使用脚本删掉workspace协议
    2. 伴随别名而来的一个问题,就是两个package依赖的时候,别名会导致检测不到被引用package的版本更新,这里也需要一个shell,每次发包去读package.json的依赖,去对比被依赖package的version,然后做到同步更新
    3. 接着,我们创建代码库,使用lerna publish,当然,为了方便,我们可以在package中package.json指定publishConfig,指向我们的私仓
    4. 接着,publish成功

目录结构

img

发布结果

http://duanxl.com:4873

参考文档

Lerna

Pnpm

verdaccio

0