如何編寫更好的React組件

前言

不知道有沒有同窗跟我同樣,學習了不少react源碼,卻仍是寫不出更優雅的代碼。咱們知道了dom-diff原理,瞭解setState是如何更新狀態的,而後呢?仍是會寫出難以維護的代碼甚至bug一堆。
html

不少時候你寫出bug不是由於你不懂dom-diff,而是由於你的屬性和狀態胡亂命名。
react

不少時候你的代碼難以維護,也不是由於你不懂dom-diff,而是你的組件劃分太不合理了。
git

因此我開始讀一些應用層框架的源碼,也但願把學習的過程和你們分享。github

爲何是antd-design-mobile

antd-design-mobile是一個不錯的學習項目,能夠學習到如何更好的使用ReactTypeScriptless,也能夠學到優秀的團隊是如何思考可維護性可擴展性組件化的。
segmentfault

antd-design-mobile項目集成了一些編譯工具,而且把各個組件分散在不通的項目中,看起來比較累。因此我建立了一個相對簡單且更適合閱讀的項目(不含動畫且從新設計了Ui),看原項目吃力的同窗能夠看這個:
安全

項目地址

github.com/alive1541/d…bash

預覽地址

http://39.100.100.217微信

預覽二維碼

由於沒有綁定域名,微信打開會有問題,可使用支付寶等其餘應用antd

源碼分析——以<Modal>爲例

項目結構

入口

先看看入口文件裏是什麼內容

能夠看到,除了這個提示按需引入的警告以外,其實就是導出了當前目錄下的全部組件。下面我以Modal組件爲例子繼續分析。

<Modal>組件解析

先看下Modal組件總體的結構:
app

接下來詳細看一下各個組件內部的細節:

Modal組件

Modal組件是根組件,也就是默認導出的組件。下圖從下往上看,文件導出的是Modal組件,它繼承成了ModalComponent抽象類,並傳入了ModalProps泛型接口。

若是對TypeScript不太瞭解能夠看 www.tslang.cn/docs/handbo… ,通讀一下「手冊指南」這一章基本就能夠看懂Ts代碼了

從接口定義上能夠看出,這個組件容許接受prefixClstransitionName等屬性,而且須要掛載三個靜態方法alertpromptoperation

組件內部有cls和一個renderFooterButton方法,其中cls結合prefixCls處理了類名(prefixCls容許用戶自定義前綴),以方便用戶統一處理自定義的樣式。而renderFooterButton方法用來渲染彈窗中的按鈕。

接下來就是alertpromptoperation三個靜態方法(官網中有相應的使用方法)。他們的做用是經過方法喚出Modal組件,以alert爲例:

import { Modal } from 'antd-mobile';

const alert = Modal.alert;

const App = () => (
    <Button
      onClick={() =>
        alert('Delete', 'Are you sure???', [
          { text: 'Cancel', onPress: () => console.log('cancel') },
          { text: 'Ok', onPress: () => console.log('ok') },
        ])
      }
    >
      confirm
    </Button>
)

複製代碼

從接口中還能看到,alertpromptoperation這三個方法都有一個函數類型的返回值close,這個方法的做用是關閉當前Modal。邏輯也比較簡單,這個靜態方法也比較簡單,就是建立一個div元素而後插入到body中,再把Modal組件插入這個div中,最後導出一個移除這個div的close方法。核心代碼以下:

export default function operation(){
    ...
    
    const div = document.createElement("div");
    document.body.appendChild(div);
    
    ReactDOM.render(
        <Modal
          visible
          operation
          transparent
          prefixCls={prefixCls}
          onClose={close}
          footer={footer}
          className="d-modal-operation"
          platform={platform}
          wrapProps={{ onTouchStart: onWrapTouchStart }}
        />, 
        div
    );
    
    function close() {
        ReactDOM.unmountComponentAtNode(div);  //銷燬指定容器內的全部React節點
        if (div && div.parentNode) {
          div.parentNode.removeChild(div);
        }
    }
    
    return { close }
}
 
複製代碼

DialogWraaper

這個組件被放在了react-component這個庫裏,這個庫是一個基礎組件庫,antd和antd mobile這兩個項目都依賴了它。
這層組件很輕,並無處理邏輯,只是建立了一個portal,同時對react16如下的版本作了兼容,兼容寫法是這樣的:

...

