《React官方文件》之教程Tutorial

青衫無名發表於2017-05-19

教程Tutorial

 我們建立一個簡單但實際的評論框,Disqus, LiveFyre或Facebook可以提供實時評論,評論框可以放在一個部落格中。

我們提供:

  • 可以看到所有評論的檢視
  • 提交評論的表單
  • 通過Hooks可以自定義後端

它有如下特點:

  • 優化的評論: 評論將會在儲存到伺服器上之前就出現在列表中,這樣看上去非常快。
  • 實時更新: 其他使用者的評論會實時的被放到評論介面。
  • Markdown格式: 使用者可以使用Markdown來格式化他們的文字。

想要跳過這些只看原始碼?

都在GitHub上

執行一個伺服器

首先,我們需要一個執行的伺服器。它將作為API埠來獲取並儲存資料。為了簡便,我們用指令碼語言建立一個服務端。你可以看原始檔 或者 下載zip檔案 。

伺服器使用一個JSON檔案作為資料庫。 在實際的產品中你不會這樣用,但是這樣可以簡單的模擬出當你使用一個API是你可能做的事情。一旦你啟動伺服器, 它將會支援API埠並且為我們需要的靜態頁面提供服務。

開始

本教程中我們儘量簡化。 上文提到的包中包含一個HTML檔案。 在你最喜歡的編輯器中開啟public/index.html,可以看到:

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>React Tutorial</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/babel" src="scripts/example.js"></script>
    <script type="text/babel">
      // To get started with this tutorial running your own code, simply remove
      // the script tag loading scripts/example.js and start writing code here.
    </script>
  </body>
</html>

本教程餘下的部份,我們將通過JavaScript語言完成<script>標籤中的內容。我們沒有任何高階的livereload(自動重新載入),因此你需要在儲存修改重新整理瀏覽器。啟動服務後在瀏覽器中開啟http://localhost:3000。當你未做任何修改載入在這個頁面時,你將看到我們想要構建的成品。刪掉字首為<script>的標籤就可以開始了。

注意:

我們在這裡用到了jQuery因為我們想要簡化ajax呼叫,但這在React中不是必要的。

你的第一個元件

React就是將各種元件模組化組合到一起。我們的評論框例子中需要以下元件結構:

- 評論框CommentBox
  - 評論列表CommentList
    - 每條評論Comment
  - 可提交的評論表單CommentForm

首先,構建評論框元件,這就是個簡單的<div>:

// tutorial1.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
});
ReactDOM.render(
  <CommentBox />,
  document.getElementById(`content`)
);

注意純TML元素名都是以小寫字母開頭,然而React類名通常以大寫字母開頭。

JSX語法

首先要注意到的是在你的 JavaScript中有XML化語法。我們有一個簡單的預編譯將語法糖轉化為普通的JavaScript:

// tutorial1-raw.js
var CommentBox = React.createClass({displayName: `CommentBox`,
  render: function() {
    return (
      React.createElement(`div`, {className: "commentBox"},
        "Hello, world! I am a CommentBox."
      )
    );
  }
});
ReactDOM.render(
  React.createElement(CommentBox, null),
  document.getElementById(`content`)
);

這種用法不是必需的,JSX確實比普通的JavaScript簡單。更多內容可參考JSX 語法

下一步

我們將一些JavaScript物件的方法傳入React.createClass()來創造一個新的React元件。這個方法中最重要的是render,它將返回一個React元件樹並最終渲染HTML。

<div>並不是真正的DOM節點,他們是React的div元件例項。你可以把這些當作React知道如何處理的資料或者標識。React是安全的。我們並不產生HTML字串,所以XSS保護是預設的。

你可以不返回基本的HTML,可以返回你或者他人建立的一個元件樹。這使得React是可組合的(可維護前端的一個關鍵原則)。

ReactDOM.render() 例項化根節點元件,開始框架,將標識注入到原生的DOM中,作為第二個引數。

通過在不同平臺上共享的React核心工具,ReactDOM方法顯示出特定的DOM方法 (e.g., React Native)。

需要注意的是在本教程中ReactDOM.render要在js檔案的底部。ReactDOM.render只有在符合元件被定義後才可以被呼叫。

複合元件

下面我們構建一個評論列表和評論表單的框架,用到的同樣是簡單的<div>。將這兩個元件新增到你的檔案中,保持評論框CommentBox定義的存在以及ReactDOM.render的呼叫:

// tutorial2.js
var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        Hello, world! I am a CommentList.
      </div>
    );
  }
});

