我們將構建一個簡單卻真實的評論框,你可以將它放入你的博客,類似disqus、livefyre、facebook提供的實時評論的基礎版。
我們將提供以下內(nèi)容:
同時也包含一些簡潔的特性:
雖然它不是入門教程的必需品,但接下來我們會添加一個功能,發(fā)送 POST ing請求到服務器。如果這是你熟知的事并且你想創(chuàng)建你自己的服務器,那么就這樣干吧。而對于另外的一部分人,為了讓你集中精力學習,而不用擔憂服務器端方面,我們已經(jīng)用了以下一系列的語言編寫了簡單的服務器代碼 - JavaScript(使用 Node.js),Python和Ruby。所有代碼都在GitHub。你可以查看代碼或者下載 zip 文件來開始學習。
開始使用下載的教程,只需開始編輯 public/index.html 。
在這個教程里面,我們將使用放在 CDN 上預構建好的 JavaScript 文件。打開你最喜歡的編輯器,創(chuàng)建一個新的 HTML 文檔:
<!-- index.html -->
<html>
<head>
<title>Hello React</title>
<script src="http://fb.me/react- {{site.react_version}}.js"></script>
<script src="http://fb.me/JSXTransformer- {{site.react_version}}.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 組件,它只是一個簡單的 <div> 而已:
// 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')
);
首先你注意到 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 語法是可選的,但是我們發(fā)現(xiàn) JSX 語句比純 JavaScript 更加容易使用。閱讀更多關于 JSX 語法的文章。
我們通過 JavaScript 對象傳遞一些方法到 React.createClass() 來創(chuàng)建一個新的React組件。其中最重要的方法是 render,該方法返回一顆 React 組件樹,這棵樹最終將會渲染成 HTML。
這個 <div> 標簽不是真實的DOM節(jié)點;他們是 React div 組件的實例。你可以認為這些就是React知道如何處理的標記或者一些數(shù)據(jù)。React 是安全的。我們不生成 HTML 字符串,因此默認阻止了 XSS 攻擊。
你沒有必要返回基本的 HTML。你可以返回一個你(或者其他人)創(chuàng)建的組件樹。這就使得 React 變得組件化:一個關鍵的前端維護原則。
React.render() 實例化根組件,啟動框架,注入標記到原始的 DOM 元素中,作為第二個參數(shù)提供。
讓我們?yōu)?CommentList 和 CommentForm 構建骨架,這也會是一些簡單的 <div> :
// 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 標簽和我們創(chuàng)建的組件。HTML 組件就是普通的 React 組件,就像你定義的一樣,只有一點不一樣。JSX 編譯器會自動重寫 HTML 標簽為 React.createElement(tagName) 表達式,其它什么都不做。這是為了避免全局命名空間污染。
讓我們創(chuàng)建我們的第三個組件,Comment。我們想傳遞給它作者名字和評論文本,以便于我們能夠?qū)γ恳粋€獨立的評論重用相同的代碼。首先讓我們添加一些評論到 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>
);
}
});
請注意,我們已經(jīng)從父節(jié)點 CommentList 組件傳遞給子節(jié)點 Comment 組件一些數(shù)據(jù)。例如,我們傳遞了 Pete Hunt (通過一個屬性)和 This is one comment (通過類似于XML的子節(jié)點)給第一個 Comment。從父節(jié)點傳遞到子節(jié)點的數(shù)據(jù)稱為 props,是屬性(properties)的縮寫。
讓我們創(chuàng)建評論組件。通過 props,就能夠從中讀取到從 CommentList 傳遞過來的數(shù)據(jù),然后渲染一些標記:
// 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 表達式放在大括號中(作為屬性或者子節(jié)點),你可以生成文本或者 React 組件到節(jié)點樹中。我們訪問傳遞給組件的命名屬性作為 this.props 的鍵,任何內(nèi)嵌的元素作為 this.props.children。
Markdown 是一種簡單的格式化內(nèi)聯(lián)文本的方式。例如,用星號包裹文本將會使其強調(diào)突出。
首先,添加第三方的 Showdown 庫到你的應用。這是一個JavaScript庫,處理 Markdown 文本并且轉換為原始的 HTML。這需要在你的頭部添加一個 script 標簽(我們已經(jīng)在 React 操練場上包含了這個標簽):
<!-- index.html -->
<head>
<title>Hello React</title>
<script src="http://fb.me/react-{{site.react_version}}.js"></script>
<script src="http://fb.me/JSXTransformer-{{site.react_version}}.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>
);
}
});
我們在這里唯一需要做的就是調(diào)用 Showdown 庫。我們需要把this.props.children從 React 的包裹文本轉換成 Showdown 能處理的原始的字符串,所以我們顯示地調(diào)用了toString()。
但是這里有一個問題!我們渲染的評論在瀏覽器里面看起來像這樣:“<p>This is <em>another</em> comment</p>”。我們想這些標簽真正地渲染成 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,我們將利用這個后門。
記?。?/strong> 使用這個功能,你會依賴于 Showdown 的安全性。
到目前為止,我們已經(jīng)在源代碼里面直接插入了評論數(shù)據(jù)。相反,讓我們渲染一小塊JSON數(shù)據(jù)到評論列表。最終,數(shù)據(jù)將會來自服務器,但是現(xiàn)在,寫在你的源代碼中:
// tutorial8.js
var data = [
{author: "Pete Hunt", text: "This is one comment"},
{author: "Jordan Walke", text: "This is *another* comment"}
];
我們需要用一種模塊化的方式將數(shù)據(jù)傳入到 CommentList。修改 CommentBox 和 React.render() 方法,通過 props 傳遞數(shù)據(jù)到 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')
);
現(xiàn)在數(shù)據(jù)在 CommentList 中可用了,讓我們動態(tài)地渲染評論:
// 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>
);
}
});
就是這樣!
讓我們用一些從服務器獲取的動態(tài)數(shù)據(jù)替換硬編碼的數(shù)據(jù)。我們將移除數(shù)據(jù)屬性,用獲取數(shù)據(jù)的URL來替換它:
// tutorial11.js
React.render(
<CommentBox url="comments.json" />,
document.getElementById('content')
);
這個組件和前面的組件是不一樣的,因為它必須重新渲染自己。該組件將不會有任何數(shù)據(jù),直到請求從服務器返回,此時該組件或許需要渲染一些新的評論。
到目前為止,每一個組件都根據(jù)自己的 props 渲染了自己一次。props 是不可變的:它們從父節(jié)點傳遞過來,被父節(jié)點“擁有”。為了實現(xiàn)交互,我們給組件引進了可變的 state。this.state 是組件私有的,可以通過調(diào)用 this.setState() 來改變它。當狀態(tài)更新之后,組件重新渲染自己。
render() methods are written declaratively as functions of this.props and this.state. 框架確保UI始終和輸入保持一致。
當服務器獲取數(shù)據(jù)的時候,我們將會用已有的數(shù)據(jù)改變評論。讓我們給 CommentBox 組件添加一個評論數(shù)組作為它的狀態(tài):
// 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()在組件的生命周期中僅執(zhí)行一次,設置組件的初始化狀態(tài)。
當組件第一次創(chuàng)建的時候,我們想從服務器獲?。ㄊ褂肎ET方法)一些JSON數(shù)據(jù),更新狀態(tài),反映出最新的數(shù)據(jù)。在真實的應用中,這將會是一個動態(tài)功能點,但是對于這個例子,我們將會使用一個靜態(tài)的JSON文件來使事情變得簡單:
// tutorial13.json
[
{"author": "Pete Hunt", "text": "This is one comment"},
{"author": "Jordan Walke", "text": "This is *another* comment"}
]
我們將會使用jQuery幫助發(fā)出一個一步的請求到服務器。
注意:因為這會變成一個AJAX應用,你將會需要使用一個web服務器來開發(fā)你的應用,而不是一個放置在你的文件系統(tǒng)上面的一個文件。如上所述,我們已經(jīng)在GitHub上面提供了幾個你可以使用的服務器。這些服務器提供了你學習下面教程所需的功能。
// 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自動調(diào)用的方法。動態(tài)更新的關鍵點是調(diào)用this.setState()。我們把舊的評論數(shù)組替換成從服務器拿到的新的數(shù)組,然后UI自動更新。正是有了這種響應式,一個小的改變都會觸發(fā)實時的更新。這里我們將使用簡單的輪詢,但是你可以簡單地使用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調(diào)用移到一個分離的方法中去,組件第一次加載以及之后每隔兩秒鐘,調(diào)用這個方法。嘗試在你的瀏覽器中運行,然后改變comments.json文件;在兩秒鐘之內(nèi),改變將會顯示出來!
現(xiàn)在是時候構造表單了。我們的CommentForm組件應該詢問用戶的名字和評論內(nèi)容,然后發(fā)送一個請求到服務器,保存這條評論。
// 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>
);
}
});
讓我們使表單可交互。當用戶提交表單的時候,我們應該清空表單,提交一個請求到服務器,然后刷新評論列表。首先,讓我們監(jiān)聽表單的提交事件和清空表單。
// 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使用駝峰命名規(guī)范的方式給組件綁定事件處理器。我們給表單綁定一個onSubmit處理器,用于當表單提交了合法的輸入后清空表單字段。
在事件回調(diào)中調(diào)用preventDefault()來避免瀏覽器默認地提交表單。
我們利用Ref屬性給子組件命名,this.refs引用組件。我們可以在組件上調(diào)用getDOMNode()獲取瀏覽器本地的DOM元素。
當用戶提交評論的時候,我們需要刷新評論列表來加進這條新評論。在CommentBox中完成所有邏輯是合適的,因為CommentBox擁有代表評論列表的狀態(tài)(state)。
我們需要從子組件傳回數(shù)據(jù)到它的父組件。我們在父組件的render方法中做這件事:傳遞一個新的回調(diào)函數(shù)(handleCommentSubmit)到子組件,綁定它到子組件的onCommentSubmit事件上。無論事件什么時候觸發(fā),回調(diào)函數(shù)都將會被調(diào)用:
// 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中調(diào)用這個回調(diào)函數(shù):
// 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>
);
}
});
現(xiàn)在回調(diào)函數(shù)已經(jīng)就緒,唯一我們需要做的就是提交到服務器,然后刷新列表:
// 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>
);
}
});
我們的應用現(xiàn)在已經(jīng)完成了所有功能,但是在你的評論出現(xiàn)在列表之前,你必須等待請求完成,感覺很慢。我們可以提前添加這條評論到列表中,從而使應用感覺更快。
// 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的內(nèi)容,或者深入學習API參考,開始專研!祝你好運!