pnpm技术体系之:打造企业级 pnpm 开源组件

pnpm技术体系之:高性能包管理工具.png

开场

pnpm 是 performant npm(高性能的 npm),它是一款快速的,节省磁盘空间的包管理工具,同时,它也较好地支持了 workspace 和 monorepos,简化开发者在多包组件开发下的复杂度和开发流程。

在上一篇《pnpm技术体系之:高性能包管理工具》讲到pnpm的优势,在本章节,我们开始着手搭建一个完整流程的开源组件。

pnpm monorepo搭建

本篇章的全部代码已上传到 github,有需要自取。

1. 初始化项目

1.1. 安装pnpm

代码语言:shell
AI代码解释
复制
npm install pnpm -g

1.2. 初始化package.json

代码语言:shell
AI代码解释
复制
pnpm init

1.3. 配置 .npmrc

此外,我们要额外创建pnpm的配置文件:.npmrc,配置如下:

代码语言:txt
AI代码解释
复制
shamefully-hoist=false detect_chromedriver_version=true strict-peer-dependencies=false

一般教程都是这样配置的:shamefully-hoist=true,但本人不推荐。这样做会把里面的依赖提升到全局node_module里面,有可能出现幽灵依赖的风险。

1.4. 创建工作空间

pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持, 你可以创建一个 workspace 以将多个项目合并到一个仓库中,这样的作用是能在我们开发调试多包时,彼此间的依赖引用更加简单。

创建工作空间也非常简单,假设我们的项目中有3个包:

代码语言:shell
AI代码解释
复制
.└── packages
    ├── playground
    ├── small-color-ui
    └── utils

这时候我们在根目录创建一个 pnpm-workspace.yaml文件,里面添加如下配置,这样在packages范围下的包都能共享工作空间了。

代码语言:yaml
AI代码解释
复制
packages: - 'packages/*'

完事后,假如我们想在small-color-ui包里面使用utils,那直接在small-color-ui终端执行安装命令(安装包名为utilspackage.json文件name字段):

代码语言:shell
AI代码解释
复制
$ cd packages/small-color-ui
$ pnpm i -D @small-color-ui/utils

接下来会看到packages/small-color-ui/package.json中已经包含utils包的依赖了。

image.png

至于utils的版本为workspace:*,是因为pnpm是由workspace管理的,所以有一个前缀workspace可以指向utils下的工作空间从而方便本地调试各个包直接的关联引用,但这种引用会在publish时自动被pnpm纠正为正常版本。你可以在 官网 找到workspace version更多信息。

2. 组件的package.json配置

基础框架搭建好后,我们再看下如何配置组件包的package.json,让其满足我们的开发&&发布需求。例如,我们的主包:packages/small-color-ui/package.json,配置如下:

代码语言:json
AI代码解释
复制
{ 'name': 'small-color-ui', 'private': false, 'version': '1.0.0', 'type': 'module', 'description': 'small-color-ui core', 'license': 'MIT', 'author': 'Johnny', 'contributors': [], 'main': 'src/main.tsx', 'module': 'src/main.tsx', 'publishConfig': { 'main': 'dist/tts-controller.cjs.js', 'module': 'dist/tts-controller.es.js', 'typings': 'dist/src/main.d.ts' }, 'repository': { 'type': 'git', 'url': '[email protected]:JohnnyZhangQiao/pnpm-monorepo-learn.git' }, 'bugs': { 'url': 'https://github.com/JohnnyZhangQiao/pnpm-monorepo-learn/issues' }, 'files': [ 'dist', 'README.md' ], 'keywords': [ 'small-color-ui' ], 'scripts': { 'dev': 'vite', 'build': 'tsc && vite build && pnpm run build:types', 'preview': 'vite preview', 'build:types': 'tsc --p tsconfig.types.json' }, 'dependencies': { '@small-color-ui/utils': 'workspace:*', 'react': '^18.2.0', 'react-dom': '^18.2.0' }, 'devDependencies': { '@types/react': '^18.0.26', '@types/react-dom': '^18.0.9', '@vitejs/plugin-react-swc': '^3.0.0', 'less': '^4.1.3', 'typescript': '^4.9.3', 'vite': '^4.0.0' }}

