玩轉 React(四)- 創造一個新的 HTML 標籤

sarike發表於2017-10-11

在第二篇文章 《新型前端開發方式》 中有說到 React 有很爽的一點就是給我們一種創造 HTML 標籤的能力,那麼今天這篇文章就詳細講解下 React 是如何提供這種能力的,作為前端開發者如何來運用這種能力。

在第三篇文章 《JavaScript程式碼裡寫HTML一樣可以很優雅》 中介紹了 JavaScript 的擴充套件語法 JSX,相信大家已經知道了,所謂的創造新的 HTML 的能力,其實就是以極其類似 HTML 的 JSX 語法來使用基於 React 編寫的檢視層元件。所以說,要完成今天的任務,我們只需要搞清楚一個問題即可:如何基於 React 編寫檢視層元件。

內容摘要

  • 定義元件兩種方式:類繼承元件、函式式元件。
  • 類繼承元件有更豐富的特性,函式式元件書寫更簡潔,執行效率更高。
  • 元件名稱首字母要大寫。
  • 屬性是一個元件的外部輸入。
  • 屬性值可以通過 {} 設定任意的 JS 表示式。
  • 屬性是隻讀的。
  • 屬性可以設定預設值。
  • 屬性可以設定型別,開發階段 React 會對屬性進行型別檢查。
  • 為元件所有屬性設定型別檢查是個好習慣,有助於協作開發。

通過內容摘要可以讓你快速瞭解本文內容是否對你有用,從而決定是否繼續閱讀,節省你的時間也是一件很有意義的事情。

定義元件的幾種姿勢

下面介紹一下在 React 中定義元件的幾種方式。

1. 類繼承

有過 Java 等物件導向開發經驗的同學一定很容易接受這種方式。ES6 為 JavaScript 增加了類和類繼承的特性。子類會繼承父類的“基因”(成員方法、屬性),如果父類是一個元件,那子類自然而然也是一個元件。

React 提供了 ComponentPureComponent 兩個父類,他們之間有一點點區別,我們在之後的文章中會詳細介紹,現在你可以將他們同等看待,暫且無須理會。

通過繼承自 React 提供的元件基類,我們可以這樣來建立一個元件:

import React, {Component} from 'react';

class HelloMessage extends Component {
    render() {
        return <div>Hello world.</div>;
    }
}複製程式碼

通過類繼承的方式建立一個元件,就是這麼簡單,只要繼承 Component 基類並實現 render 方法即可。然後就可以把 HelloMessage 當成一個新的“HTML 標籤”來用了,如下你可以把它渲染到頁面上:

ReactDOM.render(<HelloMessage />, document.querySelector('#root'));複製程式碼

你也可以用它來裝配其它元件,如:

import React, {Component} from 'react';

class HelloMessageList extends React.Component {
    render() {
        return (
            <div>
                <HelloMessage />
                <HelloMessage />
                <HelloMessage />
            </div>
        )
    }
}複製程式碼

當然,例子沒有任何實際意義,只是為了演示元件的定義及其用法。

演示程式碼:codepen.io/Sarike/pen/…

2. 函式式元件

顧名思義,函式式元件,就是以函式的形式來定義一個元件,如下所示:

import React from 'react';

function HelloMessage() {
    return <div>Hello world.</div>;
}

// 或者:

const HelloMessage = () => <div>Hello world.</div>;複製程式碼

實際上就是隻實現了類繼承方式中的 render 方法。

示例程式碼:codepen.io/Sarike/pen/…

類繼承 vs 函式式元件

這兩種定義元件的方式,在實際的開發中都經常會被用到,對大部分人來說類繼承的方式用得頻率會更高一些。

類繼承的方式,相較於函式式元件,雖然寫起來略繁瑣,但是它擁有更多的特性:

  • 內部狀態:state
  • 生命週期函式

函式式元件雖然沒有 state 和生命週期函式等特性,但是它有更簡潔的書寫方式,另外還有更好的效能,不用處理一些複雜的特性,執行效率當然高了。

現在你可以無需關心 state 和生命週期函式的具體作用,下一篇文章我會詳細講解,等你看完下一篇文章之後,至於選擇哪種方式的問題就很好解決了。在開發一個元件的時候,我是這樣來做的:當我一開始就知道這個元件會用到 state 或者生命週期函式時,毫無疑問直接使用類繼承的方式;如果一開始用不到這些特性也不確定未來會不會用到,那我就先用函式式元件,如果隨著業務的演進,元件需要應用這些特性的時候,我會再把它重構成類繼承的方式。這個重構非常簡單,只需要將原來的函式變成元件類的 render 方法即可。

另外,還有一點需要注意,不管那種方式,元件的名稱首字母必須為大寫。嚴格來說,是 JSX 要求使用者自定義的元件名首字母必須為大寫,如果是小寫字母開頭,那麼 React 會將其當成內建的元件直接將其渲染成一個 html 標籤,從而不會正確渲染使用者自定義的元件。

如果你非要將元件名稱以小寫字母開頭,那你在以 JSX 語法使用之前也必須將其賦值為一個大寫字母開頭的變數,如下所示:

