工欲善其事必先利其器(搭建组件库)
前言
本篇来讲讲组件库的搭建过程,并搭建一个简单模板集成到上篇的工欲善其事必先利其器(制作CLI)中
环境准备
为了方便测试最好先准备本地私仓,私仓搭建可以参考我的这篇:Docker 部署npm私仓 Verdaccio
Monorepo
什么是 Mono-Repo?
一言以蔽之:Mono-Repo 就是一仓多项目。与之对应的是 Multi-Repo:一仓一项目。
他们的区别可以参考 Markus Oberlehner 的 Monorepos in the Wild
Mono-Repo 的好处,简单讲,可以统一流程、规范,简化模块间相互引用等
workspaces
选择结合 lerna 来管理 workspaces
也可以,但是 lerna
已不再维护,且其体验也不是很好
现在npm
、yarn
、pnpm
都有自己对应的 workspaces
方案,参考文档 npm workspaces、yarn workspaces、pnpm workspaces
这里选择 pnpm
,原因有两点:
- 平铺的结构不是 node_modules 的唯一实现方式
pnpm
天生支持Monorepo
也就是workspace
特性,比其他方案要更简便
npm organizations
一些库的名称有@your_org_name/<pkg_name> 结构,如
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0"
}
想要这种结构的,需要先在 npm 上创建自己的组织
创建组织
- 登录npm 官网
- 点击个人头像选择添加组织
- 根据提示完成组织创建
使用
- cd 到对应项目 package.json 目录下
- 登录 npm
- 执行
npm init --scope=<your_org_name>
,如npm init --scope=@moloch
- 根据提示输入即可
tsconfig.json
然后需要增加 tsconfig.json
的 paths
配置,如
"paths": {
"@moloch/*": ["packages/*"]
}
Release工作流
在 Multi-Repo 中,Release工作流大致如下
- 进行开发
- 提交变更集
- 提升版本
- 发包
而在 Mono-Repo 中对包版本管理是一个非常复杂的工作,我们以一个小例子来体验下
例子
初始化一个项目,目录结构如下
├─.npmrc
├─package.json
├─pnpm-workspace.yaml
├─tsconfig.json
├─packages
| ├─utils
| | ├─index.ts
| | └package.json
| ├─components
| | ├─Button.tsx
| | ├─index.ts
| | └package.json
- .npmrc,设置私有源,根据自己私仓实际部署进行配置
registry=http://127.0.0.1/
- pnpm-workspace.yaml 配置
pnpm-workspace
目录
packages:
- 'packages/**'
- package.json,配置以下内容,
private:true
的意思是当前目录标记为私有,禁止发布,因为这里的包只有packages
里的components
和utils
{
"private": true,
"workspaces": [
"packages/*"
],
}
components
和utils
的包命名,假设都属于@m
这个组织的
# cd 至 components 目录下执行以下命令
npm init --scope=@m
# cd 至 utils 目录下执行以下命令
npm init --scope=@m
- 安装
vue
,components
和utils
都用到,--filter
是指定哪个子项目安装
pnpm i vue -D --filter utils
pnpm i vue -D --filter components
还可以指定匹配的包进行相应操作
# 构建 packages 下所有包
pnpm build --filter "./packages/**"
- utils/index.ts
import type { App, Plugin } from 'vue'
export type SFCWithInstall<T> = T & Plugin
export const withInstall = <T extends { name?: string }>(component: T, alias?: string): SFCWithInstall<T> => {
const comp = component as SFCWithInstall<T>
comp.install = (app: App): void => {
if (comp.name) {
app.component(comp.name, comp)
}
if (alias) {
app.config.globalProperties[alias] = component
}
}
return component as SFCWithInstall<T>
}
- Button.tsx
import { withInstall } from '@m/utils'
import { defineComponent } from 'vue'
const Button = defineComponent({
name: 'Button',
setup() {
return (): JSX.Element => {
return <div> Button.tsx </div>
}
}
})
export const _Button = withInstall(Button)
export default _Button
这里引用了 @m/utils
的内容,需要安装 @m/utils
模块
pnpm i @m/utils -r --filter @m/components
此时 components/package.json
增加了 @m/utils
"dependencies": {
"@m/utils": "workspace:^"
}
分别到 components
和 utils
目录下执行 pnpm publish
发布命令,私仓上可以看到对应包已经发布成功了
手动发布,每次都需要人为进行繁琐操作,很容易出错,有没有比较方便的版本控制工具呢?
pnpm
推荐了两个开源工具:
changesets
以 changesets (文档比较清晰)为例
- 安装
pnpm add -wD @changesets/cli
- 初始化
pnpm changeset init
- 配置
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": true,
"linked": [["@m/*"]],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
配置说明:
- changelog: changelog 生成方式,也可用 github 格式,附带 commit link,如
"changelog": ["@changesets/changelog-github", { "repo": "changesets/changesets" }]
- commit:changeset 在 publish 的时候自动 git add
- linked:配置哪些包要共享版本
- access:公有私有安全设定,内网建议 restricted ,开源使用 public
- baseBranch:项目主分支
- ignore:不需要变动 version 的包
- updateInternalDependencies:确保某包依赖的包发生 upgrade,该包也要发生 version upgrade 的衡量单位(量级)
更详细说明请参考changesets config Documentation
配置 changeset 脚本
"scripts": {
"pub": "pnpm changeset & pnpm version-packages & pnpm release",
"changeset": "changeset",
"version-packages": "changeset version",
"release": "changeset publish",
},
changeset
命令,开始交互式收集变更集,这个命令会列出全部的包给你选择要更改发布的包changeset version
命令,修改需要发布的包的版本,默认提供semver 规范的选择,分别是patch
、minor
和major
changeset publish
命令, 发布版本变化了的包- 最后由
pub
脚本来统一调用
prereleases 模式
有时我们不想发布 release 版本,可以使用 prereleases 模式 ,通过pnpm changeset pre enter <tag>
命令进入 pre 模式,操作完后,通过 pnpm changeset pre exit
命令退出该模式
tag 一般有如下几种:
名称 | 功能 |
---|---|
alpha | 内部测试版,一般不向外部发布,会有很多Bug,只有测试人员使用 |
beta | 也是测试版,这个阶段的版本会一直加入新的功能。在Alpha版之后推出 |
rc | 发行候选版,这个版本不会再加入新的功能了,主要着重于除错 |
为方便我们写个简单的脚本,新建 scripts
文件夹,在该文件夹下新建 select-pre.sh
脚本文件,并写入以下内容
#!/bin/sh
PS3='Please enter your choice: '
options=("alpha" "beta" "rc" "Quit")
pre=''
select opt in "${options[@]}"
do
case $opt in
"alpha")
echo "you chose alpha"
pre='alpha'
pnpm changeset pre enter alpha
break
;;
"beta")
echo "you chose beta"
pre='beta'
pnpm changeset pre enter beta
break
;;
"rc")
echo "you chose rc"
pre='rc'
pnpm changeset pre enter rc
break
;;
"Quit")
pnpm changeset pre exit
break
;;
*) echo invalid option;;
esac
done
# 提交变更集
pnpm changeset
# 选择版本
pnpm version-packages
# 发包
pnpm release
if $pre != '' ; then
pnpm changeset pre exit
fi
echo "✅ Publish completed"
package.json
更改 pub
脚本命令为 pnpm scripts/select-pre.sh
"scripts": {
"preinstall": "npx only-allow pnpm",
"pub": "pnpm scripts/select-pre.sh",
"changeset": "changeset",
"version-packages": "changeset version",
"release": "changeset publish"
},
再次执行 pnpm pub
,就可以根据选择进行对应发版流程了,而且每次发版会把收集到的版本日志写入在对应包里的 CHANGELOG.md
通过changesets
我们了解了 Mono-Repo 项目的 Release 工作流,当然 changesets
方案并不能满足所有场景需要,不同项目想要更好的体验需要自定义 Release 工作流
自定义 Release 工作流
要想根据不同情况针对性处理就需要自定义一套解决机制了,一部分开源项目就是根据具体情况自己实现一套管理机制,如:vue3、vite、element-plus 等等
element-plus 自定义 Release 工作流
从 element-plus 来看自定义 Release 工作流
首先 element-plus packages 有如下包
├─packages
| ├─utils
| ├─components
| ├─constants
| ├─directives
| ├─hooks
| ├─locale
| ├─test-utils
| ├─theme-chalk
| ├─element-plus
但是我们使用的时候只关注 element-plus 这个主包,这是因为经过打包,其他内容(包括样式theme-chalk)都打包进 element-plus 进行统一发布了
除了 packages
,在 element-plus internal 也还有一些插件包
在 发布脚本 publish.sh 中可以看到其 Release 工作流
#!/bin/sh
set -e
pnpm i --frozen-lockfile
pnpm update:version
pnpm build
cd dist/element-plus
npm publish
cd -
cd internal/eslint-config
npm publish
cd -
cd internal/metadata
pnpm build
npm publish
cd -
echo "✅ Publish completed"
这个流程实际上和changesets
有一些区别,就是 装依赖 -> 更新版本 -> 打包 -> 发布核心包,其中执行 pnpm build
之后会把内容都集中打包到 dist/element-plus
文件夹下,然后再进入该文件夹进行发布
从这可以看出 Mono-Repo Release 工作流可以理解为是基于 Multi-Repo Release 工作流的拓展,因为无论 Mono-Repo 多复杂,每个单包的流程大体是不变的,Mono-Repo 更多的是在复杂项目中提供更好的组织方式
完善
现在我们知道一个组件库在大的方向上要考虑2点:
- 项目组织方式
- 确定 Release 工作流
这两个方向确定了之后,就可以制定统一的规范,或者拓展流程了
统一代码规范
这里补充一点,我们的发布脚本设置了自动 commit
一条 release
记录
# commit
git add -A
git commit -m "release: $VERSION"
所以 commitlint.config.js
需要增加 release
规则
const typesConfig = require('cz-conventional-changelog-zh/src/types.json');
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', ['release', ...Object.keys(typesConfig)]]
}
}
打包
一些简单的库或许不需要打包,直接发布源码也是可以的,比如在工欲善其事必先利其器(制作CLI)篇中实现的CLI
:moloch-create
模块化规范
打包需要考虑输出符合各种 模块化
规范(AMD, CMD, CommonJS,UMD等)的文件,如果需要支持 typescript
还需要处理 .d.ts
声明文件
我们以 rollup 为打包工具,写一个简单的打包流程,新建公共打包入口 build/build.ts
,为了方便,这里仅处理两种规范
import glob from 'fast-glob'
import { rollup } from 'rollup'
import esbuild from 'rollup-plugin-esbuild'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { nodeResolve } from '@rollup/plugin-node-resolve'
export async function build() {
const modulePath = process.cwd()
const files = await glob('*.{ts,tsx}', {
cwd: modulePath,
absolute: true,
onlyFiles: true
})
const excludes = ['node_modules']
const input = files.filter(path => !excludes.some(exclude => path.includes(exclude)))
const reg = /\\([^\\]+)$/
modulePath.match(reg)
const packageJson = await import(`../packages/${RegExp.$1}/package.json`, {
assert: { type: 'json' }
})
const { dependencies = {} } = packageJson
const bundle = await rollup({
input,
plugins: [
nodeResolve({
extensions: ['.ts', '.tsx']
}),
vueJsx(),
esbuild({
sourceMap: true,
target: 'esnext'
})
],
external: Object.keys(dependencies),
treeshake: true
})
const configs = [
{ format: 'cjs', ext: 'js' },
{ format: 'esm', ext: 'mjs' }
] as const
configs.forEach(config => {
bundle.write({
format: config.format,
exports: config.format === 'cjs' ? 'named' : undefined,
dir: `${modulePath}/lib`,//指定输出到模块下lib文件夹
preserveModules: false,
sourcemap: true,
entryFileNames: `[name].${config.ext}`
})
})
}
build()
由于 node
默认是 Commonjs
规范,这里安装 ts-node
来执行 build.ts
pnpm i -wD ts-node @types/node
package.json
增加以下配置,"type": "module"
表示以ESM
模块运行,scripts
配置打包命令
"type": "module",
"scripts": {
"build": "pnpm --filter=@m/* run build",
}
packages/components/package.json
增加以下配置
"files": [
"lib"
],
"scripts": {
"build": "node --loader ts-node/esm ../../build/build.ts"
},
"exports": {
".": {
"require": "./lib/index.js",
"import": "./lib/index.mjs"
}
},
files
指定发布内容,因为打包指定输出目录为lib
,这里设置为lib
,exports
指定不同规范引用的文件
select-pre.sh
增加打包步骤
# 打包
pnpm build
# 提交变更集
pnpm changeset
# 选择版本
pnpm version-packages
# 发包
pnpm release
提交代码,再次执行 pnpm pub
发布一个版本
当然每个模块下都可以有自己的README.md
,比如components
增加README.md
使用方式
- Installation
$ pnpm i --save @m/components
- Usage
<template>
<Button>click me</Button>
</template>
<script lang="ts" setup>
import { Button } from '@m/components'
</script>
@m/utils
也是类似的处理,这里就不再细说
另外像@m/components
、@m/utils
这些包就应该合在一个包里方便使用,可以参考 element-plus 把 Mono-Repo 的多个项目打包到一个项目下统一发布
打包链路
Mono-Repo 项目涉及到打包链路的可能还需要依赖拓扑链路解决方案:turborepo,可参看 Turborepo Documentation
流程守护
在上文的例子中,发布
前都没有做任何校验,如果误操作很有可能就把还不该发布的代码发布上去了,为了不出现这种情况,就需要增加流程守护
-
在
代码规范
里有规范守护,测试
也可以纳入了提交流程 -
发布前可以增加
git-release
流程,比如element-ui就有 git-release.sh脚本检查分支是否干净
如果每个版本都想要更好的追踪源码,也可以参考element-ui在release.sh脚本里增加打上 tag
并在发布后自动推送的操作
文档
简单的库只需要在 README.md
编写使用文档即可
一些类库如果严格要求书写 JSDoc
的可以利用 JSDoc
生成文档;如 ts-morph
可以分析 TypeScript
中的 JSDoc
,最终生成包含函数名、描述和参数信息的 Markdown
或 HTML
文档
复杂的组件库文档方案可以考虑 vuepress、vitepress、storybook,因为文档都比较齐全,这里就不详细介绍了
变更日志
完善的文档还需要有变更日志,在工欲善其事必先利其器(前端代码规范)已经规范了提交信息,现在只需要利用起来即可
整个 conventional-changelog 生态 有很多,这里只需补充 conventional-changelog-cli 即可
conventional-changelog-cli
可以从git metadata
生成变更日志
- 安装 conventional-changelog-cli
pnpm i -wD conventional-changelog-cli
- 基本使用,全局安装可以不需要
pnpm
pnpm conventional-changelog -p angular -i CHANGELOG.md -s
以上命令中参数 -p angular
用来指定 angular
的 commit message
标准,使用 atom
标准只需把 angular
改为 atom
参数-i CHANGELOG.md
表示从 CHANGELOG.md
读取 changelog
, -s
表示读写 changelog
为同一个文件。
另外这条命令产生的 changelog
是基于上次 tag
版本之后的变更(Feature、Fix、Breaking Changes等)所产生的
一般项目初始化或者你想生成之前所有 commit
信息产生的 changelog
则需要使用这条命令:
pnpm conventional-changelog -p angular -i CHANGELOG.md -s -r 0
其中 -r
表示生成 changelog
所需要使用的 release
版本数量,默认为 1,全部则是 0。
集成到 Release 工作流中
在 package.json
增加 changelog
的 scripts
,
"scripts": {
"changelog": "pnpm conventional-changelog -p angular -i CHANGELOG.md -s -r 0"
},
最后增加 publish.sh
脚本中
# 更新版本
TAG_VERSION=$VERSION pnpm update:version
# changelog
pnpm changelog
其他
Mono-Repo 项目或许会非常复杂,像element-ui 新建一个组件需要改动多个地方,为了减少出错,element-ui
提供 make new
命令执行 new.js 脚本创建组件,new.js
脚本会自动处理相关的文件;其他 make
命令可参考 Makefile
总结
本篇简单讲解了组件库搭建过程需要了解的一些点,最核心的就是确定 项目组织方式 和 Release 工作流,剩下的就是根据项目的实际情况进行调整,本文例子 github: lib-template
相关文章
系列文章
转载自:https://juejin.cn/post/7221409486697709629