解析一下关键字段:

  • name:组件名,也是我们要发布到npm上面的名称。假如有子依赖包(如上面的utils包),请注册到同一个组织下面。这时候utils的包名就可以为:@small-color-ui/utils,代表隶属@small-color-ui组织。

  • private:布尔类型,true代表私有包,publish时不会执行发布操作。

  • version:发布版本。

  • type:文件引入规范,module | commonjs,分别代表采用ESModule或commonjs规范来引入文件。

  • mainmodule:定义入口文件,项目在具备ESM 规范情况下,module具备更高的识别优先级。

  • publishConfig:在publish时,里面对应的入口会替换掉外层,一般本地开发时指向src目录,发布后指向dist目录。

  • typings:组件的typescript类型描述,缺失会导致组件被引用时失去类型提示。

  • files:组件作为依赖项时会安装的目录/文件,支持正则匹配,默认会带上4项:package.jsonREADMELICENSE / LICENCE 和 主入口文件。

  • dependencies:打包带上的子依赖。

  • devDependencies:开发环境的子依赖。

3. 关于依赖安装

一般来讲,pnpm对于工作空间的依赖安装分2种,一种是普通安装,另一种是使用-w(--workspace-root)参数,它代表把依赖安装到工作空间中。关于-w的作用,举个例子:

假如你使用以下命令,那么在整个工作空间内的所有组件都能直接使用react

代码语言:shell
AI代码解释
复制
pnpm i -Sw react

但如果你在某个包使用以下命令,那么react只能在这个包内被引用,其他组件不会识别到react依赖。

代码语言:shell
AI代码解释
复制
pnpm i -S react

这里的建议是,假如多包共享的依赖,可以直接安装到工作空间里,特性包则避免使用这参数。

关于-w的更多用法,你可以参考官网说明

4. 生产.d.ts类型描述文件

一般优秀的开源组件,都会在发布时顺便发布一份类型描述文件,这样的作用:一是能友好给使用者方法引入以及参数类型提示;二是能保证组件参数传递规范。

我们要生成对应的类型文件,只需要在tsconfig.json加上以下配置:

代码语言:json
AI代码解释
复制
'compilerOptions': {
  'declaration': true,
  'emitDeclarationOnly': true,}

为了能达到更好的项目配置分离,我们可以把生成类型的配置单独抽离出来,配合extends把通用的tsconfig.json融合进来即可,如下图:

image.png

最后,在package.json增加以下命令,在构建类型文件时指定tsconfig

代码语言:json
AI代码解释
复制
'scripts': { 'build:types': 'tsc --p tsconfig.types.json'},

5. 打包配置

由于本项目用vite来做打包工具,所以主要用到rollup的打包策略,具体vite.config.ts配置如下:

代码语言:typescript
AI代码解释
复制
import { defineConfig } from 'vite';import react from '@vitejs/plugin-react-swc';import type { OutputOptions } from 'rollup';export default defineConfig({
  plugins: [react()],
  build: {
    target: 'modules',
    //打包文件目录
    outDir: 'dist',
    //压缩
    minify: false,
    //css分离
    cssCodeSplit: false,
    // rbga色值禁止转成十六进制
    cssTarget: 'chrome61',
    lib: {
      entry: './src/App.tsx',
      formats: ['es', 'cjs'],
      name: 'small-color-ui-core',
    },
    rollupOptions: {
      // 排除打包的库
      external: ['react', 'react-dom'],
      input: ['./src/App.tsx'],
      output: ['esm', 'cjs'].map((format) => ({
        name: 'small-color-ui',
        format,
        dir: 'dist',
        entryFileNames: `small-color-ui.[format].js`,
        assetFileNames: 'index.css',
        preserveModulesRoot: 'src',
      })) as OutputOptions[],
    },
  },});

6. 发布组件

6.1. npm创建账号与组织

要发布自己的软件包到npm,先要注册一个个人或企业账号,注册入口

另外,假如你包里有子依赖,并隶属一个组织下,还要再添加个组织,一般组织名和你主包名一致。组织创建入口

对于免费开源包,一般选下面Unlimited public packages即可。

image.png

6.2. 发布命令

万事俱备,我们回到项目控制台里面,在发包前先登录npm账号:

代码语言:shell
AI代码解释
复制
# 建议指定registry,避免登录到公司内部的开源库中去pnpm login --registry https://registry./

按部就班输入以下4项,便能登录成功。

image.png

6.3. 组件打包

众所周知,我们发布到npm肯定是构建产物,所以在publish前要对组件执行build操作,在根目录的package.json添加以下命令:

