利用React寫一個評論區組件(React初探)
本文是在閱讀學習了官方的React Tutorial之后的整理,實例鏈接。
開始使用React
首先從官方獲取React.js的最新版本(v0.12.2),或者下載官方的Starter Kit,并在我們的html中引入它們:
<head> <meta charset="UTF-8"> <title>React Test Page</title> <script src="../build/react.js"></script> <script src="../build/JSXTransformer.js"></script> </head>
JSX語法
我們可以在React組件的代碼中發現xml標簽似乎直接寫進了javascript里:
React.render( <CommentBox />, document.getElementById('content') );
這種寫法被稱作JSX,是React的一個可選功能,將xml標簽直接寫在javascript中看上去比調用javascript方法要更加直觀些。要正常使用這個功能,需要在你的頁面中引入JSXTransformer.js文件,或者使用npm安裝react-tools,將包含JSX語法的源文件編譯成常規的javascript文件,比較推薦的是后者,因為使用后者讓頁面可以直接使用編譯后的javascript文件而不需要在加載頁面時進行JSX編譯。
JSX中的類HTML標簽并不是真正的HTML元素,也不是一段HTML字符串,而是實例化了的React組件,關于JSX語法的更多內容,可以看這篇文章。
創建組件
React可以為我們創建模塊化、可組合的組件,對于我們需要做的評論區,我們的組件結構如下:
- CommentBox - CommentList -Comment - CommentForm
通過React.createClass()可以一個React元素,我們可以像這樣定義我們的CommentBox,并通過React.render()方法可以讓我們在指定的容器中將React元素渲染為一個DOM組件:
<body> <div id="content"></div> <script type="text/jsx"> var CommentBox = React.createClass({ render: function() { return ( <div className="contentBox"> <h1>Comments</h1> <CommentList /> <CommentForm /> </div> ); } }); React.render( <CommentBox />, document.getElementById('content') ); </script> </body>
從這個例子也可以看出一個組件可以包含子組件,組件之間是可以組合的(Composing),并呈現一個樹形結構,也可以說render方法中的的 CommentBox代表的是組件樹的根元素。那么接下來我們來創建CommentList和CommentForm這兩個子組件。
首先是CommentList組件,這個組件是用來呈現評論列表的,根據開始我們設計的組件結構樹,這個組件應該是包含許多Comment子組件的,那么,假設我們已經獲取到評論數據了:
var comments = [ {author: "Pete Hunt", text: "This is one comment"}, {author: "Jordan Walke", text: "This is *another* comment"} ];
我們需要把數據傳遞給CommentList組件才能讓它去呈現,那么如何傳遞呢?我們可以通過this.props來訪問組件標簽上的屬性,比如我們在CommentBox組件的代碼中做如下修改:
<CommentList data=comments />
于是在CommentList組件中,我們可以通過訪問this.props.data來獲取到我們的評論數據。
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> ); } });
接下來寫Comment組件,這個組件用于呈現單個評論,我們希望它可以支持markdown語法,于是我們引入showdown這個庫,在HTML中引入它之后,我們可以調用它讓我們的評論支持Markdown語法。在這里我們需要this.props.children這個屬性,它返回了該組件標簽里的所有子元素。
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> ); } });
我們發現經過解析后html標簽被直接呈現了上去,因為React默認是有XSS保護的,所有對呈現的內容進行了轉義,但在現在的場景中,我們并不需要它的轉義(如果取消React默認的XSS保護,那么就需要仰仗于我們引入的庫具有XSS保護或者我們手動處理),這時我們可以這樣:
var converter = new Showdown.converter(); var Comment = React.createClass({ render: function() { // 通過this.props.children訪問元素的子元素 var rawHtml = converter.makeHtml(this.props.children.toString()); return ( // 通過this.props訪問元素的屬性 // 不轉義,直接插入純HTML <div className="comment"> <h2 className="commentAuthor">{this.props.author}</h2> <span dangerouslySetInnerHTML={{__html: rawHtml}} /> </div> ); } });
好了,接下來我們的CommentList算是完成了,我們需要加上CommentForm組件讓我們可以提交評論:
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 修改commentList // 獲取原生DOM元素 this.refs.author.getDOMNode().value = ''; this.refs.text.getDOMNode().value = ''; }, render: function() { return ( // 為元素添加submit事件處理程序 // 用ref為子組件命名,并可以在this.refs中引用 <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> ); } });
從以上的代碼中我們可以發現,我們可以為我們的組件添加事件處理程序,比如在這里我們需要利用form的submit事件,于是直接在標簽上添加onSubmit的屬性即可。需要注意的是,事件屬性需要滿足駝峰命名規則,也就是說如果是要添加click事件,那就要添加onClick,以此類推。還有一點就是我們需要獲取兩個文本框中的內容,這里使用的方法是在input標簽上添加ref屬性,這樣就可以認為這個input是它的一個子組件,然后就可以通過訪問this.refs來訪問到這個子組件了,通過調用getDOMNode方法可以獲取原生的DOM對象進行相應的操作。
我們發現到現在為止,我們的頁面是靜態的,但我們希望可以在成功提交了評論后可以立刻在評論列表中看到自己的評論,并可以每隔一段時間獲取最新的評論,也就是說我們希望我們的CommentBox可以動態地改變狀態。
首先我們先讓CommentBox組件可以通過AJAX請求(在這里我用setTimeout來模擬獲取數據的延遲),從服務器端獲取評論數據同時更新CommentList。React組件有一個私有的this.state屬性用于保存組件可變狀態的數據,但一開始我們需要的是一個初始的狀態,初始狀態可以通過設置組件的getInitialState方法,它的返回值即為狀態初始值。這個時候我們不是從標簽的屬性上直接獲取數據了,需要通過訪問this.state來獲取(這個state屬性如果直接用javascript訪問會返回undefined,但可以在JSX中可以像this.state.data這樣使用):
var CommentBox = React.createClass({ getInitialState: function() { return {data: []}; }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm /> </div> ); } });
接下來我們需要獲取評論數據,我們可以在組件的componentDidMount方法中實現,這個方法會在組件呈現在頁面上之后會被立刻調用一次,我們就在這個方法中獲取到數據后更新下組件的狀態,要更新組件的狀態需要調用組件的this.setState方法,于是我們就這樣寫:
var CommentBox = React.createClass({ // 在組件的生命周期中僅執行一次,用于設置初始狀態 getInitialState: function() { return {data: []}; }, loadCommentsFromServer : function() { var self = this; setTimeout(function() { // 動態更新state self.setState({data: comments}); }, 2000); }, // 當組件render完成后自動被調用 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> ); } });
現在我們已經可以更新評論列表里的數據了,那么同樣的我們在CommentForm中成功提交的評論也要可以在CommentList中呈現出來,在這里需要注意的是我們現在設置的初始狀態是CommentBox這個組件的,修改狀態也是修改的CommentBox的狀態,那么如果要在 CommentForm中改變CommentBox的狀態,就需要在CommentBox組件中通過標簽屬性的方式傳遞一個方法給子組件 CommentForm,讓CommentForm組件中的handleSubmit可以調用這個方法(也就是上面TODO的位置),于是我們的代碼就是這樣的:
var CommentBox = React.createClass({ // 在組件的生命周期中僅執行一次,用于設置初始狀態 getInitialState: function() { return {data: []}; }, onCommentSubmit: function(comment) { // 模擬提交數據 comments.push(comment); var self = this; setTimeout(function() { // 動態更新state self.setState({data: comments}); }, 500); }, loadCommentsFromServer : function() { var self = this; setTimeout(function() { // 動態更新state self.setState({data: data}); }, 2000); }, // 當組件render完成后自動被調用 componentDidMount: function() { this.loadCommentsFromServer(); setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, render: function() { return ( // 并非是真正的DOM元素,是React的div組件,默認具有XSS保護 <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm onCommentSubmit={this.onCommentSubmit} /> </div> ); } }); var CommentForm = React.createClass({ handleSubmit: function(e) { e.preventDefault(); // e.returnValue = false; 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}); // 獲取原生DOM元素 this.refs.author.getDOMNode().value = ''; this.refs.text.getDOMNode().value = ''; }, render: function() { return ( // 為元素添加submit事件處理程序 // 用ref為子組件命名,并可以在this.refs中引用 <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> ); } });
到此為止,我們的CommentBox組件就大功告成了,實例鏈接。