ReactJS 官网案例分析
案例一、聊天室案例
/**
* This file provided by Facebook is for non-commercial testing and evaluation
* purposes only. Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
//定义Comment整体组件 var Comment = React.createClass({ //调用 marked 库,转义出HTML标签 rawMarkup: function() { var rawMarkup = marked(this.props.children.toString(), {sanitize: true}); return { __html: rawMarkup }; }, //输出HTML render: function() { return ( <div className="comment"> <h2 className="commentAuthor"> {this.props.author} </h2> <span dangerouslySetInnerHTML={this.rawMarkup()} /> </div> ); } }); var CommentBox = React.createClass({ //从服务器获取数据 loadCommentsFromServer: function() { $.ajax({ url: this.props.url, dataType: 'json', cache: false, //存储Ajax返回的状态数据 success: function(data) { this.setState({data: data}); //绑定到CommentBox的上下文 }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this)//绑定到CommentBox的上下文 }); }, //数据提交事件 handleCommentSubmit: function(comment) { var comments = this.state.data; // Optimistically set an id on the new comment. It will be replaced by an // id generated by the server. In a production application you would likely // not use Date.now() for this and would have a more robust system in place. //设置提交数据的id属性 comment.id = Date.now(); //链接新的数组评论对象 var newComments = comments.concat([comment]); //设置评论列表,更新组件状态数据 this.setState({data: newComments}); //发送Ajax请求,更新评论数据列表 $.ajax({ url: this.props.url, dataType: 'json', type: 'POST', data: comment, //更新数据成功执行 success: function(data) { //动态更新界面的关键点就是调用 this.setState() 。更新界面。 //我们用新的从服务器拿到的评论数组来替换掉老的评论数组,然后 UI 自动更新。有了这种反应机制,实时更新就仅需要一小点改动。在这里我们使用简单的轮询 this.setState({data: data}); }.bind(this), //更新数据失败执行 error: function(xhr, status, err) { //执行失败更新回state this.setState({data: comments}); console.error(this.props.url, status, err.toString()); }.bind(this)//绑定到CommentBox的上下文 }); }, //getInitialState() 在组件的生命周期中仅执行一次,用于设置组件的初始化 state 。 getInitialState: function() { //初始化设置data为空 return {data: []}; }, //componentDidMount 是一个组件渲染的时候被 React 自动调用的方法。 componentDidMount: function() { //首次渲染调用此方法,获取列表 this.loadCommentsFromServer(); //计时器循环更新组件的state,更新页面的评论列表 setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, render: function() { //设置Comment组件,设置CommentList组件的data属性,将获取的data值传递给它 //传递一个新的回调函数( handleCommentSubmit )到子组件CommentForm,设置子组件“onCommentSubmit”属性 return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm onCommentSubmit={this.handleCommentSubmit} /> </div> ); } }); //定义CommentList组件 var CommentList = React.createClass({ render: function() { //映射数据列表,传入Comment组件需要的author属性和id属性,并显示内容 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> ); } }); //定义用户提交表单 var CommentForm = React.createClass({ //初始化组件,设置属性的初始值 getInitialState: function() { return {author: '', text: ''}; }, //更新作者状态 handleAuthorChange: function(e) { this.setState({author: e.target.value}); }, //更新文本状态 handleTextChange: function(e) { this.setState({text: e.target.value}); }, //提交事件 handleSubmit: function(e) { e.preventDefault(); var author = this.state.author.trim(); var text = this.state.text.trim(); //获取作者和文本 if (!text || !author) { return; } //调用父组件的提交事件 this.props.onCommentSubmit({author: author, text: text}); //重置状态数据 this.setState({author: '', text: ''}); }, render: function() { //渲染form表单。设置元素值到HTML元素,并绑定事件 return ( <form className="commentForm" onSubmit={this.handleSubmit}> <input type="text" placeholder="Your name" value={this.state.author} onChange={this.handleAuthorChange} /> <input type="text" placeholder="Say something..." value={this.state.text} onChange={this.handleTextChange} /> <input type="submit" value="Post" /> </form> ); } }); //渲染Dom组件 ReactDOM.render( <CommentBox url="/api/comments" pollInterval={2000} />, document.getElementById('content') );
案例二、检索列表
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Document</title> 6 <script src="../../build/react.min.js"></script> 7 <script src="../../build/JSXTransformer.js"></script> 8 </head> 9 <body> 10 <div id="content"></div> 11 <script type="text/jsx"> 12 //目录行表头 13 var ProductCategoryRow = React.createClass({ 14 render: function() { 15 return (<tr><th colSpan="2">{this.props.category}</th></tr>); 16 } 17 }); 18 19 //产品行 20 var ProductRow = React.createClass({ 21 render: function() { 22 //判断name的值红色,还是黑色。return前面可以写应用逻辑 23 var name = this.props.product.stocked ? 24 this.props.product.name : 25 <span style={{color: 'red'}}> 26 {this.props.product.name} 27 </span>; 28 return ( 29 <tr> 30 <td>{name}</td> 31 <td>{this.props.product.price}</td> 32 </tr> 33 ); 34 } 35 }); 36 37 // 产品列表组件,根据 “用户输入( user input )” 过滤和展示 “数据集合( data collection )” 38 var ProductTable = React.createClass({ 39 render: function() { 40 //定义列表数组 41 var rows = []; 42 //记录最后一条目录 43 var lastCategory = null; 44 //遍历产品数组,将产品记录到数组 45 this.props.products.forEach(function(product) { 46 //判断记录,如果不等将“产品目录”压入“数组”中 47 // if (product.category !== lastCategory) { 48 // rows.push(<ProductCategoryRow category={product.category} key={product.category} />); 49 // } 50 // 51 // 判断过滤条件 52 if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) { 53 return; 54 } 55 56 57 //否则压入产品组件 58 // rows.push(<ProductRow product={product} key={product.name} />); 59 if (product.category !== lastCategory) { 60 rows.push(<ProductCategoryRow category={product.category} key={product.category} />); 61 } 62 63 64 // 65 // 66 //记录最后一个目录 67 lastCategory = product.category; 68 }.bind(this)); 69 //渲染出table 70 return ( 71 <table> 72 <thead> 73 <tr> 74 <th>Name</th> 75 <th>Price</th> 76 </tr> 77 </thead> 78 <tbody>{rows}</tbody> 79 </table> 80 ); 81 } 82 }); 83 84 //搜索框子组件,接受所有用户输入( user input ) 85 var SearchBar = React.createClass({ 86 //定义搜索框组件内部内容 87 render: function() { 88 return ( 89 <form> 90 <input type="text" placeholder={this.props.filterText} /> 91 <p> 92 <input type="checkbox" checked={this.props.inStockOnly}/> 93 {' '} 94 Only show products in stock 95 </p> 96 </form> 97 ); 98 } 99 }); 100 101 //整个产品列表容器 102 var FilterableProductTable = React.createClass({ 103 //设置初始State,标示json数据 104 getInitialState: function() { 105 return { 106 filterText: '', 107 inStockOnly: false 108 }; 109 }, 110 111 //设置更新State,更新Dom,这个一般和State的初始化一起存在 112 handleUserInput: function(filterText, inStockOnly) { 113 this.setState({ 114 filterText: filterText, 115 inStockOnly: inStockOnly 116 }); 117 }, 118 119 //Div层输出子组件,单纯定义。 120 //将自己的state状态传递给子组件。如果products是ajax,那么它也是state. 121 render: function() { 122 return ( 123 <div> 124 <SearchBar 125 filterText={this.state.filterText} 126 inStockOnly={this.state.inStockOnly} 127 onUserInput={this.handleUserInput} 128 /> 129 <ProductTable 130 products={this.props.products} 131 filterText={this.state.filterText} 132 inStockOnly={this.state.inStockOnly} 133 /> 134 </div> 135 ); 136 } 137 }); 138 139 var PRODUCTS = [ 140 {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'}, 141 {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'}, 142 {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'}, 143 {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'}, 144 {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'}, 145 {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'} 146 ]; 147 148 //渲染HTML 149 React.render( 150 <FilterableProductTable products={PRODUCTS} />, 151 document.getElementById('content') 152 ); 153 154 155 </script> 156 </body> 157 </html>
//1、(思考数据片段,找出state数据)思考示例应用中的所有数据片段,有:
//state组件的状态判断,识别出最小的(但是完整的)代表 UI 的 state.此处关键点在于精简:不要存储重复的数据。
//让我们分析每一项,指出哪一个是 state 。简单地对每一项数据提出三个问题:
//是否是从父级通过 props 传入的?如果是,可能不是 state 。
//是否会随着时间改变?如果不是,可能不是 state 。
//能根据组件中其它 state 数据或者 props 计算出来吗?如果是,就不是 state 。
//2、(产生数据片段的组件)
//最初的 products 列表(不是state,传入)
//用户输入的搜索文本(是,随时间变化)
//复选框的值(是,随时间变化)
//过滤后的 products 列表(不是,可以计算出)
//初始的 products 列表通过 props 传入,所以不是 state 。
//搜索文本和复选框看起来像是 state ,因为它们随着时间改变,也不能根据其它数据计算出来。
//最后,过滤的 products 列表不是 state ,因为可以通过搜索文本和复选框的值从初始的 products 列表计算出来。
//所以最终, state 是:用户输入的搜索文本,复选框的值
//3、(判断state在那个组件上更新最为合适,即数据独立更新的Dom组件)
//我们辨别出了应用的 state 数据模型的最小集合。接下来,需要指出哪个组件会改变或者说拥有这个 state 数据模型。
//记住: React 中数据是沿着组件树从上到下单向流动的。可能不会立刻明白哪个组件应该拥有哪些 state 数据模型。
//这对新手通常是最难理解和最具挑战的,因此跟随以下步骤来弄清楚这点:
//对于应用中的每一个 state 数据:
//找出每一个基于那个 state 渲染界面的组件。(ProductTable 需要基于 state 过滤产品列表,SearchBar 需要显示搜索文本和复选框状态。
//所以选择它们共同的父亲,ProductTable。)
//找出state所在组件的原则:
//找出共同的祖先组件(某个单个的组件,在组件树中位于需要这个 state 的所有组件的上面)。共同的需要State组件的上面父组件。
//要么是共同的祖先组件,要么是另外一个在组件树中位于更高层级的组件应该拥有这个 state 。
//如果找不出拥有这个 state 数据模型的合适的组件,创建一个新的组件来维护这个 state ,然后添加到组件树中,层级位于所有共同拥有者组件的上面。
//让我们在应用中应用这个策略:
//ProductTable 需要基于 state 过滤产品列表,SearchBar 需要显示搜索文本和复选框状态。
//共同拥有者组件是 FilterableProductTable 。
//理论上,过滤文本和复选框值位于 FilterableProductTable 中是合适的。
//我们决定了 state 数据模型位于 FilterableProductTable 之中。
//4、(设置state)
//给 FilterableProductTable 添加 getInitialState() 方法,该方法返回 {filterText: '', inStockOnly: false} 来反映应用的初始化状态。
//传递 filterText 和 inStockOnly 给 ProductTable 和 SearchBar 作为 prop 。
//使用这些 props 来过滤 ProductTable 中的行,设置在 SearchBar 中表单字段的值。
//开始观察应用将会如何运行:设置 filterText 为 "ball" ,然后刷新应用。将会看到数据表格被正确更新了。
案例三、更新事件触发
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="../../build/react.min.js"></script> <script src="../../build/JSXTransformer.js"></script> </head> <body> <div id="content"></div> <script type="text/jsx"> //富交互性的动态用户界面 var LikeButton = React.createClass({ //设置初始化状态 getInitialState: function() { return {liked: false}; }, //调用事件 handleClick: function(event) { //切换事件状态 //工作是从 props 里取数据并渲染出来。用户输入、服务器请求或者时间变化等作出响应,这时才需要使用 State。 // this.setState({liked: !this.state.liked}); }, render: function() { //渲染时候的处理逻辑 //常用的模式:创建多个只负责渲染数据的无状态(stateless)组件,在它们的上层创建一个有状态(stateful)组件并把它的状态通过 props //传给子级。这个有状态的组件封装了所有用户的交互逻辑,而这些无状态组件则负责声明式地渲染数据。 var text = this.state.liked ? 'like' : 'haven\'t liked'; //绑定并调用组件的方法 return ( <p onClick={this.handleClick}> You {text} this. Click to toggle. </p> ); //State 应该包括那些可能被组件的事件处理器改变并触发用户界面更新的数据。真实的应用中这种数据一般都很小且能被JSON序列化。当创建一个状态化的组件时,想象一下表示它 //的状态最少需要哪些数据,并只把这些数据存入 this.state。在 render() //里再根据state来计算你需要的其它数据。你会发现以这种方式思考和开发程序最终往往是正确的,因为如果在 state // 里添加冗余数据或计算所得数据,需要你经常手动保持数据同步,不能让 React 来帮你处理。 // this.state 应该仅包括能表示用户界面状态所需的最少数据。因此,它不应该包括:计算所得数据: 不要担心根据 state 来预先计算数据 —— 把所有的计算都放到 // render() 里更容易保证用户界面和数据的一致性。例如,在 state 里有一个数组(listItems),我们要把数组长度渲染成字符串, 直接在 render() 里使用 this.state // .listItems.length + ' list items' 比把它放到 state 里好的多。React 组件: 在 render() 里使用当前 props 和 state 来创建它。基于 props // 的重复数据: 尽可能使用 props 来作为惟一数据来源。把 props 保存到 state 的一个有效的场景是需要知道它以前值的时候,因为未来的 props 可能会变化。 } }); React.render( <LikeButton />, document.getElementById('content') ); </script> </body> </html>