通過例項,學習編寫 React 元件的“最佳實踐”

LucasHC發表於2017-07-11

現在前端程式設計師都知道,React 是元件化的。當我開始學習 React 的時候,我記得當時已經存在了很多不同編寫元件的方式了。如今,React
社群已經愈發成熟,但是對於元件正確編寫姿勢卻沒有一個相對完備的指導。

這篇文章僅從作者的觀點出發,來談一談我們究竟應該如何來寫高質量的 React 元件。

在開始前,需要說明以下幾個問題:

  • 這篇文章以及程式碼例項,都採用了 ES6 或者 ES7 的寫法;
  • 對於一些基本概念不再進行科普。適合有 React 初級經驗的讀者閱讀;
  • 如果有任何問題,歡迎留言交流。

基於 Class 的元件最佳實踐(Class Based Components)

基於 Class 的元件是狀態化的,包含有自身方法、生命週期函式、元件內狀態等。最佳實踐包括但不限於以下一些內容:

1)引入 CSS 依賴 (Importing CSS)

我很喜歡 CSS in JavaScript 這一理念。在 React 中,我們可以為每一個 React 元件引入相應的 CSS 檔案,這一“夢想”成為了現實。在下面的程式碼示例,我把 CSS 檔案的引入與其他依賴隔行分開,以示區別:

import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'複製程式碼

當然,這並不是真正意義上的 CSS in JS,具體實現其實社群上有很多方案。我的 Github 上 fork 了一份各種 CSS in JS 方案的多維度對比,感興趣的讀者可以點選這裡

2)設定初始狀態(Initializing State)

在編寫元件過程中,一定要注意初始狀態的設定。利用 ES6 模組化的知識,我們確保該元件暴露都是 “export default” 形式,方便其他模組(元件)的呼叫和團隊協作。

import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {
    state = { expanded: false }
    ......複製程式碼

3)設定 propTypes 和 defaultProps

propTypes 和 defaultProps 都是元件的靜態屬性。在元件的程式碼中,這兩個屬性的設定位置越高越好。因為這樣方便其他閱讀程式碼者或者開發者自己 review,一眼就能看到這些資訊。這些資訊就如同元件文件一樣,對於理解或熟悉當前元件非常重要。

同樣,原則上,你編寫的元件都需要有 propTypes 屬性。如同以下程式碼:

export default class ProfileContainer extends Component {
    state = { expanded: false }

    static propTypes = {
        model: React.PropTypes.object.isRequired,
        title: React.PropTypes.string
    }

    static defaultProps = {
        model: {
            id: 0
        },
        title: 'Your Name'
    }複製程式碼

Functional Components 是指沒有狀態、沒有方法,純元件。我們應該最大限度地編寫和使用這一類元件。這類元件作為函式,其引數就是 props, 我們可以合理設定初始狀態和賦值。

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
    const formStyle = expanded ? {height: 'auto'} : {height: 0}
    return (
        <form style={formStyle} onSubmit={onSubmit}>
            {children}
        <button onClick={onExpand}>Expand</button>
        </form>
    )
}複製程式碼

4)元件方法(Methods)

在編寫元件方法時,尤其是你將一個方法作為 props 傳遞給子元件時,需要確保 this 的正確指向。我們通常使用 bind 或者 ES6 箭頭函式來達到此目的。

export default class ProfileContainer extends Component {
    state = { expanded: false }

    handleSubmit = (e) => {
        e.preventDefault()
        this.props.model.save()
    }

    handleNameChange = (e) => {
        this.props.model.changeName(e.target.value)
    }

    handleExpand = (e) => {
        e.preventDefault()
        this.setState({ expanded: !this.state.expanded })
    }複製程式碼

當然,這並不是唯一做法。實現方式多種多樣,我專門有一片文章來對比 React 中對於元件 this 的繫結,可以點選此處參考。

5)setState 接受一個函式作為引數(Passing setState a Function)

在上面的程式碼示例中,我們使用了:

this.setState({ expanded: !this.state.expanded })複製程式碼

這裡,關於 setState hook 函式,其實有一個非常“有意思”的問題。React 在設計時,為了效能上的優化,採用了 Batch 思想,會收集“一波” state 的變化,統一進行處理。就像瀏覽器繪製文件的實現一樣。所以 setState 之後,state 也許不會馬上就發生變化,這是一個非同步的過程。

這說明,我們要謹慎地在 setState 中使用當前的 state,因為當前的state 也許並不可靠。
為了規避這個問題,我們可以這樣做:

this.setState(prevState => ({ expanded: !prevState.expanded }))複製程式碼

