前言
前段時間將 antd 一些組件的源碼看了一下,特別是 Button 組件的源碼,我真的是跪了,what the f**k,原來還能這么設計,less 居然還能這么用。這不,參考Antd Button源碼,結合視覺交互,經過三次的設計評審,終于在今天,把 Button 組件擼出來了。下面記錄一下自己的設計和開發思路~
希望大家能耐得住性子去看,因為這個 Button 組件有太多種情況了,包括在設計、開發中遇到的一些坑,當然我這個設計不一定是好的,所以也希望,大家看完之后可以給點建議,謝謝大家喏~
看完文章你會收獲什么?
我也不知道你能收獲什么,這篇文章主要是記錄我在開發一個公共組件的一些思考設計和遇到的坑,如果你也是跟我一樣,想知如何寫一個公共組件,或者是開發一個公共組件該做些什么準備,那么這篇文章可能會讓你有一絲絲的啟發~
效果
擔心大家一直聽我逼逼,給大家一種紙上談兵的錯覺,我就先上效果圖。最終的實現
ButtonIcon 和 ButtonText 未來得及開發,這兩個組件問題不大
設計思想
在談具體設計和開發之前,請允許我說幾句話,個人想法: 前期的設計及評審很重要,不要盲目就下手去擼代碼,也不要閉門造車,自己一個人搞,一定要在開發之前,將你能考慮到的所有情況和規則羅列出來,進行評審,組內成員提出建議并且進行查缺補漏(因為使用者就是你身邊的小伙伴,相信他們會給你提很多“無理”的要求)
組件設計
下面是我根據視覺小姐姐給的視覺稿,結合交互,最終將 Button 組件分為
使用場景
按照使用場景,Button 組件可分為 :
- 普通按鈕
- 圖標按鈕
- 文本按鈕
- 組合按鈕
- 幽靈按鈕
- 反白按鈕
按類型
按類型劃分,Button 組件可分為 :
- 主按鈕
- 次按鈕
按大小
按大小劃分,Button 組件可分為 :
- 小按鈕
- 標準按鈕
- 大按鈕
- 拇指按鈕
按主題色
按主題色劃分,Button 組件可分為 :
- 主題按鈕
- 警示按鈕
- 危險按鈕
上邊是我所能想到的所有按鈕劃分,劃分完了之后,我們得進一步確認一些屬性,這里想跟大家探討一下兩個重要的玩意:type & ghost
這里有小伙伴要懵逼了,這個屬性不是很簡單嗎,為什么要單獨拿這個屬性出來討論呢?來來來,我們討論一下 ~
且不說組內視覺給的,我們來看看業內一些優秀 UI 庫,他們對 Button type 的定義
Ant Design
對于 type 的定義,僅支持主按鈕、次按鈕、虛線按鈕和鏈接按鈕。
在我的認知中,對于按鈕的顏色說明,是這樣的
- 藍色,主色調,表按鈕是用來說明意義。
- 綠色,成功色調,表按鈕是成功
- 橙色,警示色調,表按鈕是警告、提示
- 紅色,錯誤色調,表按鈕是錯誤、危險
所以對于 Antd 有一點我是比較疑惑,如果用我的這種認知去看它的Button組件,當設置 type=primary 時是藍色,那么,自然而然的,我想要危險紅色,是不是設置 type=danger 就對了呢?
? 不好意思,你想太多了
Antd單獨提供了一個danger屬性,用于設置該按鈕為危險按鈕。也就是說,這樣才是對的
<Button type="primary" /> // 藍色
<Button type="primary" danger /> // 紅色
iView
我們再來看一下 iView 對于 Button type的一個定義。
咦,我們可以發現,好像它對 type 的定義更加符合我們的認知
<Button type="primary" /> // 藍色
<Button type="info" /> // 淡藍色
<Button type="success" /> // 綠色
<Button type="warning" /> // 橙色
<Button type="danger" /> // 紅色
Element UI
我們再看一下 element UI 是如何定義的,跟 iView 差不多類似,也是符合我們認知的。
那么問題來了,我這個Button是否跟iView、Element UI一樣,集成到 type,還是類似于Ant Design一樣,給個單獨的屬性?基于這個問題,在第一次評審,跟組里的小伙伴經過探討,最終決定,單獨給定一個屬性叫做 color ,由這個屬性決定按鈕的主題色。而 type 仍表示它的一個“類型”
再來討論一個叫做ghost的玩意,初始的時候,我是將其定義為一種“類型”,但是后來,我發現,這個 ghost 它也是受主題色的影響,比如你紅色時,幽靈按鈕的文本顏色是紅色,你綠色的時候,幽靈按鈕的文本顏色是綠色,比如這個樣子
? 這里你可能要問了,不是有一個叫做 color 字段,用于修改主題色嗎?
是的,但是有個問題我們要思考,什么是類型?舉個 ,我們常問,“你喜歡什么類型的電影”,你可以說驚悚、動作、速度等類型的電影,但你說,我喜歡好看的電影,我們認真想一下,“好看”,它屬于類型嗎?不屬于類型,“好看”是這部電影的一個“屬性”。
當我們把這些屬性定義好了之后,下邊就沒有我們擔心的了。我們來思考一下,如何去開發這個組件~
設計方案
按照使用場景,最終我們可以定義出,如 Button、ButtonIcon、ButtonText,再進一步的分析,會發現,這三種(甚至多種)類型的按鈕,都會存在一些公共的狀態屬性,如 size、style、onClick、className 等,那么我們可以通過什么方式去實現呢?
? 本來想使用繼承方式進行設計,但在 React 官網中,組合 VS 繼承中,可看到,React 推薦使用組合而非繼承來實現組件間的代碼重用。React 推崇 HOC 和組合的方式,React 希望組件是按照最小可用的思想來進行封裝,在 OOP 原則中,這叫單一職責原則。換句話說,React 希望一個組件只專注于一件事。
在這里使用繼承是比較怪異的,比如你的代碼寫成這樣
// 基類
class BaseButton extends React.Component {}
// 繼承
class Button extends BaseButton {}
class ButtonIcon extends BaseButton {}
class ButtonText extends BaseButton {}
總感覺這里的繼承是強行使用的,我們換成高階組件的方式,會不會好一些?
// 高階組件
const BaseButtonHoc = WrApperComponent => {
return class extends React.Component{
return (
<React.Fragment>
<WrapperComponent {...this.props} />
</React.Fragment>
)
}
}
export default BaseButtonHoc;
// 使用
export default BaseButtonHoc(Button);
export default BaseButtonHoc(ButtonIcon);
export default BaseButtonHoc(ButtonText);
嗯,看起來高階組件方式更加使用,就用它吧 ~
? 注意,ButtonGroup 只是一個包裹著 Button 的容器,這里不是 BaseButtonHoc衍生出來的類型。
開發遇到的問題
1. Button 樣式優先級的定義
為什么一開始我說要必須羅列好所有規則,因為中間只要有不符的,那么不好意思,你可能需要重寫樣式。
拿 ButtonHOC 高階組件來說,一開始我的設計就存在問題了,我們來看代碼(我知道你們很不想看一坨代碼,我盡量減少)
在看之前,我們先來達成共識,Button 組件所接受的屬性有:
然后我就寫下了這樣的一段代碼。
這里有人會問了,disabled為什么出現這么多?這就是我想吐槽的地方,因為視覺和交互方面就要這樣,換句話說 :
正常情況下,color 不同,disabled 之后對應的 hover、active、focus 不同。
幽靈情況下,color 不同,disabled 之后對應的 hover、active、focus 也會不同。
反白情況下,color 不同,disabled 之后對應的 hover、active、focus 也會不同。
而且最讓人惡心的是,ghost、antiWhite、color 這三種,可隨意搭配, 也就有 8 種可能,對不起,我尿了。
這就是我一開始沒定義好樣式優先級的鍋,自己給自己埋坑,于是代碼自然而然的,哎,不提也罷。
在確認了優先級規則之后 : props style > disabled > ghost > antiWhite > color
將代碼改成了這樣,果然,管它什么屬性、類型,一切按我的規則來!
自然而然的,less 代碼就相對好寫了許多。
2. ButtonGroup 的坑
前邊也說了,ButtonGroup 只是一個包裹著 Button 的容器,它不是BaseButtonHoc 衍生出來的類型。是不是你就覺得,嗨,這不就用一個div包裹按鈕組件而已嗎,這有啥好糾結的,嘿,我當時也是這么認為的,知道我真的去做了之后,才發現,這他娘玩屁啊。
當時我第一眼,沒錯了,按照代碼來說,確實應該是這樣,但這不是我想要的啊...
為什么會出現這種情況,大家想一下其實也知道,因為Button本身默認帶有圓角,我只是在外部加了一個div,所以自然就是這樣咯,相比說到這里,已經有小伙伴知道如何處理了,沒錯,就是你想的那樣,我的解決方法就是 :ButtonGroup 下把 Button 的邊框和圓角強制去掉
// 組合按鈕
.@{button-group-prefix-cls} {
// 組合下的Button邊框都去掉
.@{button-prefix-cls} {
border: none;
border-radius: 0;
}
}
這段代碼,不會再出現上邊說的實際情況了。但隨之而來的,又是一個新問題,那就是,我真的想設置圓角怎么辦?咦,不錯,你跟我想的一樣,我們把它重置了,現在再給它加回來。
.@{button-group-prefix-cls} {
// 重新設置邊框及圓角
&-circle {
.@{button-prefix-cls} {
&:first-child {
border-top-left-radius: @btn-border-radius;
border-bottom-left-radius: @btn-border-radius;
}
&:last-child {
border-top-right-radius: @btn-border-radius;
border-bottom-right-radius: @btn-border-radius;
}
}
}
}
穩妥,想一想,還有問題嗎?嘿,你還別說,還有一個大問題,那就是,我傳入的 Button 有大有小咋搞!
<ButtonGroup>
<Button size="small">小按鈕</Button>
<Button size="large">大按鈕</Button>
</ButtonGroup>
何解?我當時萌生的第一個想法,那就是 : 取得子組件中最大尺寸 size,然后重寫各Button組件的 props size,比如上邊的 demo 中,我找到最大尺寸是 large,那么我重寫每個 Button 的 size 都改為 large,但是,被現實狠狠打了一巴掌,因為我們在 ButtonGroup 里邊是一個 children 玩意。
class ButtonGroup extends React.PureComponent<AbstrunctButtonGroupProps> {
renderButtonGroup = ({ getPrefixCls }: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
className,
..., // 不展開寫了
children
} = this.props;
const prefixCls = getPrefixCls('button-group', customizePrefixCls);
const classes = classNames(prefixCls, className, ...不展開寫了);
// 渲染子children
return (
<div style={style} className={classes}>
{children}
</div>
);
};
render() {
return <ConfigConsumer>{this.renderButtonGroup}</ConfigConsumer>;
}
}
最后決定,給 ButtonGroup 一個 size 屬性,由這個屬性決定組合按鈕的樣式,換言之,我不管你Button給什么尺寸,以我為準,下面這段代碼最終顯示的樣式,是小尺寸的樣式
<ButtonGroup size="small">
<Button size="large">取消</Button>
<Button size="middle">提示</Button>
<Button size="thumb">危險</Button>
</ButtonGroup>
怎么做到的?老規矩,重寫樣式咯~
.@{button-group-prefix-cls} {
// 解決組件大小組合使得高度、寬度不一致問題
&-small {
.@{button-prefix-cls} {
min-width: @button-sm-width;
height: @button-sm-height;
line-height: @button-sm-height;
font-size: @button-sm-font-size;
}
}
}
ButtonIcon 的支持
正常來說,我們圖標組件,只需要這樣就可以解決
<Button>
<Icon />
圖標按鈕
</Button>
但我為什么還要加一個 ButtonIcon,因為視覺和交互有個騷操作,那就是 : Icon 會變色,包括它的狀態會跟你當前按鈕有強關聯的關系。所以這邊只能基于ButtonHoc 衍生出此類型按鈕~
<ButtonIcon icon={} color="danger">
帶有圖標的危險按鈕
</ButtonIcon>
其它
對于整個 Button 組件的代碼,我放在了這里 : Button 源碼,因為文章不想貼太多代碼,有想法的可以移步哈~ 當然,我更加希望的是你能去看源碼,因為你看完之后,你就覺得我寫的是渣了~ 我只是借鑒參考其中的一些設計思想,低成本的開發了一個公共組件~
總結
謝謝你看到這里,最近也是開發了一些公共組件,說一下自己感想吧,我之前一直想自己做一個組件庫,造個輪子,對于寫 UI 組件庫來講,最簡單就是寫一個Button組件了,那是“年少輕狂”,感覺 Button 組件這么簡單,是最好寫的組件了,但現在回過頭來看,越簡單的東西,越難!!!
在此之前,自己屬于使用者,創項目時,總會 npm install UI庫,基于該庫,簡單的二次封裝,但從未去看過它內部的實現原理,直到這次,“迫不得已”去看的源碼,看了之后,才發現,人與人之間真的有差距。
開源即責任,如果你做的東西,想被更多人使用,那就意味著,你得承擔更多!每個人都有一個開源夢,我之前也造過輪子,這個 vue-erek-manage 是我之前借鑒 Ant Design Pro 造的,我天真以為做完這個東西,功能實現了,就能給大家用了,But,我自己用了之后,分分鐘想捶死自己,以當時我的能力,我的設計缺陷,我的代碼風格,我的技術水平,導致我在使用過程需要不斷的去改框架里的代碼。