likes
comments
collection
share

工欲善其事必先利其器(搭建组件库)

作者站长头像
站长
· 阅读数 73

前言

本篇来讲讲组件库的搭建过程,并搭建一个简单模板集成到上篇的工欲善其事必先利其器(制作CLI)

环境准备

为了方便测试最好先准备本地私仓,私仓搭建可以参考我的这篇:Docker 部署npm私仓 Verdaccio

Monorepo

什么是 Mono-Repo

一言以蔽之:Mono-Repo 就是一仓多项目。与之对应的是 Multi-Repo:一仓一项目。

他们的区别可以参考 Markus Oberlehner 的 Monorepos in the Wild

Mono-Repo 的好处,简单讲,可以统一流程、规范,简化模块间相互引用等

workspaces

选择结合 lerna 来管理 workspaces 也可以,但是 lerna 已不再维护,且其体验也不是很好

现在npmyarnpnpm 都有自己对应的 workspaces 方案,参考文档 npm workspacesyarn workspacespnpm workspaces

这里选择 pnpm,原因有两点:

npm organizations

一些库的名称有@your_org_name/<pkg_name> 结构,如

"devDependencies": {
    "@typescript-eslint/eslint-plugin": "^5.57.0",
    "@typescript-eslint/parser": "^5.57.0"
}

想要这种结构的,需要先在 npm 上创建自己的组织

创建组织

  1. 登录npm 官网
  2. 点击个人头像选择添加组织 工欲善其事必先利其器(搭建组件库)
  3. 根据提示完成组织创建

工欲善其事必先利其器(搭建组件库)

使用

  1. cd 到对应项目 package.json 目录下
  2. 登录 npm
  3. 执行 npm init --scope=<your_org_name>,如 npm init --scope=@moloch
  4. 根据提示输入即可

tsconfig.json

然后需要增加 tsconfig.jsonpaths 配置,如

 "paths": {
    "@moloch/*": ["packages/*"]
  }

Release工作流

Multi-Repo 中,Release工作流大致如下

  1. 进行开发
  2. 提交变更集
  3. 提升版本
  4. 发包

而在 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 里的 componentsutils
{
  "private": true,
  "workspaces": [
    "packages/*"
  ],
}
  • componentsutils 的包命名,假设都属于 @m 这个组织的
# cd 至 components 目录下执行以下命令
npm init --scope=@m
# cd 至 utils 目录下执行以下命令
npm init --scope=@m
  • 安装 vuecomponentsutils 都用到,--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:^"
  }

分别到 componentsutils 目录下执行 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 规范的选择,分别是 patchminor 和 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 工作流

要想根据不同情况针对性处理就需要自定义一套解决机制了,一部分开源项目就是根据具体情况自己实现一套管理机制,如:vue3viteelement-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)篇中实现的CLImoloch-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,这里设置为libexports 指定不同规范引用的文件

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-plusMono-Repo 的多个项目打包到一个项目下统一发布

打包链路

Mono-Repo 项目涉及到打包链路的可能还需要依赖拓扑链路解决方案:turborepo,可参看 Turborepo Documentation

流程守护

在上文的例子中,发布前都没有做任何校验,如果误操作很有可能就把还不该发布的代码发布上去了,为了不出现这种情况,就需要增加流程守护

  • 代码规范 里有规范守护,测试 也可以纳入了提交流程

  • 发布前可以增加 git-release 流程,比如element-ui就有 git-release.sh脚本检查分支是否干净

如果每个版本都想要更好的追踪源码,也可以参考element-uirelease.sh脚本里增加打上 tag 并在发布后自动推送的操作

文档

简单的库只需要在 README.md 编写使用文档即可

一些类库如果严格要求书写 JSDoc 的可以利用 JSDoc 生成文档;如 ts-morph 可以分析 TypeScript 中的 JSDoc,最终生成包含函数名、描述和参数信息的 MarkdownHTML 文档

复杂的组件库文档方案可以考虑 vuepressvitepressstorybook,因为文档都比较齐全,这里就不详细介绍了

变更日志

完善的文档还需要有变更日志,在工欲善其事必先利其器(前端代码规范)已经规范了提交信息,现在只需要利用起来即可

整个 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 用来指定 angularcommit 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 增加 changelogscripts

"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

相关文章

Monorepo基于gulp+rollup打包简析

系列文章

转载自:https://juejin.cn/post/7221409486697709629
评论
请登录