React實踐(一)
該實踐取自官方教程:https://github.com/reactjs/react-tutorial
主要是自實現的過程以及一些心得體會
該實踐是實現一個評論框。
- 一個展示所有評論的視圖
- 一個提交評論的表單
- 與后台的接口hook
特點:
- 評論提交之前就先顯示在列表中,提高體驗
- 其他用戶的評論實時更新
- 可用markdown格式編寫文本
開始
下面就是我們的index.html模板文件,看官copy過去吧。之后的所有代碼都寫在script里面
1 <!-- index.html --> 2 <html> 3 <head> 4 <title>Hello React</title> 5 <script src="http://fb.me/react-0.13.0.js"></script> 6 <script src="http://fb.me/JSXTransformer-0.13.0.js"></script> 7 <script src="http://code.jquery.com/jquery-1.10.0.min.js"></script> 8 </head> 9 <body> 10 <div id="content"></div> 11 <script type="text/jsx"> 12 // Your code here 13 </script> 14 </body> 15 </html>
其中jquery不是對React必要的,只是我們想簡化ajax的代碼
組件結構
React是全組件化的,可組裝的。我們的組件結構如下:
- CommentBox - CommentList - Comment - CommentForm
CommentBox
讓我們先來把最基本的組件構造出來
1 var CommentBox = React.createClass({ 2 render: function() { 3 return ( 4 <div className="commentBox"> 5 Hello, world! I am a CommentBox. 6 </div> 7 ); 8 } 9 }); 10 React.render( 11 <CommentBox />, 12 document.getElementById('content') 13 );
從代碼不難看出,也不過是簡單的div罷了
CommentList、CommentForm
1 var CommentList = React.createClass({ 2 render: function() { 3 return ( 4 <div className="commentList"> 5 Hello, world! I am a CommentList. 6 </div> 7 ); 8 } 9 }); 10 11 var CommentForm = React.createClass({ 12 render: function() { 13 return ( 14 <div className="commentForm"> 15 Hello, world! I am a CommentForm. 16 </div> 17 ); 18 } 19 });
這兩個組件也不過就是div而已
那我們根據組件結構,把這兩個組件放進CommentBox:
1 var CommentBox = React.createClass({ 2 render: function() { 3 return ( 4 <div className="commentBox"> 5 <h1>Comments</h1> 6 <CommentList /> 7 <CommentForm /> 8 </div> 9 ); 10 } 11 });
Comment
組件結構里,現在還剩CommentList里的Comment。我們想在評論中傳評論人和評論文本過去。那我們來實現一下:
1 var CommentList = React.createClass({ 2 render: function() { 3 return ( 4 <div className="commentList"> 5 <Comment author="Pete Hunt">This is one comment</Comment> 6 <Comment author="Jordan Walke">This is *another* comment</Comment> 7 </div> 8 ); 9 } 10 });
我們已經從父組件CommenList傳遞了一些數據給子組件Comment
那我們來實現Comment組件,看官應該還記得,我們通過this.props在子組件中獲取數據:
1 var Comment = React.createClass({ 2 render: function() { 3 return ( 4 <div className="comment"> 5 <h2 className="commentAuthor"> 6 {this.props.author} 7 </h2> 8 {this.props.children} 9 </div> 10 ); 11 } 12 });
其中,this.props.children是任何內嵌的元素。
而前面說了,我們提供markdown格式的輸入,那就修改一下。
添加Markdown
這里我們要用到第三方庫Showdown,作用是處理Markdown文本且轉換成原始HTML。
我們先添加<script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>到head中
接着我們修改this.props.children,且添加Showdown的調用
1 var converter = new Showdown.converter(); 2 var Comment = React.createClass({ 3 render: function() { 4 return ( 5 <div className="comment"> 6 <h2 className="commentAuthor"> 7 {this.props.author} 8 </h2> 9 {converter.makeHtml(this.props.children.toString())} 10 </div> 11 ); 12 } 13 });
其中,為了轉換成Showdown能處理的原始字符串,所以顯示調用了toString()
但是React為了防止XSS,我們的顯示會類似這樣,<p>
This is<em>
another</em>
comment</p>
並沒有渲染成真正的HTML
當然我們這里有一個方法:
1 var converter = new Showdown.converter(); 2 var Comment = React.createClass({ 3 render: function() { 4 var rawMarkup = converter.makeHtml(this.props.children.toString()); 5 return ( 6 <div className="comment"> 7 <h2 className="commentAuthor"> 8 {this.props.author} 9 </h2> 10 <span dangerouslySetInnerHTML={{__html: rawMarkup}} /> 11 </div> 12 ); 13 } 14 });
注意:框架會警告你別使用這種方法
接入數據模型
上面,我們是把數據直接寫入React中,實際開發中,數據是來自於數據庫的,那我們這里就暫且用hard code的形式寫在JSON對象中。
1 var data = [ 2 {author: "Pete Hunt", text: "This is one comment"}, 3 {author: "Jordan Walke", text: "This is *another* comment"} 4 ];
我們需要把數據通過props的形式傳到CommentList。而首先就是把數據傳入到CommentBox
這里是React的單向數據流。關於這個,會在之后另開一篇文章來研究下。
1 var CommentBox = React.createClass({ 2 render: function() { 3 return ( 4 <div className="commentBox"> 5 <h1>Comments</h1> 6 <CommentList data={this.props.data} /> 7 <CommentForm /> 8 </div> 9 ); 10 } 11 }); 12 13 React.render( 14 <CommentBox data={data} />, 15 document.getElementById('content') 16 );
這時候,CommentList就可以使用數據了。讓我們來動態地去渲染評論,而不是之前一條一條地寫<Comment >xxx</Comment>
來看下代碼:
1 var CommentList = React.createClass({ 2 render: function() { 3 var commentNodes = this.props.data.map(function (comment) { 4 return ( 5 <Comment author={comment.author}> 6 {comment.text} 7 </Comment> 8 ); 9 }); 10 return ( 11 <div className="commentList"> 12 {commentNodes} 13 </div> 14 ); 15 } 16 });
就這樣了。
從數據庫獲取數據
實際開發中,往往是后台提供了數據接口,而這時候,我們就需要這個接口跟我們上面的實現結合起來了。
而且,現在的這個組件在請求數據回來之前,是沒有數據的。並且,評論是需要更新的。
我們之前是通過data傳給CommentBox,每個組件也只在初始化的時候更新一次。
props是不可變的,它們從父節點傳過來,被父節點所擁有。而為了實現交互,這里就需要用到state,this.state是組件私有的,當state變化的時候,組件會重新渲染自己。
關於props和state,之后也會寫一篇來具體介紹一下。
1 React.render( 2 <CommentBox url="comments.json" />, 3 document.getElementById('content') 4 );
因為,我們這里沒有去實現后台,所以姑且用本地的文件comments.json來返回這個JSON對象,就創建一個comments.json文件放在index.html同目錄下,把下面的復制進去:
// comments.json [ {"author": "Pete Hunt", "text": "This is one comment"}, {"author": "Jordan Walke", "text": "This is *another* comment"} ]
這里我們用的是JQ的AJAX,下面是修改后的CommentBox:
1 var CommentBox = React.createClass({ 2 getInitialState: function() { 3 return {data: []}; 4 }, 5 componentDidMount: function() { 6 $.ajax({ 7 url: this.props.url, 8 dataType: 'json', 9 success: function(data) { 10 this.setState({data: data}); 11 }.bind(this), 12 error: function(xhr, status, err) { 13 console.error(this.props.url, status, err.toString()); 14 }.bind(this) 15 }); 16 }, 17 render: function() { 18 return ( 19 <div className="commentBox"> 20 <h1>Comments</h1> 21 <CommentList data={this.state.data} /> 22 <CommentForm /> 23 </div> 24 ); 25 } 26 });
這里我們給CommentBox添加了一個data數組的state,作為取到的評論的保存。
組件會在組件構建完后,去取數據,動態更新的要點就是this.setState()
而我們的評論是實時更新的,即別人如果在數據庫里添加了評論,那我們是要實時去檢測是否更新了的。
這里我們就簡單的用輪訓的方法:
1 var CommentBox = React.createClass({ 2 loadCommentsFromServer: function() { 3 $.ajax({ 4 url: this.props.url, 5 dataType: 'json', 6 success: function(data) { 7 this.setState({data: data}); 8 }.bind(this), 9 error: function(xhr, status, err) { 10 console.error(this.props.url, status, err.toString()); 11 }.bind(this) 12 }); 13 }, 14 getInitialState: function() { 15 return {data: []}; 16 }, 17 componentDidMount: function() { 18 this.loadCommentsFromServer(); 19 setInterval(this.loadCommentsFromServer, this.props.pollInterval); 20 }, 21 render: function() { 22 return ( 23 <div className="commentBox"> 24 <h1>Comments</h1> 25 <CommentList data={this.state.data} /> 26 <CommentForm /> 27 </div> 28 ); 29 } 30 }); 31 32 React.render( 33 <CommentBox url="comments.json" pollInterval={2000} />, 34 document.getElementById('content') 35 );
關於顯示的,我們就弄的差不多了。
接下來是我們的表單提交部分
添加新評論
我們的表單要求用戶輸入評論人和評論內容,當用戶提交表單的時候,會把數據提交到服務器,然后保存這條數據。
1 var CommentForm = React.createClass({ 2 render: function() { 3 return ( 4 <form className="commentForm"> 5 <input type="text" placeholder="Your name" /> 6 <input type="text" placeholder="Say something..." /> 7 <input type="submit" value="Post" /> 8 </form> 9 ); 10 } 11 });
我們的表單是可交互的,所以這里要添加表單的提交事件,且刷新評論列表。為了更好的體驗,在提交完表單之后,表單應該是清空了的。
1 var CommentForm = React.createClass({ 2 handleSubmit: function(e) { 3 e.preventDefault(); 4 var author = this.refs.author.getDOMNode().value.trim(); 5 var text = this.refs.text.getDOMNode().value.trim(); 6 if (!text || !author) { 7 return; 8 } 9 // TODO: send request to the server 10 this.refs.author.getDOMNode().value = ''; 11 this.refs.text.getDOMNode().value = ''; 12 return; 13 }, 14 render: function() { 15 return ( 16 <form className="commentForm" onSubmit={this.handleSubmit}> 17 <input type="text" placeholder="Your name" ref="author" /> 18 <input type="text" placeholder="Say something..." ref="text" /> 19 <input type="submit" value="Post" /> 20 </form> 21 ); 22 } 23 });
其中,
- 我們利用了ref屬性給子組件命名,this.refs引用組件,getDOMNode()獲取本地的DOM元素。
- React使用駝峰命名的方式給組件綁定事件,我們給表單綁定了onSubmit()事件,當數據合法,清空輸入框
當用戶提交了表單后,我們需要添加我們的評論到評論列表。上面我們是給了commentBox一個state來保存評論列表。正是因為這樣,我們的所有邏輯在commentBox中完成是最合理的。因為我們需要從子組件傳回數據給父組件,這里我們把回調函數作為屬性傳給子組件。
1 var CommentBox = React.createClass({ 2 loadCommentsFromServer: function() { 3 $.ajax({ 4 url: this.props.url, 5 dataType: 'json', 6 success: function(data) { 7 this.setState({data: data}); 8 }.bind(this), 9 error: function(xhr, status, err) { 10 console.error(this.props.url, status, err.toString()); 11 }.bind(this) 12 }); 13 }, 14 handleCommentSubmit: function(comment) { 15 // TODO: submit to the server and refresh the list 16 }, 17 getInitialState: function() { 18 return {data: []}; 19 }, 20 componentDidMount: function() { 21 this.loadCommentsFromServer(); 22 setInterval(this.loadCommentsFromServer, this.props.pollInterval); 23 }, 24 render: function() { 25 return ( 26 <div className="commentBox"> 27 <h1>Comments</h1> 28 <CommentList data={this.state.data} /> 29 <CommentForm onCommentSubmit={this.handleCommentSubmit} /> 30 </div> 31 ); 32 } 33 });
當用戶提交表單的時候,調用回調函數:
1 var CommentForm = React.createClass({ 2 handleSubmit: function(e) { 3 e.preventDefault(); 4 var author = this.refs.author.getDOMNode().value.trim(); 5 var text = this.refs.text.getDOMNode().value.trim(); 6 if (!text || !author) { 7 return; 8 } 9 this.props.onCommentSubmit({author: author, text: text}); 10 this.refs.author.getDOMNode().value = ''; 11 this.refs.text.getDOMNode().value = ''; 12 return; 13 }, 14 render: function() { 15 return ( 16 <form className="commentForm" onSubmit={this.handleSubmit}> 17 <input type="text" placeholder="Your name" ref="author" /> 18 <input type="text" placeholder="Say something..." ref="text" /> 19 <input type="submit" value="Post" /> 20 </form> 21 ); 22 } 23 });
回調函數等一切都搞定,現在把提交到服務器的代碼和刷新評論的代碼補上來:
1 var CommentBox = React.createClass({ 2 loadCommentsFromServer: function() { 3 $.ajax({ 4 url: this.props.url, 5 dataType: 'json', 6 success: function(data) { 7 this.setState({data: data}); 8 }.bind(this), 9 error: function(xhr, status, err) { 10 console.error(this.props.url, status, err.toString()); 11 }.bind(this) 12 }); 13 }, 14 handleCommentSubmit: function(comment) { 15 $.ajax({ 16 url: this.props.url, 17 dataType: 'json', 18 type: 'POST', 19 data: comment, 20 success: function(data) { 21 this.setState({data: data}); 22 }.bind(this), 23 error: function(xhr, status, err) { 24 console.error(this.props.url, status, err.toString()); 25 }.bind(this) 26 }); 27 }, 28 getInitialState: function() { 29 return {data: []}; 30 }, 31 componentDidMount: function() { 32 this.loadCommentsFromServer(); 33 setInterval(this.loadCommentsFromServer, this.props.pollInterval); 34 }, 35 render: function() { 36 return ( 37 <div className="commentBox"> 38 <h1>Comments</h1> 39 <CommentList data={this.state.data} /> 40 <CommentForm onCommentSubmit={this.handleCommentSubmit} /> 41 </div> 42 ); 43 } 44 });
代碼基本上都已經完成了。
還有一個點,我們可以做的,就是為了提高體驗,我們可以本地先把用戶提交的評論顯示出來,之后再異步提交到服務器,讓用戶覺得應用快快快。
1 var CommentBox = React.createClass({ 2 loadCommentsFromServer: function() { 3 $.ajax({ 4 url: this.props.url, 5 dataType: 'json', 6 success: function(data) { 7 this.setState({data: data}); 8 }.bind(this), 9 error: function(xhr, status, err) { 10 console.error(this.props.url, status, err.toString()); 11 }.bind(this) 12 }); 13 }, 14 handleCommentSubmit: function(comment) { 15 var comments = this.state.data; 16 var newComments = comments.concat([comment]); 17 this.setState({data: newComments}); 18 $.ajax({ 19 url: this.props.url, 20 dataType: 'json', 21 type: 'POST', 22 data: comment, 23 success: function(data) { 24 this.setState({data: data}); 25 }.bind(this), 26 error: function(xhr, status, err) { 27 console.error(this.props.url, status, err.toString()); 28 }.bind(this) 29 }); 30 }, 31 getInitialState: function() { 32 return {data: []}; 33 }, 34 componentDidMount: function() { 35 this.loadCommentsFromServer(); 36 setInterval(this.loadCommentsFromServer, this.props.pollInterval); 37 }, 38 render: function() { 39 return ( 40 <div className="commentBox"> 41 <h1>Comments</h1> 42 <CommentList data={this.state.data} /> 43 <CommentForm onCommentSubmit={this.handleCommentSubmit} /> 44 </div> 45 ); 46 } 47 });
其實就添加了15-17行。
簡單吧~~~~~~~
這就是React官方提供的最基本的實踐~~~~~~
看到了這里的都是真愛~~~~~~~~~
晚安~~~~~~~~