代码语言:json
AI代码解释
复制
'build': 'pnpm build:utils && pnpm build:core','build:core': 'pnpm --filter small-color-ui build','build:utils': 'pnpm --filter @small-color-ui/utils build',

因为有2个发布包,所以要对它们都要构建,其中pnpm --filter <package_name> <command>是pnpm的检索属性,它能执行指定的package目录下的某个命令。上面的 build:corebuild:utils 就是分别执行2个包的构建,再把2条命令整合到 build 中,完成发包前的组件构建流程。

6.4. 自动化发布流和生成发布记录

这里要借用到某个插件——changesets

它是一款切合pnpm体系下的一款管理版本控制和变更日志的工具,专注于多包存储库。虽然pnpm下暂时没有像lerna完善的发布流程工具,但changesets也算的上是官方推荐的一款,将就用吧。

changesets的执行流程大概可以理解为:生成临时的changelog → 消耗changelog生成组件的更新记录,并更新组件version → 发布组件

6.4.1. 安装changeset
代码语言:shell
AI代码解释
复制
pnpm install @changesets/cli
6.4.2. 初始化changeset配置

根目录运行changeset init,会生成一个 .changeset 目录,里面会生成一个 changesetconfig 文件(linked字段改成你自己的包名):

代码语言:json
AI代码解释
复制
{
  '$schema': 'https:///@changesets/[email protected]/schema.json',
  'changelog': '@changesets/cli/changelog',
  'commit': false,
  'fixed': [],
  'linked': [['@small-color-ui/*']],
  'access': 'restricted',
  'baseBranch': 'main',
  'updateInternalDependencies': 'patch',
  'ignore': [],
  '___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH': {
    'onlyUpdatePeerDependentsWhenOutOfRange': true
  }}
6.4.3. 配置changeset发布流命令

然后在根目录的package.json添加以下命令:

代码语言:json
AI代码解释
复制
'changeset': 'changeset','update:version': 'changeset version','release': 'changeset publish',

其中:

  • changeset:生成临时的changelog

  • update:version:消耗changelog生成组件的更新记录,并更新组件version

  • release:发布组件

6.4.4. 生成changeset临时日志

执行命令:pnpm changeset,按提示输出,最后生成临时日志。

2023-01-06 17.34.19.gif
image.png

日志里面包含发版的组件包,版本更新类型(major | minor | patch),最下面带有更新内容。

6.4.5. 消耗日志

执行命令:pnpm update:version,临时日志被消耗,会在组件包生成CHANGELOG.md,另外,package.json的版本号也同步修改。

image.png
6.4.6. 发版

执行命令:pnpm release,更新组件会发布到npm。

image.png

7. eslint与prettier

到上面为止,我们已经完成在pnpm monorepo的完整开发到发布流程,但对于企业开发者来讲,代码仓库的质量也是追求的重要指标之一,我们现在把eslintprettier引入到项目中。

7.1. eslint

根目录安装:

代码语言:shell
AI代码解释
复制
pnpm i -Dw eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react

新建.eslintrc

