关于pnpm workspace管理项目 monorepo

173 阅读10分钟

一、什么是pnpm

pnpm又称 performant npm,翻译过来就是高性能的npm。

  • 节省磁盘空间提高安装效率
  • pnpm通过使用硬链接和符号链接(又称软链接)的方式来避免重复安装以及提高安装效率。 硬链接:和原文件共用一个磁盘地址,相当于别名的作用,如果更改其中一个内容,另一个也会跟着改变 符号链接(软链接)::是一个新的文件,指向原文件路径地址,类似于快捷方式 官网原话:

当使用 npm 时,如果你有 100 个项目,并且所有项目都有一个相同的依赖包,那么,你在硬盘上就需要保存 100 份该相同依赖包的副本。然而,如果是使用 pnpm,依赖包将被存放在一个统一的位置,因此: 1.如果你对同一依赖包需要使用不同的版本,则仅有 版本之间不同的文件会被存储起来。例如,如果某个依赖包包含 100 个文件,其发布了一个新 版本,并且新版本中只有一个文件有修改,则pnpm update 只需要添加一个新文件到存储中,而不会因为一个文件的修改而保存依赖包的所有文件。 2.所有文件都保存在硬盘上的统一的位置。当安装软件包时,其包含的所有文件都会硬链接自此位置,而不会占用额外的硬盘空间。这让你可以在项目之间方便地共享相同版本的依赖包。 最终结果就是以项目和依赖包的比例来看,你节省了大量的硬盘空间,并且安装速度也大大提高了!

  • 创建非扁平的node_modules目录结构

image.png

  • Monorepo 简介及其与包管理工具(npm、yarn、pnpm)之间的关系

  • Monorepo模式:

    • Monorepo 是一种项目开发与管理的策略模式,它代表"单一代码仓库"(Monolithic Repository)。在 Monorepo 模式中,所有相关的项目和组件都被存储在一个统一的代码仓库中,而不是分散在多个独立的代码仓库中,这些项目之间还可能会有依赖关系。
  • 包管理工具:

    • npm、yarn、pnpm 等是用来管理项目依赖、发布包、安装依赖的工具,它们都提供了对工作区(workspace)的支持,允许在单个代码库中管理多个项目或包。这种工作区支持在单个代码库中同时开发、测试和管理多个相关的项目,而无需使用多个独立的代码仓库。 关系: 这些包管理工具与 monorepo 的关系在于它们可以为 monorepo 提供依赖安装与依赖管理的支持,借助自身对 workspace 的支持,允许在 monorepo 中的不同子项目之间共享依赖项,并提供一种管理这些共享依赖项的方式,这可以简化依赖项管理和构建过程,并提高开发效率。 4.Monorepo (单仓多模块)开发模式
  • 回归单体管理:Monorepo 是一种试图回归单体管理优势的方法,但保留了多仓库开发的某些优点。它允许在一个代码库中管理多个项目、组件或服务,提供更好的代码共享和重用性。

  • 现代工具支持:现代的版本控制系统和工具链使得 Monorepo 开发模式更为可行,例如像 Pnpm、Yarn 、Lerna 和 Turborepo 等工具,它们提供了更好的管理、构建和部署多个项目的能力。

  • 优点:

    • 保留 multirepo 的主要优势

      • 代码复用
      • 模块独立管理
      • 分工明确,业务场景独立
      • 代码耦合度降低
    • 管理所有项目的版本控制更加容易和一致,降低了不同项目之间的版本冲突。

    • 可以统一项目的构建和部署流程,降低了配置和维护多个项目所需的工作量。

  • 缺点:

    • Monorepo 可能随着时间推移变得庞大和复杂,导致构建时间增长和管理困难,git clone、pull 的成本增加。
    • 权限管理问题:项目粒度的权限管理较为困难,容易产生非owner管理者的改动风险。

