ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# React 的思维 在我们的主张中,React 是使用 JavaScript 构建大型、快速 Web 应用的第一选择。它在 Facebook 和 Instagram 中都能很好的应用。 React 中许多伟大的部分中的一个是,它让你思考如何构建应用。在这个文档中,我们会带你了解用 React 构建一个可搜索产品数据表格的思考过程。 ## 从一个模拟开始 试想我们已经有一个 JSON API,和从设计者那里得来的一个 模拟。我们的设计者显然水平一般,因为模拟界面是下面这样的: ![](https://box.kancloud.cn/56fffff9f170375d2a34d42db6b39884_228x277.png) 我们的 JSON API 返回像这样的一些数据: ~~~ [ {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"}, {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"}, {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"}, {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"}, {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"}, {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"} ]; ~~~ ## Step 1:打碎 UI 分解为一个组件层级 你要做的第一件事是绘制每个组件的盒子(和子组件),并给它们命名。如果你跟一个设计师一起工作,它们可能已经做了这些,所以去告诉它们!它们的 Photoshop 层名称可能最终是你的 React 组件的名称。 但是你如何知道什么会是我们的组件?只要用决定是否应该创建一个新的函数或者对象的技巧就可以了。其中一种方式就是[独立责任原则](https://en.wikipedia.org/wiki/Single_responsibility_principle),也就是说,一个组件应该在理想情况下只做一件事情。如果最终增加,就应该分解到更小的子组件。 由于经常显示一个 JSON 数据 模型给用户,你会发现如果你的模型能够正确构建,你的 UI(包括你的组件结构)将可以很好的进行映射。这是因为 UI 和数据模型坚守同样的信息结构,也就是分离你的 UI 到组件通常是比较琐碎的。只要打碎为表示明确的一部分数据模型的组件即可。 ![](https://box.kancloud.cn/9b6934d31d42fff35b91ddadc9e9b2ab_275x319.png) 你将看到这里我们有 5 个组件。我们已经为每个组件代表的数据做了斜体标示。 1. FilterableProductTable (orange):包含整个示例 2. SearchBar (blue):接受所有的用户输入 3. ProductTable (green): 根据用户输入显示和过滤数据集合 4. ProductCategoryRow (turquoise):对每个类别显示一个头 5. ProductRow (red):为每个产品显示一个行 如果你查看 ProductTable,将会看到这个表头(包含”Name“ 和”Price“标签)并不是它自己的组件。这是一个个人喜好,还有一个参数进行制作。对于这个例子,我们使它作为 ProductTable 的一部分,因为它是渲染数据集合的一部分,这是 ProductTable 的责任。 然而,如果 header 变得太复杂(即,如果我们添加用来排序的场景),可能更适用于只做它自己的 ProductTableHeader 组件。 现在我们已经在模拟设计中做了标识,让我们对它们进行层级排列。这很简单。在设计中出现在其它组件中的组件,应该作为层级中的子组件: * FilterableProductTable * SearchBar * ProductTable * ProductCategoryRow * ProductRow ## Step 2:用 React 构建一个静态版本 HTML: ~~~ <div id="container"> <!-- This element's contents will be replaced with your component. --> </div> ~~~ CSS: ~~~ body { padding: 5px } ~~~ BABEL: ~~~ class ProductCategoryRow extends React.Component { render() { return <tr><th colSpan="2">{this.props.category}</th></tr>; } } class ProductRow extends React.Component { render() { var name = this.props.product.stocked ? this.props.product.name : <span style={{color: 'red'}}> {this.props.product.name} </span>; return ( <tr> <td>{name}</td> <td>{this.props.product.price}</td> </tr> ); } } class ProductTable extends React.Component { render() { var rows = []; var lastCategory = null; this.props.products.forEach(function(product) { if (product.category !== lastCategory) { rows.push(<ProductCategoryRow category={product.category} key={product.category} />); } rows.push(<ProductRow product={product} key={product.name} />); lastCategory = product.category; }); return ( <table> <thead> <tr> <th>Name</th> <th>Price</th> </tr> </thead> <tbody>{rows}</tbody> </table> ); } } class SearchBar extends React.Component { render() { return ( <form> <input type="text" placeholder="Search..." /> <p> <input type="checkbox" /> {' '} Only show products in stock </p> </form> ); } } class FilterableProductTable extends React.Component { render() { return ( <div> <SearchBar /> <ProductTable products={this.props.products} /> </div> ); } } var PRODUCTS = [ {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'}, {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'}, {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'}, {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'}, {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'}, {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'} ]; ReactDOM.render( <FilterableProductTable products={PRODUCTS} />, document.getElementById('container') ); ~~~ 结果: ![](https://box.kancloud.cn/d0d6240162fb85e15d9a5b54d8c73649_287x317.png) 现在你已经写好了组件层级,是时候实现 app 了。最简单的办法是构建一个传递你的数据模型并渲染 UI 但是不包括交互性的版本。最好解耦这些处理,因为构建静态版本需要许多大量输入而不需要思考,而添加交互需要大量思考而不是输入。我们将看到原因。 要构建一个静态版本的 app,用于渲染数据模型,你会想要构建复用其它组件的组件,并使用 props 传递数据。props 是从父到子传递数据的一种方式。如果你熟悉 state 的概念,在静态版本构建时不要使用 state 。state 只用于交互,也就是说,数据可以被改变。由于这是一个静态版本 app,并不需要使用 state 。 可以从上向下或者从下向上构建。也就是说,可以从层级中高层的组件(即 FilterableProductTable)或者使用底层的组件(ProductRow)作为开始。在简单的例子中,通常从上向下是容易的,而在大型项目,则从下向上较为容易,编写测试也是。 在这个步骤的结尾,你已经有了一些可复用的组件用来渲染数据模型。组件只有 render() 方法,因为这是静态版本。层级顶部的组件(FilterableProductTable)将接受你的数据模型作为一个 prop 。如果改变你的底层数据模型并再次调用 ReactDOM.render(),UI 会被更新。很容易看到你的 UI 是如何更新的,包括哪里发生了改变,因为这里并没有什么复杂的内容。React 的单向数据流(也称为 one-way binding)使每个部分都能模块化和快速运行。 如果你需要执行这一步的帮助,可以简单的参考 [React 文档](https://facebook.github.io/react/docs/)。 > 小插曲:Props 和 state React 中有两种类型的”模型“数据:props 和 state。重要的是理解它们之间的差异;如果你不确定它们之间的区别,参考[官方 React 文档](https://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html)。 ## Step 3:标识最小化(但是完整的)UI 状态表示 要使你的 UI 可以交互,需要可以触发底层数据模型的改变。React 通过 state 很容易完成。 要正确构建你的 app ,首先你需要思考你的 app 需要的最小的改变状态。其中的关键是 :不要重复自己(DRY,don't repeat yourself)。想出绝对最小化的状态表示并计算随需的所有内容。例如,如果你构建一个 TODO 列表,只要围绕一个数组的 TODO 项;不要保留一个单独的状态变量用于计数。反而,当你希望渲染 TODO 计数,只要简单的使用 TODO 数组的 length 即可。 思考示例应用中数据的所有部分。我们有: * 原始的产品列表 * 用户输入的搜索文本 * 复选框的值 * 过滤后的产品列表 我们思考每一个,并想想哪个是state 。简单的问三个问题关于每个数据: 1. 是否从一个父组件中通过 props 传递?如果是,它可能不是 state 。 2. 它是否一直不会被改变?如果是,它可能不是 state 。 3. 你可以在你的组件中基于任何其它的 state 或者 props 来计算它吗?如果是,它可能不是 state 。 原始的产品列表是被作为 props 传递,所以它不是 state 。搜索文本和复选框看起来是 state 因为它们会被改变并且不能被从其它内容中计算。最后,过滤后的列表不是 state ,因为它可以通过结合原来的产品列表和搜索文本和复选框的值进行计算。 所以最终,你的 state 是: * 用户输入的搜索文本 * 复选框的值 ## Step 4:标识你的 state 应该存在于哪里 BABEL: ~~~ class ProductCategoryRow extends React.Component { render() { return (<tr><th colSpan="2">{this.props.category}</th></tr>); } } class ProductRow extends React.Component { render() { var name = this.props.product.stocked ? this.props.product.name : <span style={{color: 'red'}}> {this.props.product.name} </span>; return ( <tr> <td>{name}</td> <td>{this.props.product.price}</td> </tr> ); } } class ProductTable extends React.Component { render() { var rows = []; var lastCategory = null; this.props.products.forEach((product) => { if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) { return; } if (product.category !== lastCategory) { rows.push(<ProductCategoryRow category={product.category} key={product.category} />); } rows.push(<ProductRow product={product} key={product.name} />); lastCategory = product.category; }); return ( <table> <thead> <tr> <th>Name</th> <th>Price</th> </tr> </thead> <tbody>{rows}</tbody> </table> ); } } class SearchBar extends React.Component { render() { return ( <form> <input type="text" placeholder="Search..." value={this.props.filterText} /> <p> <input type="checkbox" checked={this.props.inStockOnly} /> {' '} Only show products in stock </p> </form> ); } } class FilterableProductTable extends React.Component { constructor(props) { super(props); this.state = { filterText: '', inStockOnly: false }; } render() { return ( <div> <SearchBar filterText={this.state.filterText} inStockOnly={this.state.inStockOnly} /> <ProductTable products={this.props.products} filterText={this.state.filterText} inStockOnly={this.state.inStockOnly} /> </div> ); } } var PRODUCTS = [ {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'}, {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'}, {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'}, {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'}, {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'}, {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'} ]; ReactDOM.render( <FilterableProductTable products={PRODUCTS} />, document.getElementById('container') ); ~~~ OK,那么我们已经标识了最小的 app 状态。接下来,需要标识哪个组件改变或者拥有这个状态。 记住,React 是一个单向数据流从层级中自上而下进行。它可能没有被立即明白哪个组件拥有什么状态。这是通常对于新用户最有疑问的部分难于理解,所以跟随这个步骤来思考: 对于应用中每个部分的 state: * 标识每个基于这个 state 渲染内容的组件 * 寻找一个通用的拥有者组件(一个单独的组件,在层级中位于所有需要这个状态的组件之上) * 通用的拥有者或者另一个更高级组件拥有这个状态 * 如果找不出适合拥有这个状态的组件,创建一个简单的新组件来保留这个state 并在层级中通用拥有者组件之上添加它。 我们在应用中贯穿这个策略: * ProductTable 需要基于 state 过滤产品列表,SearchBar 需要显示 搜索文本和选中状态 state。 * 通用拥有者组件是 FilterableProductTable。 * 它从概念上讲适用于过滤文本和选中状态值存在于 FilterableProductTable。 那么我们已经决定 state 保存在 FilterableProductTable 中。首先,添加一个实例属性 this.state = {filterText:'' ,inStockOnly: false} 到 FilterableProductTable 的 构造函数来反映应用的初始状态。然后,传递 filterText 和 inStockOnly 到 ProductTable 和 SearchBar 作为一个 prop 。最后,使用这些 props 来过滤 ProductTable 中的行,并设置 SearchBar 中的表单字段的值。 你可以看一下应用的行为:设置 filterText 为 "ball" 并刷新你的应用。你将发现数据表被正确的更新了。 ## Step 5:添加反响数据流 ~~~ class ProductCategoryRow extends React.Component { render() { return (<tr><th colSpan="2">{this.props.category}</th></tr>); } } class ProductRow extends React.Component { render() { var name = this.props.product.stocked ? this.props.product.name : <span style={{color: 'red'}}> {this.props.product.name} </span>; return ( <tr> <td>{name}</td> <td>{this.props.product.price}</td> </tr> ); } } class ProductTable extends React.Component { render() { var rows = []; var lastCategory = null; this.props.products.forEach((product) => { if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) { return; } if (product.category !== lastCategory) { rows.push(<ProductCategoryRow category={product.category} key={product.category} />); } rows.push(<ProductRow product={product} key={product.name} />); lastCategory = product.category; }); return ( <table> <thead> <tr> <th>Name</th> <th>Price</th> </tr> </thead> <tbody>{rows}</tbody> </table> ); } } class SearchBar extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); } handleChange() { this.props.onUserInput( this.filterTextInput.value, this.inStockOnlyInput.checked ); } render() { return ( <form> <input type="text" placeholder="Search..." value={this.props.filterText} ref={(input) => this.filterTextInput = input} onChange={this.handleChange} /> <p> <input type="checkbox" checked={this.props.inStockOnly} ref={(input) => this.inStockOnlyInput = input} onChange={this.handleChange} /> {' '} Only show products in stock </p> </form> ); } } class FilterableProductTable extends React.Component { constructor(props) { super(props); this.state = { filterText: '', inStockOnly: false }; this.handleUserInput = this.handleUserInput.bind(this); } handleUserInput(filterText, inStockOnly) { this.setState({ filterText: filterText, inStockOnly: inStockOnly }); } render() { return ( <div> <SearchBar filterText={this.state.filterText} inStockOnly={this.state.inStockOnly} onUserInput={this.handleUserInput} /> <ProductTable products={this.props.products} filterText={this.state.filterText} inStockOnly={this.state.inStockOnly} /> </div> ); } } var PRODUCTS = [ {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'}, {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'}, {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'}, {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'}, {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'}, {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'} ]; ReactDOM.render( <FilterableProductTable products={PRODUCTS} />, document.getElementById('container') ); ~~~ 目前,我们已经构建了一个 app ,正确的渲染为一个层级中的状态流。现在是时候支持另一种数据流方式:层级深层的 form 组件需要更新 FilterableProductTable 中的 state 。 React 使这个数据流非常明确,来使你容易理解程序如何运行。但是相比传统的两种数据绑定来说,的确需要一点额外的输入。 如果你尝试 输入或者选中当前版本示例中的盒子,你发现 React 忽略了你的输入。这是蓄意的,因为我们已经设置了 input 的 value prop 总是等于从 FilterableProductTable 中传递的 state 。 想一下我们希望发生什么。我们要确保无论何时用户改变了表单,我们更新 state 来反映用户的输入。由于组件只能更新它们自己的状态,FilterableProductTable 将传递一个回调到 FilterableProductTable,然后在 state 被更新的时候触发。我们可以使用 input 的 onChange 事件来关照它。而且被 FilterableProductTable 传递的回调 会调用 setState(),然后 app 被更新。 尽管这听起来很复杂,其实只需要几行代码。也很清晰数据在你应用中的流向。 ## 就是这样 希望这给你一个如何使用 React 构建组件和应用的一些想法。虽然可能比你以前接触到的要多一些输入,但是记住代码被阅读要更重于它的编写,它非常容易阅读这个模块化、明确的代码。当你开始构建大量组件,你将会开始欣赏这种明确性和模块化,通过代码的复用,你的代码将有效减少。