我們給 setState 方法傳遞一個函式,函式引數為上一刻 state,便保證setState 能夠立刻執行。

關於 React setState 的設計, Eric Elliott 也曾經這麼噴過:setState() Gate,並由此展開了多方“撕逼”。作為圍觀群眾,我們在吃瓜的同時,一定會在大神論道當中收穫很多思想,建議閱讀。

如果你對 setState 方法的非同步性還有困惑,可以同我討論,這裡不再展開。

6)合理利用解構(Destructuring Props)

這個其實沒有太多可說的,仔細觀察程式碼吧:我們使用瞭解構賦值。除此之外,如果一個元件有很多的 props 的話,每個 props 應該都另起一行,這樣書寫上和閱讀性上都有更好的體驗。

export default class ProfileContainer extends Component {
    state = { expanded: false }

    handleSubmit = (e) => {
        e.preventDefault()
        this.props.model.save()
    }

    handleNameChange = (e) => {
        this.props.model.changeName(e.target.value)
    }

    handleExpand = (e) => {
        e.preventDefault()
        this.setState(prevState => ({ expanded: !prevState.expanded }))
    }

    render() {
        const {model, title} = this.props

        return ( 
            <ExpandableForm 
            onSubmit={this.handleSubmit} 
            expanded={this.state.expanded} 
            onExpand={this.handleExpand}>
                <div>
                    <h1>{title}</h1>
                    <input
                    type="text"
                    value={model.name}
                    onChange={this.handleNameChange}
                    placeholder="Your Name"/>
                </div>
            </ExpandableForm>
        )
    }
}複製程式碼

7)使用修飾器(Decorators)

這一條是對使用 mobx 的開發者來說的。如果你不懂 mobx,可以大體掃一眼。
我們強調使用 ES next decorate 來修飾我們的元件,如同:

@observer
export default class ProfileContainer extends Component {複製程式碼

使用修飾器更加靈活且可讀性更高。即便你不使用修飾器,也需要如此暴露你的元件:

class ProfileContainer extends Component {
    // Component code
}
export default observer(ProfileContainer)複製程式碼

8)閉包(Closures)

一定要儘量避免以下用法:

<input
    type="text"
    value={model.name}
    // onChange={(e) => { model.name = e.target.value }}
    // ^ Not this. Use the below:
    onChange={this.handleChange}
    placeholder="Your Name"/>複製程式碼

不要:

onChange = {(e) => { model.name = e.target.value }}複製程式碼

而是:

onChange = {this.handleChange}複製程式碼

原因其實很簡單,每次父元件 render 的時候,都會新建一個新的函式並傳遞給 input。
如果 input 是一個 React 元件,這會粗暴地直接導致這個元件的 re-render,需要知道,Reconciliation 可是 React 成本最高的部分。

另外,我們推薦的方法,會使得閱讀、除錯和更改更加方便。

9)JSX中的條件判別(Conditionals in JSX)

真正寫過 React 專案的同學一定會明白,JSX 中可能會存在大量的條件判別,以達到根據不同的情況渲染不同元件形態的效果。
就像下圖這樣:

返例
返例

這樣的結果是不理想的。我們丟失了程式碼的可讀性,也使得程式碼組織顯得混亂異常。多層次的巢狀也是應該避免的。

針對於此,有很對類庫來解決 JSX-Control Statements 此類問題,但是與其引入第三方類庫的依賴,還不如我們先自己嘗試探索解決問題。

此時,是不是有點懷念if...else?
我們可以使用大括號內包含立即執行函式IIFE,來達到使用 if...else 的目的:

解決思路
解決思路

當然,大量使用立即執行函式會造成效能上的損失。所以,考慮程式碼可讀性上的權衡,還是有必要好好斟酌的。
我更加建議的做法是分解此元件,因為這個元件的邏輯已經過於複雜而臃腫了。如何分解?請看我這篇文章。

總結

其實所謂 React “最佳實踐”,想必每個團隊都有自己的一套“心得”,哪裡有一個統一套? 本文指出的幾種方法未必對任何讀者都適用。針對不同的程式碼風格,開發習慣,擁有自己團隊一套“最佳實踐”是很有必要的。從另一方面,也說明了 React 技術棧本身的靈活於強大。

另外,這篇文章並不是我原創,而是翻譯了Our Best Practices for Writing React Components一文,並在此基礎上進行了較大幅度擴充套件。

如果您對React生態有興趣,同樣推薦我的其他幾篇文章:

Happy Coding!

PS:
作者Github倉庫知乎問答連結
歡迎各種形式交流。

相關文章