📢 博客首發 : 阿寬的博客html
前段時間發了一篇《前端渣渣開發 UI 公共組件的新認識》隨筆,以後將 antd 一些組件的源碼看了一下,特別是 Button
組件的源碼,我真的是跪了,what the f**k
,原來還能這麼設計,less
竟然還能這麼用。這不,參考Antd Button
源碼,結合視覺交互,通過三次的設計評審,終於在今天,把 Button
組件擼出來了。下邊記錄一下本身的設計和開發思路~前端
👌 但願你們能耐得住性子去看,由於這個 Button 組件有太多種狀況了,包括在設計、開發中遇到的一些坑,固然我這個設計不必定是好的,因此也但願,你們看完以後能夠給點建議,謝謝你們喏~vue
我也不知道你能收穫什麼,這篇文章主要是記錄我在開發一個公共組件的一些思考設計和遇到的坑,若是你也是跟我同樣,想知如何寫一個公共組件,或者是開發一個公共組件該作些什麼準備,那麼這篇文章可能會讓你有一絲絲的啓發~react
擔憂你們一直聽我逼逼,給你們一種紙上談兵的錯覺,我就先上效果圖。最終的實現 👇git
💨 ButtonIcon 和 ButtonText 將來得及開發,這兩個組件問題不大github
在談具體設計和開發以前,請容許我說幾句話,我的想法: 前期的設計及評審很重要,不要盲目就下手去擼代碼,也不要閉門造車,本身一我的搞,必定要在開發以前,將你能考慮到的全部狀況和規則羅列出來,進行評審,組內成員提出建議而且進行查缺補漏(由於使用者就是你身邊的小夥伴,相信他們會給你提不少「無理」的要求)npm
下邊是我根據視覺小姐姐給的視覺稿,結合交互,最終將 Button 組件分爲 👇bash
按照使用場景,Button 組件可分爲 :antd
按類型劃分,Button 組件可分爲 :app
按大小劃分,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
同樣,給個單獨的屬性?基於這個問題,由於咱們的組件還有些不一樣,不全像Antd
那樣全面開放給全部開發者使用,是帶有一絲絲的「特點」,因此在第一次評審,跟組裏的小夥伴通過探討,最終決定,單獨給定一個屬性叫作 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
衍生出來的類型。
爲何一開始我說要必須羅列好全部規則,由於中間只要有不符的,那麼很差意思,你可能須要重寫樣式。
拿 ButtonHOC
高階組件來講,一開始個人設計就存在問題了,咱們來看代碼(我知道大家很不想看一坨代碼,我儘可能減小)
在看以前,咱們先來達成共識,Button
組件所接受的屬性有:👉
參數 | 說明 |
---|---|
size | 按鈕大小 |
type | 設置按鈕類型 |
color | 按鈕顏色,搭配 type 共同使用 |
ghost | 幽靈屬性,使按鈕背景透明 |
antiWhite | 反白屬性,適用於深色背景 |
block | 將按鈕寬度調整爲其父寬度的選項 |
disabled | 按鈕失效狀態 |
style | 配置按鈕的樣式 |
onClick | 點擊按鈕時的回調 |
而後我就寫下了這樣的一段代碼。
這裏有人會問了,disabled
爲何出現這麼多?這就是我想吐槽的地方,由於視覺和交互方面就要這樣,換句話說 :
正常狀況下,color 的不一樣,disabled 以後對應的 hover、active、focus 不一樣。
幽靈狀況下,color 的不一樣,disabled 以後對應的 hover、active、focus 也會不一樣。
反白狀況下,color 的不一樣,disabled 以後對應的 hover、active、focus 也會不一樣。
並且最讓人難受的是,type
、ghost
、antiWhite
、color
這四種,可隨意搭配, 也就有 16 種可能,對不起,我尿了。(但實際上,type 的影響只在antiWhite反白
屬性下體現)
這就是我一開始沒定義好樣式優先級的鍋,本身給本身埋坑,因此代碼寫的,一開始還挺好,到後邊,添加不一樣屬性以後,它的表現明顯與個人指望不符(舒適提醒各位,必定要注意 hover、active、focus 這些問題)
這裏我要誇一下 Antd
作法,我就不貼代碼了,你們移步這裏 : Ant Design Button less ,你們確定疑惑,爲何要注意這個問題,由於我真的遇到坑了。我舉個 🌰
<Button color="orange" />
複製代碼
這是一個危險按鈕,對應的 less 樣式多是這樣的 👇(具體色值我不寫,主要看透明度)
.@{prefix-button-cls} {
// 危險按鈕樣式
&-danger {
color: rgba(xxx, xxx, xxx, 1); // 透明度 1 的白色文字
background-color: rgba(xxx, xxx, xxx, 1); // 透明度 1 的紅色背景
&:hover {
color: rgba(xxx, xxx, xxx, 0.8); // 透明度 0.8 的白色文字
background-color: rgba(xxx, xxx, xxx, 0.7); // 透明度 0.7 的紅色背景
}
&:active {
color: rgba(xxx, xxx, xxx, 0.4); // 透明度 0.4 的白色文字
background-color: rgba(xxx, xxx, xxx, 0.5); // 透明度 0.5 的紅色背景
}
}
}
複製代碼
這段代碼你們其實都看得懂,當咱們鼠標 hover
、active
以後,透明度會改變,而後達到咱們的指望,這時候,咱們手賤,加了一個屬性 ghost
<Button color="orange" ghost={true} />
複製代碼
這是一個危險的幽靈按鈕,對應的 less 樣式多是這樣的 👇(具體色值我不寫,主要看透明度)
.@{prefix-button-cls} {
// 幽靈按鈕
&-ghost {
&-danger {
color: rgba(xxx, xxx, xxx, 1); // 透明度 1 的紅色文字
background-color: rgba(xxx, xxx, xxx, 1); // 透明度 1 的白色背景
}
}
}
複製代碼
這個也很簡單,可是咱們忽略了,對應的hover、active、focus
樣式也要修改,否則會出現什麼問題? 危險幽靈按鈕的hover、active、focus
是「危險按鈕」的,而不是咱們所指望的。
你覺得這就結束了嘛?咱們再賤一下,給它再加個disabled
屬性。爽上天了~~~~
<Button color="danger" ghost={true} disabled={true} />
複製代碼
不用我說了吧,由於disabled
下,不只背景色、文本顏色變了,同時它不存在hover、active、focus
這些交互所帶來的「樣式變化」,你能夠理解爲:按鈕被禁止,它不存在hover、active、focus
。
這時大家這時候也應該知道樣式怎麼寫了。在開發這個Button
組件,讓我很絕望的是,咱們視覺上,對於hover、active、focus
還不同,它除了有些能夠改透明度,有些是直接改顏色。不像 Ant Design
那樣。給大家摘抄了一小段源碼~
而後全局的style
中定義了一個方法處理,具體想看源碼的看這裏 : colorPalettle.less
Antd
是統一了規則,對於這種都是隻能改透明度,咱們不行,因此這個我是用不到了,只能手寫,本身一個一個調了。ok,不扯這塊,這裏之因此講是由於我只是想讓大家明白,搞清楚優先級的重要性。
在確認了優先級規則以後 : props style > disabled > ghost > antiWhite > color
將代碼改爲了這樣,果真,管他什麼屬性、類型,一切按個人規則來!
天然而然的,less 代碼就相對好寫了許多。
前邊也說了,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;
}
}
}
複製代碼
正常來講,咱們圖標組件,只須要這樣就能夠解決 👇
<Button>
<Icon />
圖標按鈕
</Button>
複製代碼
但我爲何還要加一個 ButtonIcon
,由於視覺和交互有個騷操做,那就是 : Icon 會變色,包括它的狀態會跟你當前按鈕有強關聯的關係。因此這邊只能基於 ButtonHoc
衍生出此類型按鈕~
<ButtonIcon icon={} color="danger">
帶有圖標的危險按鈕
</ButtonIcon>
複製代碼
我覺得這個應該不會有啥問題的,but ,是我年輕了。這裏我要糾錯一點,對不起,讓你們想歪了,ButtonText
不是 BaseButtonHoc
衍生的,怎麼理解呢,咱們認真想想,其實文本按鈕與普通按鈕區別在哪?不就是外邊的邊框沒了嗎?這麼一想是否是以爲,很簡單了?
那如何作?咱們總不能在 BaseButtonHoc
裏邊去添加一個屬性,用於判斷它是否是文本按鈕吧?
if (!this.props.isText) {
classes = `has-border` // 普通按鈕
} else {
classes = `not-border` // 文本按鈕
}
複製代碼
且不說咱們的 BaseButtonHoc
已經寫好了,咱們要更加搞清楚,ButtonText
是咱們封裝的一個按鈕,而不是基按鈕上的一個屬性。
因此怎麼作?作法跟 ButtonGroup
有些同樣~
import Button from './Button';
class ButtonText extends React.PureComponent<AbstrunctButtonTextProps, {}> {
renderButtonText = ({ getPrefixCls }: ConfigConsumerProps) => {
const {
..., // 不展開寫
children
} = this.props;
const prefixCls = getPrefixCls('button-text', customizePrefixCls);
const classes = classNames(prefixCls, className, {
// 不展開寫了
});
return (
<div className={classes}>
<Button {...this.props}>{children}</Button>
</div>
);
};
render() {
return <ConfigConsumer>{this.renderButtonText}</ConfigConsumer>;
}
}
複製代碼
樣式怎麼寫?也很簡單,你們搞清楚了以後,就能下手了。
這個文本組件很好搞,可是組合文本按鈕就很難搞了
<ButtonGroup>
<ButtonText>文本按鈕1</ButtonText>
<ButtonText>文本按鈕2</ButtonText>
</ButtonGroup>
複製代碼
實際上,並非這樣的。由於咱們是 : ButtonGroup -> ButtonText -> Button,實際上是三層DOM的,爲何 ?
如何處理?我就不寫了,看成一個思考題,你們有興趣的能夠思考一下如何處理哈~
對於整個 Button
組件的代碼,我放在了這裏 : Button 源碼,由於文章不想貼太多代碼,有想法的能夠移步哈~ 固然,我更加但願的是你能去看源碼,由於你看完以後,你就以爲我寫的是渣了~ 我只是借鑑參考其中的一些設計思想,低成本的開發了一個公共組件~
謝謝你看到這裏,最近也是開發了一些公共組件,說一下本身感想吧,我以前一直想本身作一個組件庫,造個輪子,對於寫 UI 組件庫來說,最簡單就是寫一個Button
組件了,那是「年少輕狂」,感受 Button 組件這麼簡單,是最好寫的組件了,但如今回過頭來看,越簡單的東西,越難!!!
在此以前,本身屬於使用者,創項目時,總會 npm install UI庫
,基於該庫,簡單的二次封裝,但從未去看過它內部的實現原理,直到此次,「無可奈何」去看的源碼,看了以後,才發現,人與人之間真的有差距。
開源即責任,若是你作的東西,想被更多人使用,那就意味着,你得承擔更多!每一個人都有一個開源夢,我以前也造過輪子,這個 vue-erek-manage 是我以前借鑑 Ant Design Pro
造的,我天真覺得作完這個東西,功能實現了,就能給你們用了,But,我本身用了以後,分分鐘想捶死本身,以當時個人能力,個人設計缺陷,個人代碼風格,個人技術水平,致使我在使用過程須要不斷的去改框架裏的代碼。
好像扯遠了,好了,不跟大家嘮叨這麼多了,大家這些有技術的人,講話都不負責任的,咱們這些菜雞講話是要負責任的,明早還要起來幹活呢~(引用一個抖音段子來結尾,逃...)