組件,是前端最常打交道的東西,對于 React、Vue 等應(yīng)用來說,萬物皆組件毫不為過。
有些工作經(jīng)驗(yàn)的同學(xué)都知道,組件其實(shí)也分等級的,有的組件可以被上萬開發(fā)者復(fù)用,有些組件就只能在項(xiàng)目中運(yùn)行,甚至挪動到自己的另外一個項(xiàng)目都不行。
如何考察一個前端的水平,首先可以看看他有沒有對團(tuán)隊提供過可復(fù)用的組件,一個前端如果一直只能用自己寫的東西,或者從沒有對外提供過可復(fù)用的技術(shù),那么他對于一個團(tuán)隊的貢獻(xiàn)一定是有限的。
所以開始寫一個能開放的組件應(yīng)該考慮些什么呢???
本篇文章類似一個菜譜,比較零碎的記錄一些組件設(shè)計的內(nèi)容,我分別按照 1~5 ?? 區(qū)分其重要性。
意識
首先在意識層面,我們需要站在使用組件的開發(fā)者角度來觀察這個組件,所以下面幾點(diǎn)需要在組件開發(fā)過程中種在意識里面:?
1.我應(yīng)該注重?TypeScript API?定義,好用的組件API都應(yīng)該看上去 理所應(yīng)當(dāng) 且 絕不多余。
2.我應(yīng)該注重 README 和 Mock ,一個沒有文檔的組件 = 沒有,最好不要使用 link 模式去開發(fā)組件。
3.我不應(yīng)引入任何副作用依賴,比如全局狀態(tài)(Vuex、Redux),除非他們能自我收斂。
4.我在開發(fā)一個開放組件,以后很有可能會有人來看我的代碼,我得寫好點(diǎn)。
接口設(shè)計
好的?Interface?是開發(fā)者最快能搞清楚組件入?yún)⒌耐緩?,也是讓你后續(xù)開發(fā)能夠有更好代碼提示的前提。
type Size = any; // ?? ?
type Size = string; // ????♀?
type Size = "small" | "medium" | "large"; // ?
DOM屬性(??????????)
組件最終需要變成頁面DOM,所以如果你的組件不是那種一次性的,請默認(rèn)順手定義基礎(chǔ)的DOM屬性類型。className 可以使用 classnames[1]或者 clsx[2]處理,別再用手動方式處理 className 啦!
export interface IProps {
className?: string;
style?: React.CSSProperties;
}
對于內(nèi)部業(yè)務(wù)來說,還會有?data-spm?這類 dom 屬性,主要用于埋點(diǎn)上報內(nèi)容,所以可以直接對你的 Props 類型做一個基礎(chǔ)封裝:
export type CommonDomProps = {
className?: string;
style?: React.CSSProperties;
} & Record<`data-${string}`, string>
// component.tsx
export interface IComponentProps extends CommonDomProps {
...
}
// or
export type IComponentProps = CommonDomProps & {
...
}
類型注釋(??????)
1.export?組件 props 類型定義
2.為組件暴露的類型添加 規(guī)范的注釋
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>>;
}
上面的類型注釋就是一個規(guī)范的類型注釋,清晰的類型注釋可以讓消費(fèi)者,直接點(diǎn)擊進(jìn)入你的類型定義中查看到關(guān)于這個參數(shù)的清晰解釋。
同時這類符合 jsdoc[3]規(guī)范的類型注釋,也是一個標(biāo)準(zhǔn)的社區(qū)規(guī)范。利用 vitdoc[4]這類組件DEMO生成工具也可以幫你快速生成美觀的 API 說明文檔。
小技巧:如果你非常厭倦寫這些注釋,不如試試著名的AI代碼插件:Copilot[5],它可以幫你快速生成你想要表達(dá)的文字。
以下是 ? 錯誤示范:
toolbar?: React.ReactNode; // List toolbar.
// ???? Columns
// defaultValue is "[]"
columns?: IColumns[];
組件插槽(??????)
對于一個組件開發(fā)新手來說,往往會犯?string?類型替代?ReactNode?的錯誤。
比如要對一個 Input 組件定義一個 label 的 props ,許多新手開發(fā)者會使用?string?作為 label 類型,但這是錯誤的。
export type IInputProps = {
label?: string; // ?
}
export type IInputProps = {
label?: React.ReactNode; // ?
}
遇到這種類型時,需要意識到我們其實(shí)是在提供一個 React 插槽類型,如果在組件消費(fèi)中僅僅是讓他展示出來,而不做其他處理的話,就應(yīng)當(dāng)使用?ReactNode?類型作為類型定義。
受控 與 非受控(??????????)
如果要封裝的組件類型是 數(shù)據(jù)輸入 的用途,也就是存在雙向綁定的組件。請務(wù)必提供以下類型定義:
export type IFormProps<T = string> = {
value?: T;
defaultValue?: T;
onChange?: (value: T, ...args) => void;
};
并且,這類接口定義不一定是針對?value, 其實(shí)對于所有有 受控需求 的組件都需要,比如:
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]
消費(fèi)方式推薦使用:ahooks useControllableValue?[7]
表單類常用屬性(????????)
如果你正在封裝一個表單類型的組件,未來可能會配合 antd[8]/ fusion[9]等?Form?組件來消費(fèi),以下這些類型定義你可能會需要到:
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;
};
選擇類型(????????)
如果你正在開發(fā)一個需要選擇的組件,可能以下類型你會用到:
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;
}
上述參數(shù)定義,你可以參照 Merlion UI - useSelection[10]查看并消費(fèi)。?
另外,單選與多選存在時,組件的?value?可能會需要根據(jù)下傳的?mode?自動變化數(shù)據(jù)類型。
比如,在?Select?組件中就會有以下區(qū)別:
mode="single" -> value: string | number
mode="multiple" -> value: string[] | number[]
所以對于需要 多選、單選 的組件來說,value 的類型定義會有更多區(qū)別。
對于這類場景可以使用 Merlion UI - useCheckControllableValue[11]進(jìn)行抹平。
組件設(shè)計
服務(wù)請求(??????????)
這是一個在業(yè)務(wù)組件設(shè)計中經(jīng)常會遇到的組件設(shè)計,對于很多場景來說,或許我們只是需要替換一下請求的 url ,于是便有了類似下面這樣的API設(shè)計:
export type IAsyncProps {
requestUrl?: string;
extParams?: any;
}
后面接入方增多后,出現(xiàn)了后端的 API 結(jié)果不符合組件解析邏輯,或者出現(xiàn)了需要請求多個API組合后才能得到組件所需的數(shù)據(jù),于是一個簡單的請求就出現(xiàn)了以下這些參數(shù):
export type IAsyncProps {
requestUrl?: string;
extParams?: any;
beforeUpload?: (res: any) => any
format?: (res: any) => any
}
這還只是其中一個請求,如果你的業(yè)務(wù)組件需要 2個、3個呢?組件的API就會變得越來越多,越來越復(fù)雜,這個組件慢慢的也就變得沒有易用性 ,也慢慢沒有了生氣。?
對于異步接口的API設(shè)計最佳實(shí)踐應(yīng)該是:提供一個?Promise?方法,并且詳細(xì)定義其入?yún)?、出參類型?/p>
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;
}
通過這樣的參數(shù)定義后,對外只暴露了 1 個參數(shù),該參數(shù)類型為一個?async?的方法。開發(fā)者需要下傳一個符合上述入?yún)⒑统鰠㈩愋投x的函數(shù)。
在使用時組件內(nèi)部并不關(guān)心請求是如何發(fā)生的,使用什么方式在請求,組件只關(guān)系返回的結(jié)果是符合類型定義的即可。
這對于使用組件的開發(fā)者來說是完全白盒的,可以清晰的看到需要下傳什么,以及友好的錯誤提示等等。
Hooks(??????????)
很多時候,或許你不需要組件!
對于很多業(yè)務(wù)組件來說,很多情況我們只是在原有的組件基礎(chǔ)上封裝一層淺淺的業(yè)務(wù)服務(wù)特性,比如:
-
Lazada Uploader:Upload + Lazada Upload Service
-
Address Selector: Select + Address Service
-
Brand Selector: Select + Brand Service
-
...
而對于這種淺淺的膠水組件,實(shí)際上組件封裝是十分脆弱的。因?yàn)闃I(yè)務(wù)會對UI有各種調(diào)整,對于這種重寫成本極低的組件,很容易導(dǎo)致組件的垃圾參數(shù)激增。
實(shí)際上,對于這類對服務(wù)邏輯的狀態(tài)封裝,更好的辦法是將其封裝為?React Hooks?,比如上傳:
export function Page() {
const lzdUploadProps = useLzdUpload({ bu: 'seller' });
return <Upload {...lzdUploadProps} />
}
這樣的封裝既能保證邏輯的高度可復(fù)用性,又能保證 UI 的靈活性。
Consumer(??????)
對于插槽中需要使用到組件上下文的情況,我們可以考慮使用 Consumer 的設(shè)計進(jìn)行組件入?yún)⒃O(shè)計。
比如?Expand?這個組件,就是為了讓部分內(nèi)容在收起時不展示。?
對于這種類型的組件,明顯容器內(nèi)的內(nèi)容需要拿到?isExpand?這個關(guān)鍵屬性,從而決定索要渲染的內(nèi)容,所以我們在組件設(shè)計時,可以考慮將其設(shè)計成可接受一個回調(diào)函數(shù)的插槽:
export type IExpandProps = {
children?: (ctx: { isExpand: boolean }) => React.ReactNode;
}
而在消費(fèi)側(cè),則可以通過以下方式輕松消費(fèi):
export function Page() {
return (
<Expand>
{({ isExpand }) => {
return isExpand ? <Table /> : <AnotherTable />;
}}
</Expand>
);
}
文檔設(shè)計
package.json(??????????)
請確保你的?repository?是正確的倉庫地址,因?yàn)檫@里的配置是很多平臺溯源的唯一途徑,比如: 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(????????)
如果你在做一個庫,并希望有人來使用它,請至少為你的庫提供一段描述,在我們的腳手架模板中已經(jīng)為你生成了一份模板,并且會在編譯過程中自動加入在線 DEMO 地址,但如果可以請至少為它添加一段描述。?
這里的辦法有很多,如果你實(shí)在不知道該如何寫,可以找一些知名的開源庫來參考,比如 `antd` \ `react` \ `vue` 等。文章來源:http://www.zghlxwxcb.cn/news/detail-459667.html
還有一個辦法,或許你可以尋求 `ChatGPT` 的幫助,屢試不爽??。文章來源地址http://www.zghlxwxcb.cn/news/detail-459667.html
到了這里,關(guān)于如何開發(fā)一個人人愛的組件?的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!