React 入門例項教程

阮一峰發表於2015-03-31

上週,基於 React 的 React Native 釋出,結果一天之內,就獲得了 5000 顆星,受矚目程度可見一斑。

React 起源於 Facebook 的內部專案,因為該公司對市場上所有 JavaScript MVC 框架,都不滿意,就決定自己寫一套,用來架設 Instagram 的網站。做出來以後,發現這套東西很好用,就在2013年5月開源了。

由於 React 的設計思想極其獨特,屬於革命性創新,效能出眾,程式碼邏輯卻非常簡單。所以,越來越多的人開始關注和使用,認為它可能是將來 Web 開發的主流工具。

這個專案本身也越滾越大,從最早的UI引擎變成了一整套前後端通吃的 Web App 解決方案。衍生的 React Native 專案,目標更是宏偉,希望用寫 Web App 的方式去寫 Native App。如果能夠實現,整個網際網路行業都會被顛覆,因為同一組人只需要寫一次 UI ,就能同時執行在伺服器、瀏覽器和手機(參見《也許,DOM 不是答案》)。

既然 React 這麼熱門,看上去充滿希望,當然應該好好學一下。從技術角度,可以滿足好奇心,提高技術水平;從職業角度,有利於求職和晉升,有利於參與潛力大的專案。但是,好的 React 教程卻不容易找到,這一方面因為這項技術太新,剛剛開始走紅,大家都沒有經驗,還在摸索之中;另一方面因為 React 本身還在不斷變動,API 一直在調整,至今沒釋出1.0版。

我學習 React 時,就很苦惱。有的教程討論一些細節問題,對入門沒幫助;有的教程寫得不錯,但比較短,無助於看清全貌。我斷斷續續學了幾個月,看過二十幾篇教程,在這個過程中,將對自己有幫助的 Demo 都收集下來,做成了一個庫 React Demos

下面,我就根據這個庫,寫一篇全面又易懂的 React 入門教程。你只需要跟著每一個 Demo 做一遍,就能初步掌握 React 。當然,前提是你必須擁有基本 JavaScript 和 DOM 知識,但是你讀完就會發現,React 所要求的預備知識真的很少。

零、安裝

React 的安裝包,可以到官網下載。不過,React Demos 已經自帶 React 原始碼,不用另外安裝,只需把這個庫複製到你的硬碟就行了。


$ git clone [email protected]:ruanyf/react-demos.git

如果你沒安裝 git, 那就直接下載 zip 壓縮包

下面要講解的12個例子在各個 Demo 子目錄,每個目錄都有一個 index.html 檔案,在瀏覽器開啟這個檔案(大多數情況下雙擊即可),就能立刻看到效果。

需要說明的是,React 可以在瀏覽器執行,也可以在伺服器執行,但是本教程只涉及瀏覽器。一方面是為了儘量保持簡單,另一方面 React 的語法是一致的,伺服器的用法與瀏覽器差別不大。Demo13 是伺服器首屏渲染的例子,有興趣的朋友可以自己去看原始碼。

一、HTML 模板

使用 React 的網頁原始碼,結構大致如下。


<!DOCTYPE html>
<html>
  <head>
    <script src="../build/react.js"></script>
    <script src="../build/react-dom.js"></script>
    <script src="../build/browser.min.js"></script>
  </head>
  <body>
    <div id="example"></div>
    <script type="text/babel">
      // ** Our code goes here! **
    </script>
  </body>
</html>

上面程式碼有兩個地方需要注意。首先,最後一個 <script> 標籤的 type 屬性為 text/babel 。這是因為 React 獨有的 JSX 語法,跟 JavaScript 不相容。凡是使用 JSX 的地方,都要加上 type="text/babel"

其次,上面程式碼一共用了三個庫: react.jsreact-dom.jsBrowser.js ,它們必須首先載入。其中,react.js 是 React 的核心庫,react-dom.js 是提供與 DOM 相關的功能,Browser.js 的作用是將 JSX 語法轉為 JavaScript 語法,這一步很消耗時間,實際上線的時候,應該將它放到伺服器完成。


$ babel src --out-dir build

上面命令可以將 src 子目錄的 js 檔案進行語法轉換,轉碼後的檔案全部放在 build 子目錄。

二、ReactDOM.render()

ReactDOM.render 是 React 的最基本方法,用於將模板轉為 HTML 語言,並插入指定的 DOM 節點。


ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('example')
);

上面程式碼將一個 h1 標題,插入 example 節點(檢視 demo01),執行結果如下。

三、JSX 語法

上一節的程式碼, HTML 語言直接寫在 JavaScript 語言之中,不加任何引號,這就是 JSX 的語法,它允許 HTML 與 JavaScript 的混寫(檢視 Demo02 )。