var CommentForm = React.createClass({
  render: function() {
    return (
      <div className="commentForm">
        Hello, world! I am a CommentForm.
      </div>
    );
  }
});

接下來,評論框使用這兩個新元件:

// tutorial3.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList />
        <CommentForm />
      </div>
    );
  }
});

注意我們是怎樣將HTML標籤和我們構造的元件結合到一起的。HTML元件就是常規的React元件,和你自己定義的就只有首字母大小寫的區別。 JSX編譯器可以自動的將HTML標籤重寫到React.createElement(標籤名)表示式中而不管其他的,這將避免全域性名稱空間被汙染。

使用屬性

我們建立Comment元件,它將依賴於它的父元件傳遞給它的資料。 對於子元件來說,從父元件傳遞來的資料可以像一個屬性一樣被獲取。這些屬性通過this.props得到。使用屬性我們可以讀取從CommentList傳遞給Comment的資料,並且渲染標識:

// tutorial4.js
var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {this.props.children}
      </div>
    );
  }
});

通過將JavaScript表示式包含在JSX中(作為屬性或者子節點),你可以將文字或者React元件放在樹中。我們通過傳給元件的this.props 及其他像this.props.children的鍵來獲取值。我們獲取傳遞給元件的屬性傳遞給元件,比如this.props的鍵或者其他被大括號包起來的比如this.props.children。

元件屬性

既然我們定義了Comment 元件,我們就希望利用它來傳遞作者名和評論內容。這將使得我們對每一段評論重用同樣的程式碼。現在讓我們為我們的CommentList新增一些評論:

// tutorial5.js
var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        <Comment author="Pete Hunt">This is one comment</Comment>
        <Comment author="Jordan Walke">This is *another* comment</Comment>
      </div>
    );
  }
});

注意到我們已經從父元件CommentList 向子元件Comment傳遞資料。例如:我們將名字Pete Hunt (通過author屬性) 和評論內容This is one comment (通過類XML的子節點)傳遞給第一個Comment。如上所述,Comment元件將通過this.props.author和this.props.children獲取屬性。

新增Markdown

Markdown是一個簡單的方式來格式化你的文字。例如星號圍繞的文字將會被強調。

在本教程中我們使用一個第三方庫marked,它將把Markdown文字轉化為純HTML.。我們為頁面引用這個庫然後直接使用,把文字轉化為Markdown並輸出:

// tutorial6.js
var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {marked(this.props.children.toString())}
      </div>
    );
  }
});

我們現在所做的就是呼叫marked庫。我們需要把this.props.children從React包裹的文字轉化為一個字串,因此我們顯式的呼叫toString()。

但仍然有問題!我們渲染好的評論在顯示器中這樣顯示:”<p>This is<em>another</em> comment</p>“。我們想把這些標籤真正的渲染成HTML。

這是因為React會防止 XSS攻擊。有種方法可以解決這個問題但建議儘量不這麼做:

// tutorial7.js

var Comment = React.createClass({
  rawMarkup: function() {
    var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
    return { __html: rawMarkup };
  },

  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        <span dangerouslySetInnerHTML={this.rawMarkup()} />
      </div>
    );
  }
});

這個特殊的API使得插入純HTML變得困難,但是為了marked也不得不使用這個伎倆。

記住: 使用這個要確保marked是安全的。在這裡我們傳遞sanitize: true 來告訴marked不保留任何的HTML標識

連線上資料模型

到目前為止我們已經可以將評論直接插入到原始碼中了。接下來我們將JSON物件傳入評論列表。最終這個資料將來源於伺服器,但現在這樣寫你的程式碼:

// tutorial8.js
var data = [
  {id: 1, author: "Pete Hunt", text: "This is one comment"},
  {id: 2, author: "Jordan Walke", text: "This is *another* comment"}
];

我們需要模組化的把這資料傳入到CommentList。修改CommentBoxReactDOM.render()呼叫來將資料通過屬性傳入到CommentList

// tutorial9.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.props.data} />
        <CommentForm />
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox data={data} />,
  document.getElementById(`content`)
);

既然在CommentList中資料可得,我們可以動態渲染評論:

// tutorial10.js
var CommentList = React.createClass({
  render: function() {
    var commentNodes = this.props.data.map(function(comment) {
      return (
        <Comment author={comment.author} key={comment.id}>
          {comment.text}
        </Comment>
      );
    });
    return (
      <div className="commentList">
        {commentNodes}
      </div>
    );
  }
});