function helloMessage() {
    return <div>Hello world.</div>
}

const HelloMessage = helloMessage;

ReactDOM.render(<HelloMessage />, mountNode);複製程式碼

但這有事何必呢,純粹是沒事兒找事兒,大家在實際專案開發時,直接將元件名以大寫字母開頭即可。

屬性

上面說完了在 React 中兩種定義元件的方式。在上面的例子中,我們定義的元件都是靜態的,然而在實際的開發中,檢視層元件往往會進行頻繁更新,或者需要從後端 API 獲取動態資料在元件中展示。這就需要元件擁有接收外部輸入的能力。

屬性是元件的輸入

在第二篇文章 《新型前端開發方式》 中有說到 “檢視是資料的對映”,那麼其中說的資料指的就是屬性。

如果把元件理解為一個函式,那麼屬性就是這個函式的引數,函式的返回值就是呈現到頁面上的檢視。而且通過上面部分的學習,在 React 中元件確實可以以函式的形式來定義,而且函式的引數就是一個包含當前元件接收到的所有屬性的物件。

如下所示帶有屬性 name 的元件定義:

import React, {Component} from 'react';

class HelloMessage extends Component {
    render() {
        return <div>Hello {this.props.name}.</div>;
    }
}複製程式碼

函式式:

import React from 'react';

function HelloMessage(props) {
    return <div>Hello {props.name}.</div>;
}

// 或者:

const HelloMessage = props => <div>Hello {props.name}.</div>;複製程式碼

屬性的傳遞也跟 HTML 一樣(在本文的最後一部分會有各種型別屬性的詳細介紹),如下所示:

import React, {Component} from 'react';
import ReactDOM from 'react-dom';

class HelloMessageList extends Component {
    render() {
        return (
            <div>
                <HelloMessage name="Lucy" />
                <HelloMessage name="Tom" />
                <HelloMessage name="Jack" />
            </div>
        )
    }
}

ReactDOM.render(<HelloMessageList />, document.querySelector('#root'));複製程式碼

這樣頁面上會展示出:

Hello Lucy.
Hello Tom.
Hello Jack.複製程式碼

示例程式碼:codepen.io/Sarike/pen/…

屬性必須為只讀的

屬性必須為只讀的,這一點非常重要,請嚴格遵守。對應到上面說到的,如果把元件理解為一個函式,那麼這個函式必須為一個純函式(Pure function),在純函式中不能修改其引數,確定的輸入必須有確定的輸出。

雖然有些時候,你修改了元件的屬性,貌似也能正常工作。沒錯,因為 JavaScript 語言特性的原因,沒人能阻止你這麼做。但是請先相信我,嚴格遵守這條規則不僅能讓你少踩很多坑,而且能讓你的應用穩定性更強、維護性更強。如果你直接修改元件的屬性,React 並不會感知到此修改,從而不會重新渲染元件,就導致了當前元件的檢視展示與資料不一致,但這個被修改的屬性會隨著下一次元件的渲染被生效到檢視上,而且這次渲染的時機是不確定的,不難想象,如果一個規模較大的專案裡充滿了這種不確定性是多麼痛苦的一件事情。總之,如果你隨意修改元件的屬性,會很容易讓你的應用充滿許多難以排查的 BUG。

預設屬性

通常情況下,我們需要為元件的屬性設為預設值。就像 HTML 標籤的屬性也有預設值一樣,例如 form 標籤的 method 屬性預設值是 GET,input 標籤的 type 屬性預設值是 text 一樣。

還是上面 HelloMessage 元件,如果需求是當不傳入 name 屬性時,預設展示 Hello World.,也就是說 name 屬性的預設值是 World。

一種很容易想到的做法:

<div>Hello {this.props.name || 'World'}.</div>複製程式碼

這樣確實可以解決當前這個需求,但是屬性可能還會是個 Object,也可能是個函式,你當然可以先判斷下這個屬性是否為 undefined 然後決定是否使用預設值,但是這樣會讓程式碼顯得很不優雅,而且也會增加很多繁瑣的判斷邏輯。

因此,React 提供了相應的機制可以設定元件屬性的預設值,如下所示,你需要通過元件的靜態欄位 defaultProps 來設定元件屬性的預設值。如下所示:

import React, {Component} from 'react';

class HelloMessage extends Component {
    render() {
        return <div>Hello {this.props.name}.</div>;
    }
}
HelloMessage.defaultProps = {
    name: 'World'
}複製程式碼

這樣就可以了,<HelloMessage /> 這樣不為元件設定任何屬性,那麼它就會在頁面上展示Hello World.

示例程式碼:codepen.io/Sarike/pen/…

屬性的型別及校驗

在開發較複雜的前端應用時,我們經常會遇到許多因為型別檢查導致的問題,例如上面的 HelloMessage 元件,我期望其 name 屬性只能是字串型別的,你要是給我一個 Object,我是沒法正確展示的。為了在開發過程中儘快的發現這類問題,React 為元件新增了型別檢查的機制,你需要給元件設定靜態欄位 propTypes 來設定元件各個屬性的型別檢查器。

