React 中的五種元件形式

scq000發表於2017-07-18

目前的前端開發主流技術都已經往元件化方向發展了,而每學一種新的框架的時候,最基礎的部分一定是學習其元件的編寫方式。這就好像學習一門新的程式語言的時候,總是要從hello world開始一樣。而在React中,我們常用的元件編寫方式又有哪些呢?或者說各種不同的元件又可以分為幾類呢?

無狀態元件

無狀態元件(Stateless Component)是最基礎的元件形式,由於沒有狀態的影響所以就是純靜態展示的作用。一般來說,各種UI庫裡也是最開始會開發的元件類別。如按鈕、標籤、輸入框等。它的基本組成結構就是屬性(props)加上一個渲染函式(render)。由於不涉及到狀態的更新,所以這種元件的複用性也最強。

const PureComponent = (props) => (
    <div>
        //use props
    </div>
)複製程式碼

無狀態元件的寫法十分簡單,比起使用傳統的元件定義方式,我通常就直接使用ES6語法中提供的箭頭函式來宣告這種元件形式。當然,如果碰到稍微複雜點的,可能還會帶有生命週期的hook函式。這時候就需要用到Class Component的寫法了。

有狀態元件

在無狀態元件的基礎上,如果元件內部包含狀態(state)且狀態隨著事件或者外部的訊息而發生改變的時候,這就構成了有狀態元件(Stateful Component)。有狀態元件通常會帶有生命週期(lifecycle),用以在不同的時刻觸發狀態的更新。這種元件也是通常在寫業務邏輯中最經常使用到的,根據不同的業務場景元件的狀態數量以及生命週期機制也不盡相同。

class StatefulComponent extends Component {

    constructor(props) {
        super(props);
        this.state = {
            //定義狀態
        }
    }

    componentWillMount() {
        //do something
    }

    componentDidMount() {
        //do something
    }
    ... //其他生命週期

    render() {
        return (
            //render
        );
    }
}複製程式碼

容器元件

在具體的專案實踐中,我們通常的前端資料都是通過Ajax請求獲取的,而且獲取的後端資料也需要進一步的做處理。為了使元件的職責更加單一,引入了容器元件(Container Component)的概念。我們將資料獲取以及處理的邏輯放在容器元件中,使得元件的耦合性進一步地降低。

var UserListContainer = React.createClass({
  getInitialState: function() {
    return {
      users: []
    }
  },

  componentDidMount: function() {
    var _this = this;
    axios.get('/path/to/user-api').then(function(response) {
      _this.setState({users: response.data});
    });
  },

  render: function() {
    return (<UserList users={this.state.users} />);
  }
});複製程式碼

如上面這個容器元件,就是負責獲取使用者資料,然後以props的形式傳遞給UserList元件來渲染。容器元件也不會在頁面中渲染出具體的DOM節點,因此,它通常就充當資料來源的角色。目前很多常用的框架,也都採用這種元件形式。如:React Redux的connect(), Relay的createContainer(), Flux Utils的Container.create()等。

高階元件

其實對於一般的中小專案來說,你只需要用到以上的這三種元件方式就可以很好地構造出所需的應用了。但是當面對複雜的需求的時候,我們往往可以利用高階元件(Higher-Order Component)編寫出可重用性更強的元件。那麼什麼是高階元件呢?其實它和高階函式的概念類似,就是一個會返回元件的元件。或者更確切地說,它其實是一個會返回元件的函式。就像這樣:

const HigherOrderComponent = (WrappedComponent) => {
  return class WrapperComponent extends Component {
    render() {
      //do something with WrappedComponent
    }
  }
}複製程式碼

做為一個高階元件,可以在原有元件的基礎上,對其增加新的功能和行為。我們一般希望編寫的元件儘量純淨或者說其中的業務邏輯儘量單一。但是如果各種元件間又需要增加新功能,如列印日誌,獲取資料和校驗資料等和展示無關的邏輯的時候,這些公共的程式碼就會被重複寫很多遍。因此,我們可以抽象出一個高階元件,用以給基礎的元件增加這些功能,類似於外掛的效果。

一個比較常見的例子是表單的校驗。

//檢驗規則,表格元件
const FormValidator = (WrappedComponent, validator, trigger) => {

   getTrigger(trigger, validator) {
      var originTrigger = this.props[trigger];

      return function(event) {
          //觸發驗證機制,更新狀態
          // do something ...
          originTrigger(event);
      }
  }

  var newProps = {
    ...this.props,
    [trigger]:   this.getTrigger(trigger, validator) //觸發時機,重新繫結原有觸發機制
  };

  return <WrappedComponent  {...newProps} />
}複製程式碼

值得提一句,同樣是給元件增加新功能的方法,相比於使用mixins這種方式高階元件則更加簡潔和職責更加單一。你如果使用過多個mixins的時候,狀態汙染就十分容易發生,以及你很難從元件的定義上看出隱含在mixins中的邏輯。而高階元件的處理方式則更加容易維護。

另一方面,ES7中新的語法Decorator也可以用來實現和上面寫法一樣的效果。

function LogDecorator(msg) {
  return (WrappedComponent) => {
    return class LogHoc extends Component {
      render() {
        // do something with this component
        console.log(msg);
        <WrappedComponent {...this.props} />
      }
    }
  }
}

@LogDecorator('hello world')
class HelloComponent extends Component {

  render() {
    //...
  }
}複製程式碼

Render Callback元件

還有一種元件模式是在元件中使用渲染回撥的方式,將元件中的渲染邏輯委託給其子元件。就像這樣:

import { Component } from "react";

class RenderCallbackCmp extends Component {
  constructor(props) {
    super(props);
    this.state = {
      msg: "hello"
    };
  }

  render() {
    return this.props.children(this.state.msg);
  }
}

const ParentComponent = () =>
  (<RenderCallbackCmp>
    {msg =>
      //use the msg
      <div>
        {msg}
      </div>}
  </RenderCallbackCmp>);複製程式碼

父元件獲取了內部的渲染邏輯,因此在需要控制渲染機制時可以使用這種元件形式。

總結

以上這些元件編寫模式基本上可以覆蓋目前工作中所需要的模式。在寫一些複雜的框架元件的時候,仔細設計和研究元件間的解耦和組合方式,能夠使後續的專案可維護性大大增強。

參考文件

React Patterns - Render Callback
React Higher Order Components in depth

相關文章