前端渣渣的我不再敢說我會寫Button組件了

前言

📢 博客首發 : 阿寬的博客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 字段,用於修改主題色嗎?

是的,可是有個問題咱們要思考,什麼是類型?舉個 🌰,咱們常問,「你喜歡什麼類型的電影」,你能夠說驚悚動做速度等類型的電影,但你說,我喜歡好看的電影,咱們認真想一下,「好看」,它屬於類型嗎?不屬於類型,「好看」是這部電影的一個「屬性」。

當咱們把這些屬性定義好了以後,下邊就沒有咱們擔憂的了。咱們來思考一下,如何去開發這個組件~

設計方案

按照使用場景,最終咱們能夠定義出,如 ButtonButtonIconButtonText,再進一步的分析,會發現,這三種(甚至多種)類型的按鈕,都會存在一些公共的狀態屬性,如 sizestyleonClickclassName 等,那麼咱們能夠經過什麼方式去實現呢?

✋ 原本想使用繼承方式進行設計,但在 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 組件所接受的屬性有:👉

參數 說明
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 也會不一樣。

並且最讓人難受的是,typeghostantiWhitecolor 這四種,可隨意搭配, 也就有 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 的紅色背景
    }
  }
}
複製代碼

這段代碼你們其實都看得懂,當咱們鼠標 hoveractive 以後,透明度會改變,而後達到咱們的指望,這時候,咱們手賤,加了一個屬性 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 代碼就相對好寫了許多。

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>
複製代碼

ButtonText 的坑

我覺得這個應該不會有啥問題的,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,我本身用了以後,分分鐘想捶死本身,以當時個人能力,個人設計缺陷,個人代碼風格,個人技術水平,致使我在使用過程須要不斷的去改框架裏的代碼。

好像扯遠了,好了,不跟大家嘮叨這麼多了,大家這些有技術的人,講話都不負責任的,咱們這些菜雞講話是要負責任的,明早還要起來幹活呢~(引用一個抖音段子來結尾,逃...)

相關連接

相關文章
相關標籤/搜索