概述
在我們進行日常的專案開發的過程中,我們經常會遇到使用一些通用的UI元件庫如BootStrap、Ant Design等。作為成熟的UI元件庫,它能夠提供提供一整套UI元件用來滿足使用需求,能大大減少開發成本。
在使用了他人提供的元件庫後,我自然就會有興趣去了解一下別人開發的元件庫到底是如何設計的,如何進行相關的元件封裝。本文以Ant Design為例,讓我們來了解一下目前較為有名的UI元件庫是如何設計與實現的?同時,我們又能夠有哪些經驗可以借鑑?
閱讀本文,你最好有如下的基礎知識來幫助你理解本文內容:
- 對React相關開發有一定的瞭解,對React的元件有一定的認知。
- 對JavaScript有一定的瞭解,最好能夠了解TypeScript(不瞭解也能夠理解文章內容)。
- [選]對單元測試有一定的瞭解(不瞭解可以先閱讀我的前一篇文章提高程式碼質量——使用Jest和Sinon給已有的程式碼新增單元測試)。
PS: 部落格寫一半,手受傷骨折了囧囧囧,後面大部分文字是通過語音輸入轉換的,如果有什麼錯誤或者邏輯不清晰,歡迎在評論中指正。
如何實現單個的React UI元件
首先,需要了解Ant Design提供的元件,我們先來看下單個元件是如何實現的。
目錄結構
需要了解一個元件的內容,我們應該先從目錄介面開始。我們以avatar頭像元件為例,目錄結構如下圖所示:
我們一個一個來看一下:
- 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
, size
,src
等欄位,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的官網進行了解。