這就可以了!

從伺服器取資料

下面我們從伺服器動態獲取資料來代替固化的程式碼。我們去掉data屬性並以一個URL來代替取資料:

// tutorial11.js
ReactDOM.render(
  <CommentBox url="/api/comments" />,
  document.getElementById(`content`)
);

這個元件不同於前面的那些因為它必須重新渲染它自己。伺服器端響應之前元件不會有任何資料,這時間段元件不會渲染新的評論。

注意:程式碼在這個階段是不能工作的。

反應狀態

到目前為止,每個元件都可以基於自己的屬性渲染自己。 屬性props是不可改變的: 它們來自父節點並且屬於父節點。為了實現互動,我們為元件引入一個可變的狀態。 this.state是元件私有的並且可以通過呼叫this.setState()被改變。當狀態state更新時,元件也會重新渲染。

render()方法就像this.propsthis.state函式一樣以宣告形式寫入。框架卻表UI始終與輸入一致。

當伺服器端取資料,我們可以修改我們已有的評論。下面我們為 CommentBox元件增加一組資料作為它的狀態:

// tutorial12.js
var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

getInitialState()在元件生命週期中被執行並且建立元件初始狀態。

更新狀態

當元件一開始被建立時,我們想要從伺服器端獲取JSON,並且更新狀態反映到最新的資料中。我們將使用jQuery來創造一個非同步的請求給我們前面獲取資料的伺服器。資料已經在你的伺服器上了(基於comments.json檔案),所以一旦資料被取出,this.state.data將會變成這樣:

