为 react-simplikit 做贡献
react-simplikit 的设计鼓励任何人参与贡献。如果你想参与贡献,请遵循下面的指南。
包的范围
react-simplikit 专注于与平台无关的 Hook、组件和工具函数,它们可以在所有 JavaScript 环境(浏览器、服务端、React Native 等)中运行。
在贡献之前,请先确认你的实现属于哪个包:
| 包 | 范围 | 示例 |
|---|---|---|
react-simplikit | 与平台无关的纯状态/逻辑 | useToggle、useAsyncEffect、useLoading |
移动端工具函数(src/mobile) | 解决移动端 Web 特有的问题 | useAvoidKeyboard、useBodyScrollLock、useVisualViewport |
TIP
移动端包并不收录所有依赖浏览器 API 的 Hook。它专门针对移动端 Web 环境中遇到的问题(视口管理、键盘处理、iOS Safari 和 Android Chrome 上的布局问题)。举例来说,快捷键 Hook 虽然用到了浏览器 API,但并不属于移动端包。
贡献实现
贡献实现时,请根据类型(components、hooks 或 utils)把它放到对应的目录下。每个实现都必须包含以下内容:
- 实现
- 测试代码
- JSDoc
TIP
我需要自己写文档吗?
不需要,你不用另外写文档。请改为写详细的 JSDoc 注释,然后运行 yarn docs:gen <name>,它会根据 JSDoc 生成英文文档;请把生成结果和你的 PR 一起提交。翻译由单独维护;在翻译完成之前,该页面会以英文显示并附带提示。
编写实现
你必须遵循 react-simplikit 的设计原则。我们不提供依赖特定库或与 React 生命周期紧密耦合的实现。请按照这些设计原则来编写实现。
编写 JSDoc
所有实现都必须包含 JSDoc 注释。它们在使用实现时提供提示,在生成文档的过程中也起着关键作用。 JSDoc 注释必须包含 @description 和 @example;如果有参数或返回值,还应该包含 @param 和 @returns。
为了准确生成文档,必须遵守 JSDoc 的编写规则。如果 JSDoc 校验失败,CI 也可能失败。
JSDoc 必须用英文编写。
@description:必填标签,用于清楚说明该实现的功能或作用。@example:必填标签,用于展示如何使用该实现的示例代码。@param:写明参数的名称和说明。如果实现带有参数,就必须包含这个标签。必填参数:
@param {<类型>} <参数名> - <参数说明>可选参数:
@param {<类型>} [<参数名>] - <参数说明>对象类型的参数,对象本身和它的属性都需要
@param标签。如果想在说明下面写列表,请用
--代替-。tstype Props = { name: string; age: number; nickname?: string; company: { name: string; address?: string; }; paymentMethod?: { type: 'card' | 'account'; number?: string; }; }; /** * @param {string} name - Name of the user. * @param {number} age - Age of the user. * @param {string} [nickname] - Nickname of the user. * @param {Object} company - Company information of the user. * @param {string} company.name - Name of the company. * @param {string} [company.address] - Address of the company. * @param {Object} [paymentMethod] - Payment information of the user. * @param {string} [paymentMethod.type] - Payment method. * @param {string} [paymentMethod.number] - Card or account number. * -- Card or account number without `-`. * -- If the number is a card number, it should be 15 or 16 digits. */这段 JSDoc 会转换成下面这样的文档。
- namerequired · string
Name of the user.
- agerequired · number
Age of the user.
- nicknamestring
Nickname of the user.
- companyrequired · Object
Company information of the user.
- company.namerequired · string
Name of the company.
- company.addressstring
Address of the company.
- company.namerequired · string
- paymentMethodObject
Payment information of the user.
- paymentMethod.typerequired · string
Payment method.
- paymentMethod.numberstring
Card or account number.
- Card or account number without `-`.
- If the number is a card number, it should be 15 or 16 digits.
- paymentMethod.typerequired · string
- namerequired · string
@returns:写明返回值的名称和说明。如果实现有返回值,就必须包含这个标签。格式:
@returns {<类型>} <返回值说明>如果返回值是对象或元组,请为每个成员写上说明。
如果某个成员需要补充说明,请使用
:。tstype ReturnValue = [Object, () => void]; /** * @returns {[Object, () => void]} A tuple containing: * - obj `Object` - An object containing: * : label `string` - The label of the input. * : value `string` - The value of the input. * - onChange `() => void` - A function to update the value. */这段 JSDoc 会转换成下面这样的文档。
- [value: string, onChange: () => void]
A tuple containing:
- objObject
The value of the input.
: labelstring- The label of the input.
: valuestring- The value of the input. - onChange() => void
A function to update the value.
- objObject
对象类型的返回值也可以用类似的方式书写。
tstype ReturnValue = { value: string; onChange: () => void }; /** * @returns {Object} An object containing: * - value `string` - The value of the input. * - onChange `() => void` - A function to update the value. */这段 JSDoc 会转换成下面这样的文档。
- Object
An object containing:
- valuestring
The value of the input.
- onChange() => void
A function to update the value.
- valuestring
- [value: string, onChange: () => void]
编写测试代码
所有实现都必须包含测试代码,文件名与实现同名。测试覆盖率必须始终达到 100%。可以用下面的命令确认覆盖率:
yarn test:coverage请确认在 SSR 环境中能安全运行
react-simplikit 的所有实现都使用特殊的渲染函数,来验证它们在 SSR 环境中能否安全运行。
组件测试
tsxit('is safe on server side rendering', () => { // renderSSR.serverOnly 是在服务端环境中渲染组件的方法。 // 在这个环境中,useEffect 这类 Hook 不会执行,window、document 这类对象也无法使用,用到它们就会报错。 renderSSR.serverOnly(() => ( <Component> <div>Test Content</div> </Component> )); expect(screen.getByText('Test Content')).toBeInTheDocument(); }); it('should render children correctly', async () => { // renderSSR 是在客户端环境中渲染组件的方法。 // 不过,如果服务端渲染出的 HTML 和客户端渲染出的 HTML 不一致,就会出现 hydration 不匹配的错误。 await renderSSR(() => ( <Component> <div>Test Content</div> </Component> )); expect(screen.getByText('Test Content')).toBeInTheDocument(); }); it('should hydration mismatch error occurred', async () => { // 这段测试代码会因为 hydration 不匹配的错误而失败。 await renderSSR(() => ( <Component> <div>Test Content</div> <div>{Math.random()}</div> </Component> )); expect(screen.getByText('Test Content')).toBeInTheDocument(); });Hook 测试
tsit('is safe on server side rendering', () => { // renderHookSSR.serverOnly 是在服务端环境中渲染 Hook 的方法。 // 在这个环境中,useEffect 这类 Hook 不会执行,window、document 这类对象也无法使用,用到它们就会报错。 const result = renderHookSSR.serverOnly(() => useToggle(true)); const [bool] = result.current; expect(bool).toBe(true); }); it('should initialize with the default value true', async () => { const { result } = await renderHookSSR(() => useToggle(true)); const [bool] = result.current; expect(bool).toBe(true); });
创建 Changeset
当你的代码改动会影响这个包时,就需要创建一个 changeset。Changesets 是一个把版本管理和 changelog 生成自动化的工具。
如何创建 Changeset
- 实现改动之后,运行下面的命令:
yarn changeset选择改动的类型:
patch:修复 bug 或小改动minor:新增功能(保持向后兼容)major:破坏性变更(打破向后兼容)
简要写下这次改动的内容。
TIP
两个包目前都处于 0.0.x 阶段。在这个阶段,大多数改动都应该使用 patch。 如果你不确定该用哪种版本类型,请与维护者讨论。
- 把生成的 changeset 文件和 PR 一起提交。
TIP
Changeset 文件会创建在 .changeset 文件夹中,必须和 PR 一起提交。PR 合并后,版本会自动更新,并生成 changelog。
发布
当改动合并到 main 分支后,发布流程会自动执行:
- PR 合并到
main分支后,GitHub Actions 会运行。 - 如果存在 changeset,系统会自动创建一个更新版本的 PR。
- 更新版本的 PR 合并后,新版本会发布到 npm。
你可以在 GitHub Actions 中查看发布结果。
贡献文档
贡献文档没有特别的条件。如果你发现了错误的信息、质量不佳的翻译,或者有想补充的内容,欢迎随时修改。请从读者的角度出发,把文档写得清晰、简洁。
脚手架
我们提供了一个命令,用来生成贡献所需的最小骨架。用下面的命令可以创建一个带有基本结构的实现文件夹:
yarn run scaffold <name> --type <type>type:实现的类型,必须是component、hook或util之一。name:实现的名称。
示例
yarn run scaffold Button --type component这个命令会在 src/components/Button 文件夹中创建三个文件:
/**
* @description
* <description-here>
*
* @param {<param-type>} <param-name> - <param-description>
* @param {<param-type>} [<param-name>] - <optional-param-description>
*
* @returns {<return-type>} <return-description>
* - <member-description> `<member-name>` - <member-description>
*
* @example
* <example-code>
*/
export function Button() {
// TODO: Implement Button
}import { describe, expect, it } from 'vitest';
import { renderSSR } from '../../_internal/test-utils/renderSSR.tsx';
import { Button } from './Button.tsx';
describe('Button', () => {
it('is safe on server side rendering', async () => {
const result = renderSSR.serverOnly(() => <Button />);
expect(true).toBe(true);
});
it('should work', async () => {
const result = renderSSR.serverOnly(() => <Button />);
expect(true).toBe(true);
});
});export { Button } from './Button.tsx';TIP
你也可以使用这些简写:
yarn run scaffold Button --t c // 创建组件
yarn run scaffold useButton --t h // 创建 Hook
yarn run scaffold getButton --t u // 创建工具函数