const IS_REACT_16 = !!(ReactDOM as any).createPortal;
componentDidUpdate() {
    if (!IS_REACT_16) {
      this.renderDialog();
    }
}
renderDialog() {
    ReactDOM.unstable_renderSubtreeIntoContainer(
      this,
      this.getComponent(),
      this.getContainer()
    );
}
render() {
    const { visible } = this.props;
    if (IS_REACT_16 && visible ) {
      return ReactDOM.createPortal(this.getComponent(), this.getContainer());
    }
    return null as any;
 }
複製代碼

Dialog

這個組件主要作了三件事,一、渲染遮罩 二、渲染彈框 三、處理關閉事件 這裏渲染了彈出框的元素,代碼以下:

...
onMaskClick = (e: any) => {
    if (e.target === e.currentTarget) {
    //e.target和e.currentTarget的區別參考https://www.jianshu.com/p/1dd668ccc97a
      this.close(e);
    }
};
close = (e: any) => {
    if (this.props.onClose) {
      this.props.onClose(e);
    }
};
render() {
    const { props } = this;
    const { prefixCls, maskClosable } = props;
    return (
      <div>
        {this.getMaskElement()}   //渲染遮罩
        <div
          className={`${prefixCls}-wrap ${props.wrapClassName || ""}`}
          onClick={maskClosable ? this.onMaskClick : undefined}
          {...props.wrapProps}
        >
          {this.getDialogElement()}   //渲染彈框
        </div>
      </div>
    );
  }
複製代碼

getMaskElementgetDialogElement兩個方法渲染元素時,元素外層包裹了LazyRender組件,意思很好理解,就是避免沒必要要的渲染。代碼也很簡單,就是在shouldComponentUpdate中作是否更新的判斷。

export default class LazyRender extends React.Component<lazyRenderProps, any> {
  shouldComponentUpdate(nextProps: lazyRenderProps) {
    return !!nextProps.visible;
  }
  render() {
    const props: any = { ...this.props };
    delete props.visible;
    return <div {...props} />;
  }
}
複製代碼

到此,這個組件已經結束了。上面只粘貼了部分代碼,有表達不是很清晰的地方能夠查看 github.com/alive1541/d… 相比於源碼,這裏的代碼更加簡單、清晰。

學到了什麼

  1. 經過react-component庫統一封裝了基礎組件,在這一層只處理了基本邏輯和樣式。方便在antd-mobile這類上層庫中經過prefixCls統一重寫樣式,極大的提升了組件的複用能力。同時,若是也方便其餘人使用這個庫去開發本身的組件。
  2. 使用TypeScript,提升了代碼的健壯性和可讀性,這種代碼維護起來會很輕鬆。
  3. 深度使用了less,經過變量、Mixin、函數等特性,對關鍵變量統一維護在了themes文件中的default.less中,對1px的處理封裝在了hairline.less文件中。也經過prefixCls前綴的方式避免了全局樣式污染的問題。

其餘小知識點

1px處理

antd-mobile對1px的處理是經過transform縮放來實現了,方法就是二倍屏縮小一半,三倍屏縮小到三分之一,詳細能夠查看個人項目中的/src/components/style/mixins/hairline.less

html:not([data-scale]) & {
    @media (min-resolution: 2dppx) {  //判斷是2倍屏幕
      border-left: none;

      &::before {
        width: 1px;
        height: 100%;
        transform-origin: 100% 50%;   //設置縮放原點
        transform: scaleX(0.5);  //x軸縮放50%

        @media (min-resolution: 3dppx) {  //判斷是3倍屏幕
          transform: scaleX(0.33);  //x軸縮放三分之一
        }
      }
    }
  }
複製代碼

iphoneX適配

有一段less是這樣的

.@{prefixCls}-content {
      padding-top: env(safe-area-inset-top);
}
複製代碼

這段代碼的意思是設置padding-top爲iphoneX的頂部部安全區域,也就是iPhoneX的頂部劉海的高度。

除此以外還safe-area-inset-bottom,由於手機有可能橫屏使用,因此還有safe-area-inset-leftsafe-area-inset-right,想詳細瞭解的話能夠看看這篇文章 segmentfault.com/a/119000001…

結語

以上是我學習的一些感悟和分享,但願一塊兒學習交流。

參考連接

antd-mobile官網 mobile.ant.design/index-cn

antd-mobile倉庫 github.com/ant-design/…

react-component倉庫 github.com/react-compo…

相關文章
相關標籤/搜索