5.如何解决monorepo无法进行细粒度权限管理的缺点

  1. 使用代码所有权文件 使用如 CODEOWNERS 文件(GitHub 等平台支持)来指定某个目录或文件的所有者。当这些文件或目录被修改时,只有指定的所有者才能批准更改。这种方法能够实现对项目或模块级别的权限粒度控制。
  2. 利用CI/CD流程 在持续集成/持续部署(CI/CD)流程中设置权限和访问控制。例如,可以配置流程,只允许具有特定权限的用户触发构建或部署到生产环境。这种方式可以在流程层面上控制谁可以对代码库进行重要操作。
  3. 分支策略 通过严格的分支管理策略,如Git Flow,控制不同级别的开发人员可以访问和修改的分支。比如只允许项目负责人合并代码到主分支,而其他开发人员只能在特定的功能分支上工作。
  4. 使用Git钩子 配置Git钩子(Hooks),在代码提交或合并前执行脚本来检查提交者的权限。例如,可以设定pre-commit钩子,确保提交的代码符合访问权限要求。
  5. 利用子模块 虽然这种做法在传统Monorepo中较少使用,但通过Git子模块(submodules)可以实现对特定部分的仓库独立控制,从而在需要时提供更细粒度的权限管理。
  6. 第三方工具和扩展 考虑使用一些第三方工具和扩展来管理权限。例如,GitLab和Bitbucket等平台提供了更细粒度的权限控制设置,允许在项目或组织级别进行详细的访问控制。 6.为什么组件库项目会选用 Monorepo 模式 对于组件库项目,很自然的会涉及到划分以下模块

components 包,作为组件库的主要代码,实现各个 UI 组件的核心逻辑。 shared 包,主要存放各种杂七杂八的工具方法。 theme 包,实现组件库的主题样式定制方案。 cli 包,实现组件库模板脚手架的命令行工具。 docs 包,组件库的示例 demo 与使用文档。 playground 包,组件库的在线编辑、演示应用。

细化拆分不同模块的好处非常明显,一句话总结就是:模块划分的越清晰,复用时的灵活性、可操作性就越强,每个独立模块产物的体积也会越轻量。

  • 注意
// .npmrc    pnpm安装依赖时,如果没有特殊指定,它更倾向于使用远程的。确保你的根目录下有个 .npmrc配置文件,其内容是优先使用Workspace中的package
link-workspace-packages = true
prefer-workspace-packages = true
recursive-install = true

如:

// pnpm-workspace.yaml
packages:
 - 'packages/**'
 - 'main'
 - 'common'
 - 'common2'
// 目录结构
- packages
  - vue-demo
  - react-demo
- main
- common
- common2
- package.json
- pnpm-workspace.yaml

导入公共包common 如:

// common:package.json   npm init -y  修改name
{
  "name": "@webxeq/common",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rollup -c"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
}

例如为 main 安装公共包common

pnpm -F main add @webxeq/common

启动子包的程序

pnpm -F './packages/vue-demo' dev
  • 注意
  • 假如 common2 是 pnpm create vite@latest 创建的,需要修改package.json 的version0.0.0 -> 1.0.0,导入子项目包,version不能以0开头
  • 例如为packages下的子项目安装 common
pnpm -F './packages/**' add @webxeq/common
// 必须是绝对路径 
  • 加入react-demo 要引入 vue-demo,也是一样 必须修改子项目的name,方便引入
// react-demo的package.json name: "@webxeq/react-demo"
// vue-demo的package.json name: "@webxeq/vue-demo"

pnpm -F @webxeq/vue-demo add @webxeq/react-demo

只允许pnpm

  • 当在项目中使用 pnpm 时,如果不希望用户使用 yarn 或者 npm 安装依赖,可以将下面的这个 preinstall 脚本添加到工程根目录下的 package.json中:
{
  "scripts": {
    "preinstall": "npx only-allow pnpm"
  }
}

