我們將構建一個簡單卻真實的評論框,你可以將它放入你的博客,類似disqus、livefyre、facebook提供的實時評論的基礎版。
我們將提供以下內容:
* 一個展示所有評論的視圖
* 一個提交評論的表單
* 用于構建自定制后臺的接口鏈接(hooks)
同時也包含一些簡潔的特性:
* **評論體驗優化:**?評論在保存到服務器之前就展現在評論列表,因此用戶體驗很快。
* **實時更新:**?其他用戶的評論將會實時展示。
* **Markdown格式:**?用戶可以使用MarkDown格式來編輯文本。
目錄 :
[TOC]
### 想要跳過所有的內容,只查看源代碼?
[所有代碼都在GitHub。](https://github.com/reactjs/react-tutorial)
### 運行一個服務器
雖然它不是入門教程的必需品,但接下來我們會添加一個功能,發送?`POST`?ing請求到服務器。如果這是你熟知的事并且你想創建你自己的服務器,那么就這樣干吧。而對于另外的一部分人,為了讓你集中精力學習,而不用擔憂服務器端方面,我們已經用了以下一系列的語言編寫了簡單的服務器代碼 - JavaScript(使用Node.js),Python和Ruby。所有代碼都在GitHub。你可以[查看代碼](ttps://github.com/reactjs/react-tutorial/)或者[下載 zip 文件](https://github.com/reactjs/react-tutorial/archive/master.zip)來開始學習。
開始使用下載的教程,只需開始編輯?`public/index.html`?。
### 開始學習
在這個教程里面,我們將使用放在 CDN 上預構建好的 JavaScript 文件。打開你最喜歡的編輯器,創建一個新的 HTML 文檔:
~~~
<!-- index.html -->
<html>
<head>
<title>Hello React</title>
<script src="http://fb.me/react-0.13.0.js"></script>
<script src="http://fb.me/JSXTransformer-0.13.0.js"></script>
<script src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
</head>
<body>
<div id="content"></div>
<script type="text/jsx">
// Your code here
</script>
</body>
</html>
~~~
在本教程其余的部分,我們將在此 script 標簽中編寫我們的 JavaScript 代碼。
> 注意:
>
> 因為我們想簡化 ajax 請求代碼,所以在這里引入 jQuery,但是它對 React 并不是必須的。
### 你的第一個組件
React 中全是模塊化、可組裝的組件。以我們的評論框為例,我們將有如下的組件結構:
~~~
- CommentBox
- CommentList
- Comment
- CommentForm
~~~
讓我們構造?`CommentBox`?組件,它只是一個簡單的?``?而已:
~~~
// tutorial1.js
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
Hello, world! I am a CommentBox.
</div>
);
}
});
React.render(
<CommentBox />,
document.getElementById('content')
);
~~~
#### 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."
)
);
}
});
React.render(
React.createElement(CommentBox, null),
document.getElementById('content')
);
~~~
JSX 語法是可選的,但是我們發現 JSX 語句比純 JavaScript 更加容易使用。閱讀更多關于[JSX 語法的文章](http://reactjs.cn/react/docs/jsx-in-depth.html)。
#### 發生了什么
我們通過 JavaScript 對象傳遞一些方法到?`React.createClass()`?來創建一個新的React組件。其中最重要的方法是?`render`,該方法返回一顆 React 組件樹,這棵樹最終將會渲染成 HTML。
這個?``?標簽不是真實的DOM節點;他們是 React?`div`?組件的實例。你可以認為這些就是React知道如何處理的標記或者一些數據。React 是**安全的**。我們不生成 HTML 字符串,因此默認阻止了 XSS 攻擊。
你沒有必要返回基本的 HTML。你可以返回一個你(或者其他人)創建的組件樹。這就使得 React 變得**組件化**:一個關鍵的前端維護原則。
`React.render()`?實例化根組件,啟動框架,注入標記到原始的 DOM 元素中,作為第二個參數提供。
## 制作組件
讓我們為?`CommentList`?和?`CommentForm`?構建骨架,這也會是一些簡單的?``?:
~~~
// 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>
);
}
});
~~~
下一步,更新?`CommentBox`?組件,使用這些新的組件:
~~~
// 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(tagName)`?表達式,其它什么都不做。這是為了避免全局命名空間污染。
### 組件屬性
讓我們創建我們的第三個組件,`Comment`。我們想傳遞給它作者名字和評論文本,以便于我們能夠對每一個獨立的評論重用相同的代碼。首先讓我們添加一些評論到?`CommentList`:
~~~
// tutorial4.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_?(通過一個屬性)和?_This is one comment * (通過類似于XML的子節點)給第一個?`Comment`。從父節點傳遞到子節點的數據稱為 *_props**,是屬性(properties)的縮寫。
### 使用props
讓我們創建評論組件。通過?**props**,就能夠從中讀取到從?`CommentList`?傳遞過來的數據,然后渲染一些標記:
~~~
// tutorial5.js
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
{this.props.children}
</div>
);
}
});
~~~
在 JSX 中通過將 JavaScript 表達式放在大括號中(作為屬性或者子節點),你可以生成文本或者 React 組件到節點樹中。我們訪問傳遞給組件的命名屬性作為?`this.props`?的鍵,任何內嵌的元素作為?`this.props.children`。
### 添加 Markdown
Markdown 是一種簡單的格式化內聯文本的方式。例如,用星號包裹文本將會使其強調突出。
首先,添加第三方的?**Showdown**?庫到你的應用。這是一個JavaScript庫,處理 Markdown 文本并且轉換為原始的 HTML。這需要在你的頭部添加一個 script 標簽(我們已經在 React 操練場上包含了這個標簽):
~~~
<!-- index.html -->
<head>
<title>Hello React</title>
<script src="http://fb.me/react-0.13.0.js"></script>
<script src="http://fb.me/JSXTransformer-0.13.0.js"></script>
<script src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
</head>
~~~
下一步,讓我們轉換評論文本為 Markdown 格式,然后輸出它:
~~~
// tutorial6.js
var converter = new Showdown.converter();
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
{converter.makeHtml(this.props.children.toString())}
</div>
);
}
});
~~~
我們在這里唯一需要做的就是調用 Showdown 庫。我們需要把`this.props.children`從 React 的包裹文本轉換成 Showdown 能處理的原始的字符串,所以我們顯示地調用了`toString()`。
但是這里有一個問題!我們渲染的評論在瀏覽器里面看起來像這樣:“``This is``another``?comment``”。我們想這些標簽真正地渲染成 HTML。
那是 React 在保護你免受 XSS 攻擊。這里有一種方法解決這個問題,但是框架會警告你別使用這種方法:
~~~
// tutorial7.js
var converter = new Showdown.converter();
var Comment = React.createClass({
render: function() {
var rawMarkup = converter.makeHtml(this.props.children.toString());
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={{__html: rawMarkup}} />
</div>
);
}
});
~~~
這是一個特殊的 API,故意讓插入原始的 HTML 變得困難,但是對于 Showdown,我們將利用這個后門。
**記住:**?使用這個功能,你會依賴于 Showdown 的安全性。
### 接入數據模型
到目前為止,我們已經在源代碼里面直接插入了評論數據。相反,讓我們渲染一小塊JSON數據到評論列表。最終,數據將會來自服務器,但是現在,寫在你的源代碼中:
~~~
// tutorial8.js
var data = [
{author: "Pete Hunt", text: "This is one comment"},
{author: "Jordan Walke", text: "This is *another* comment"}
];
~~~
我們需要用一種模塊化的方式將數據傳入到?`CommentList`。修改?`CommentBox`?和`React.render()`?方法,通過 props 傳遞數據到?`CommentList`:
~~~
// tutorial9.js
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm />
</div>
);
}
});
React.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}>
{comment.text}
</Comment>
);
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
});
~~~
就是這樣!
### 從服務器獲取數據
讓我們用一些從服務器獲取的動態數據替換硬編碼的數據。我們將移除數據屬性,用獲取數據的URL來替換它:
~~~
// tutorial11.js
React.render(
<CommentBox url="comments.json" />,
document.getElementById('content')
);
~~~
這個組件和前面的組件是不一樣的,因為它必須重新渲染自己。該組件將不會有任何數據,直到請求從服務器返回,此時該組件或許需要渲染一些新的評論。
### 響應狀態變化(Reactive state)
到目前為止,每一個組件都根據自己的 props 渲染了自己一次。`props`?是不可變的:它們從父節點傳遞過來,被父節點“擁有”。為了實現交互,我們給組件引進了可變的**state**。`this.state`?是組件私有的,可以通過調用?`this.setState()`?來改變它。當狀態更新之后,組件重新渲染自己。
`render()`?methods are written declaratively as functions of?`this.props`?and?`this.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()`在組件的生命周期中僅執行一次,設置組件的初始化狀態。
#### 更新狀態
當組件第一次創建的時候,我們想從服務器獲取(使用GET方法)一些JSON數據,更新狀態,反映出最新的數據。在真實的應用中,這將會是一個動態功能點,但是對于這個例子,我們將會使用一個靜態的JSON文件來使事情變得簡單:
~~~
// tutorial13.json
[
{"author": "Pete Hunt", "text": "This is one comment"},
{"author": "Jordan Walke", "text": "This is *another* comment"}
]
~~~
我們將會使用jQuery幫助發出一個一步的請求到服務器。
注意:因為這會變成一個AJAX應用,你將會需要使用一個web服務器來開發你的應用,而不是一個放置在你的文件系統上面的一個文件。[如上所述](http://reactjs.cn/react/docs/tutorial.html#running-a-server),我們已經在[GitHub](https://github.com/reactjs/react-tutorial/)上面提供了幾個你可以使用的服務器。這些服務器提供了你學習下面教程所需的功能。
~~~
// tutorial13.js
var CommentBox = React.createClass({
getInitialState: function() {
return {data: []};
},
componentDidMount: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
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自動更新。正是有了這種響應式,一個小的改變都會觸發實時的更新。這里我們將使用簡單的輪詢,但是你可以簡單地使用WebSockets或者其它技術。
~~~
// tutorial14.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
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>
);
}
});
React.render(
<CommentBox url="comments.json" 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>
);
}
});
~~~
讓我們使表單可交互。當用戶提交表單的時候,我們應該清空表單,提交一個請求到服務器,然后刷新評論列表。首先,讓我們監聽表單的提交事件和清空表單。
~~~
// tutorial16.js
var CommentForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var author = this.refs.author.getDOMNode().value.trim();
var text = this.refs.text.getDOMNode().value.trim();
if (!text || !author) {
return;
}
// TODO: send request to the server
this.refs.author.getDOMNode().value = '';
this.refs.text.getDOMNode().value = '';
return;
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
});
~~~
##### 事件
React使用駝峰命名規范的方式給組件綁定事件處理器。我們給表單綁定一個`onSubmit`處理器,用于當表單提交了合法的輸入后清空表單字段。
在事件回調中調用`preventDefault()`來避免瀏覽器默認地提交表單。
##### Refs
我們利用`Ref`屬性給子組件命名,`this.refs`引用組件。我們可以在組件上調用`getDOMNode()`獲取瀏覽器本地的DOM元素。
##### 回調函數作為屬性
當用戶提交評論的時候,我們需要刷新評論列表來加進這條新評論。在`CommentBox`中完成所有邏輯是合適的,因為`CommentBox`擁有代表評論列表的狀態(state)。
我們需要從子組件傳回數據到它的父組件。我們在父組件的`render`方法中做這件事:傳遞一個新的回調函數(`handleCommentSubmit`)到子組件,綁定它到子組件的`onCommentSubmit`事件上。無論事件什么時候觸發,回調函數都將會被調用:
~~~
// tutorial17.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
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>
);
}
});
~~~
當用戶提交表單的時候,讓我們在`CommentForm`中調用這個回調函數:
~~~
// tutorial18.js
var CommentForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var author = this.refs.author.getDOMNode().value.trim();
var text = this.refs.text.getDOMNode().value.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text});
this.refs.author.getDOMNode().value = '';
this.refs.text.getDOMNode().value = '';
return;
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
});
~~~
現在回調函數已經就緒,唯一我們需要做的就是提交到服務器,然后刷新列表:
~~~
// tutorial19.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
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>
);
}
});
~~~
### 優化:提前更新
我們的應用現在已經完成了所有功能,但是在你的評論出現在列表之前,你必須等待請求完成,感覺很慢。我們可以提前添加這條評論到列表中,從而使應用感覺更快。
~~~
// tutorial20.js
var CommentBox = React.createClass({
loadCommentsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
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;
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) {
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>
);
}
});
~~~
### 祝賀你!
你剛剛通過一些簡單步驟夠早了一個評論框。了解更多關于[為什么使用React](http://reactjs.cn/react/docs/why-react.html)的內容,或者深入學習[API參考](http://reactjs.cn/react/docs/top-level-api.html),開始專研!祝你好運!