开场
pnpm
是 performant npm
(高性能的 npm
),它是一款快速的,节省磁盘空间的包管理工具,同时,它也较好地支持了 workspace
和 monorepos
,简化开发者在多包组件开发下的复杂度和开发流程。
在上一篇《pnpm技术体系之:高性能包管理工具》讲到pnpm的优势,在本章节,我们开始着手搭建一个完整流程的开源组件。
pnpm monorepo搭建
本篇章的全部代码已上传到 github,有需要自取。
1. 初始化项目
1.1. 安装pnpm
npm install pnpm -g 复制代码
1.2. 初始化package.json
pnpm init 复制代码
1.3. 配置 .npmrc
此外,我们要额外创建pnpm的配置文件:.npmrc
,配置如下:
shamefully-hoist=false detect_chromedriver_version=true strict-peer-dependencies=false 复制代码
一般教程都是这样配置的:
shamefully-hoist=true
,但本人不推荐。这样做会把里面的依赖提升到全局node_module
里面,有可能出现幽灵依赖的风险。
1.4. 创建工作空间
pnpm 内置了对单一存储库(也称为多包存储库、多项目存储库或单体存储库)的支持, 你可以创建一个 workspace 以将多个项目合并到一个仓库中,这样的作用是能在我们开发调试多包时,彼此间的依赖引用更加简单。
创建工作空间也非常简单,假设我们的项目中有3个包:
. └── packages ├── playground ├── small-color-ui └── utils 复制代码
这时候我们在根目录创建一个 pnpm-workspace.yaml
文件,里面添加如下配置,这样在packages
范围下的包都能共享工作空间了。
packages: - 'packages/*' 复制代码
完事后,假如我们想在small-color-ui
包里面使用utils
,那直接在small-color-ui
终端执行安装命令(安装包名为utils
的package.json
文件name
字段):
$ cd packages/small-color-ui $ pnpm i -D @small-color-ui/utils 复制代码
接下来会看到packages/small-color-ui/package.json
中已经包含utils包的依赖了。
至于utils的版本为workspace:*
,是因为pnpm是由workspace管理的,所以有一个前缀workspace可以指向utils下的工作空间从而方便本地调试各个包直接的关联引用,但这种引用会在publish时自动被pnpm纠正为正常版本。你可以在 官网 找到workspace version
更多信息。
2. 组件的package.json配置
基础框架搭建好后,我们再看下如何配置组件包的package.json
,让其满足我们的开发&&发布需求。例如,我们的主包:packages/small-color-ui/package.json
,配置如下:
{ "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": "git@github.com: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规范来引入文件。
main
和module
:定义入口文件,项目在具备ESM 规范情况下,module
具备更高的识别优先级。- publishConfig:在publish时,里面对应的入口会替换掉外层,一般本地开发时指向src目录,发布后指向dist目录。
- typings:组件的
typescript
类型描述,缺失会导致组件被引用时失去类型提示。 - files:组件作为依赖项时会安装的目录/文件,支持正则匹配,默认会带上4项:
package.json
、README
、LICENSE
/LICENCE
和 主入口文件。 - dependencies:打包带上的子依赖。
- devDependencies:开发环境的子依赖。
3. 关于依赖安装
一般来讲,pnpm对于工作空间的依赖安装分2种,一种是普通安装,另一种是使用-w
(--workspace-root
)参数,它代表把依赖安装到工作空间中。关于-w
的作用,举个例子:
假如你使用以下命令,那么在整个工作空间内的所有组件都能直接使用react
。
pnpm i -Sw react 复制代码
但如果你在某个包使用以下命令,那么react
只能在这个包内被引用,其他组件不会识别到react
依赖。
pnpm i -S react 复制代码
这里的建议是,假如多包共享的依赖,可以直接安装到工作空间里,特性包则避免使用这参数。
关于-w
的更多用法,你可以参考官网说明。
4. 生产.d.ts
类型描述文件
一般优秀的开源组件,都会在发布时顺便发布一份类型描述文件,这样的作用:一是能友好给使用者方法引入以及参数类型提示;二是能保证组件参数传递规范。
我们要生成对应的类型文件,只需要在tsconfig.json
加上以下配置:
"compilerOptions": { "declaration": true, "emitDeclarationOnly": true, } 复制代码
为了能达到更好的项目配置分离,我们可以把生成类型的配置单独抽离出来,配合extends
把通用的tsconfig.json
融合进来即可,如下图:
最后,在package.json
增加以下命令,在构建类型文件时指定tsconfig
:
"scripts": { "build:types": "tsc --p tsconfig.types.json" }, 复制代码
5. 打包配置
由于本项目用vite
来做打包工具,所以主要用到rollup
的打包策略,具体vite.config.ts
配置如下:
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
即可。
6.2. 发布命令
万事俱备,我们回到项目控制台里面,在发包前先登录npm账号:
# 建议指定registry,避免登录到公司内部的开源库中去 pnpm login --registry https://registry.npmjs.org/ 复制代码
按部就班输入以下4项,便能登录成功。
6.3. 组件打包
众所周知,我们发布到npm肯定是构建产物,所以在publish
前要对组件执行build
操作,在根目录的package.json
添加以下命令:
"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:core
和 build:utils
就是分别执行2个包的构建,再把2条命令整合到 build
中,完成发包前的组件构建流程。
6.4. 自动化发布流和生成发布记录
这里要借用到某个插件——changesets
。
它是一款切合pnpm体系下的一款管理版本控制和变更日志的工具,专注于多包存储库。虽然pnpm下暂时没有像lerna
完善的发布流程工具,但changesets
也算的上是官方推荐的一款,将就用吧。
changesets
的执行流程大概可以理解为:生成临时的changelog → 消耗changelog生成组件的更新记录,并更新组件version → 发布组件
6.4.1. 安装changeset
pnpm install @changesets/cli 复制代码
6.4.2. 初始化changeset配置
根目录运行changeset init
,会生成一个 .changeset
目录,里面会生成一个 changeset
的 config
文件(linked
字段改成你自己的包名):
{ "$schema": "https://unpkg.com/@changesets/config@2.3.0/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
添加以下命令:
"changeset": "changeset", "update:version": "changeset version", "release": "changeset publish", 复制代码
其中:
changeset
:生成临时的changelogupdate:version
:消耗changelog生成组件的更新记录,并更新组件versionrelease
:发布组件
6.4.4. 生成changeset临时日志
执行命令:pnpm changeset
,按提示输出,最后生成临时日志。
日志里面包含发版的组件包,版本更新类型(major | minor | patch
),最下面带有更新内容。
6.4.5. 消耗日志
执行命令:pnpm update:version
,临时日志被消耗,会在组件包生成CHANGELOG.md
,另外,package.json
的版本号也同步修改。
6.4.6. 发版
执行命令:pnpm release
,更新组件会发布到npm。
7. eslint与prettier
到上面为止,我们已经完成在pnpm monorepo
的完整开发到发布流程,但对于企业开发者来讲,代码仓库的质量也是追求的重要指标之一,我们现在把eslint
与prettier
引入到项目中。
7.1. eslint
根目录安装:
pnpm i -Dw eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react 复制代码
新建.eslintrc
:
{ "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
增加以下命令,一键检查代码:
"scripts": { "lint": "eslint --fix --ext .js,.tsx,ts packages" }, 复制代码
7.2. prettier
根目录安装:
pnpm i -Dw prettier eslint-config-prettier eslint-plugin-prettier 复制代码
新建.prettierrc.js
:
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的例子:
8.2. 配置代码提交规范
8.2.1. 工具
- husky:
git
钩子捕获 - lint-staged:暂存区代码检查工具
8.2.2. 安装
pnpm i -Dw husky lint-staged 复制代码
8.2.3. 初始化husky
添加husky安装命令,执行完后会自动在package.json
添加一条script:
npm pkg set scripts.prepare="husky install" 复制代码
接下来执行prepare
命令,完成husky初始化,最终会在项目根路径生成.husky
目录。
pnpm prepare" 复制代码
8.2.4. husky关联lint-staged
上面讲了,lint-staged
会检查缓存区代码,但假如需要git hooks触发时执行检查操作,那么就要把lint-staged
关联到husky
中去了。
关联pre-commit hook
:
pnpx husky add .husky/pre-commit "pnpx lint-staged" 复制代码
完成后.husky
目录如下:
8.2.5. 添加lint-staged检查逻辑
在package.json文件下添加如下代码:
"lint-staged": { "*.{js,jsx,ts,tsx}": [ "prettier --write", "eslint --fix" ] }, 复制代码
这里在触发代码检查会做两件事:1. 修复缓存区代码风格;2. 修复缓存区代码格式错误;
测试一下,OJBK了。
8.3. 配置提交message规范
对于提交信息的规范,当然是大名鼎鼎的Google AnguarJS 规范。 格式如下:
<type>(<scope>): <subject> <BLANK LINE> <body> <BLANK LINE> <footer> 复制代码
要完成上面的规范化提交格式,我们需要借用2个工具。
8.3.1. 工具
- commitlint:commit 信息校验工具
- commitizen:命令行交互插件
8.3.2. 安装
pnpm i -Dw commitizen cz-conventional-changelog @commitlint/config-conventional @commitlint/cli 复制代码
8.3.3. 配置commitlint
在根目录创建commitlint.config.js
,并写入以下配置:
module.exports = { extends: ['@commitlint/config-conventional'] }; 复制代码
接下来,我们要在husky配置commit-msg
钩子,让提交信息与commitlint
关联起来:
pnpx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"' 复制代码
最后,在根目录的package.json
添加配置:
"config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } }, 复制代码
至此,我们测试下,又OJBK了。。。因为commit
信息不规范,所以被husky
拦截了。
8.3.4. 配置commitizen
假如是我们纯粹输入commit message的话,要完全符合规范实属鸡肋,接下来,我们要使用命令交互式流程嵌入到commitlint
中。
我们再增加一条script
:
npm pkg set scripts.commit="cz" 复制代码
然后运行pnpm commit
命令,控制台交互如下:
10. 单元测试
对于规范的组件开源法则来讲,单测也是重要一环,它能保证组件的稳定性。由于单测是持续性建设的工作,这块日后有空再补齐。👻👻
结尾
好了好了,卷到这里又要和朋友们说拜拜,聪明的小伙伴已经跟着搭建起来了,赶紧去试试吧。。。
感谢大家阅览并欢迎纠错,欢迎大家关注本人公众号「似马非马」,一起玩耍起来!🌹🌹