[
  {"id": "1", "author": "Pete Hunt", "text": "This is one comment"},
  {"id": "2", "author": "Jordan Walke", "text": "This is *another* comment"}
]
// tutorial13.js
var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    $.ajax({
      url: this.props.url,
      dataType: `json`,
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

這裡, componentDidMount是個在元件第一次被渲染後自動被React呼叫的方法。動態更新的關鍵是呼叫this.setState()。我們用來自伺服器的評論組替代舊的評論組,並且UI可以自動更新。 由於這個反應特性,新增實時更新只需要有也很小的改變。我們在這裡使用的是簡單的輪詢,但是你可以使用WebSocket或者其他技術。

// tutorial14.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: `json`,
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox url="/api/comments" pollInterval={2000} />,
  document.getElementById(`content`)
);

這裡我們做的是將AJAX呼叫移到一個單獨的方法中,並且在元件第一次被載入時以及之後每隔兩秒時被呼叫。區執行你的瀏覽器並且修改 comments.json檔案 (在你服務端的同一個目錄下),兩秒鐘後你就可以看到變化!

增加新評論

現在我們建立表單。我們的 CommentForm 元件應該獲取使用者的姓名和評論文字,並且傳送一個請求給服務端將評論儲存下來。

// tutorial15.js
var CommentForm = React.createClass({
  render: function() {
    return (
      <form className="commentForm">
        <input type="text" placeholder="Your name" />
        <input type="text" placeholder="Say something..." />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

控制元件

傳統的DOM,input元素是由瀏覽器管理其狀態(它渲染的值)。結果DOM的狀態和元件的狀態不同。檢視狀態與元件狀態不同,這不是理想的。在React中,元件狀態和檢視狀態始終一樣,不僅限於初始化時相同。

因此我們使用this.state來儲存使用者的輸入。我們定義初始狀態state,並賦予兩個屬性作者名author和評論文字text並置空。在我們的<input>元素中,我們使用value屬性並反映元件的狀態 state,另外附onChange事件。 這些 含有待賦值的<input>元素叫做控制元件。更多關於控制元件的內容可以參考表單

// tutorial16.js
var CommentForm = React.createClass({
  getInitialState: function() {
    return {author: ``, text: ``};
  },
  handleAuthorChange: function(e) {
    this.setState({author: e.target.value});
  },
  handleTextChange: function(e) {
    this.setState({text: e.target.value});
  },
  render: function() {
    return (
      <form className="commentForm">
        <input
          type="text"
          placeholder="Your name"
          value={this.state.author}
          onChange={this.handleAuthorChange}
        />
        <input
          type="text"
          placeholder="Say something..."
          value={this.state.text}
          onChange={this.handleTextChange}
        />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

事件

React為元件附上事件並用駱駝拼寫法命名。我們為兩個<input>元素附上 onChange 事件。現在作為使用者在<input>區域輸入文字,被附上的 onChange被啟用,元件狀態被改變。隨後,<input>元素渲染的值將會被重新整理,當前元件狀態state也隨之改變。

提交表單

下面我們讓表單互動化。當使用者提交表單,我們應該清空表單,並向服務端傳送請求,重新整理評論列表。首先我們要堅監聽表單提交事件並清空它。

// tutorial17.js
var CommentForm = React.createClass({
  getInitialState: function() {
    return {author: ``, text: ``};
  },
  handleAuthorChange: function(e) {
    this.setState({author: e.target.value});
  },
  handleTextChange: function(e) {
    this.setState({text: e.target.value});
  },
  handleSubmit: function(e) {
    e.preventDefault();
    var author = this.state.author.trim();
    var text = this.state.text.trim();
    if (!text || !author) {
      return;
    }
    // TODO: send request to the server
    this.setState({author: ``, text: ``});
  },
  render: function() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit}>
        <input
          type="text"
          placeholder="Your name"
          value={this.state.author}
          onChange={this.handleAuthorChange}
        />
        <input
          type="text"
          placeholder="Say something..."
          value={this.state.text}
          onChange={this.handleTextChange}
        />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

我們為表單附上onSubmit控制程式碼,這樣表單被有效填寫並提交後表單區域會立即清空。

呼叫 preventDefault() 來避免瀏覽器預設提交表單的動作。

作為屬性回撥

當一個使用者提交一個評論,我們需要重新整理列表把這個新評論放進來。在 CommentBox中完成這個邏輯非常合理,因為CommentBox擁有評論列表的狀態。

我們需要從子元件中將資料傳給父元件。我們在父元件 render方法中傳一個回撥函式 (handleCommentSubmit)給子元件,將它繫結到子元件的onCommentSubmit事件。一旦事件被啟用,回撥就被喚醒:

// tutorial18.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: `json`,
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  handleCommentSubmit: function(comment) {
    // TODO: submit to the server and refresh the list
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});

既然 CommentBox 已經讓 CommentForm通過onCommentSubmit屬性獲得回撥,當使用者提交表單時CommentForm可以呼叫回撥

// tutorial19.js
var CommentForm = React.createClass({
  getInitialState: function() {
    return {author: ``, text: ``};
  },
  handleAuthorChange: function(e) {
    this.setState({author: e.target.value});
  },
  handleTextChange: function(e) {
    this.setState({text: e.target.value});
  },
  handleSubmit: function(e) {
    e.preventDefault();
    var author = this.state.author.trim();
    var text = this.state.text.trim();
    if (!text || !author) {
      return;
    }
    this.props.onCommentSubmit({author: author, text: text});
    this.setState({author: ``, text: ``});
  },
  render: function() {
    return (
      <form className="commentForm" onSubmit={this.handleSubmit}>
        <input
          type="text"
          placeholder="Your name"
          value={this.state.author}
          onChange={this.handleAuthorChange}
        />
        <input
          type="text"
          placeholder="Say something..."
          value={this.state.text}
          onChange={this.handleTextChange}
        />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

現在回撥已經在了,我們要去做的就是提交給服務端並重新整理列表:

// tutorial20.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: `json`,
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  handleCommentSubmit: function(comment) {
    $.ajax({
      url: this.props.url,
      dataType: `json`,
      type: `POST`,
      data: comment,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});

優化: 優化的重新整理策略

我們的應用現在已經具備了各功能了,但是在提交的評論出現在列表上之前還需要等待相應,這感覺上有點慢。我們可以直接將評論新增到列表中來使應用更快。

// tutorial21.js
var CommentBox = React.createClass({
  loadCommentsFromServer: function() {
    $.ajax({
      url: this.props.url,
      dataType: `json`,
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  handleCommentSubmit: function(comment) {
    var comments = this.state.data;
    // Optimistically set an id on the new comment. It will be replaced by an
    // id generated by the server. In a production application you would likely
    // not use Date.now() for this and would have a more robust system in place.
    comment.id = Date.now();
    var newComments = comments.concat([comment]);
    this.setState({data: newComments});
    $.ajax({
      url: this.props.url,
      dataType: `json`,
      type: `POST`,
      data: comment,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        this.setState({data: comments});
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  getInitialState: function() {
    return {data: []};
  },
  componentDidMount: function() {
    this.loadCommentsFromServer();
    setInterval(this.loadCommentsFromServer, this.props.pollInterval);
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});

轉載自 併發程式設計網 – ifeve.com


相關文章