如何設計實現一個React UI元件庫——Ant Design原始碼閱讀與淺析

黃Java發表於2018-04-29

概述

在我們進行日常的專案開發的過程中,我們經常會遇到使用一些通用的UI元件庫如BootStrap、Ant Design等。作為成熟的UI元件庫,它能夠提供提供一整套UI元件用來滿足使用需求,能大大減少開發成本。

在使用了他人提供的元件庫後,我自然就會有興趣去了解一下別人開發的元件庫到底是如何設計的,如何進行相關的元件封裝。本文以Ant Design為例,讓我們來了解一下目前較為有名的UI元件庫是如何設計與實現的?同時,我們又能夠有哪些經驗可以借鑑?

閱讀本文,你最好有如下的基礎知識來幫助你理解本文內容:

PS: 部落格寫一半,手受傷骨折了囧囧囧,後面大部分文字是通過語音輸入轉換的,如果有什麼錯誤或者邏輯不清晰,歡迎在評論中指正。

如何實現單個的React UI元件

首先,需要了解Ant Design提供的元件,我們先來看下單個元件是如何實現的。

目錄結構

需要了解一個元件的內容,我們應該先從目錄介面開始。我們以avatar頭像元件為例,目錄結構如下圖所示:

如何設計實現一個React UI元件庫——Ant Design原始碼閱讀與淺析

我們一個一個來看一下:

  • index.tsx,UI元件原始檔,即TSX(TypeScript+JSX),包含整個元件的內容和邏輯。
  • index.zh-CN.md,元件使用說明文件
  • style,UI元件樣式檔案,包含當前UI元件的相關樣式
  • tests,UI元件測試檔案,包含當前UI元件相關的單元測試,使用Jest單元測試框架。
  • demo,用來進行展示和用法示例說明的文件

介紹完了目錄結構,我們來看下這個外掛的具體內容。

TSX檔案

我們首先來看一下這個外掛的TSX檔案。這個檔案包含了外掛的結構和功能。如程式碼示例示例所示:

export interface AvatarProps {
  /** Shape of avatar, options:`circle`, `square` */
  shape?: 'circle' | 'square';
  /** Size of avatar, options:`large`, `small`, `default` */
  size?: 'large' | 'small' | 'default';
  /** Src of image avatar */
  src?: string;
  /** Type of the Icon to be used in avatar */
  icon?: string;
  style?: React.CSSProperties;
  prefixCls?: string;
  className?: string;
  children?: any;
}

export interface AvatarState {
  scale: number;
  isImgExist: boolean;
}
複製程式碼

我們先看一下宣告檔案。在你TypeScript宣告中我們可以看到:它通過interface定義了props和state兩個屬性的值。這樣可以明確界定傳入的屬性和內部的屬性型別,在程式碼規範和質量中也能夠有一個保證。

下面讓我們來看一下具體的元件類。具體示例如下:

export default class Avatar extends React.Component<AvatarProps, AvatarState> {
    render() {
        const {
          prefixCls, shape, size, src, icon, className, ...others,
        } = this.props;
    
        const sizeCls = classNames({
          [`${prefixCls}-lg`]: size === 'large',
          [`${prefixCls}-sm`]: size === 'small',
        });
    
        const classString = classNames(prefixCls, className, sizeCls, {
          [`${prefixCls}-${shape}`]: shape,
          [`${prefixCls}-image`]: src && this.state.isImgExist,
          [`${prefixCls}-icon`]: icon,
        });
    
        let children = this.props.children;
        if (src && this.state.isImgExist) {
          children = (
            <img
              src={src}
              onError={this.handleImgLoadError}
            />
          );
        } else if (icon) {
          children = <Icon type={icon} />;
        } else {
          const childrenNode = this.avatarChildren;
          if (childrenNode || this.state.scale !== 1) {
            const childrenStyle: React.CSSProperties = {
              msTransform: `scale(${this.state.scale})`,
              WebkitTransform: `scale(${this.state.scale})`,
              transform: `scale(${this.state.scale})`,
              position: 'absolute',
              display: 'inline-block',
              left: `calc(50% - ${Math.round(childrenNode.offsetWidth / 2)}px)`,
            };
            children = (
              <span
                className={`${prefixCls}-string`}
                ref={span => this.avatarChildren = span}
                style={childrenStyle}
              >
                {children}
              </span>
            );
          } else {
            children = (
              <span
                className={`${prefixCls}-string`}
                ref={span => this.avatarChildren = span}
              >
                {children}
              </span>
            );
          }
        }
    return (
      <span {...others} className={classString}>
        {children}
      </span>
    );
  }
}
複製程式碼

從上面的示例程式碼中我們可以看到,這是一個很常規的React的元件類。它通過傳入的屬性來判斷應該選擇哪種方式渲染頭像,然後完成元件的渲染過程。同時,在元件渲染完成後,這個元件會根據引數來調整相關的圖片大小用於適配。

