前言
前端开发的过程中,组件库是必不可少的一环,但是在日常的使用中,往往更关注它的api使用,并没有真正的去思考其背后的实现思路以及设计思想。
为了加强自己对前端工程化的了解,从0到1去搭建一个react的组件库,并且在这里记录下整个过程。(如果文章中有错误内容,欢迎大家交流指正。)
相关文档链接
项目说明
项目结构
本项目是基于pnpm包管理器的monorepo结构,通过pnpm的workspace特性来统一管理多个互相关联的项目和模块。
由于在项目中会包含多个模块,例如组件的实现,hooks的封装,以及文档站点搭建、测试的集成,因此选用monorepo结构(monorepo可以简单理解为在一个仓库下去维护多个包的内容)。
项目初始化
- 创建项目文件夹,这里将项目命名为mini-ui。
- cd到mini-ui项目目录中,然后通过
npm init -y命令生成package.json文件
- 创建如下的目录结构。
- 在根目录下,创建一个pnpm-workspace.yaml文件,在文件中声明包的内容。其中*代表目录下的所有文件夹。声明之后,就可以在其它的子包中通过npm包的形式引用。
packages:
- 'packages/*'
- 'apps/*'
技术选型
项目规范
ESlint
eslint主要通常是在开发环境中检查代码书写是否符合规范,因此安装在devDependencies中即可。
- 下载安装。这里直接继承了js和ts的推荐配置,省去自定义配置规则。因此还下载了
@eslint/js以及typescript-eslint。 配置好pacakge.json后,在根目录命令行中通过pnpm i的命令安装依赖。
//package.json
{
...,
"devDependencies": {
"eslint": "9.10.0",
"@eslint/js": "9.10.0",
"typescript-eslint": "8.6.0"
}
}
- 配置文件。由于没有在package.json声明type为mudule,这里选用cjs文件声明配置。在根目录创建eslint.config.cjs文件,通过commonjs的规范导出配置规则。
// eslint.config.cjs
const js = require("@eslint/js")
const tsLint = require("typescript-eslint")
module.exports = tsLint.config({
extends: [
js.configs.recommended,
...tsLint.configs.recommended
],
files: ['**/*.{js, ts, tsx}'],
ignores: ['node_modules/**'],
rules: {
"no-console": "error"
}
})
- 设置脚本。 在package.json中设置执行脚本。
// 追加package.json内容
{
...
"scripts": {
"eslint": "eslint "**/*.{js,ts,tsx}" --fix"
},
}
- 测试。 在@mini-ui/packages/ui目录下,新建一个js文件,可以先行测试配置是否成功。
- 创建index.js
const a = {}
- 在命令行中执行pnpm eslint命令,也就是运行第三步的脚本,查看是否执行完成。
pnpm eslint
- 出现下面的内容, 即说明检测出声明了a变量,但并未使用a变量,证明eslint配置成功。
Stylelint
stylelint是在开发环境中检测样式是否符合规范,这里我们使用less作为组件库的样式方案,因此还需要下载配置postcss-less、stylelint-less等,否则无法识别less语法。
- 下载安装。在package.json的devDependencies字段中添加下面代码块中的内容,然后在根目录命令行中执行
pnpm i命令,更新依赖。
// packages.json
{
...
"devDependencies": {
"stylelint": "16.9.0",
"stylelint-config-standard": "36.0.1",
"postcss-less": "6.0.0",
"stylelint-less": "3.0.1"
}
}
- 配置文件。在根目录创建stylelint.config.cjs文件。
// stylelint.config.cjs
module.exports = {
extends: [
'stylelint-config-standard'
],
customSyntax: "postcss-less",
plugin: ['stylelint-less'],
rules: {
"at-rule-no-unknown": null,
"selector-class-pattern": null
}
}
- 设置脚本。在package.json的脚本命令中追加stylelint命令。
// package.json
{
"scripts": {
...
"stylelint": "stylelint "**/*.{css,less}""
},
}
- 测试。
- 创建index.less
// packages/ui/index.less
@prefix: mini-ui;
.@{prefix} {
background-color: #fff;
.aaa {
}
}
- 在命令行中执行pnpm stylelint命令,也就是运行第三步的脚本,查看是否执行完成。
pnpm stylelint
- 出现下面的内容, 即说明检测出在样式选择器前需要空白行, 不期望有一个空白的规则。符合预期,证明stylelint配置成功。
Husky
husky提供了一系列的git-hooks, 可以在git流程中对不同的步骤进行拦截,在这里主要是通过配置pre-commit,在代码提交之前,运行stylelint和eslint,检测是否存在不合规的代码,在检测中如果抛出error,git流程就会终止。(要先关联远程仓库,这一部分不做示范。)
- 下载安装。在package.json的devDependencies字段中添加下面代码块中的内容,然后在根目录命令行中执行
pnpm i命令,更新依赖。
// packages.json
{
...
"devDependencies": {
...
"husky": "9.1.6"
}
}
- 配置文件, 并运行husky init命令。
- 在package.json中汇总eslint和stylelint的命令,在scripts中新增一个命令
lint,用来执行上面两个的lint规则。
// package.json
{
"scripts": {
...
"lint": "pnpm eslint && pnpm stylelint",
},
}
- husky的配置可以通过命令直接生成,在命令行中运行命令
pnpm exec husky init, 后即可生成对应的.husky目录,在pre-commit中配置pnpm lint。
# .husky/pre-commit
pnpm lint
- 测试。
- 命令行中依次执行
git add .和git commit -m "test", 会出现出下图的报错,即证明husky配置成功,git commit失败。
cz-git & commitlint
commitlint主要是用来对commit message进行规范校验,即对用户的提交信息进行验证,如果提交信息没有按照规定的格式,就会报错,终止commit流程。大家可以根据灵活选择配置cz-git,笔者个人比较喜欢cz-git的emoji方案,因此进行配置。
- 下载安装。在package.json中的devDependencies字段中追加
commitizen、cz-git、commitlint、@commitlint/config-conventional的包。然后运行pnpm i命令安装所有的包内容。
// package.json
{
"devDependencies": {
...,
"commitizen": "4.3.0",
"cz-git": "1.9.4",
"commitlint": "19.5.0",
"@commitlint/config-conventional": "19.5.0"
}
}
- 配置文件。
- 在项目根目录下建立commitlint.config.cjs, 并配置如下内容
// commitlint.config.cjs
// .commitlintrc.js
/** @type {import('cz-git').UserConfig} */
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
// @see: https://commitlint.js.org/#/reference-rules
},
prompt: {
alias: { fd: "docs: fix typos" },
messages: {
type: "Select the type of change that you're committing:",
scope: "Denote the SCOPE of this change (optional):",
customScope: "Denote the SCOPE of this change:",
subject: "Write a SHORT, IMPERATIVE tense description of the change:\n",
body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n',
breaking: 'List any BREAKING CHANGES (optional). Use "|" to break new line:\n',
footerPrefixesSelect: "Select the ISSUES type of changeList by this change (optional):",
customFooterPrefix: "Input ISSUES prefix:",
footer: "List any ISSUES by this change. E.g.: #31, #34:\n",
generatingByAI: 'Generating your AI commit subject...',
generatedSelectByAI: 'Select suitable subject by AI generated:',
confirmCommit: "Are you sure you want to proceed with the commit above?"
},
types: [
{ value: "feat", name: "feat: ✨ A new feature", emoji: ":sparkles:" },
{ value: "fix", name: "fix: 🐛 A bug fix", emoji: ":bug:" },
{ value: "docs", name: "docs: 📝 Documentation only changes", emoji: ":memo:" },
{ value: "style", name: "style: 💄 Changes that do not affect the meaning of the code", emoji: ":lipstick:" },
{ value: "refactor", name: "refactor: ♻️ A code change that neither fixes a bug nor adds a feature", emoji: ":recycle:" },
{ value: "perf", name: "perf: ⚡️ A code change that improves performance", emoji: ":zap:" },
{ value: "test", name: "test: ✅ Adding missing tests or correcting existing tests", emoji: ":white_check_mark:" },
{ value: "build", name: "build: 📦️ Changes that affect the build system or external dependencies", emoji: ":package:" },
{ value: "ci", name: "ci: 🎡 Changes to our CI configuration files and scripts", emoji: ":ferris_wheel:" },
{ value: "chore", name: "chore: 🔨 Other changes that don't modify src or test files", emoji: ":hammer:" },
{ value: "revert", name: "revert: ⏪️ Reverts a previous commit", emoji: ":rewind:" }
],
useEmoji: true,
emojiAlign: "center",
useAI: false,
aiNumber: 1,
themeColorCode: "",
scopes: [],
allowCustomScopes: true,
allowEmptyScopes: true,
customScopesAlign: "bottom",
customScopesAlias: "custom",
emptyScopesAlias: "empty",
upperCaseSubject: false,
markBreakingChangeMode: false,
allowBreakingChanges: ['feat', 'fix'],
breaklineNumber: 100,
breaklineChar: "|",
skipQuestions: [],
issuePrefixes: [{ value: "closed", name: "closed: ISSUES has been processed" }],
customIssuePrefixAlign: "top",
emptyIssuePrefixAlias: "skip",
customIssuePrefixAlias: "custom",
allowCustomIssuePrefix: true,
allowEmptyIssuePrefix: true,
confirmColorize: true,
scopeOverrides: undefined,
defaultBody: "",
defaultIssues: "",
defaultScope: "",
defaultSubject: ""
}
};
- 配置commitlint, 结合husky, 在commit-msg文件下配置执行
npx commitlint --edit $1
- 配置命令。
//package.json
{
"scripts": {
...,
"commit": "git-cz"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
}
- 测试运行结果。
- 执行pnpm commit命令, 就会出现emoji提示内容。
- 如果git commit的命令不遵从规范的话,会被中止。(需要配置rule,这里继承了推荐的配置。)
lint-staged
lint-staged主要是为了在对文件是否符合规范进行校验时,仅对git暂存区的内容进行校验。
- 下载安装。在package.json中的devDependencies字段中追加
lint-staged的包。然后运行pnpm i命令安装所有的包内容。
// package.json
{
...,
"devDependencies": {
...,
"lint-staged": "15.2.10"
}
}
- 配置命令。在package.json中配置lint-staged的校验规则,针对不同的后缀名走不同的校验。
// package.json
{
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix"
],
"*.{css,less}": [
"stylelint --fix"
]
},
}
- 测试运行结果。
- 在packages/ui/index.less文件中,增加不符合stylelint的规则,但是不将该文件存入暂存区,发现不影响commit时的校验。
项目地址
未完待续...
后续补充进开发方案以及打包方案的内容。