代码语言:json
AI代码解释
复制
{ 'env': { 'node': true, 'browser': true, 'es2021': true }, 'extends': [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:prettier/recommended', 'plugin:@typescript-eslint/recommended' ], 'parser': '@typescript-eslint/parser', 'parserOptions': { 'ecmaFeatures': { 'jsx': true }, 'ecmaVersion': 'latest', 'sourceType': 'module' }, 'plugins': ['react', '@typescript-eslint', 'prettier'], 'rules': { 'react/react-in-jsx-scope': 'off', 'react/display-name': 0 }}

最后,在根目录的package.json增加以下命令,一键检查代码:

代码语言:json
AI代码解释
复制
'scripts': {
  'lint': 'eslint --fix --ext .js,.tsx,ts packages'},

7.2. prettier

根目录安装:

代码语言:shell
AI代码解释
复制
pnpm i -Dw prettier eslint-config-prettier eslint-plugin-prettier

新建.prettierrc.js

代码语言:javascript
AI代码解释
复制
module.exports = {
  // 一行最多 120 字符..
  printWidth: 120,
  // 使用 2 个空格缩进
  tabWidth: 2,
  // 不使用缩进符,而使用空格
  useTabs: false,
  // 行尾需要有分号
  semi: true,
  // 使用单引号
  singleQuote: true,
  // 对象的 key 仅在必要时用引号
  quoteProps: 'as-needed',
  // 末尾需要有逗号
  trailingComma: 'all',
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // jsx 标签的反尖括号需要换行
  jsxBracketSameLine: false,
  // 箭头函数,只有一个参数的时候,也需要括号
  arrowParens: 'always',
  // 每个文件格式化的范围是文件的全部内容
  rangeStart: 0,
  rangeEnd: Infinity,
  // 不需要写文件开头的 @prettier
  requirePragma: false,
  // 不需要自动在文件开头插入 @prettier
  insertPragma: false,
  // 使用默认的折行标准
  proseWrap: 'preserve',
  // 根据显示样式决定 html 要不要折行
  htmlWhitespaceSensitivity: 'css',
  // 换行符使用 lf
  endOfLine: 'lf',};

8. git规范

8.1. git hooks

众所周知 Git 有很多的钩子函数,让我们在不同的阶段对代码进行不同的操作。我们可以在项目的.git/hooks目录中,找到所有的hooks的例子:

image.png

8.2. 配置代码提交规范

8.2.1. 工具
  • huskygit 钩子捕获

  • lint-staged:暂存区代码检查工具

8.2.2. 安装
代码语言:shell
AI代码解释
复制
pnpm i -Dw husky lint-staged
8.2.3. 初始化husky

添加husky安装命令,执行完后会自动在package.json添加一条script:

代码语言:shell
AI代码解释
复制
npm pkg set scripts.prepare='husky install'

接下来执行prepare命令,完成husky初始化,最终会在项目根路径生成.husky目录。

代码语言:shell
AI代码解释
复制
pnpm prepare'
8.2.4. husky关联lint-staged

上面讲了,lint-staged会检查缓存区代码,但假如需要git hooks触发时执行检查操作,那么就要把lint-staged关联到husky中去了。

关联pre-commit hook

代码语言:shell
AI代码解释
复制
pnpx husky add .husky/pre-commit 'pnpx lint-staged'

完成后.husky目录如下:

image.png
8.2.5. 添加lint-staged检查逻辑

在package.json文件下添加如下代码:

代码语言:json
AI代码解释
复制
'lint-staged': { '*.{js,jsx,ts,tsx}': [ 'prettier --write', 'eslint --fix' ]},

这里在触发代码检查会做两件事:1. 修复缓存区代码风格;2. 修复缓存区代码格式错误;

测试一下,OJBK了。

image.png

8.3. 配置提交message规范

对于提交信息的规范,当然是大名鼎鼎的Google AnguarJS 规范。 格式如下:

代码语言:txt
AI代码解释
复制
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

要完成上面的规范化提交格式,我们需要借用2个工具。

8.3.1. 工具
  • commitlint:commit 信息校验工具

  • commitizen:命令行交互插件

8.3.2. 安装
代码语言:shell
AI代码解释
复制
pnpm i -Dw commitizen cz-conventional-changelog @commitlint/config-conventional @commitlint/cli
8.3.3. 配置commitlint

在根目录创建commitlint.config.js,并写入以下配置:

代码语言:javascript
AI代码解释
复制
module.exports = { extends: ['@commitlint/config-conventional'] };

接下来,我们要在husky配置commit-msg钩子,让提交信息与commitlint关联起来:

代码语言:shell
AI代码解释
复制
pnpx husky add .husky/commit-msg 'npx --no-install commitlint --edit '$1''

最后,在根目录的package.json添加配置:

代码语言:json
AI代码解释
复制
'config': {
  'commitizen': {
    'path': './node_modules/cz-conventional-changelog'
  }},

至此,我们测试下,又OJBK了。。。因为commit信息不规范,所以被husky拦截了。

image.png
8.3.4. 配置commitizen

假如是我们纯粹输入commit message的话,要完全符合规范实属鸡肋,接下来,我们要使用命令交互式流程嵌入到commitlint中。

我们再增加一条script

代码语言:shell
AI代码解释
复制
npm pkg set scripts.commit='cz'

然后运行pnpm commit命令,控制台交互如下:

image.png

10. 单元测试

对于规范的组件开源法则来讲,单测也是重要一环,它能保证组件的稳定性。由于单测是持续性建设的工作,这块日后有空再补齐。👻👻

结尾

好了好了,卷到这里又要和朋友们说拜拜,聪明的小伙伴已经跟着搭建起来了,赶紧去试试吧。。。