前言
[Component Driven User Interfaces] The development and design practice of building user interfaces with modular components. UIs are built from the “bottom up” starting with basic components then progressively combined to assemble screens.
现代用户界面日趋复杂,组件驱动用户界面的理念开始流行,而其中的核心自然是抽象程度极高的“标准组件”。 本文将与大家共同探讨站在巨人的肩膀上,我们可以如何搭建一个React组件库。
组成要素
一个典型的UI组件库通常包含以下几个要素:
- 组件库源码
- 组件库文档
- 组件库包发布
以下的内容将围绕着我们迫切需要的这几个要素来进行探索。
巨人的肩膀
Component Story Format(CSF)
CSF是一个基于 JavaScript ES6 模块的组件示例的开放标准。这使得开发、测试和设计工具之间能够互操作。
组件已经成为用户界面的主流。在开发、测试、设计和原型制作方面出现了新的面向组件的工具。这些工具可以创建和使用组件和组件示例(又称故事)。但每种工具都有自己的专有格式,因为目前还不存在一种简单的、与平台无关的组件示例表达方式。
Component Story Format(CSF)是一种基于JavaScript ES6模块的开放标准,用于编写组件示例,这些示例也被称作"故事"(stories)。CSF的主要目标是提供一个简单、非专有且声明式的方式来表达组件的不同状态和行为。这种格式允许开发、测试和设计工具之间的互操
总的来说,Component Story Format(CSF)为组件库的开发者提供了一种标准化、模块化且高效的方式来展示组件的不同用法和状态。
Storybook
Storybook是一个开源的前端组件开发环境和文档工具,它允许开发者独立于应用程序构建和测试UI组件。CSF是Storybook推荐的故事编写标准。自从Storybook 5.2版本开始,CSF就成为了编写组件故事的首选方式。
Vite
Vite 是一个现代前端构建工具,专注于提供快速的开发服务器和优化的生产构建。Vite 在开发时利用原生 ES 模块导入(ESM)来提供极速的响应,而在构建时使用 Rollup 或其他打包器来生成最终的生产代码。
Vite 可以作为 Storybook 的构建工具之一。具体来说,Storybook 支持使用 Vite 作为其构建器(builder),这意味着开发者可以利用 Vite 的快速启动和 HMR 功能来构建和开发 Storybook 中的组件。从 Storybook 6.4 版本开始,Vite 被用作 Storybook 的官方构建器之一。
React
React 是一个用于构建用户界面的开源 JavaScript 库,由 Facebook 维护。它允许开发者使用组件化的方式来创建可重用的 UI 组件,并能够高效地更新和渲染应用程序。
搭建步骤
一、ESLint和Prettier配置
首先要进行的是项目初始化,并安装react、typescript依赖
git init
npm init -y
npm install -D react react-dom @types/react typescript
由于我们最终的打包产物是一个js库,而不是一个app项目,所以需要配置package.json中的对等依赖peerDependencies:
{
...
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
...
}
Prettier配置
Prettier 是一个流行的代码格式化工具,用于自动格式化和美化 JavaScript、CSS 和其他语言的代码,以提高代码的可读性和一致性。
安装prettier包:
npm install -D prettier
在项目根路径创建一个.prettierrc配置文件:
{
"printWidth": 80,
"tabWidth": 2
}
配置用于格式化项目代码的快捷指令:
{
...
"scripts": {
"format": "prettier --write --parser typescript '**/*.{ts,tsx}'"
},
...
}
ESLint配置
ESLint 是一个强大的、插件化的 JavaScript 代码质量和代码风格检查工具,用于识别和报告源代码中的错误,同时提供自动修复功能。
安装ESLint包:
npm install -D eslint @typescript-eslint/parser eslint-config-prettier eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-storybook @typescript-eslint/eslint-plugin
在项目根目录创建一个.eslintrc配置文件:
{
// Specify the environments where the code will run
"env": {
"jest": true, // Enable Jest for testing
"browser": true // Enable browser environment
},
// Stop ESLint from searching for configuration in parent folders
"root": true,
// Specify the parser for TypeScript (using @typescript-eslint/parser)
"parser": "@typescript-eslint/parser", // Leverages TS ESTree to lint TypeScript
// Add additional rules and configuration options
"plugins": ["@typescript-eslint"],
// Extend various ESLint configurations and plugins
"extends": [
"eslint:recommended", // ESLint recommended rules
"plugin:react/recommended", // React recommended rules
"plugin:@typescript-eslint/recommended", // TypeScript recommended rules
"plugin:@typescript-eslint/eslint-recommended", // ESLint overrides for TypeScript
"prettier", // Prettier rules
"plugin:prettier/recommended", // Prettier plugin integration
"plugin:react-hooks/recommended", // Recommended rules for React hooks
"plugin:storybook/recommended" // Recommended rules for Storybook
],
"rules": {
"react/react-in-jsx-scope": "off",
}
}
在根目录创建一个.gitignore文件并罗列以下忽略项:
node_modules
dist
#storybook build directory
storybook-static
*storybook.log
结合.eslintrc配置项和.gitignore配置项,设置用于代码风格检测的快捷指令:
{
...
"scripts": {
"lint": "eslint . --ext .ts,.tsx --ignore-path .gitignore --fix"
},
...
}
二、Typescript和Vite 配置
在根目录创建一个用于TypeScript的配置项tsconfig.json:
{
"compilerOptions": {
"target": "ES5", // Specifies the JavaScript version to target when transpiling code.
"useDefineForClassFields": true, // Enables the use of 'define' for class fields.
"lib": ["ES2020", "DOM", "DOM.Iterable"], // Specifies the libraries available for the code.
"module": "ESNext", // Defines the module system to use for code generation.
"skipLibCheck": true, // Skips type checking of declaration files.
/* Bundler mode */
"moduleResolution": "bundler", // Specifies how modules are resolved when bundling.
"allowImportingTsExtensions": true, // Allows importing TypeScript files with extensions.
"resolveJsonModule": true, // Enables importing JSON modules.
"isolatedModules": true, // Ensures each file is treated as a separate module.
"noEmit": true, // Prevents TypeScript from emitting output files.
"jsx": "react-jsx", // Configures JSX support for React.
/* Linting */
"strict": true, // Enables strict type checking.
"noUnusedLocals": true, // Flags unused local variables.
"noUnusedParameters": true, // Flags unused function parameters.
"noFallthroughCasesInSwitch": true, // Requires handling all cases in a switch statement.
"declaration": true, // Generates declaration files for TypeScript.
},
"include": ["src"], // Specifies the directory to include when searching for TypeScript files.
"exclude": [
"src/**/__docs__","src/**/__test__"
]
}
安装vite、插件vite-plugin-dts、预处理器sass、@types/css-modules:
npm install -D vite vite-plugin-dts
vite-plugin-dts 是一个 Vite 插件,用于在 Vite 项目中生成和更新 TypeScript 声明文件(.d.ts 文件)。这个插件特别有助于在构建过程中自动维护类型声明,确保它们与源代码的更新保持同步,从而提高开发效率和类型安全性。
在根目录创建一个配置文件vite.config.ts:
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
import { peerDependencies } from "./package.json";
export default defineConfig({
build: {
lib: {
entry: "./src/index.ts", // Specifies the entry point for building the library.
name: "vite-react-ts-button", // Sets the name of the generated library.
fileName: (format) => `index.${format}.js`, // Generates the output file name based on the format.
formats: ["cjs", "es"], // Specifies the output formats (CommonJS and ES modules).
},
rollupOptions: {
external: [...Object.keys(peerDependencies)], // Defines external dependencies for Rollup bundling.
},
sourcemap: true, // Generates source maps for debugging.
emptyOutDir: true, // Clears the output directory before building.
},
plugins: [dts()], // Uses the 'vite-plugin-dts' plugin for generating TypeScript declaration files (d.ts).
});
修改package.json:
{
...
"type": "module",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"files": [
"/dist"
],
"scripts":{
...
"build": "tsc && vite build",
}
}
三、编写一个组件
样式方案
为了支持sass和css-module以及css-in-js,我们先安装相关模块:
npm install -D sass @types/css-modules styled-components
然后修改vite.config.ts配置项:
export default defineConfig({
...
css:{
// 预处理器的配置,可以为空对象以使用默认配置
preprocessorOptions:{
scss: {
// 额外的 SCSS 配置
}
}
},
...
}
配置别名
配置路径别名有助于减少诸如../../../此类的模块路径引用,首先安装@types/node:
npm install -D @types/node
修改vite.config.ts配置项:
...
import path from 'path';
...
export default defineConfig({
...
resolve: {
alias: {
'@': path.resolve(__dirname,'./src'), // 将 @ 映射到 src 目录
}
},
...
}
修改tsconfig.json配置项:
{
...
"compilerOptions": {
"baseUrl": ".", //基础目录
"paths": {
"@/*": ["./src/*"]
// 其他路径映射...
},
...
}
编写组件
在根目录中创建一个 src 文件夹,然后为我们的按钮组件创建一个名为 button 的文件夹。在此文件夹中添加 button.tsx 和 index.ts 并粘贴以下代码:
// components/button/button.tsx
import React, { MouseEventHandler } from "react";
import styled from "styled-components";
export type ButtonProps = {
text?: string;
primary?: boolean;
disabled?: boolean;
size?: "small" | "medium" | "large";
onClick?: MouseEventHandler<HTMLButtonElement>;
};
const StyledButton = styled.button<ButtonProps>`
border: 0;
line-height: 1;
font-size: 15px;
cursor: pointer;
font-weight: 700;
font-weight: bold;
border-radius: 10px;
display: inline-block;
color: ${(props) => (props.primary ? "#fff" : "#000")};
background-color: ${(props) => (props.primary ? "#FF5655" : "#f4c4c4")};
padding: ${(props) =>
props.size === "small"
? "7px 25px 8px"
: props.size === "medium"
? "9px 30px 11px"
: "14px 30px 16px"};
`;
const Button: React.FC<ButtonProps> = ({
size,
primary,
disabled,
text,
onClick,
...props
}) => {
return (
<StyledButton
type="button"
onClick={onClick}
primary={primary}
disabled={disabled}
size={size}
{...props}
>
{text}
</StyledButton>
);
};
export default Button;
// components/button/index.ts
export { default as Button } from './button';
将index.ts 文件添加到组件文件夹中,因为该文件将允许您从组件文件夹中导出所有组件。
// components/index.ts
export * from './button'; // Add more exports for other components as needed
将 index.ts 文件添加到 src 文件夹,因为它充当整个库的入口点。从这里,您可以导出组件及其类型和实用程序。
// src/index.ts
export * from './components'; // This will export all components from the 'components' folder
四、使用 Vitest 和 React-Testing-Library 进行测试
Vitest是建立在Vite之上的单元测试框架,是一个优秀的单元测试框架,具有许多现代功能。 要安装 Vitest,请运行以下命令:
npm install -D vitest @testing-library/react jsdom @testing-library/jest-dom
修改package.json中的配置项:
"scripts": {
"test": "vitest run",
"test-watch": "vitest",
"test:ui": "vitest --ui"
}
在项目根路径创建setupTests.ts模块并进行如下配置:
import { expect } from "vitest";
import * as matchers from "@testing-library/jest-dom/matchers";
import { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
declare module "vitest" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface Assertion<T = any>
extends jest.Matchers<void, T>,
TestingLibraryMatchers<T, void> {}
}
expect.extend(matchers);
现在,将以下配置添加到defineConfig下的vite.config.ts文件中:
export default defineConfig({
...
test: {
globals: true,
environment: "jsdom",
setupFiles: "./setupTests.ts",
},
...
})
在 button 文件夹中创建 test 目录,并添加名为 Button.test.tsx 的文件,用以下代码测试按钮组件:
//button/__test__/button.test.tsx
import React from "react";
import { describe, expect, it } from "vitest";
import { render, screen } from "@testing-library/react";
import Button from "../button";
describe("Button component", () => {
it("Button should render correctly", () => {
render(<Button />);
const button = screen.getByRole("button");
expect(button).toBeInTheDocument();
});
});
执行如下指令即可完成对组件库的单元测试:
npm run test
五、Storybook和Husky配置
storybook配置
storybook用于支撑组件库的文档+demo站点。 首先安装storybook:
npx storybook@latest init
由于我们已经有了 Vite,它将被检测为 Storybook 中的运行程序,并在 package.json 文件中添加 .storybook 文件夹和所需脚本。 它还会在 src 文件夹中生成一个stories文件夹(我们也可以删掉它)。 我们为每个组件都分配一个 docs 目录,用于在其中添加我们的组件文档和示例。为此,我们必须更新 .stroybook/main.ts 文件中的stories 字段。
stories: ["../src/**/__docs__/*.stories.tsx", "../src/**/__docs__/*.mdx"],
在 src/button/docs 目录下创建三个文件:
import { Canvas, Meta } from "@storybook/blocks";
import Example from "./example.tsx";
import * as Button from "./button.stories.tsx";
<Meta of={Button} title="Button" />
# Button
Button component with different props.
#### Example
<Canvas of={Button.Primary} />
## Usage
```ts
import {Button} from "sld-ui";
const Example = () => {
return (
<Button
size={"small"}
text={"Button"}
onClick={()=> console.log("Clicked")}
primary
/>
);
};
export default Example;
Arguments
- text
() => void- A string that represents the text content of the button. - primary - A boolean indicating whether the button should have a primary styling or not. Typically, a primary button stands out as the main action in a user interface.
- disabled - A boolean indicating whether the button should be disabled or not. When disabled, the button cannot be clicked or interacted with.
- size - A string with one of three possible values: "small," "medium," or "large." It defines the size or dimensions of the button.
- onClick - A function that is called when the button is clicked. It receives a MouseEventHandler for handling the click event on the button element.
```bash
import React, { FC } from "react";
import Button, { ButtonProps } from "../button";
const Example: FC<ButtonProps> = ({
disabled = false,
onClick = () => {},
primary = true,
size = "small",
text = "Button",
}) => {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
>
<Button
size={size}
text={text}
disabled={disabled}
onClick={onClick}
primary={primary}
/>
</div>
);
};
export default Example;
import type { Meta, StoryObj } from "@storybook/react";
import Example from "./example";
const meta: Meta<typeof Example> = {
title: "Button",
component: Example,
};
export default meta;
type Story = StoryObj<typeof Example>;
export const Primary: Story = {
args: {
text: "Button",
primary: true,
disabled: false,
size: "small",
onClick: () => console.log("Button"),
},
};
export const Secondary: Story = {
args: {
text: "Button",
primary: false,
disabled: false,
size: "small",
onClick: () => console.log("Button"),
},
};
然后通过如下指令即可预览storybook站点:
npm run storybook
Husky配置
Husky 是一个流行的 Node.js 工具,用于管理 Git 钩子(Git Hooks)。Git 钩子允许开发者在特定的 Git 事件(如提交、推送等)发生时执行自定义脚本,比如运行测试或格式化代码。 lint-staged 和 ESLint 之间通常是通过工作流程来联系起来的,它们各自在代码质量控制的不同阶段发挥作用,通过 lint-staged 结合 ESLint 的使用,团队可以更加高效地管理和维护代码质量,避免不一致的代码风格和常见的代码问题。
执行如下指令:
npm install -D husky lint-staged
npx husky install
然后在.husky目录下创建一个名为pre-commit的文件作为钩子:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
同时在package.json中加入如下配置:
{
...
"lint-staged": {
"*.{ts,tsx}": [
"npm run format",
"npm run lint",
"npm run test"
]
}
...
}
如此一来,当我们执行git commit的时候,就会在暂存文件上运行我们指定的格式化和 linting 脚本。
六、组件库包发布
打包
修改vite.config.ts相关配置
export default defineConfig({
...
build: {
lib: {
entry: "./src/index.ts", // Specifies the entry point for building the library.
name: "my-react-component-lib", // Sets the name of the generated library.
fileName: (format) => `index.${format}.js`, // Generates the output file name based on the format.
formats: ["cjs", "es", "umd"], // Specifies the output formats (CommonJS and ES modules).
},
rollupOptions: {
external: [...Object.keys(peerDependencies)], // Defines external dependencies for Rollup bundling.
output: {
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
globals: {
react: "React"
}
}
},
sourcemap: true, // Generates source maps for debugging.
emptyOutDir: true, // Clears the output directory before building.
},
...
}
修改package.json如下:
{
"name": "my-react-component-lib",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"files": [
"/dist",
"README.md"
],
"scripts": {
...
"build": "tsc && vite build",
...
}
...
}
执行如下代码即可完成打包:
npm run build
打包产物如下:
发布
执行如下指令即可完成npm发布
npm publish
七、组件库包使用
在react项目中安装组件库包:
npm install -S my-react-component-lib
在react项目中使用组件库:
import {Button} from 'my-react-component-lib';
const Example = () => {
return (
<Button primary/>
);
}
export default Example;
Refs
Component Driven User Interfaces ComponentDriven / csf Building a Component Library with React, Typescript, and Storybook: A Comprehensive Guide