import React, {Component} from 'react';
import PropTypes from 'prop-types';

class HelloMessage extends Component {
    render() {
        return <div>Hello {this.props.name}.</div>;
    }
}
HelloMessage.defaultProps = {
    name: 'World'
}
HelloMessage.propTypes = {
    name: PropTypes.string
}複製程式碼

這樣在開發過程中 React 就能校驗元件接收到的屬性值是否符合指定的型別,如果校驗不通過,將會丟擲警告。React 只會在開發模式下進行屬性型別檢查,當程式碼進行生產釋出後,為了減少額外的效能開銷,型別檢查將會被略過。

其實,為每一個元件編寫完善的屬性型別是一個非常好的習慣,這不僅能及時發現問題,更重要的是配合幾句簡單額註釋,這將成為該元件一份非常好的文件,一個完善的元件應該具有良好的封裝性和易複用性,在一個協作開發的專案中,其他開發者需要引用你開發的元件時,只需要看一下元件的屬性列表,大致就可以瞭解如何來使用這個元件,省去了很多不必要的溝通。

下面是 React 提供的可用的資料型別檢查器。

  • PropTypes.array
  • PropTypes.bool
  • PropTypes.func
  • PropTypes.number
  • PropTypes.object
  • PropTypes.string
  • PropTypes.symbol
  • PropTypes.element 元素,其實就是 JSX 表示式,上一篇文章有說過 JSX 是 React.createElement 的語法糖,一個 JSX 表示式實際上會生成一個 JS 物件,在 React 中稱之為元素(Element)。
  • PropTypes.node 所有可以被渲染的資料型別,包括:數值, 字串, 元素或者這些型別的陣列。
  • PropTypes.instanceOf(Message) 某個類的例項
  • PropTypes.oneOf(['News', 'Photos']) 列舉,屬性值必須為其中的某一個值。
  • PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 型別列舉,屬性必須為其中某一個型別。
  • PropTypes.arrayOf(PropTypes.number) 屬性為一個陣列,且陣列中的元素必須符合指定型別。
  • PropTypes.objectOf(PropTypes.number) 屬性為一個物件,且物件中的各個欄位的值必須符合指定型別。
  • PropTypes.any 任何型別

如果你想指定某些屬性為必需屬性,你可以鏈式調動其 isRequired 來標識某個屬性對於當前元件來說是必需的。如果在使用元件時未指定則會丟擲警告提醒。

另外你還可以通過一個函式自定義屬性驗證器,如果驗證不通過你需要返回一個 Error 例項,如下所示:

function(props, propName, componentName) {
  if (!/matchme/.test(props[propName])) {
    return new Error(
      'Invalid prop `' + propName + '` supplied to' +
      ' `' + componentName + '`. Validation failed.'
    );
  }
}複製程式碼

設定元件的屬性值

上面我們們瞭解到元件的屬性有很多種型別,下面說一下各種型別的屬性是如何傳遞給元件的。其實很簡單,屬性的值可以用一對大括號 { } 來包圍,其中可以指定任意的 JavaScript 表示式。如下所示:

return (
    <User
        name="Tom"                            // 字串
        age={18}                              // 數值
        isActivated={true}                    // 布林值
        interests={['basketball', 'music']}   // 陣列
        address={{ city: 'Beijing', road: 'BeiWuHuan' }} // 物件
    />
)複製程式碼

展開操作符

你也可以用展開操作符 ... 將一個物件的所有欄位展開,依次作為屬性傳遞給元件,上面的程式碼等價於:

const userInfo = {
    name: 'Tom',
    age: 18,
    isActivated: true,
    interests: ['basketball', 'music'],
    address: { city: 'Beijing', road: 'BeiWuHuan' }
}
return <User {...userInfo} />複製程式碼

值為 true 的屬性的簡寫

如果是屬性型別為布林值,且當前屬性值為 true 可以只寫屬性名,如下所示:

<input
    disabled     // 禁用該輸入框
    type="text"
/>複製程式碼

children 屬性

使用者自定義的元件內可以通過 this.props.children 來獲取一個特殊的屬性。該屬性與其它屬性的區別就是傳遞方式不同。

children 屬性的值是指一對閉合的 JSX 標籤中間的內容,如下所示:

<UserList>
    <User name="Tom" />
    <User name="Lucy" />
</UserList>複製程式碼

那麼在 UserList 內部可以通過 this.props.children 來獲取下面這個 JSX 片段:

<User name="Tom" />
<User name="Lucy" />複製程式碼

該示例中,獲取到的實際上是一個包含兩個 User 元素物件的陣列。

總結

本文主要介紹了在 React 中元件的定義方式,以及幾個關鍵的注意事項。另外介紹了元件屬性的作用、屬性預設值、屬性型別校驗以及如何為元件傳遞屬性。

希望內容對大家有用,如有任何問題和建議可以給我留言,謝謝。


PS:本系列的所有文章將在 segmentfault 和 掘金 同步釋出。

本作品保留所有權利。未獲得許可人許可前,不允許他人複製、發行、展覽和表演作品。不允許他人基於該作品創作演繹作品 。