如何开发一个人人爱的组件?
阿里妹导读
本篇文章类似一个菜谱,比较零碎的记录一些组件设计的内容,作者分别按照 1~5 星 区分其重要性。(后台回复大数据即可获得《大数据&AI实战派》电子书)
意识
1.我应该注重 TypeScript API 定义,好用的组件API都应该看上去 理所应当 且 绝不多余。
2.我应该注重 README 和 Mock ,一个没有文档的组件 = 没有,最好不要使用 link 模式去开发组件。
3.我不应引入任何副作用依赖,比如全局状态(Vuex、Redux),除非他们能自我收敛。
接口设计
type Size = any; // 😖 ❌type Size = string; // 🤷🏻♀️type Size = "small" | "medium" | "large"; // ✅
DOM属性(⭐️⭐️⭐️⭐️⭐️)
export interface IProps {className?: string;style?: React.CSSProperties;}
export type CommonDomProps = {className?: string;style?: React.CSSProperties;} & Record<`data-${string}`, string>// component.tsxexport interface IComponentProps extends CommonDomProps {...}// orexport type IComponentProps = CommonDomProps & {...}
类型注释(⭐️⭐️⭐️)
export type IListProps{/*** Custom suffix element.* Used to append element after list*/suffix?: React.ReactNode;/*** List column definition.* This makes List acts like a Table, header depends on this property* @default []*/columns?: IColumn[];/*** List dataSource.* Used with renderRow* @default []*/dataSource?: Array<Record<string, any>>;}
上面的类型注释就是一个规范的类型注释,清晰的类型注释可以让消费者,直接点击进入你的类型定义中查看到关于这个参数的清晰解释。
toolbar?: React.ReactNode; // List toolbar.// 👇🏻 Columns// defaultValue is "[]"columns?: IColumns[];
组件插槽(⭐️⭐️⭐️)
export type IInputProps = {label?: string; // ❌}export type IInputProps = {label?: React.ReactNode; // ✅}
遇到这种类型时,需要意识到我们其实是在提供一个 React 插槽类型,如果在组件消费中仅仅是让他展示出来,而不做其他处理的话,就应当使用 ReactNode 类型作为类型定义。
受控 与 非受控(⭐️⭐️⭐️⭐️⭐️)
如果要封装的组件类型是 数据输入 的用途,也就是存在双向绑定的组件。请务必提供以下类型定义:
export type IFormProps<T = string> = {value?: T;defaultValue?: T;onChange?: (value: T, ...args) => void;};
并且,这类接口定义不一定是针对 value, 其实对于所有有 受控需求 的组件都需要,比如:
export type IVisibleProps = {/*** The visible state of the component.* If you want to control the visible state of the component, you can use this property.* @default false*/visible?: boolean;/*** The default visible state of the component.* If you want to set the default visible state of the component, you can use this property.* The component will be controlled by the visible property if it is set.* @default false*/defaultVisible?: boolean;/*** Callback when the visible state of the component changes.*/onVisibleChange?: (visible: boolean, ...args) => void;};
消费方式推荐使用:ahooks useControllableValue[7]
表单类常用属性(⭐️⭐️⭐️⭐️)
如果你正在封装一个表单类型的组件,未来可能会配合 antd[8]/ fusion[9]等 Form 组件来消费,以下这些类型定义你可能会需要到:
export type IFormProps = {/*** Field name*/name?: string;/*** Field label*/label?: ReactNode;/*** The status of the field*/state?: 'loading' | 'success' | 'error' | 'warning';/*** Whether the field is disabled* @default false*/disabled?: boolean;/*** Size of the field*/size?: 'small' | 'medium' | 'large';/*** The min value of the field*/min?: number;/*** The max value of the field*/max?: number;};
选择类型(⭐️⭐️⭐️⭐️)
如果你正在开发一个需要选择的组件,可能以下类型你会用到:
export interface ISelection<T extends object = Record<string, any>> {/*** The mode of selection* @default 'multiple'*/mode?: 'single' | 'multiple';/*** The selected keys*/selectedRowKeys?: string[];/*** The default selected keys*/defaultSelectedRowKeys?: string[];/*** Max count of selected keys*/maxSelection?: number;/*** Whether take a snapshot of the selected records* If true, the selected records will be stored in the state*/keepSelected?: boolean;/*** You can get the selected records by this function*/getProps?: (record: T, index: number) => { disabled?: boolean; [key: string]: any };/*** The callback when the selected keys changed*/onChange?: (selectedRowKeys: string[], records?: Array<T>, ...args: any[]) => void;/*** The callback when the selected records changed* The difference between `onChange` is that this function will return the single record*/onSelect?: (selected: boolean, record: T, records: Array<T>, ...args: any[]) => void;/*** The callback when the selected all records*/onSelectAll?: (selected: boolean, keys: string[], records: Array<T>, ...args: any[]) => void;}
另外,单选与多选存在时,组件的 value 可能会需要根据下传的 mode 自动变化数据类型。
比如,在 Select 组件中就会有以下区别:
mode="single" -> value: string | numbermode="multiple" -> value: string[] | number[]
对于这类场景可以使用 Merlion UI - useCheckControllableValue[11]进行抹平。
组件设计
服务请求(⭐️⭐️⭐️⭐️⭐️)
这是一个在业务组件设计中经常会遇到的组件设计,对于很多场景来说,或许我们只是需要替换一下请求的 url ,于是便有了类似下面这样的API设计:
export type IAsyncProps {requestUrl?: string;extParams?: any;}
后面接入方增多后,出现了后端的 API 结果不符合组件解析逻辑,或者出现了需要请求多个API组合后才能得到组件所需的数据,于是一个简单的请求就出现了以下这些参数:
export type IAsyncProps {requestUrl?: string;extParams?: any;beforeUpload?: (res: any) => anyformat?: (res: any) => any}
对于异步接口的API设计最佳实践应该是:提供一个 Promise 方法,并且详细定义其入参、出参类型。
export type ProductList = {total: number;list: Array<{id: string;name: string;image: string;...}>}export type AsyncGetProductList = (pageInfo: { current: number; pageSize: number },searchParams: { name: string; id: string; },) => Promise<ProductList>;export type IComponentProps = {/*** The service to get product list*/loadProduct?: AsyncGetProductList;}
通过这样的参数定义后,对外只暴露了 1 个参数,该参数类型为一个 async 的方法。开发者需要下传一个符合上述入参和出参类型定义的函数。
在使用时组件内部并不关心请求是如何发生的,使用什么方式在请求,组件只关系返回的结果是符合类型定义的即可。
这对于使用组件的开发者来说是完全白盒的,可以清晰的看到需要下传什么,以及友好的错误提示等等。
Hooks(⭐️⭐️⭐️⭐️⭐️)
很多时候,或许你不需要组件!
对于很多业务组件来说,很多情况我们只是在原有的组件基础上封装一层浅浅的业务服务特性,比如:
Lazada Uploader:Upload + Lazada Upload Service
Address Selector: Select + Address Service
Brand Selector: Select + Brand Service
...
实际上,对于这类对服务逻辑的状态封装,更好的办法是将其封装为 React Hooks ,比如上传:
export function Page() {const lzdUploadProps = useLzdUpload({ bu: 'seller' });return <Upload {...lzdUploadProps} />}
Consumer(⭐️⭐️⭐️)
对于插槽中需要使用到组件上下文的情况,我们可以考虑使用 Consumer 的设计进行组件入参设计。
比如 Expand 这个组件,就是为了让部分内容在收起时不展示。
对于这种类型的组件,明显容器内的内容需要拿到 isExpand 这个关键属性,从而决定索要渲染的内容,所以我们在组件设计时,可以考虑将其设计成可接受一个回调函数的插槽:
export type IExpandProps = {children?: (ctx: { isExpand: boolean }) => React.ReactNode;}
export function Page() {return (<Expand>{({ isExpand }) => {return isExpand ? <Table /> : <AnotherTable />;}}</Expand>);}
文档设计
package.json(⭐️⭐️⭐️⭐️⭐️)
请确保你的 repository 是正确的仓库地址,因为这里的配置是很多平台溯源的唯一途径,比如: npmjs.com\npm.alibaba-inc.com\mc.lazada.com
请确保 package.json 中存在常见的入口定义,比如 main\module\types\exports,以下是一个 package.json 的示范:
{"name": "xxx-ui","version": "1.0.0","description": "Out-of-box UI solution for enterprise applications from B-side.","author": "[email protected]","exports": {".": {"import": "./dist/esm/index.js","require": "./dist/cjs/index.js"}},"main": "./dist/cjs/index.js","module": "./dist/esm/index.js","types": "./dist/cjs/index.d.ts","repository": {"type": "git","url": "[email protected]:yee94/xxx.git"}}
README.md(⭐️⭐️⭐️⭐️)
如果你在做一个库,并希望有人来使用它,请至少为你的库提供一段描述,在我们的脚手架模板中已经为你生成了一份模板,并且会在编译过程中自动加入在线 DEMO 地址,但如果可以请至少为它添加一段描述。
这里的办法有很多,如果你实在不知道该如何写,可以找一些知名的开源库来参考,比如 `antd` \ `react` \ `vue` 等。
还有一个办法,或许你可以寻求 `ChatGPT` 的帮助,屡试不爽😄。
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。