閱讀851 返回首頁    go 阿裏雲 go 技術社區[雲棲]


《React官方文檔》之教程Tutorial

教程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(自動重新加載),因此你需要在保存修改刷新瀏覽器。啟動服務後在瀏覽器中打開https://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(th

最後更新:2017-05-19 15:02:48

  上一篇:go  哪個線程執行 CompletableFuture’s tasks 和 callbacks?
  下一篇:go  《ELK Stack權威指南 》第3章 場景示例