var names = ['Alice', 'Emily', 'Kate'];

ReactDOM.render(
  <div>
  {
    names.map(function (name) {
      return <div>Hello, {name}!</div>
    })
  }
  </div>,
  document.getElementById('example')
);

上面程式碼體現了 JSX 的基本語法規則:遇到 HTML 標籤(以 < 開頭),就用 HTML 規則解析;遇到程式碼塊(以 { 開頭),就用 JavaScript 規則解析。上面程式碼的執行結果如下。

JSX 允許直接在模板插入 JavaScript 變數。如果這個變數是一個陣列,則會展開這個陣列的所有成員(檢視 demo03 )。


var arr = [
  <h1>Hello world!</h1>,
  <h2>React is awesome</h2>,
];
ReactDOM.render(
  <div>{arr}</div>,
  document.getElementById('example')
);

上面程式碼的arr變數是一個陣列,結果 JSX 會把它的所有成員,新增到模板,執行結果如下。

四、元件

React 允許將程式碼封裝成元件(component),然後像插入普通 HTML 標籤一樣,在網頁中插入這個元件。React.createClass 方法就用於生成一個元件類(檢視 demo04)。


var HelloMessage = React.createClass({
  render: function() {
    return <h1>Hello {this.props.name}</h1>;
  }
});

ReactDOM.render(
  <HelloMessage name="John" />,
  document.getElementById('example')
);

上面程式碼中,變數 HelloMessage 就是一個元件類。模板插入 <HelloMessage /> 時,會自動生成 HelloMessage 的一個例項(下文的"元件"都指元件類的例項)。所有元件類都必須有自己的 render 方法,用於輸出元件。

注意,元件類的第一個字母必須大寫,否則會報錯,比如HelloMessage不能寫成helloMessage。另外,元件類只能包含一個頂層標籤,否則也會報錯。


var HelloMessage = React.createClass({
  render: function() {
    return <h1>
      Hello {this.props.name}
    </h1><p>
      some text
    </p>;
  }
});

上面程式碼會報錯,因為HelloMessage元件包含了兩個頂層標籤:h1p

元件的用法與原生的 HTML 標籤完全一致,可以任意加入屬性,比如 <HelloMessage name="John"> ,就是 HelloMessage 元件加入一個 name 屬性,值為 John。元件的屬性可以在元件類的 this.props 物件上獲取,比如 name 屬性就可以透過 this.props.name 讀取。上面程式碼的執行結果如下。

新增元件屬性,有一個地方需要注意,就是 class 屬性需要寫成 classNamefor 屬性需要寫成 htmlFor ,這是因為 classfor 是 JavaScript 的保留字。

五、this.props.children

this.props 物件的屬性與元件的屬性一一對應,但是有一個例外,就是 this.props.children 屬性。它表示元件的所有子節點(檢視 demo05)。


var NotesList = React.createClass({
  render: function() {
    return (
      <ol>
      {
        React.Children.map(this.props.children, function (child) {
          return <li>{child}</li>;
        })
      }
      &lt/ol>
    );
  }
});

ReactDOM.render(
  <NotesList>
    <span>hello</span>
    <span>world</span>
  </NotesList>,
  document.body
);

上面程式碼的 NoteList 元件有兩個 span 子節點,它們都可以透過 this.props.children 讀取,執行結果如下。

這裡需要注意, this.props.children 的值有三種可能:如果當前元件沒有子節點,它就是 undefined ;如果有一個子節點,資料型別是 object ;如果有多個子節點,資料型別就是 array 。所以,處理 this.props.children 的時候要小心。

React 提供一個工具方法 React.Children 來處理 this.props.children 。我們可以用 React.Children.map 來遍歷子節點,而不用擔心 this.props.children 的資料型別是 undefined 還是 object。更多的 React.Children 的方法,請參考官方文件

六、PropTypes

元件的屬性可以接受任意值,字串、物件、函式等等都可以。有時,我們需要一種機制,驗證別人使用元件時,提供的引數是否符合要求。

元件類的PropTypes屬性,就是用來驗證元件例項的屬性是否符合要求(檢視 demo06)。


var MyTitle = React.createClass({
  propTypes: {
    title: React.PropTypes.string.isRequired,
  },

  render: function() {
     return <h1> {this.props.title} </h1>;
   }
});

上面的Mytitle元件有一個title屬性。PropTypes 告訴 React,這個 title 屬性是必須的,而且它的值必須是字串。現在,我們設定 title 屬性的值是一個數值。


var data = 123;

ReactDOM.render(
  <MyTitle title={data} />,
  document.body
);

這樣一來,title屬性就通不過驗證了。控制檯會顯示一行錯誤資訊。


Warning: Failed propType: Invalid prop `title` of type `number` supplied to `MyTitle`, expected `string`.

更多的PropTypes設定,可以檢視官方文件

此外,getDefaultProps 方法可以用來設定元件屬性的預設值。


var MyTitle = React.createClass({
  getDefaultProps : function () {
    return {
      title : 'Hello World'
    };
  },

  render: function() {
     return <h1> {this.props.title} </h1>;
   }
});

ReactDOM.render(
  <MyTitle />,
  document.body
);

上面程式碼會輸出"Hello World"。

七、獲取真實的DOM節點

元件並不是真實的 DOM 節點,而是存在於記憶體之中的一種資料結構,叫做虛擬 DOM (virtual DOM)。只有當它插入文件以後,才會變成真實的 DOM 。根據 React 的設計,所有的 DOM 變動,都先在虛擬 DOM 上發生,然後再將實際發生變動的部分,反映在真實 DOM上,這種演算法叫做 DOM diff ,它可以極大提高網頁的效能表現。

但是,有時需要從元件獲取真實 DOM 的節點,這時就要用到 ref 屬性(檢視 demo07 )。


var MyComponent = React.createClass({
  handleClick: function() {
    this.refs.myTextInput.focus();
  },
  render: function() {
    return (
      <div>
        <input type="text" ref="myTextInput" />
        <input type="button" value="Focus the text input" onClick={this.handleClick} />
      </div>
    );
  }
});

ReactDOM.render(
  <MyComponent />,
  document.getElementById('example')
);

上面程式碼中,元件 MyComponent 的子節點有一個文字輸入框,用於獲取使用者的輸入。這時就必須獲取真實的 DOM 節點,虛擬 DOM 是拿不到使用者輸入的。為了做到這一點,文字輸入框必須有一個 ref 屬性,然後 this.refs.[refName] 就會返回這個真實的 DOM 節點。

需要注意的是,由於 this.refs.[refName] 屬性獲取的是真實 DOM ,所以必須等到虛擬 DOM 插入文件以後,才能使用這個屬性,否則會報錯。上面程式碼中,透過為元件指定 Click 事件的回撥函式,確保了只有等到真實 DOM 發生 Click 事件之後,才會讀取 this.refs.[refName] 屬性。

React 元件支援很多事件,除了 Click 事件以外,還有 KeyDownCopyScroll 等,完整的事件清單請檢視官方文件

八、this.state

元件免不了要與使用者互動,React 的一大創新,就是將元件看成是一個狀態機,一開始有一個初始狀態,然後使用者互動,導致狀態變化,從而觸發重新渲染 UI (檢視 demo08 )。


var LikeButton = React.createClass({
  getInitialState: function() {
    return {liked: false};
  },
  handleClick: function(event) {
    this.setState({liked: !this.state.liked});
  },
  render: function() {
    var text = this.state.liked ? 'like' : 'haven\'t liked';
    return (
      <p onClick={this.handleClick}>
        You {text} this. Click to toggle.
      </p>
    );
  }
});

ReactDOM.render(
  <LikeButton />,
  document.getElementById('example')
);

上面程式碼是一個 LikeButton 元件,它的 getInitialState 方法用於定義初始狀態,也就是一個物件,這個物件可以透過 this.state 屬性讀取。當使用者點選元件,導致狀態變化,this.setState 方法就修改狀態值,每次修改以後,自動呼叫 this.render 方法,再次渲染元件。

由於 this.propsthis.state 都用於描述元件的特性,可能會產生混淆。一個簡單的區分方法是,this.props 表示那些一旦定義,就不再改變的特性,而 this.state 是會隨著使用者互動而產生變化的特性。

九、表單

使用者在表單填入的內容,屬於使用者跟元件的互動,所以不能用 this.props 讀取(檢視 demo9 )。


var Input = React.createClass({
  getInitialState: function() {
    return {value: 'Hello!'};
  },
  handleChange: function(event) {
    this.setState({value: event.target.value});
  },
  render: function () {
    var value = this.state.value;
    return (
      <div>
        <input type="text" value={value} onChange={this.handleChange} />
        <p>{value}</p>
      </div>
    );
  }
});

ReactDOM.render(<Input/>, document.body);

上面程式碼中,文字輸入框的值,不能用 this.props.value 讀取,而要定義一個 onChange 事件的回撥函式,透過 event.target.value 讀取使用者輸入的值。textarea 元素、select元素、radio元素都屬於這種情況,更多介紹請參考官方文件

十、元件的生命週期

元件的生命週期分成三個狀態:

  • Mounting:已插入真實 DOM
  • Updating:正在被重新渲染
  • Unmounting:已移出真實 DOM

React 為每個狀態都提供了兩種處理函式,will 函式在進入狀態之前呼叫,did 函式在進入狀態之後呼叫,三種狀態共計五種處理函式。

  • componentWillMount()
  • componentDidMount()
  • componentWillUpdate(object nextProps, object nextState)
  • componentDidUpdate(object prevProps, object prevState)
  • componentWillUnmount()

此外,React 還提供兩種特殊狀態的處理函式。

  • componentWillReceiveProps(object nextProps):已載入元件收到新的引數時呼叫
  • shouldComponentUpdate(object nextProps, object nextState):元件判斷是否重新渲染時呼叫

這些方法的詳細說明,可以參考官方文件。下面是一個例子(檢視 demo10 )。


var Hello = React.createClass({
  getInitialState: function () {
    return {
      opacity: 1.0
    };
  },

  componentDidMount: function () {
    this.timer = setInterval(function () {
      var opacity = this.state.opacity;
      opacity -= .05;
      if (opacity < 0.1) {
        opacity = 1.0;
      }
      this.setState({
        opacity: opacity
      });
    }.bind(this), 100);
  },

  render: function () {
    return (
      <div style={{opacity: this.state.opacity}}>
        Hello {this.props.name}
      </div>
    );
  }
});

ReactDOM.render(
  <Hello name="world"/>,
  document.body
);

上面程式碼在hello元件載入以後,透過 componentDidMount 方法設定一個定時器,每隔100毫秒,就重新設定元件的透明度,從而引發重新渲染。

另外,元件的style屬性的設定方式也值得注意,不能寫成


style="opacity:{this.state.opacity};"

而要寫成


style={{opacity: this.state.opacity}}

這是因為 React 元件樣式是一個物件,所以第一重大括號表示這是 JavaScript 語法,第二重大括號表示樣式物件。

十一、Ajax

元件的資料來源,通常是透過 Ajax 請求從伺服器獲取,可以使用 componentDidMount 方法設定 Ajax 請求,等到請求成功,再用 this.setState 方法重新渲染 UI (檢視 demo11 )。


var UserGist = React.createClass({
  getInitialState: function() {
    return {
      username: '',
      lastGistUrl: ''
    };
  },

  componentDidMount: function() {
    $.get(this.props.source, function(result) {
      var lastGist = result[0];
      if (this.isMounted()) {
        this.setState({
          username: lastGist.owner.login,
          lastGistUrl: lastGist.html_url
        });
      }
    }.bind(this));
  },

  render: function() {
    return (
      <div>
        {this.state.username}'s last gist is
        <a href={this.state.lastGistUrl}>here</a>.
      </div>
    );
  }
});

ReactDOM.render(
  <UserGist source="https://api.github.com/users/octocat/gists" />,
  document.body
);

上面程式碼使用 jQuery 完成 Ajax 請求,這是為了便於說明。React 本身沒有任何依賴,完全可以不用jQuery,而使用其他庫。

我們甚至可以把一個Promise物件傳入元件,請看Demo12


ReactDOM.render(
  <RepoList
    promise={$.getJSON('https://api.github.com/search/repositories?q=javascript&sort=stars')}
  />,
  document.body
);

上面程式碼從Github的API抓取資料,然後將Promise物件作為屬性,傳給RepoList元件。

如果Promise物件正在抓取資料(pending狀態),元件顯示"正在載入";如果Promise物件報錯(rejected狀態),元件顯示報錯資訊;如果Promise物件抓取資料成功(fulfilled狀態),元件顯示獲取的資料。


var RepoList = React.createClass({
  getInitialState: function() {
    return { loading: true, error: null, data: null};
  },

  componentDidMount() {
    this.props.promise.then(
      value => this.setState({loading: false, data: value}),
      error => this.setState({loading: false, error: error}));
  },

  render: function() {
    if (this.state.loading) {
      return <span>Loading...</span>;
    }
    else if (this.state.error !== null) {
      return <span>Error: {this.state.error.message}</span>;
    }
    else {
      var repos = this.state.data.items;
      var repoList = repos.map(function (repo) {
        return (
          <li>
            <a href={repo.html_url}>{repo.name}</a> ({repo.stargazers_count} stars) <br/> {repo.description}
          </li>
        );
      });
      return (
        <main>
          <h1>Most Popular JavaScript Projects in Github</h1>
          <ol>{repoList}</ol>
        </main>
      );
    }
  }
});

十二、參考連結

  1. React's official site
  2. React's official examples
  3. React (Virtual) DOM Terminology, by Sebastian Markbåge
  4. The React Quick Start Guide, by Jack Callister
  5. Learning React.js: Getting Started and Concepts, by Ken Wheeler
  6. Getting started with React, by Ryan Clark
  7. React JS Tutorial and Guide to the Gotchas, by Justin Deal
  8. React Primer, by Binary Muse
  9. jQuery versus React.js thinking, by zigomir

(完)

相關文章