新聞中心
本文為來(lái)自 字節(jié)跳動(dòng)-國(guó)際化電商-S 項(xiàng)目團(tuán)隊(duì) 成員的文章,已授權(quán) ELab 發(fā)布。
創(chuàng)新新互聯(lián),憑借十余年的成都做網(wǎng)站、網(wǎng)站設(shè)計(jì)經(jīng)驗(yàn),本著真心·誠(chéng)心服務(wù)的企業(yè)理念服務(wù)于成都中小企業(yè)設(shè)計(jì)網(wǎng)站有上千家案例。做網(wǎng)站建設(shè),選創(chuàng)新互聯(lián)。
?為何要進(jìn)行前端組件設(shè)計(jì)??
與僅承擔(dān)數(shù)據(jù)處理邏輯的后端不同,前端需要負(fù)責(zé)界面渲染、數(shù)據(jù)處理、和接口調(diào)用,在框架誕生前,更多地是編寫頁(yè)面維度的順序腳本代碼。隨著前端繼續(xù)的持續(xù)發(fā)展,ES6推出了class語(yǔ)法糖,React提出了函數(shù)式組件,Vue則以模版語(yǔ)法的形式組織代碼,前端代碼逐漸從“平鋪”轉(zhuǎn)變到了“層級(jí)”結(jié)構(gòu),從“面向過(guò)程”進(jìn)階為“面向?qū)ο蟆?,前端組件也成為了近幾年來(lái)的熱門議題。
“組件是對(duì)數(shù)據(jù)和方法的簡(jiǎn)單封裝,是軟件中具有相對(duì)獨(dú)立功能、接口由契約指定、和語(yǔ)境有明顯依賴關(guān)系、可獨(dú)立部署、可組裝的軟件實(shí)體?!边@段百科中摘取的組件定義,揭示了組件所需要具備的特性:功能獨(dú)立、約定一致、可集成、服務(wù)于場(chǎng)景。
在軟件工程中,軟件設(shè)計(jì)是軟件開(kāi)發(fā)流程中的必要階段,在需求分析后、軟件開(kāi)發(fā)前進(jìn)行。軟件復(fù)雜度是每一個(gè)項(xiàng)目演進(jìn)的產(chǎn)物,隨著需求和代碼行數(shù)的增加,復(fù)雜度將持續(xù)提升。軟件設(shè)計(jì)的優(yōu)劣為對(duì)復(fù)雜度帶來(lái)的影響是不同的,優(yōu)雅、合理的設(shè)計(jì)使待開(kāi)發(fā)的代碼復(fù)雜度可控,而拙劣的設(shè)計(jì)將會(huì)給軟件帶來(lái)無(wú)序、偶然的復(fù)雜度變更。一個(gè)優(yōu)秀的前端組件需要在滿足需求的前提下,具備高易用性和良好的可擴(kuò)展性,這是我們進(jìn)行前端組件設(shè)計(jì)的目標(biāo)。
?如何提升組件易用性??
合理的組件封裝
組件既生于頁(yè)面,又能夠獨(dú)立于頁(yè)面。我們不能將整個(gè)頁(yè)面雜糅為一個(gè)組件,也不能將每一小塊UI都封裝為組件。前端組件按類型可以分為容器組件、功能組件和展示組件,一個(gè)優(yōu)秀的組件應(yīng)該保證:功能內(nèi)聚、樣式統(tǒng)一、并且與父元素僅通過(guò)Props通信。
組件的封裝粒度并不是越小越好,很多時(shí)候一個(gè)組件是在其他一個(gè)或多個(gè)組件的基礎(chǔ)上開(kāi)發(fā)的,無(wú)法完全以功能點(diǎn)的數(shù)量衡量是否遵循單一職責(zé)原則,組件開(kāi)發(fā)者需要根據(jù)組件功能和目標(biāo)來(lái)確定組件封裝粒度:
- 當(dāng)該組件需要承載具體的額外功能時(shí),相較于新增 API ,封裝成獨(dú)立的組件是更好的選擇。
:InputTag組件 在 Input、Tag 的基礎(chǔ)上,增加了部分交互功能,API整合了兩個(gè)組件的屬性,作為一個(gè)全新的組件提供給開(kāi)發(fā)者使用。相似的,InputNumber、AutoComplete、Mentions等組件也是基于單一職責(zé)原則封裝的特定功能組件。
- 當(dāng)組件中存在可能被單獨(dú)使用、可以承載獨(dú)立功能的子組件時(shí),可以將其以內(nèi)部組件的形式提供。
:圖片預(yù)覽功能通常依托著圖片組件使用,在實(shí)際系統(tǒng)中,喚起圖片預(yù)覽的觸發(fā)器不一定是圖片,可能是按鈕或其他觸發(fā)事件,因此預(yù)覽組件需要單獨(dú)提供給開(kāi)發(fā)者使用。預(yù)覽組件作為 Image 的內(nèi)部組件,開(kāi)發(fā)者能夠以Image.Preview、Image.PreviewGroup的方式使用,并提供左右切換、圖片縮放等功能,用戶可以通過(guò) srcList、visible、actions、scales等API來(lái)控制并定制化預(yù)覽組件。
規(guī)范的API編寫
一個(gè)易用的組件,使用者無(wú)需閱讀文檔或僅快速瀏覽文檔即可上手使用,并且應(yīng)當(dāng)在使用過(guò)程中給予清晰的注釋和代碼提示。希望以下API編寫建議能夠給組件開(kāi)發(fā)者一些參考:
- 減少必填的API項(xiàng),盡可能多地提供默認(rèn)值,降低組件的使用成本;
- 使用通用且有意義的API命名:
- onXXX:命名監(jiān)聽(tīng)/觸發(fā)方法
- renderXXX:命名渲染方法
- beforeXXX/afterXXX:命名前置/后置動(dòng)作
- xxxProps:命名子組件屬性
- 優(yōu)先使用常見(jiàn)單詞進(jìn)行命名,如:value、visible、size、disabled、label、type等等
- 單獨(dú)維護(hù)類型文件,并將其打包至組件產(chǎn)物包中,這樣使用者在開(kāi)發(fā)過(guò)程中能夠?qū)崟r(shí)看到對(duì)應(yīng)的類型提示;
- 在類型文件中,為API編寫注釋;
[Slot] 與 [Props] 的選擇
使用Props存在的問(wèn)題?
當(dāng)我們需要實(shí)現(xiàn)一個(gè)較為復(fù)雜的卡片需求組件時(shí),為了最大程度地還原UI、減少用戶的樣式開(kāi)發(fā)成本,首次設(shè)計(jì)時(shí)我們會(huì)設(shè)計(jì)出這樣的API:
export type CardProps = {
// 底部信息展示
infoProps?: {
title?: ReactNode;
content?: ReactNode;
};
// 彈層信息展示
moreInfoProps?: {
info?: ReactNode;
triggerProps?: TriggerProps;
descriptionsProps?: DescriptionsProps;
};
className?: string;
style?: CSSStyleSheet;
width?: number | string;
imageProps?: {
srcList?: Array; // 圖片url數(shù)組
afterImgs?: React.ReactNode; // 插槽,在圖片dom節(jié)點(diǎn)中
aspectRatio?: string; // 寬高比 默認(rèn)3:4
buttonProps?: ButtonProps;
current?: number; // 受控展示圖
defaultCurrent?: number; // 默認(rèn)展示圖
onChangeCurrent?: (current: number) => undefined; // 設(shè)置current
PreviewGroupProps?: ImagePreviewGroupProps;
src?: string;
};
children?: React.ReactNode;
} & CardCheckboxProps; 可以看到,這個(gè)業(yè)務(wù)卡片組件是由多個(gè)不同組件組合而成,其承載了渲染和操作(選中操作、圖片切換和彈層操作),這個(gè)設(shè)計(jì)的缺陷是顯而易見(jiàn)的:
- 需要編寫很多分散的JSX代碼,無(wú)論是寫在Props中還是定義成單獨(dú)的組件,其可讀性都不高;
- 需要在Card組件中雜糅許多額外的Props,例如triggerProps和descriptionsProps,增加了該組件的學(xué)習(xí)成本;
如果以插槽的方式對(duì)Card組件進(jìn)行改造,通過(guò)內(nèi)部組件間的組合來(lái)實(shí)現(xiàn)需求,避免了大量組件Props的堆砌,層次清晰、可讀性高,這樣的組件結(jié)構(gòu)明顯易用性更高。
title
content
Tag
什么是插槽(Slot)?
Slot是Vue框架提出的概念,可以理解為臨時(shí)占位,可以用其他組件進(jìn)行填充,Slot能夠?qū)崿F(xiàn)父組件向子組件分發(fā)內(nèi)容的功能。Vue框架中提供了
- 使用 props.children 獲取子組件,若需要區(qū)分使用不同子組件,只能通過(guò)數(shù)組下標(biāo)讀取。
- 使用 Props 傳遞 ReactNode 元素。
- 將組件劃分為多個(gè)內(nèi)部組件,交由開(kāi)發(fā)者自行組裝。
Slot(內(nèi)部組件)的使用時(shí)機(jī)?
- 布局類組件優(yōu)先使用Slot,為開(kāi)發(fā)者提供更靈活的使用方式,參考Typography、Layout、Card等組件,開(kāi)發(fā)者可以隨意地在這些組件內(nèi)部插入自定義實(shí)現(xiàn)。上文提到的業(yè)務(wù)卡片組件,實(shí)質(zhì)上也是一個(gè)封裝了多圖預(yù)覽功能的布局組件,因此更適合使用Slot來(lái)組織代碼。
- 內(nèi)容復(fù)雜、定制化程度高的組件更適合使用Slot
- 功能類組件中,以Props傳遞ReactNode的方式來(lái)接管內(nèi)部元素,盡量避免傳遞基礎(chǔ)類型元素進(jìn)行展示。
// 擴(kuò)展性低
type CardProps {
title?: string;
tags?: string[];
}
// 為開(kāi)發(fā)者提供對(duì)應(yīng)的“插槽”
type CardProps {
title?: string | ReactNode;
tag?: string[] | ReactNode;
}
?如何提升組件可擴(kuò)展性??
開(kāi)閉原則:對(duì)擴(kuò)展開(kāi)放,模塊的行為可以被擴(kuò)展;對(duì)修改關(guān)閉,模塊中的源代碼不應(yīng)該被修改
將DOM交予用戶接管
在前端組件中,應(yīng)該提供對(duì)應(yīng)的API屬性或方法來(lái)支持額外的功能,給予開(kāi)發(fā)者更充分的擴(kuò)展空間,而不是有部分需求無(wú)法滿足時(shí)放棄使用組件。以 Cascader組件 為例(以下例子若無(wú)特殊說(shuō)明,均來(lái)自Arco組件庫(kù)),在通常情況下,僅需要使用左側(cè)的基礎(chǔ)選擇器即可滿足需求。
此時(shí)若開(kāi)發(fā)者需要在級(jí)聯(lián)選擇器底部添加操作按鈕或文字展示,可能會(huì)直接修改組件源代碼、甚至放棄使用該組件。作為組件開(kāi)發(fā)者,為了提升組件可擴(kuò)展性,在此處增加了 dropdownRender?屬性,接收一個(gè)返回 ReactNode? 的函數(shù),并將此 ReactNode 渲染在級(jí)聯(lián)選擇器的特定位置。
optinotallow={options}
dropdownRender={(menu) => {
// menu 為所有選擇器元素
return (
{menu}
The footer content
);
}}
/>
與此類似的,還有 renderFooter、renderOption、renderFormat 等API,這些API實(shí)現(xiàn)難度并不高,一定程度上將DOM元素的掌控權(quán)交予組件使用者,作為通用組件,為開(kāi)發(fā)者提供了部分功能和樣式的可擴(kuò)展性。我們?cè)谠O(shè)計(jì)前端組件時(shí),多多留意組件中能夠接管給使用者的渲染邏輯和操作邏輯,并將這些邏輯暴露出去。
設(shè)計(jì)可擴(kuò)展的API
組件開(kāi)發(fā)前,整理組件所需實(shí)現(xiàn)的功能,并以功能為維度設(shè)計(jì)組件API。以下是一個(gè)設(shè)計(jì)移動(dòng)端選擇器的例子,這個(gè)選擇器需要支持單選、多選和時(shí)間選擇,于是這樣寫了第一版API,它可以滿足我們當(dāng)前的選擇器需求。
type PickerProps {
dataSource?: PickerData[] | PickerData[][];
multiple?: boolean; // 是否多選
time?: boolean; // 是否時(shí)間選擇
value?: (string|number)[];
onChange?: (value: (string|number)[]) => void;
...
}在后續(xù)迭代中發(fā)現(xiàn)還有地區(qū)選擇和級(jí)聯(lián)選擇的需求,選擇器需要進(jìn)行優(yōu)化更新,在以上API的基礎(chǔ)上只能通過(guò)添加cascader、region兩個(gè)布爾值字段用于標(biāo)識(shí)不同選擇需求。這樣做的缺陷是很明顯的,每當(dāng)我們新增不同類型的選擇功能,都需要新增一個(gè)API字段,并且這些字段還是互斥關(guān)系。理清問(wèn)題后,更新了第二版API。在組件庫(kù)中很多API都設(shè)計(jì)為常量枚舉值的形式,即使其只有兩個(gè)取值,這樣擴(kuò)展性相較于布爾值類型更好。
type PickerProps {
dataSource?: PickerData[] | PickerData[][];
type?: "single" | "multiple" | "cascader" | "region" | "time"; // 選擇器類型
value?: (string|number)[];
onChange?: (value: (string|number)[]) => void;
...
}除上述例子外,還可以利用ts的泛型和可選類型來(lái)實(shí)現(xiàn)API擴(kuò)展,例如 Table 組件的pagination、border等字段,既可以直接設(shè)置為true/false,也能夠以對(duì)象的形式進(jìn)行更詳細(xì)的配置。
極致的擴(kuò)展性——Headless UI
首次接觸headless概念是在chrome瀏覽器中,在headless模式下用戶無(wú)需看到網(wǎng)頁(yè)界面即可進(jìn)行網(wǎng)頁(yè)操作,現(xiàn)在廣泛用于web自動(dòng)化測(cè)試和爬蟲場(chǎng)景中。與之相似的,Headless UI 是基于 React Hooks 的組件開(kāi)發(fā)設(shè)計(jì)理念,強(qiáng)調(diào)只負(fù)責(zé)組件的狀態(tài)及交互邏輯,不關(guān)注組件的樣式實(shí)現(xiàn)。其本質(zhì)思想其實(shí)就是關(guān)注點(diǎn)分離,將組件的“狀態(tài)及交互邏輯”和“UI 展示層”實(shí)現(xiàn)解耦。
Headless UI目前有兩種主流實(shí)現(xiàn)方式,其一是將組件劃分為多個(gè)原子組件,使用者可以通過(guò)填充組件或修改樣式的方式來(lái)實(shí)現(xiàn)自己的需求,其二是以Hooks的方式暴露內(nèi)置交互功能的子組件屬性,使用者可以將這些屬性應(yīng)用于任意組件上,由于沒(méi)有將樣式封裝到組件中,Headless組件實(shí)現(xiàn)了最大程度的視圖層可擴(kuò)展性。
// chakra-ui 提供的數(shù)字增減框組件
function HookUsage() {
const { getInputProps, getIncrementButtonProps, getDecrementButtonProps } =
useNumberInput({
step: 0.01,
defaultValue: 1.53,
min: 1,
max: 6,
precision: 2,
})
const inc = getIncrementButtonProps()
const dec = getDecrementButtonProps()
const input = getInputProps()
return (
)
}
由于Headless組件的抽象程度較高,所需的設(shè)計(jì)成本也更高,因此并不適用于所有前端場(chǎng)景。其更適合用于開(kāi)發(fā)橫跨多個(gè)不同業(yè)務(wù)或跨端的通用組件,開(kāi)發(fā)者需要在開(kāi)發(fā)底層Headless組件和開(kāi)發(fā)多套組件中衡量成本、作出抉擇。
?Reference?
https://arco.design/react/docs/start
https://juejin.cn/post/7160223720236122125
https://juejin.cn/post/6844904032700481550
網(wǎng)站欄目:淺談前端組件設(shè)計(jì)
文章來(lái)源:http://www.5511xx.com/article/dphecsh.html


咨詢
建站咨詢

