React+TypeScript 组件库开发全攻略:集成Storybook可视化与Jest测试,一键发布至npm
平时我除了业务需求,偶尔会投入到UI组件的开发中,大多数时候只会负责自己业务场景相关或者一小部分公共组件,极少有从创建项目、集成可视化、测试到发布的整个过程的操作,这篇文章就是记录组件开发全流程,UI组件在此仅作为调试用,重点在于集成项目环境。
组件
我们使用 React + TypeScript 来开发UI组件库,为了简化 webpack 环境和 Typescript 环境配置,这里直接使用 create-react-app
通过如下命令来创建一个新项目。
npx create-react-app 项目名称 --template typescript
创建项目后先将无用文件删除,在 scr/components/Button/index.tsx 下定义一个简单的 Button 组件。
import React, { FC, ReactNode } from "react";
import cn from "classnames";
import "./index.scss";
interface BaseButtonProps {
className?: string;
size?: "small" | "middle" | "large";
disabled?: boolean;
children?: ReactNode;
}
type ButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLElement>;
const Index: FC<ButtonProps> = (props) => {
const {
size = "middle",
children = "按钮",
className,
disabled,
...restProps
} = props;
return (
<button
className={cn("btn", `btn-${size}`, className, { disabled })}
{...restProps}
>
{children}
</button>
);
};
export default Index;
使用 scss 对其进行样式编写,这里需要注意,create-react-app 中没有自动支持 scss 文件,如果需要使用需要手动安装 sass
资源来处理。
.btn {
background-color: #fff;
border: 1px solid #d9d9d9;
color: rgba(0, 0, 0, 0.88);
line-height: 1.5;
&.disabled {
cursor: not-allowed;
color: rgba(0, 0, 0, 0.25);
background-color: rgba(0, 0, 0, 0.04);
}
}
.btn-large {
font-size: 16px;
height: 40px;
padding: 7px 15px;
border-radius: 8px;
}
.btn-middle {
font-size: 14px;
height: 32px;
padding: 4px 15px;
border-radius: 6px
}
.btn-small {
font-size: 14px;
height: 24px;
padding: 0 7px;
border-radius: 4px
}
在 App.tsx 文件中引入组件并测试,验证其功能是否可用。
import React from "react";
import Button from "./components/Button";
import "./app.scss";
function App() {
return (
<div className="container">
<Button
size="large"
onClick={() => {
console.log("我是一个大按钮");
}}
>
大按钮
</Button>
<Button disabled>中等按钮</Button>
<Button size="small">小按钮</Button>
</div>
);
}
export default App;
在浏览器中可以看到组件效果,以及点击【大按钮】会触发对应的事件
Jest测试
功能简单自测后,我们需要编写测试用例来对组件进行测试,一方面是为了提高代码质量减少bug,另一方面在后期的维护升级或者重构中,只需要执行自动化脚本,便可以确认是否兼容历史版本。
这里选用 jest
和 testing-library
,我们通过 create-react-app 创建的 react 脚手架已经集成了单元测试的能力。
首先查看项目中的 setupTests.ts 有如下 Jest 断言增强的导入语句
import '@testing-library/jest-dom/extend-expect';
然后在 Button 文件夹下增加 index.test.tsx 文件编写测试用例,先判断是否成功渲染一个 Button 组件。
import React from 'react';
import { render, screen } from "@testing-library/react";
import Button from ".";
describe("Button组件", () => {
it("默认Button", () => {
render(<Button>查询</Button>); // 渲染一个名为查询的按钮
const element = screen.getByText("查询");
expect(element).toBeInTheDocument(); // 判断按钮是否在页面上
});
});
脚手架 package.json 中已经添加了 test 指令的配置,我们在执行单元测试的时候只需要执行 npm run test,等待几秒便可以看到单元测试的执行结果
当我们将 const element = screen.getByText("查询");
中的字符串 查询 改为 上传 时,会立马给出错误信息。
另外,还可以进一步的对渲染的组件进行测试,如判断 tagName、判断类名、是否是 disabled 状态、点击事件是否执行。
import React from 'react';
import { fireEvent, render, screen } from "@testing-library/react";
import Button from ".";
const defaultProps = {
onClick: jest.fn()
}
describe("Button组件", () => {
it("默认Button", () => {
render(<Button {...defaultProps}>查询</Button>);
const element = screen.getByText("查询");
expect(element).toBeInTheDocument();
expect(element.tagName).toEqual('BUTTON');
expect(element.disabled).toBeFalsy();
expect(element).toHaveClass('btn btn-middle');
fireEvent.click(element)
expect(defaultProps.onClick).toHaveBeenCalled()
});
});
为组件增加新属性或者新特性时,记得为其在原来测试用例的基础上新增用例并执行,这样能在保证兼容历史功能的基础上验证新功能。
Storybook
通过项目入口文件 app.tsx 引入 Button 组件可以在本地服务运行的页面中预览效果,但随着组件的开发,我们需要不断 import 新的组件注释旧的引用来调试。
这样开发起来非常的繁琐,我们期望能有一个地方可以根据对组件进行分类,并且随时预览组件的效果,最好还能展示所有的组件配置,能根据选择配置展示组件效果。
针对以上的诉求,Storybook 就是一个非常好的解决方案。
使用 npm install storybook -d
安装,并通过 npx sb init
初始化,此时 storybook 会开启个一本地端口来展示默认生成的 stories 文件夹内案例。
我们在开发组件的 Button 文件夹下新增 index.stories.tsx 文件,来编写我们自己组件描述,一开始如果不知道如何定义,可以直接在案例组件的基础上进行修改。
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import Button from ".";
const meta = {
title: "Example/iceButton", // 用于展示组件的目录
component: Button,
tags: ["autodocs"], // 是否存在 Docs 页面
args: { onClick: fn() },
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
children: "按钮", // 配置自己组件的属性
},
};
这样我们自己编写的组件就加入到了页面中,右下方 Control 中是我们为组件添加的
interface BaseButtonProps,storybook 会自动将这部分填充进来,并且部分属性还可以直接在页面上修改并预览效果。
每新增一个组件,我们为其添加一个对应的 stories 文件,不仅便于预览功能,同时还能为提供详细的配置说明,直接省去编写文档的时间。
编译
在进行发布之前,我们还需要做一些配置,首先是修改入口文件,原本在 React 项目中,index.jsx 文件是找到页面中 id 为 root 的根元素并渲染组件的,现在要修改成将所有定义的组件导出,开发者者通过 import 就能导入使用。
export { default as Button} from "./components/Button";
然后我们使用 typecript 工具来对项目进行编译,react 项目初始化了 tsconfig.json,这个文件和开发相关,编译的配置需要我们自定义 tsconfig.build.json 文件。
{
"compilerOptions": {
// 输出文件夹
"outDir": "dist",
// 是 esmodule 的形式,还可以选 amd、cmd
"module": "esnext",
// 输出的ES版本,ES3-ESNext
"target": "ES5",
// typescript 使用库的时候,可以获取类型提示,在 .d.ts 文件,所以这个文件也要导出
"declaration": true,
// jsx 是 React.createElement 的语法糖,可选 preserve | react | react-native,编译出来的文件使用 React.createElement 代替 jsx 语法
"jsx": "react",
// 加载资源的方案,有classic 和 node 两种,classic 对应的是相对路径的方案,从当前路径一直往上找到 root。但是 node 是去 node_modules 中查找
"moduleResolution": "node",
// 支持默认导出的方式,不定义时只支持 import * as React from 'react'
"allowSyntheticDefaultImports": true
},
// 编译src下的文件
"include": ["src"],
// 排除 src 无需编译的文件
"exclude": ["src/**/*.test.tsx", "src/**/*.stories.tsx", "src/setupTests.ts"]
}
可以看到常用的组件库,样式资源都是单独加载的,比如 antd 👉 import 'antd/dist/antd.css'
,element 👉 npm install element-theme-default
,我们项目也按照这种方式来做一些调整。
去除各组件 scss 文件的引入,统一收口在 src 下的 index.scss 文件中,如 @import './components/Button/index.scss';
,然后为以上修改在 package.json 中添加指令。
"scripts": {
"build-ts": "tsc -p tsconfig.build.json",
"build-css": "sass ./src/index.scss ./dist/index.css --no-source-map",
"build": "npm run build-css && npm run build-ts"
}
执行 npm run build
后生成如下文件
发布npm
在发布 npm 之前,我们需要确保用户通过 npm 下载的组件资源是可用的,在本地通过 npm link
先验证一下功能。
我的UI组件项目名称为 ice-ts-app
,对它执行 npm link
,测试项目执行 npm link ice-ts-app
,并引入测试代码。
import { Button } from 'ice-ts-app';
import 'ice-ts-app/dist/index.css';
运行测试项目,如果组件及其功能生效则代表验证成功。
验证完成后,还需要对 package.json 的配置做一些调整,包含项目的入口文件 dist/inde.js,TypeScript 类型定义文件 dist/index.d.ts,发布到 npm 的文件夹 dist ,调整 dependencies 和 devDependencies 的依赖,将 react 和 react-dom 迁移至 peerDependencies 中。
{
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
},
}
还有一些通用的属性,包括 description、license、author、homepage 等等,开发者按需配置。
另外每次执行 npm run build 都需要手动删除 dist 文件夹,这里可以安装并使用 rimraf
自动删除,同时再增加一条 script 指令,用于发布前执行。安装: npm install rimraf --save
"script": {
"clean": "rimraf ./dist",
"build": "npm run clean && npm run build-css && npm run build-ts",
"prepublishOnly": "npm run build"
}
发布之前先在 npm 仓库 上登录,然后执行 npm publish
,可以看到发布日志中有我们提交的文件名称、文件大小,版本号等信息。
接着我们将用于测试的项目执行 npm unlink ice-ts-app
来解除本地的绑定,并通过 npm install app-ts-app
安装并验证刚刚发布到 npm 仓库的资源,如果组件能够正常使用就代表成功啦~
完整代码
以上便是 React + TypeScript 组件开发、测试、可视化及发布解析,完整代码我放在了 github 上,戳 ice-ts-app 可查看,欢迎大家点个 star~