如何开发一个人人爱的组件?|全球微动态
王银业(风水) 阿里开发者 2023-05-22 09:01发表于浙江
阿里妹导读
(资料图片仅供参考)
本篇文章类似一个菜谱,比较零碎的记录一些组件设计的内容,作者分别按照 1~5 星 区分其重要性。(后台回复大数据即可获得《大数据&AI实战派》电子书)
组件,是前端最常打交道的东西,对于 React、Vue 等应用来说,万物皆组件毫不为过。
有些工作经验的同学都知道,组件其实也分等级的,有的组件可以被上万开发者复用,有些组件就只能在项目中运行,甚至挪动到自己的另外一个项目都不行。
如何考察一个前端的水平,首先可以看看他有没有对团队提供过可复用的组件,一个前端如果一直只能用自己写的东西,或者从没有对外提供过可复用的技术,那么他对于一个团队的贡献一定是有限的。
所以开始写一个能开放的组件应该考虑些什么呢?
本篇文章类似一个菜谱,比较零碎的记录一些组件设计的内容,我分别按照 1~5 ⭐️ 区分其重要性。
意识
首先在意识层面,我们需要站在使用组件的开发者角度来观察这个组件,所以下面几点需要在组件开发过程中种在意识里面:
1.我应该注重 TypeScript API定义,好用的组件API都应该看上去 理所应当 且 绝不多余。
2.我应该注重 README 和 Mock ,一个没有文档的组件 = 没有,最好不要使用 link 模式去开发组件。
3.我不应引入任何副作用依赖,比如全局状态(Vuex、Redux),除非他们能自我收敛。
4.我在开发一个开放组件,以后很有可能会有人来看我的代码,我得写好点。
接口设计
好的 Interface是开发者最快能搞清楚组件入参的途径,也是让你后续开发能够有更好代码提示的前提。
type Size = any; // ❌type Size = string; // ♀️type Size = "small" | "medium" | "large"; // ✅
DOM属性(⭐️⭐️⭐️⭐️⭐️)
组件最终需要变成页面DOM,所以如果你的组件不是那种一次性的,请默认顺手定义基础的DOM属性类型。className 可以使用 classnames[1]或者 clsx[2]处理,别再用手动方式处理 className 啦!
export interface IProps {className?: string; style?: React.CSSProperties;}
对于内部业务来说,还会有 data-spm这类 dom 属性,主要用于埋点上报内容,所以可以直接对你的 Props 类型做一个基础封装:
export type CommonDomProps = {className?: string; style?: React.CSSProperties;} & Record<`data-${string}`, string>// component.tsxexport interface IComponentProps extends CommonDomProps { ...}// orexport type IComponentProps = CommonDomProps & { ...}
类型注释(⭐️⭐️⭐️)
1.export组件 props 类型定义
2.为组件暴露的类型添加 规范的注释
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>>;}
上面的类型注释就是一个规范的类型注释,清晰的类型注释可以让消费者,直接点击进入你的类型定义中查看到关于这个参数的清晰解释。
同时这类符合 jsdoc[3]规范的类型注释,也是一个标准的社区规范。利用 vitdoc[4]这类组件DEMO生成工具也可以帮你快速生成美观的 API 说明文档。
小技巧:如果你非常厌倦写这些注释,不如试试著名的AI代码插件:Copilot[5],它可以帮你快速生成你想要表达的文字。
以下是 ❌ 错误示范:
toolbar?: React.ReactNode; // List toolbar.// Columns // defaultValue is "[]" columns?: IColumns[];
组件插槽(⭐️⭐️⭐️)
对于一个组件开发新手来说,往往会犯 string类型替代 ReactNode的错误。
比如要对一个 Input 组件定义一个 label 的 props ,许多新手开发者会使用 string作为 label 类型,但这是错误的。
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;};
具体原因请查看:《受控组件和非受控组件》[6]
消费方式推荐使用: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;}
上述参数定义,你可以参照 Merlion UI - useSelection[10]查看并消费。
另外,单选与多选存在时,组件的 value可能会需要根据下传的 mode自动变化数据类型。
比如,在 Select组件中就会有以下区别:
mode="single" -> value: string | numbermode="multiple" -> value: string[] | number[]
所以对于需要 多选、单选 的组件来说,value 的类型定义会有更多区别。
对于这类场景可以使用 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) => any format?: (res: any) => any}
这还只是其中一个请求,如果你的业务组件需要 2个、3个呢?组件的API就会变得越来越多,越来越复杂,这个组件慢慢的也就变得没有易用性 ,也慢慢没有了生气。
对于异步接口的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 ServiceAddress Selector: Select + Address ServiceBrand Selector: Select + Brand Service...而对于这种浅浅的胶水组件,实际上组件封装是十分脆弱的。因为业务会对UI有各种调整,对于这种重写成本极低的组件,很容易导致组件的垃圾参数激增。
实际上,对于这类对服务逻辑的状态封装,更好的办法是将其封装为 React Hooks ,比如上传:
export function Page() {const lzdUploadProps = useLzdUpload({ bu: "seller" });return <Upload {...lzdUploadProps} />}
这样的封装既能保证逻辑的高度可复用性,又能保证 UI 的灵活性。
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": "yee.wang@xxx.com","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": "git@github.com:yee94/xxx.git" }}
README.md(⭐️⭐️⭐️⭐️)
如果你在做一个库,并希望有人来使用它,请至少为你的库提供一段描述,在我们的脚手架模板中已经为你生成了一份模板,并且会在编译过程中自动加入在线 DEMO 地址,但如果可以请至少为它添加一段描述。
这里的办法有很多,如果你实在不知道该如何写,可以找一些知名的开源库来参考,比如 `antd` \ `react` \ `vue` 等。
还有一个办法,或许你可以寻求 `ChatGPT` 的帮助,屡试不爽。
参考链接:
[1]https://www.npmjs.com/package/classnames
[2]https://www.npmjs.com/package/clsx
[3]https://jsdoc.app/
[4]https://vitdocjs.github.io/
[5]https://github.com/features/copilot
[6]https://segmentfault.com/a/1190000040308582
[7]https://ahooks.js.org/hooks/use-controllable-value
[8]https://ant.design/
[9]https://github.com/alibaba-fusion/next
[10]https://mc.lazada.com/package/@ali/merlion-ui#/src/hooks/use-selection/README.md
[11]https://mc.lazada.com/package/@ali/merlion-ui#/src/hooks/use-selection/README.md
关键词:
推荐阅读
首都新机场叫什么名字 机场是24小时开放的吗?
首都新机场叫什么名字?一般指北京大兴国际机场。北京大兴国际机场定位为大型国际航空枢纽,国家发展新动力源,支撑雄安新区建设的京津冀区 【详细】
什么牌子的插排好 优质的插排应该具备哪些特质呢?
什么牌子的插排好品牌插座有公牛、西门子, TCL, 西蒙, 奇胜, 松下, 施耐德, ABB、朗能,等。为了满足大众对插座的各种需求,各 【详细】
李嘉诚的车是什么 李嘉诚长江塑料厂怎么样?
李嘉诚的车是什么说到李嘉诚,我们肯定会很熟悉。出生于潮汕的他是香港首富,也是中国顶级房地产大亨。根据《福布斯》、2020年发布的数据, 【详细】
飞龙股份002536今日主力资金流向 飞龙股份002536主力控盘分析
飞龙股份002536今日主力资金流向【飞龙股份(002536)】 今日主力资金流向,资金净流入105 11万元,今日超大单净流入296 82万元,大单净流入 【详细】
中国获得诺贝尔奖的人汇总 诺贝尔奖介绍
中国获得诺贝尔奖的人汇总截至目前为止,我国获得诺贝尔奖的人一共有十一个1、杨振宁,美籍华人,1957年获诺贝尔物理学奖。2、李政道,美籍 【详细】
相关新闻
- 如何开发一个人人爱的组件?|全球微动态
- 人工智能时代必须了解的CPU秘密
- 如何面对XBB病毒株感染?钟南山:特殊人群需注射新疫苗 每日速读
- 环球微头条丨美团进军香港!旺角、大角咀抢先送单,APP最大亮点是?
- 每日播报!精工奢华的极致追求,上海揽胜高级定制中心开业
- 环球讯息:减肥可以吃什么坚果(减肥吃什么坚果)
- 贵阳一地出现暴雨,今天倍凉爽 环球热讯
- 百亿补贴可享买贵双倍赔 京东20周年打造全行业投入最大的618
- 天天看热讯:2023四川养老金调整最新消息(持续更新)
- iOS16.5更新以后续航降低
- 天天看热讯:网传清华院长:科学无国界,我们应开放尖端技术与美日合作?
- 4000万台!麒麟芯片或回归,外媒:华为的天亮了
- 加速信创生态建设 焱融科技与优炫软件完成兼容性互认证
- 人类身体极限大揭秘:深潜、耐高温、耐久力,谁是真正的极限王?_全球焦点
- 每日速读!飘落的枫叶像什么(秋天到了,枫叶的叶子像什么从树上飘下来,真美啊)
- 要闻:内参酒当前库存约为全年销售35%-40%,称控货为稳价格,将丰富销售品项保证总体规模不减少
- 全球快资讯丨2023鹿晗演唱会北京站门票可以退换吗?
- 【跨境周报】TikTok电商拟推出“全托管模式”;FedEx助力广州建设成为国际航空枢纽
- 荣耀90系列配置曝光,相比Reno 10系列谁更香?网友:就看如何定价
- 美团香港开送外卖!骑手送16单就能赚近500港元|当前观点