這樣,一個簡單的UI元件就已經基本滿足相關的功能了。

接下來,讓我們來看下樣式相關的檔案。

Style檔案

還是以Avatar元件為例,具體的樣式程式碼這裡就不舉例了,只像大家介紹下:Ant Design的樣式是通過Less語言來完成的,並且通過TSX檔案來進行引入。

Test檔案

Test檔案通過enzyme來對React元件進行測試。我們簡單介紹以下enzyme,這是一個對React元件進行測試的JavaScript框架,能夠提供mount等相關API介面來對元件選人和相關的資料更新操作進行測試。

我們選取一部分測試示例如下:

import React from 'react';
import { mount } from 'enzyme';
import Avatar from '..';

describe('Avatar Render', () => {
  it('Render long string correctly', () => {
    const wrapper = mount(<Avatar>TestString</Avatar>);
    const children = wrapper.find('.ant-avatar-string');
    expect(children.length).toBe(1);
  });

  it('should render fallback string correctly', () => {
    const div = global.document.createElement('div');
    global.document.body.appendChild(div);

    const wrapper = mount(<Avatar src="http://error.url">Fallback</Avatar>, { attachTo: div });
    wrapper.instance().setScale = jest.fn(() => wrapper.instance().setState({ scale: 0.5 }));
    wrapper.setState({ isImgExist: false });

    const children = wrapper.find('.ant-avatar-string');
    expect(children.length).toBe(1);
    expect(children.text()).toBe('Fallback');
    expect(wrapper.instance().setScale).toBeCalled();
    expect(div.querySelector('.ant-avatar-string').style.transform).toBe('scale(0.5)');

    wrapper.detach();
    global.document.body.removeChild(div);
  });
});
複製程式碼

通過上面的示例我們可以知道,enzyme能夠根據Avatar元件渲染後的資料來對元件進行測試。

UI元件庫到底是如何實現以及與使用者互動的

其實UI元件與我們自己開發的React元件沒有什麼太大的區別,只是一個提供了部分UI和功能的第三方元件而已。想明白了這點,我們就能知道,我們開發的元件與第三方UI元件的互動就是通過Props的方式。

以上面的Avatar元件為例,我們給元件傳遞sharp, sizesrc等欄位,Avatar元件收到相關資料後,在內部進行相關的處理,最終返回一個React元件。具體示例如下:

import Avatar from './avatar/';

class Container extends React.Component {
    render() {
        return (
            <div>
                <Avatar sharp="circle"  size="large"  src="https://www.baidu.com">Avatar!!</Avatar>
            </div>
        );
    }
}
複製程式碼

如果我們需要引入相關樣式檔案,我們則需要引入編譯後的css檔案,具體方式如下:

@import '~antd/dist/antd.css';
複製程式碼

通過Ant Design,我們在開發UI元件時學到了什麼

通過對UI元件庫原始碼的閱讀,我得到了如下的一些經驗。

結構清晰

每一個UI元件都是一個完整的模組,都應該有自己獨立的目錄結構;同時所有的UI元件都是屬於同一類,因此所有的UI元件的目錄結構應該相似。

Ant Design中每一個UI元件的目錄結構都如前幾章中所述。擁有一個清晰的目錄結構能夠方便我們進行程式碼管理,同時也可以使用指令碼做一些自動化的處理。比如Ant Design就通過指令碼來對所有components/**/style資料夾中的less檔案進行合併編譯。

元件分離

每個UI元件應該都是可以獨立被引用的,而且也應該優先使用“獨立引用”的方式。

Ant Design的每一個元件都可以被獨立引用,引用方式如下:

import Button from 'antd/lib/button';
複製程式碼

我們在使用第三方UI元件庫時,通常不會使用到上面所有的元件,而是經常使用到部分元件。因此我們在設計UI元件庫時,處於檔案大小的考慮,我們也應該保證每個UI元件都互不依賴(同一層級的元件,排除本身業務上就有依賴關係的元件),做到不使用的元件不引入,減小業務方檔案大小。

測試覆蓋

每一個UI元件都是獨立的,因此我們需要為每一個獨立的元件進行測試覆蓋。

Ant Design中通過Jest和上文提到的enzyme來對每一個元件進行測試,從而保證UI元件的程式碼質量。

總結

總體上來說,Ant Design相關的原始碼簡單易懂,結構也很清晰,非常容易閱讀。如果你對React開發有一定的瞭解,但是不知道如何進行元件的封裝,或者想了解當前主流的元件庫是如何實現的,推薦可以閱讀一下相關原始碼。你能夠從中瞭解到我們如何對UI元件進行切割和封裝。

當然,Ant Design不僅僅是一個UI元件庫,而是一整套UI規範,我們今天分享的只是這套規範在React上面的實現。如果對相關的UI規範有興趣的同學,可以去Ant Design的官網進行了解。

相關文章