preinstall 脚本会在 install 之前执行,现在,只要有人运行 npm install 或 yarn install,就会调用 only-allow 去限制只允许使用 pnpm 安装依赖。

限制node版本和npm版本

{
  "name": "my-node-project",
  "version": "1.0.0",
  "description": "My Node.js project",
  "engines": {
    "node": ">=12.0.0",
    "pnpm": ">=6.0.0"
  },
}

Release工作流:changesets 发布npm包

  • 个人觉得挺容易上手的
  • 简而言之就是管理包的version和生成changelog

配置changesets

  • install
pnpm install -Dw @changeset/cli
  • 初始化
pnpm changeset init

初始化完成之后会在根目录生成 .changeset,其中的 config.json 作为默认的 changeset 的配置文件。

// 
{
  "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "linked": [["@qftjs/*"]], 
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": [],
  "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
      "onlyUpdatePeerDependentsWhenOutOfRange": true
  }
}

说明如下:

  • changelog: changelog 生成方式
  • commit: 不要让 changeset 在 publish 的时候帮我们做 git add
  • linked: 配置哪些包要共享版本
  • access: 公私有安全设定,内网建议 restricted ,开源使用 public
  • baseBranch: 项目主分支
  • updateInternalDependencies: 确保某包依赖的包发生 upgrade,该包也要发生 version upgrade 的衡量单位(量级)
  • ignore: 不需要变动 version 的包
  • ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH: 在每次 version 变动时一定无理由 patch 抬升依赖他的那些包的版本,防止陷入 major 优先的未更新问题

使用changesets

  1. 编译阶段
build": "pnpm --filter './packages/**' run build",
  1. 执行changeset, 交互变更需集,选择需要发布的版本
"changeset""changeset"
  1. changeset version ,修改发布包的版本
"packages-version": "changeset version",

这里需要注意的是,版本的选择一共有三种类型,分别是 patch、minor 和 major,严格遵循 semver 规范。

这里还有个细节,如果我不想直接发 release 版本,而是想先发一个带 tag 的 prerelease版本呢(比如beta或者rc版本)?

这里提供了两种方式:

  • 手工调整 这种方法最简单粗暴,但是比较容易犯错。

首先需要修改包的版本号:


{
  "name": "monorepo-demo2",
  "version": "1.0.0-bate.1",
 }
 
 然后运行 pnpm changeset publish --tag beta

注意发包的时候不要忘记加上 --tag 参数。

  • 通过 changeset 提供的 Prereleases 模式 利用官方提供的 Prereleases 模式,通过 pre enter 命令进入先进入 pre 模式。

常见的tag如下所示:

名称功能
alpha是内部测试版,一般不向外部发布,会有很多Bug,一般只有测试人员使用
beta也是测试版,这个阶段的版本会一直加入新的功能。在Alpha版之后推出
rcRelease Candidate) 系统平台上就是发行候选版本。RC版不会再加入新的功能了,主要着重于除错
    pnpm changeset pre enter beta

之后在此模式下的 changeset publish 均将默认走 beta 环境,下面在此模式下任意的进行你的开发,举一个例子如下:

    
# 1-1 进行了一些开发...
# 1-2 提交变更集
pnpm changeset
# 1-3 提升版本
pnpm version-packages # changeset version
# 1-4 发包
pnpm release # pnpm build && pnpm changeset publish --registry=...
# 1-5 得到 1.0.0-beta.1

# 2-1 进行了一些开发...
# 2-2 提交变更集
pnpm changeset
# 2-3 提升版本
pnpm version-packages
# 2-4 发包
pnpm release
# 2-5 得到 1.0.0-beta.2

完成版本发布之后,退出 Prereleases 模式:

    pnpm changeset pre exit

构建产物后发版本

  "release": "pnpm build && pnpm publish",
  "publish": "changeset publish --registry=https://registry.npmmirror.com"  

注意: 其中npm镜像要换回原来的镜像,然后也要登录npm, 还需要在npm创建组织