在《这里有从零开始构建现代化前端UI组件库所需要的一切(一)》中,我们已经确定了项目的组织策略和构建工具(Monorepo
:pnpm Workspace
&turborepo
),也为组件提供了一个专业的开发环境(Storybook
),接下来我们将为组件库添加代码打包的功能。
打包工具
前端开发中,有很多打包工具可以选择,它们用于将源代码转换、优化,并生成可在浏览器中运行的静态资源。以下是一些常见的前端打包工具:
- Webpack :
- 特点: 强大的模块打包工具,支持各种资源的打包和优化。
- 适用场景: 适用于大型项目,复杂的模块依赖,以及需要处理多种资源类型的应用。具有丰富的插件生态,支持热模块替换(HMR)。
- Rollup :
- 特点: 针对 ES6 模块的打包工具,专注于打包 JavaScript 库,生成更小的代码包。
- 适用场景: 适用于构建库或组件,提供 Tree-shaking 功能,减小输出文件大小。对于发布给其他开发者使用的库,Rollup 是一个不错的选择。
- Gulp :
- 特点: 自动化构建工具,基于任务流的方式执行各种构建任务。
- 适用场景: 适用于多任务、多步骤的构建流程,可集成各类插件。Gulp 提供了灵活的配置和插件系统,使得可以处理各种构建需求,例如文件压缩、图片优化、CSS 合并等。
- esbuild :
- 特点: 极快的 JavaScript 打包器和构建工具,以速度著称。
- 适用场景: 适用于快速构建、打包 JavaScript 项目,支持 TypeScript。esbuild 的快速编译速度使其在开发和构建过程中表现出色。
- tsup :
- 特点: 针对 TypeScript 项目的零配置打包工具,基于 esbuild。
- 适用场景: 适用于简单的 TypeScript 项目,提供简洁的开发体验。tsup 的零配置设计使得 TypeScript 项目的打包变得非常简单,同时结合 esbuild 的性能,具备快速的构建速度。
- Snowpack :
- 特点: 非常快速的构建工具,支持 ESM(ES Modules)直接在浏览器中运行。
- 适用场景: 适用于现代前端开发,以及需要零配置和快速开发的项目。Snowpack 在开发环境中提供了零延迟的模块热替换。
- Parcel :
- 特点: 零配置的打包工具,支持多种资源类型,自动化处理依赖。
- 适用场景: 适用于快速搭建简单项目,无需繁琐的配置。它的零配置特性使得初学者和小型项目能够更快速地上手。
每个工具都有其独特的优势和适用场景。选择合适的打包工具通常取决于项目的规模、技术栈和开发团队的偏好。但是...
那么我们直接打开终端运行pnpm add tsup -D -w
,安装好tsup
之后我们开始为项目加上对应打包的配置:
还是补充一下选择 tsup 的理由吧 😃:
- 零配置设计: tsup 提供了零配置的设计,减少了繁琐的配置过程,使得项目能够更快速上手。这对于简单的 TypeScript 项目而言是一个优势,降低了学习曲线。
- esbuild 驱动: tsup 基于 esbuild,这是一个非常快速的打包工具。这意味着构建速度会很快,使得开发者在修改代码后能够更快地看到变更生效。
- 适用于简单项目: 如果项目相对简单,不需要复杂的配置和多任务构建流程,tsup 提供了一种简便而高效的构建选择。
- 快速迭代: tsup 的快速构建速度支持快速迭代开发,提高了开发效率。这对于需要频繁调整代码的阶段尤为重要。
而且tsup很适合我们的组件库的开发场景。
安装完tsup
之后,我们开始为组件添加对应的打包的配置:
-
首先来到
/packages/components/button/
目录,新建tsup.config.ts
:tsimport { defineConfig } from "tsup"; export default defineConfig({ clean: true, target: "esnext", format: ["cjs", "esm"], // 打包出 Commonjs & ESMoudle 规范的代码 });
这样就可以了,几乎不需要写什么配置,
esBuild
默认启用Tree Shaking
。 -
对于
CSS
,tsup
则支持了PostCSS
插件:-
pnpm add postcss autoprefixer -D -w
因为所有组件都需要用,所以我们将postcss
安装到根项目下。 -
然后
/packages/components/button/
下新建postcss.config.cjs
文件:jsconst config = { plugins: { autoprefixer: {}, }, }; module.exports = config;
-
-
同时我们也为
packages/core/react/
也添加上述的配置,这里就不展开了。 -
然后我们更改一下相关的
packae.json
文件,完善build
命令:packages/components/button/package.json
&packages/core/react/package.json
:json// ... "scripts": { "build": "tsup src --dts" }, // ...
加上
--dts
会自动生成TypeScript
的类型声明文件。根目录下
package.json
:json// ... "scripts": { "dev": "turbo dev", "dev:sb": "turbo dev --filter=@blankui-org/storybook", "build": "turbo build --filter=!@blankui-org/storybook", "lint": "turbo lint" }, // ...
过滤掉
@blankui-org/storybook
。
这时候我们在终端运行pnpm build
,可以看到@blankui-org/button
&@blankui-org/react
的代码已经成功编译并输出到其对应目录下的dist/
文件下了(button
为例):
但是对于esBuild
本身来说,它不会处理样式文件注入到HTML
中,意思就是我们在项目中实际使用这些组件的时候需要手动引入对应的CSS
文件:
esbuild 生成的捆绑 JavaScript 不会自动将生成的 CSS 导入到您的 HTML 页面中。相反,您应该自己将生成的 CSS 与生成的 JavaScript 一起导入到您的 HTML 页面中。这意味着浏览器可以并行下载 CSS 和 JavaScript 文件,这是最有效的方法。
看起来像这样:
ts
import {Button} from "@blankui-org/button"
import "@blankui-org/button/dist/index.css";
我们可以使用sass
、less
、stylus
等CSS
预处理器(需要安装对应的PostCSS插件),但是这些方式在组件的主题开发方面其实还不是那么友好(代码的组织以及扩展方面),所以后续我们会使用CSS-in-JS
方案来替换,这是一些比较好的方案:
大家可以提前去了解一下。
到这里为止的源代码:commit 02a5bc4
自动化测试
自动化测试对于组件库的开发和维护是非常重要的,它可以确保组件在不断迭代中保持稳定、可靠,并防止引入新的 bug。以下是一些常见的JavaScript测试框架:
- Jest:由Facebook开发,全能型选手,具备完善的生态系统。Jest内置了断言库、模拟和覆盖率报告等功能,具有快速且易用的特点。
- Mocha:一个灵活且可扩展的测试框架,适用于浏览器和Node.js环境。Mocha提供了多种测试运行器和支持多种断言库,使其非常灵活。
- Jasmine:一个行为驱动开发(BDD)的测试框架,适用于JavaScript和TypeScript。Jasmine提供了清晰的语法和测试套件的结构。
- QUnit:由jQuery团队开发,专注于简单的单元测试。QUnit适用于测试浏览器中的JavaScript代码。它的语法简单明了,适合初学者。
- AVA:一个具有并行测试运行的JavaScript测试框架,适用于Node.js环境。AVA注重并发和隔离性,能够快速运行测试。
- Tape:一个简单的测试框架,适用于Node.js环境。Tape的设计理念是简洁、小巧,可以轻松与其他工具集成。它产生的输出格式也比较容易解释。
- Cypress:一个端到端测试框架,专注于实际浏览器中的用户行为。Cypress提供了实时查看、自动等待和调试功能,适用于JavaScript项目。
这里选择了 Jest 作为我们组件库的测试框架 😃:
- 全面的功能:Jest 提供了一个全面的测试框架,包括单元测试、集成测试和端到端测试。它内置了断言库、模拟功能和覆盖率报告等功能,使得可以在一个工具中完成多种测试任务。
- React 生态系统支持:如果你的项目中使用了 React,Jest 是一个为 React 应用设计的测试框架,可以很好地集成和支持 React 组件的测试。它还提供了 Enzyme 和 React Testing Library 的集成。
- 易用性和配置:Jest 的配置相对简单,而且默认配置已经足够满足大多数项目的需求。同时,Jest 提供了灵活的配置选项,可以根据项目的特定需求进行定制。
- 快速运行速度:Jest 在并行运行测试用例方面表现出色,因此可以更快地执行测试套件。这对于大型项目或拥有大量测试用例的项目来说是一个重要的优势。
- 自动化 Mocking:Jest 提供了自动化 Mocking 的功能,可以方便地模拟模块、函数等,简化了对依赖关系的测试。
- 强大的断言库:Jest 自带了强大而灵活的断言库,同时也支持使用其他流行的断言库,如 chai。
- 社区支持和文档丰富:Jest 拥有庞大的社区支持,有丰富的文档和示例,使得解决问题和获取支持变得更加容易。
- 持续更新和改进:Jest 是一个活跃维护的项目,定期发布新版本,引入新功能和改进。这意味着你可以始终享受到最新的测试工具和功能。
我们快速的安装一下Jest
相关的包:pnpm add jest jest-environment-jsdom @types/jest @swc/jest whatwg-fetch @testing-library/jest-dom @testing-library/react @testing-library/react-hooks -D -w
,然后为项目添加Jest
的相关配置:
-
项目根目录下创建
jest.config.js
&jest.setup.ts
文件:jsmodule.exports = { testEnvironment: "jsdom", moduleDirectories: ["node_modules"], moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json"], // collectCoverage: true, collectCoverageFrom: [ "packages/**/*.{ts,tsx}", "!packages/storybook/**", "!**/stories/**", ], // React is not defined // https://github.com/swc-project/swc-node/issues/635 transform: { "\\.(ts|tsx|js|jsx)$": [ "@swc/jest", { jsc: { transform: { react: { runtime: "automatic", }, }, }, }, ], }, // transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$"], moduleNameMapper: { "\\.(css|less)$": "identity-obj-proxy", }, setupFilesAfterEnv: ["./jest.setup.ts"], };
使用 ES6 Proxy 来模拟 CSS:
pnpm add identity-obj-proxy -D -w
ts// Polyfill "window.fetch" used in the React component. import "whatwg-fetch"; // Extend Jest "expect" functionality with Testing Library assertions. import "@testing-library/jest-dom";
关于 Jest 框架的更多配置可查看其官方文档:jestjs.io/docs/config...
-
根目录下
package.json
文件中添加test
命令:json// ... "scripts": { "dev": "turbo dev", "dev:sb": "turbo dev --filter=@blankui-org/storybook", "build": "turbo build --filter=!@blankui-org/storybook", "test": "jest --verbose", "lint": "turbo lint" }, // ...
--verbose 层次显示测试套件中每个测试的结果。
-
为
Button
组件添加自动化测试,新建packages/components/button/__tests__/button.test.tsx
文件:tsimport { act, render } from "@testing-library/react"; import { Button } from "../src"; describe("Button", () => { it("should render correctly", () => { const wrapper = render(<Button label="button" />); expect(() => wrapper.unmount()).not.toThrow(); }); it("should trigger onClick function", () => { const onClick = jest.fn(); const { getByRole } = render(<Button label="button" onClick={onClick} />); act(() => { getByRole("button").click(); }); expect(onClick).toHaveBeenCalled(); }); });
最后在终端运行pnpm test
,显示测试通过:
shell
> blankui@1.0.0 test /Users/******/Documents/public/blankui
> jest --verbose
PASS packages/components/button/__tests__/button.test.tsx
Button
✓ should render correctly (9 ms)
✓ should trigger onClick function (23 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.448 s, estimated 1 s
Ran all test suites.
那么我们已经为组件库完成了自动化测试的所有配置,后续只需要为组件完善自动化测试的案例即可。
到这里为止的源代码:commit 00857a0
代码的质量和风格
在项目代码的质量和风格方面我们主要会用到这几个库来保证:
- ESLint :
- 职责:ESLint 主要用于静态代码分析,检测 JavaScript 代码中的潜在问题、错误和不一致之处。它强调代码质量、最佳实践和规范性。
- 特点:
- 提供了丰富的规则集,可以根据项目需求自定义配置。
- 可以集成到开发工具和持续集成流程中,确保代码在提交前或构建阶段进行检查。
- 支持自定义规则和插件,适用于不同的项目需求和框架。
- Prettier :
- 职责:Prettier 主要用于代码格式化,它专注于规范代码的外观,使其具有一致的风格,而不关心代码逻辑。
- 特点:
- 自动格式化代码,提供一致的代码风格。
- 不同于传统的 linters,Prettier 不需要进行复杂的配置,减少了项目中关于代码样式的争议。
- 支持多种语言,包括 JavaScript、CSS、HTML、Markdown 等。
- Husky :
- 职责:Husky 是一个 Git 钩子工具,可以在代码提交、推送等操作前执行预定义的脚本。它通常与 ESLint、Prettier 等工具一起使用,确保代码在提交前通过代码检查和格式化。
- 特点:
- 通过在 Git 钩子中运行脚本,可以在提交前执行代码检查和格式化,防止低质量的代码进入代码仓库。
- 配合 lint-staged 可以只对暂存区中的文件执行 lint 和格式化,提高效率。
通过整合ESLint
、Prettier
和Husky
,可以形成一个强大的代码质量管理和格式化工具链,确保项目代码具有高质量、一致的风格,并在提交前进行严格的代码检查。
那我们就开始将它们集成到项目中来,并且分别添加一些基本的配置规则:
-
ESLint
:-
安装
pnpm add eslint @types/eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-jest eslint-config-prettier eslint-plugin-prettier eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks -D -w
-
配置,根目录下新建
.eslintrc.cjs
&.eslintignore
文件:js/** @type {import("eslint").Linter.Config} */ const config = { $schema: "https://json.schemastore.org/eslintrc.json", env: { browser: true, es2021: true, node: true, }, extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:prettier/recommended", "plugin:react-hooks/recommended", "plugin:jsx-a11y/recommended", ], settings: { react: { version: "detect", }, }, overrides: [ { env: { node: true, }, files: [".eslintrc.{js,cjs}"], parserOptions: { sourceType: "script", }, }, ], parser: "@typescript-eslint/parser", parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: "latest", sourceType: "module", }, plugins: ["@typescript-eslint", "react", "jsx-a11y", "prettier", "jest"], rules: { "no-console": "warn", "prettier/prettier": "warn", "react/react-in-jsx-scope": "off", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/ban-ts-comment": [ "error", { "ts-expect-error": "allow-with-description", }, ], "@typescript-eslint/ban-types": [ "error", { types: { // un-ban a type that's banned by default "{}": false, }, extendDefaults: true, }, ], "@typescript-eslint/no-unused-vars": [ "warn", { args: "after-used", ignoreRestSiblings: false, argsIgnorePattern: "^_.*?$", }, ], "jest/no-disabled-tests": "warn", "jest/no-focused-tests": "error", "jest/no-identical-title": "error", "jest/prefer-to-have-length": "warn", "jest/valid-expect": "error", }, }; module.exports = config;
yml.now/* .next/* *.css .changeset dist esm/* public/* tests/* scripts/* *.config.js .DS_Store node_modules coverage .next build !.storybook /**/.storybook/** !.commitlintrc.cjs !.lintstagedrc.cjs !jest.config.js !plopfile.js !react-shim.js !tsup.config.ts
-
-
Prettier
:-
安装
pnpm add prettier prettier-eslint prettier-eslint-cli -D -w
-
配置,根目录下新建
.prettierrc.cjs
&.prettierignore
文件:js/** @type {import("prettier").Config} */ const config = { $schema: "https://json.schemastore.org/prettierrc.json", tabWidth: 2, semi: true, singleQuote: false, endOfLine: "auto", trailingComma: "all", }; module.exports = config;
ymldist node_modules plop coverage .changeset .next build scripts pnpm-lock.yaml !.storybook !.commitlintrc.cjs !.lintstagedrc.cjs !jest.config.js !tsup.config.ts
-
这时候我们先修改一下根目录下的package.json
文件,添加几个命令:
json
// ...
"scripts": {
"dev": "turbo dev",
"dev:sb": "turbo dev --filter=@blankui-org/storybook",
"build": "turbo build --filter=!@blankui-org/storybook",
"test": "jest --verbose",
"lint": "pnpm lint:packages",
"lint:packages": "eslint packages/**/*.{ts,tsx}",
"lint:packages-fix": "eslint --fix ./packages/**/*.{ts,tsx}",
"format:check": "prettier --check packages/**/**/src --cache",
"format:write": "prettier --write packages/**/**/src --cache"
},
// ...
此时运行pnpm lint:packages
会输出:
yml
> blankui@1.0.0 lint:packages /Users/******/Documents/public/blankui
> eslint packages/**/*.{ts,tsx}
/Users/zhangyifan/Documents/public/blankui/packages/components/button/src/button.tsx
30:3 error 'primary' is missing in props validation react/prop-types
31:3 error 'size' is missing in props validation react/prop-types
32:3 error 'backgroundColor' is missing in props validation react/prop-types
33:3 error 'label' is missing in props validation react/prop-types
43:12 warning Insert `,` prettier/prettier
✖ 5 problems (4 errors, 1 warning)
0 errors and 1 warning potentially fixable with the `--fix` option.
ELIFECYCLE Command failed with exit code 1.
然后我们通过pnpm lint:packages-fix
自动修复:
yml
> blankui@1.0.0 lint:packages-fix /Users/******/Documents/public/blankui
> eslint --fix ./packages/**/*.{ts,tsx}
/Users/zhangyifan/Documents/public/blankui/packages/components/button/src/button.tsx
30:3 error 'primary' is missing in props validation react/prop-types
31:3 error 'size' is missing in props validation react/prop-types
32:3 error 'backgroundColor' is missing in props validation react/prop-types
33:3 error 'label' is missing in props validation react/prop-types
✖ 4 problems (4 errors, 0 warnings)
ELIFECYCLE Command failed with exit code 1.
等等,怎么没有效果...(这里自动修复了最后的那个warn
,button.tsx
第43行多了一个逗号)。其实上面的eslint错误是由于我们使用React.FC<Props>
的形式来声明组件的类型的,这个问题其实由来已久:github.com/jsx-eslint/...,只需要在文件的顶部引入React
就行:import React from "react";
。(其实通过tsconfig.json
的"jsx": "react-jsx"
配置会自动引入的,但是ESLint会抱怨识别不了,只能加回来)。
这时候在运行pnpm lint:packages
,一切都正常了。
-
Husky
:-
我们通过
pnpm dlx husky-init && pnpm install
命令使用husky
快速初始化项目,它会:- 添加
prepare
脚本到package.json
,安装husky
依赖 - 创建一个
pre-commit
可以编辑的示例挂钩(默认情况下,npm test将在提交时运行) - 配置
Git
钩子路径
- 添加
-
使用
husky add
再添加另一个钩子:pnpm exec husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
此时在根目录下会存在
".husky/"
目录,结构如下:lua|-- .husky |-- _ |-- .gitignore |-- husky.sh |-- commit-msg |-- pre-commit
-
因为到目前为止我们都是用
husky
默认提供的配置,接下来我们自定义一下,首先安装lint-staged
和commitlint
这两个库pnpm add lint-staged @commitlint/cli @commitlint/config-conventional commitlint-plugin-function-rules -D -w
-
根目录下分别新建
.lintstagedrc.cjs
和.commitlintrc.cjs
文件:.lintstagedrc.cjs
jsconst { relative } = require("node:path"); const { ESLint } = require("eslint"); const removeIgnoredFiles = async (files) => { const cwd = process.cwd(); const eslint = new ESLint(); const relativePaths = files.map((file) => relative(cwd, file)); const isIgnored = await Promise.all( relativePaths.map((file) => { return eslint.isPathIgnored(file); }), ); const filteredFiles = files.filter((_, i) => !isIgnored[i]); return filteredFiles.join(" "); }; module.exports = { // 提交之前对所有的匹配到的文件进行eslint检查 "**/*.{ts,tsx,js,jsx}": async (files) => { const filesToLint = await removeIgnoredFiles(files); return [`eslint --max-warnings=0 --fix ${filesToLint}`]; }, };
.commitlintrc.cjs
jsconst conventional = require("@commitlint/config-conventional"); module.exports = { extends: ["@commitlint/config-conventional"], plugins: ["commitlint-plugin-function-rules"], helpUrl: "https://github.com/1111mp/blankui", rules: { ...conventional.rules, "type-enum": [ 2, "always", [ "feat", "feature", "fix", "refactor", "docs", "build", "test", "ci", "chore", ], ], "function-rules/header-max-length": [0], }, };
-
修改一下
.husky/pre-commit
和.husky/commit-msg
文件:json#!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" # Avoid excessive outputs if [ -t 2 ]; then exec >/dev/tty 2>&1 fi pnpm lint-staged -c .lintstagedrc.cjs
json#!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" pnpm commitlint --config .commitlintrc.cjs --edit ${1}
-
这时候在代码提交之前就会进行代码的校验了,一切都通过的话代码才能被提交。当然不通过的话我们跟着控制台的报错信息将相关代码一个一个改正即可。
补充一下lint-staged
和commitlint
的相关知识:
- lint-staged :
- 职责:
- lint-staged 是一个在 Git 提交前运行 linters 的工具。它允许你配置只对 Git 暂存区中的文件运行 linters,确保只有相关的文件会受到代码检查和格式化。
- 特点:
- 可以配置在提交前运行指定的 linters。
- 配合 Husky 使用,确保只有通过代码检查和格式化的文件才能被提交。
- 职责:
- commitlint :
- 职责:
- commitlint 用于规范化 commit message,确保团队的提交信息遵循统一的格式和规范。
- 特点:
- 提供了一组规则,用于检查提交信息的格式和内容。
- 可以通过配置规则和使用预设(presets)来满足项目的需求。
- 帮助维护良好的提交历史,方便生成 changelog。
- 职责:
其实这里对于ESLint
、Prettier
和Husky
的一些具体的配置并没有过多介绍,特别是ESLint
(因为它的配置真的太多了...),这里的建议就是结合官方的文档了解一下各自的功能就行,至于那些具体的规则其实自己在写代码的时候如果不符合规范就会一个一个遇到,遇到的时候结合报错再去了解就行。
我们的组件库的功能又完善了一些了 😃。
到这里为止的源代码:commit 7c42771
这一篇文章应该就到此结束了,下一篇我们会实现组件库的统一发布、变更记录等功能...那我们就下一篇文章见~