我們將建造一個簡單但是實用的評論框,你可以把它用在博客或者作為一個基礎版的實時評論組件,比如淘寶、微博。
我們將提供:
- 所有評論的查看
- 一個提供評論功能的表單
- 將你定制的后端與之關聯
它還有一些優雅的特性
- 積極的評論:提交之后的評論會馬上出現在評論列表,因此它看起來很快。
- 動態更新:其它用戶的評論將會實時出現。
- Markdown 支持:用戶可用MarkDown語法給自己的評論排版。
想跳過所有步驟僅僅查看代碼?
啟動一個服務器
為了開始我們的教程,我們需要一個運行中的服務器。它僅僅提供一個用來獲取和提交數據的 API 端點服務。為了做起來更加簡單,我們用簡單的腳本語言來搭建服務。你可以查看源碼或者下載壓縮文件來包含一切我們需要的環境并開始我們的教程。
開始搭建
我們致力于讓本教程變得相對簡單,在我們上文提到的服務器的包中,包含著一個 HTML 文件,我們將用它來開始工作。在你喜歡的編輯器中打開 public/index.html
,它看起來應該像下面這樣:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>React Tutorial</title>
<script src="https://npmcdn.com/react@15.3.1/dist/react.js"></script>
<script src="https://npmcdn.com/react-dom@15.3.1/dist/react-dom.js"></script>
<script src="https://npmcdn.com/babel-core@5.8.38/browser.min.js"></script>
<script src="https://npmcdn.com/jquery@3.1.0/dist/jquery.min.js"></script>
<script src="https://npmcdn.com/remarkable@1.6.2/dist/remarkable.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>
在接下來的教程中,我們將在 script 標簽中編寫JavaScript代碼,我們沒有任何先進的動態加載功能因此你只需在保存后刷新你的瀏覽器就可以看到你的更新。通過在瀏覽器中打開http://localhost:3000
來跟進你的進度(在啟動服務器之后)。當你第一次加載這個文件并且沒有做任何改動時,你將會看到我們要打造功能的最終樣子。當你準備好接下來的教程了,只需要刪除<script>標簽。
備注:
我們包含進了JQuery庫因為我們想要簡化將來用到的AJAX代碼,但是它并不是強制地為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>
);
}
});
ReactDOM.render(
<CommentBox />,
document.getElementById('content')
);
注意到原生的 HTML 元素采用小駝峰命名法,同時定制的 React 類名采用大駝峰命名法。
JSX 語法
你第一個注意到的會是 XML 語法聯合在你的JavaScript 中。我們有一個前置編譯器來將這種語法糖轉化為樸素的JS語法:
// 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語法比樸素的JS語法更加好用。在JSX 語法文章中閱讀更多。
接下來做什么
我們通過在JS類中的一些方法如 React.createClass()
來創建一個 React 組件。最重要的方法是調用 render()
,該方法返回一個組件樹,該樹用來渲染成HTML。
<div>
標簽并不是真實的 DOM 節點,他們是實例化的 React div
組件。你可以把它們想象成React用來處理數據的標記。React 是安全的,我們不生成 HTML 字符串,所以默認有 XSS 跨站腳本攻擊的防護。
你不用返回一個基本的 HTML。你可以返回一個自己或他人的組件樹。這就是React的組成:前端可維護的信條。
ReactDOM.render()
實例化根組件,啟動框架,把標記映射成新的DOM 元素。
重要的是ReactDOM.render
放在腳本的最底部,ReactDOM.render
應該確保等所有組件被初始化完畢再調用。
拼裝組件
讓我們創建CommentList
和CommentForm
的骨骼,同樣采用簡單的<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>
);
}
});
接下來,更新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
組件,它依靠父組件傳遞給他的數據。從父組件傳遞過來的數據通過屬性
的方式傳遞給子組件。這些屬性
通過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>
);
}
});
花括號中的JS表達式被嵌套在JSX中,你可以把文本或者React組件放入樹中,我們通過命名的屬性訪問傳遞過來的數據,并且訪問元素中的數據。
組件的屬性
既然我們定義了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和This is one comment給Comment
。如上文那樣,Comment
組件通過this.props.author
和this.props.children
來訪問這些屬性
。
添加Markdown 支持
Markdown是格式化你的文本的一種簡單的方法。比如,用幸好環繞文本會讓它加重。
在本教程中,我們使用第三發的庫remarkable來實現。在初始的頁面中,我們已經包含了這個庫,所以我們可以直接使用它。
// tutorial6.js
var Comment = React.createClass({
render: function() {
var md = new Remarkable();
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
{md.render(this.props.children.toString())}
</div>
);
}
});
我們在這里做的事情就是調用 remarkable 庫,我們需要把 this.props.children
的React的文字換行格式轉化成未加工的字符串以使 remarkable 很好的工作。
問題出現了,我們的加工過的字符串成了這樣子<p>This is <em>another</em> comment</p>
,我們希望這些標簽編程HTML。
這是因為React為了防止XSS 攻擊所做的工作。有一種方法擺脫這種防護,但是框架會警告你最好不要這么做。
// tutorial7.js
var Comment = React.createClass({
rawMarkup: function() {
var md = new Remarkable();
var rawMarkup = md.render(this.props.children.toString());
return { __html: rawMarkup };
},
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
<span dangerouslySetInnerHTML={this.rawMarkup()} />
</div>
);
}
});
這是一個特殊的API 故意使插入一個未加工的HTML變得困難,但是為了使用 remarkable 我們可以利用這個后門。
聯系數據模型
目前為止,我們直接在源代碼中插入了評論。現在,讓我們用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。修改 CommentBox 和 ReactDOM.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>
);
}
});
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>
);
}
});
就是這樣!
從服務器獲取數據
讓我們用一些來自服務器的動態數據替換硬編碼的數據。我們將移除數據的prop,用獲取數據的URL來替換它:
// tutorial11.js
ReactDOM.render(
<CommentBox url="/api/comments" />,
document.getElementById('content')
);
這個組件不同于和前面的組件,因為它必須重新渲染自己。該組件將不會有任何數據,直到請求從服務器返回,此時該組件或許需要渲染一些新的評論。
注意: 此代碼在這個階段不會工作。
Reactive state
迄今為止,基于它自己的props,每個組件都渲染了自己一次。props 是不可變的:它們從父級傳來并被父級“擁有”。為了實現交互,我們給組件引進了可變的 state。this.state 是組件私有的,可以通過調用 this.setState() 改變它。每當state更新,組件就重新渲染自己。
render() 方法被聲明為一個帶有 this.props 和 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() 在生命周期里只執行一次,并設置組件的初始狀態。