From 0c8141f68109151f50277a4eb39c7f76d63eef57 Mon Sep 17 00:00:00 2001 From: iamchenxin Date: Fri, 25 Sep 2015 16:34:53 +0800 Subject: [PATCH] [docs] Sync up tutorial.zh-CN with en(a440f40) [docs] Amend wrong words in zh-CN 08.1-more-about-refs.zh-CN.md Update zh-CN docs add thinking-in-react.zh-CN.md [docs] Update two zh-CN docs new file: ref-01-top-level-api.zh-CN.md new file: ref-02-component-api.zh-CN.md --- docs/08.1-more-about-refs.zh-CN.md | 2 +- docs/ref-01-top-level-api.zh-CN.md | 191 ++++++++ docs/ref-02-component-api.zh-CN.md | 146 ++++++ docs/thinking-in-react.zh-CN.md | 147 ++++++ docs/tutorial.zh-CN.md | 712 +++++++++++++++++++++++++++++ 5 files changed, 1197 insertions(+), 1 deletion(-) create mode 100644 docs/ref-01-top-level-api.zh-CN.md create mode 100644 docs/ref-02-component-api.zh-CN.md create mode 100644 docs/thinking-in-react.zh-CN.md create mode 100644 docs/tutorial.zh-CN.md diff --git a/docs/08.1-more-about-refs.zh-CN.md b/docs/08.1-more-about-refs.zh-CN.md index 5091143c..3bce67db 100644 --- a/docs/08.1-more-about-refs.zh-CN.md +++ b/docs/08.1-more-about-refs.zh-CN.md @@ -170,5 +170,5 @@ Refs是一种很好的发送消息给特定子实例(通过流式的Reactive `pr - *绝不* 在任何组件的 render 方法中访问 refs - 或者当任何组件的render方法还在调用栈上的任何地方运行时。 - 如果你想要保留Google Closure Compiler Crushing resilience,务必不要把指明为字符串的以属性来访问。这意味这你必须用`this.refs['myRefString']`访问,如果你的ref被定义为`ref="myRefString"`。 -- 如果你没有用React写过数个程序,你的第一反应通常是打算试着用refs来在你的应用里"让事情发生"。如果是这样,花一些时间并且更精密的思考`state`应该属于组件层级的哪个位置。常常,这会变得清晰:正确的"拥有"那个属性的地方应该在层级的更高层上。把state放在那里往往消除了任何使用`ref`来 "让事情发生"的渴望 - 作为替代,数据流通常将完成你的目录。 +- 如果你没有用React写过数个程序,你的第一反应通常是打算试着用refs来在你的应用里"让事情发生"。如果是这样,花一些时间并且更精密的思考`state`应该属于组件层级的哪个位置。常常,这会变得清晰:正确的"拥有"那个属性的地方应该在层级的更高层上。把state放在那里往往消除了任何使用`ref`来 "让事情发生"的渴望 - 作为替代,数据流通常将完成你的目标。 diff --git a/docs/ref-01-top-level-api.zh-CN.md b/docs/ref-01-top-level-api.zh-CN.md new file mode 100644 index 00000000..eb45c81c --- /dev/null +++ b/docs/ref-01-top-level-api.zh-CN.md @@ -0,0 +1,191 @@ +--- +id: top-level-api-zh-CN +title: Top-Level API +permalink: top-level-api-zh-CN.html +next: component-api-zh-CN.html +redirect_from: "/docs/reference-zh-CN.html" +--- + +## React + +`React` 是 React 库的入口点。如果你使用预编译包中的一个,则 `React` 为全局变量;如果你使用 CommonJS 模块,你可以 `require()` 它。 + + +### React.Component + +```javascript +class Component +``` + +当使用ES6 类定义时,React.Component是 React 组件的基类。如何在React中使用 ES6 class 请参见 [可重用组件](/react/docs/reusable-components-zh-CN.html#es6-classes)。基类实际提供了哪些方法 请参见 [组件 API](/react/docs/component-api-zh-CN.html). + + +### React.createClass + +```javascript +ReactClass createClass(object specification) +``` + +给定一份规格(specification),创建一个组件类。组件通常要实现一个 `render()` 方法,它返回 **单个的** 子级。该子级可能包含任意深度的子级结构。组件与标准原型类的不同之处在于,你不需要对它们调用 new。 它们是为你在后台构造实例(通过 new)的便利的包装器。 + +更多关于规格对象(specification object)的信息,请见 [组件规格和生命周期](/react/docs/component-specs-zh-CN.html) 。 + + +### React.createElement + +```javascript +ReactElement createElement( + string/ReactClass type, + [object props], + [children ...] +) +``` + +创建并返回一个新的给定类型的 `ReactElement`。type 参数既可以是一个 html 标签名字符串(例如. “div”,“span”,等等),也可以是一个 `ReactClass` (用 `React.createClass` 创建的)。 + + + +### React.cloneElement + +``` +ReactElement cloneElement( + ReactElement element, + [object props], + [children ...] +) +``` + +使用 `element` 作为起点,克隆并返回一个新的 `ReactElement` 。生成的 element 将会拥有原始 element 的 props 与新的 props 的浅合并。新的子级将会替换现存的子级。 不同于 `React.addons.cloneWithProps`,来自原始 element 的 `key` 和 `ref` 将会保留。对于合并任何 props 没有特别的行为(不同于 `cloneWithProps`)。更多细节详见[v0.13 RC2 blog post](/react/blog/2015/03/03/react-v0.13-rc2.html) 。 + + +### React.createFactory + +```javascript +factoryFunction createFactory( + string/ReactClass type +) +``` + +返回一个生成给定类型的 ReactElements 的函数。如同 `React.createElement`,type 参数既可以是一个 html 标签名字符串(例如. “div”,“span”,等等),也可以是一个 `ReactClass` 。 + + + + +### React.render + +```javascript +ReactComponent render( + ReactElement element, + DOMElement container, + [function callback] +) +``` + +渲染一个 ReactElement 到 DOM 里提供的 `容器(container)`中,并返回一个对组件的引用。 + +如果 ReactElement 之前被渲染到了 `container` 中,这将对它执行一次更新,并仅变动需要变动的 DOM 来反映最新的 React 组件。 + +如果提供了可选的回调函数,则该函数将会在组件渲染或者更新之后被执行。 + +> 注意: +> +> `React.render()` 控制你传入的 container 节点的内容。 +> 当初次调用时,任何现存于内的 DOM 元素将被替换。 +> 其后的调用使用 React的 diffing 算法来有效率的更新。 +> +> `React.render()` 不会修改 container 节点(只修改 container 的子级)。 +> 将来,也许能够直接插入一个组件到已经存在的 DOM 节点而不覆盖 +> 现有的子级。 + + +### React.unmountComponentAtNode + +```javascript +boolean unmountComponentAtNode(DOMElement container) +``` + +从 DOM 中移除已经挂载的 React 组件,并清除它的事件处理器和 state。如果在 container 中没有组件被挂载,调用此函数将什么都不做。如果组件被卸载返回 `true`,如果没有组件被卸载返回 `false`。 + + +### React.renderToString + +```javascript +string renderToString(ReactElement element) +``` + +把 ReactElement 渲染为它原始的 HTML 。这应该仅在服务器端使用。React 将会返回一个 HTML 字符串。你可以用这种方法在服务器端生成 HTML,然后在初始请求下传这些标记,以获得更快的页面加载速度及允许搜索引擎抓取页面(便于 SEO)。 + +如果在一个在已经有了这种服务器预渲染标记的节点上面调用 `React.render()`,React 将会维护该节点,仅绑定事件处理器,让你有一个非常高效的初次加载体验。 + + +### React.renderToStaticMarkup + +```javascript +string renderToStaticMarkup(ReactElement element) +``` + +类似于 `renderToString` ,除了不创建额外的 DOM 属性,比如 `data-react-id`,这仅在 React 内部使用的属性。如果你想用 React 做一个简单的静态页面生成器,这是很有用的,因为去除额外的属性能够节省很多字节。 + + +### React.isValidElement + +```javascript +boolean isValidElement(* object) +``` + +验证对象是否是一个 ReactElement。 + + +### React.findDOMNode + +```javascript +DOMElement findDOMNode(ReactComponent component) +``` +如果这个组件已经被挂载到了 DOM,它返回相应的浏览器原生的 DOM 元素。这个方法对于读取 DOM 的值很有用,比如表单域的值和执行 DOM 的测量。如果 `render` 返回 `null` 或者 `false`, `findDOMNode` 返回 `null`. + + +### React.DOM + +`React.DOM` 用 `React.createElement` 为 DOM 组件提供了便利的包装器。该方式应该只在不使用 JSX 的时使用。例如,`React.DOM.div(null, 'Hello World!')`。 + + +### React.PropTypes + +`React.PropTypes` 包含了能与 组件的`propTypes` 对象一起使用的类型,用以验证传入你的组件的 props。更多有关 `propTypes` 的信息,请见 [可重用组件](/react/docs/reusable-components-zh-CN.html)。 + + +### React.Children + +`React.Children` 为处理 `this.props.children` 这个不透明的数据结构提供了工具。 + +#### React.Children.map + +```javascript +object React.Children.map(object children, function fn [, object thisArg]) +``` + +在每一个包含在 `children` 中的直接子级上调用 `fn` ,`fn`中的 `this` 设置为 `thisArg`。如果 `children` 是一个嵌套的对象或者数组,它将被遍历:不会传入容器对象到 `fn` 中。如果 children 是 `null` 或者 `undefined`,则返回 `null` 或者 `undefined` 而不是一个空对象。 + +#### React.Children.forEach + +```javascript +React.Children.forEach(object children, function fn [, object thisArg]) +``` + +类似 `React.Children.map()`,但是不返回对象。 + +#### React.Children.count + +```javascript +number React.Children.count(object children) +``` + +返回 `children` 中的组件总数,相等于传递给 `map` 或者 `forEach` 的回调函数应被调用次数。 + +#### React.Children.only + +```javascript +object React.Children.only(object children) +``` + +返回 `children` 中仅有的子级。否则抛出异常。 diff --git a/docs/ref-02-component-api.zh-CN.md b/docs/ref-02-component-api.zh-CN.md new file mode 100644 index 00000000..231e33cf --- /dev/null +++ b/docs/ref-02-component-api.zh-CN.md @@ -0,0 +1,146 @@ +--- +id: component-api-zh-CN +title: 组件 API +permalink: component-api-zh-CN.html +prev: top-level-api-zh-CN.html +next: component-specs-zh-CN.html +--- + +## React.Component + +当渲染时,React 组件的实例在 React 内部被创建。这些实例在随后的渲染中被重复使用,并可以在组件方法中通过 `this` 访问。唯一的在 React 之外获取 React 组件实例句柄的方法是保存 `React.render` 的返回值。在其它组件内,你可以使用 [refs](/react/docs/more-about-refs-zh-CN.html) 得到相同的结果。 + + +### setState + +```javascript +setState( + function|object nextState, + [function callback] +) +``` +执行一个 nextState 到当前 state 的浅合并。这是你从事件处理器和服务器请求回调用来触发 UI 更新的主要手段。 + +第一个参数可以是一个对象(包含0或者多个keys来更新)或者一个(state 和 props的)函数,它返回一个包含要更新的keys的对象。 + +这里是一个简单的运用: + +```javascript +setState({mykey: 'my new value'}); +``` + +也可以以 `function(state, props)` 传递一个函数。当你想要把一个在设置任何值之前参考前一次 state+props 的值的原子更新放在队列中 这会有很用。例如,假如我们想在 state 增加一个值。 + +```javascript +setState(function(previousState, currentProps) { + return {myInteger: previousState.myInteger + 1}; +}); +``` + +第二个(可选)的参数是一个将会在 `setState` 完成和组件被重绘后执行的回调函数。 + +> 注意: +> +> *绝对不要* 直接改变 `this.state`,因为之后调用 `setState()` 可能会替换掉你做的改变。把 `this.state` 当做是不可变的。 +> +> `setState()` 不会立刻改变 `this.state`,而是创建一个即将处理的 state 转变。在调用该方法之后访问 `this.state` 可能会返回现有的值。 +> +> 对 `setState` 的调用没有任何同步性的保证,并且调用可能会为了性能收益批量执行。 +> +> `setState()` 将总是触发一次重绘,除非在 `shouldComponentUpdate()` 中实现了条件渲染逻辑。如果可变对象被使用了,但又不能在 `shouldComponentUpdate()` 中实现这种逻辑,仅在新 state 和之前的 state 存在差异的时候调用 `setState()` 可以避免不必要的重新渲染。 + + +### replaceState + +```javascript +replaceState( + object nextState, + [function callback] +) +``` + +类似于 `setState()`,但是删除任何 先前存在但不在 nextState 里的 state 键。 + +> 注意: +> +> 这个方法在从 `React.Component` 扩展的 ES6 `class` 组件里不可用。它也许会在未来的 React 版本中被完全移除。 + + +### forceUpdate + +```javascript +forceUpdate( + [function callback] +) +``` + +默认情况下,当你的组件的 state 或者 props 改变,你的组件将会重绘。然而,如果它们隐式的改变(例如:在对象深处的数据改变了但没有改变对象本身)或者如果你的 `render()` 方法依赖于其他的数据,你可以用调用 `forceUpdate()` 来告诉 React 它需要重新运行 `render()`。 + +调用 `forceUpdate()` 将会导致 `render()` 跳过 `shouldComponentUpdate()` 在组件上被调用,这会为子级触发正常的生命周期方法。包括每个子级的 `shouldComponentUpdate()` 方法。如果标记改变了,React 仍仅只更新 DOM。 + +通常你应该试着避免所有对 `forceUpdate()` 的使用并且在 `render()` 里只从 `this.props` 和 `this.state` 读取。这会使你的组件 "纯粹" 并且你的组件会更简单和高效。 + + +### getDOMNode + +```javascript +DOMElement getDOMNode() +``` + +如果这个组件已经被挂载到了 DOM,它返回相应的浏览器原生的 DOM 元素。这个方法对于读取 DOM 的值很有用,比如表单域的值和执行 DOM 的测量。如果 `render` 返回 `null` 或者 `false` 的时候,`this.getDOMNode()` 返回 `null`。 + +> Note: +> +> getDOMNode 被废弃了,已经被 [React.findDOMNode()] 替换(/react/docs/top-level-api-zh-CN.html#react.finddomnode). +> +> 这个方法在从 `React.Component` 扩展的 ES6 `class` 组件里不可用。它也许会在未来的 React 版本中被完全移除。 + + +### isMounted + +```javascript +bool isMounted() +``` + +如果组件渲染到了 DOM 中,`isMounted()` 返回 true,否则返回 `false`。可以使用该方法来控制对 `setState()` 和 `forceUpdate()` 的异步调用。 + +> 注意: +> +> 这个方法在从 `React.Component` 扩展的 ES6 `class` 组件里不可用。它也许会在未来的 React 版本中被完全移除。 + + +### setProps + +```javascript +setProps( + object nextProps, + [function callback] +) +``` + +当和一个外部的 JavaScript 应用整合的时候,你也许会想用 `React.render()` 给 React 组件标示一个改变。 + +在根组件上调用 `setProps()` 会改变他的属性并触发一次重绘。另外,你可以提供一个可选的回调函数,一旦 `setProps` 完成并且组件被重绘它就执行。 + +> 注意: +> +> 如果可能,上述的在同一个节点上再次调用 `React.render()` 的方法是优先替代的。它往往使更新更容易理解。(两种方法并没有显著的性能区别。) +> +> 这个方法仅能在根组件上被调用。也就是说,它仅在直接传给 `React.render()` 的组件上可用,在它的子级上不可用。如果你倾向于在子组件上使用 `setProps()`,不要利用响应式更新,而是当子组件在 `render()` 中创建的时候传入新的 prop 到子组件中。 +> +> 这个方法在从 `React.Component` 扩展的 ES6 `class` 组件里不可用。它也许会在未来的 React 版本中被完全移除。 + +### replaceProps + +```javascript +replaceProps( + object nextProps, + [function callback] +) +``` + +类似于 `setProps()`,但是删除任何先前存在的 props,而不是合并这两个对象。 + +> 注意: +> +> 这个方法在从 `React.Component` 扩展的 ES6 `class` 组件里不可用。它也许会在未来的 React 版本中被完全移除。 diff --git a/docs/thinking-in-react.zh-CN.md b/docs/thinking-in-react.zh-CN.md new file mode 100644 index 00000000..5b378d6d --- /dev/null +++ b/docs/thinking-in-react.zh-CN.md @@ -0,0 +1,147 @@ +--- +id: thinking-in-react-zh-CN +title: React 编程思想 +prev: tutorial-zh-CN.html +next: conferences-zh-CN.html +redirect_from: 'blog/2013/11/05/thinking-in-react.html' +--- + +by Pete Hunt + +在我看来,React 是构建大型,快速 web app 的首选方式。它已经在 Facebook 和 Instagram 被我们有了广泛的应用。 + +React许多优秀的部分之一是它如何使你随着构建 app 来思考 app。在本文里,我将带领你学习一个用 React 构建可搜索的数据表的思考过程。 + +## 从模型(mock)开始 + +想象我们已经有个一个 JSON API 和一个来自设计师的模型。我们的设计师似乎不是很好因为模型看起来像是这样: + +![Mockup](/react/img/blog/thinking-in-react-mock.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"} +]; +``` + +## 第一步:把UI拆分为一个组件的层级 + +首先你想要做的是在模型里的每一个组建周围绘制边框并给他们命名。如果你和设计师一起工作,他们应该已经完成这步了,所以去和他们谈谈!他们的photoshop图层名也许最终会成为你的React组件名。 + +但是你如何知道什么东西应该是独立的组件?只需使用你决定是否创建一个函数或者对象的相同技术。这样一种技术是[single responsibility principle](https://en.wikipedia.org/wiki/Single_responsibility_principle),即是,一个组件在理想情况下只做一件事情。如果它最终变大,它就应该被分解为更小的组件。 + +既然你频繁显示一个JSON的数据模型给用户,你会发现,如果你的模型构建正确,你的UI(因此也有你的组件结构)就将映射良好.那是因为UI和数据模型趋向于附着于相同的 *信息架构*,这意味着把你的 UI 分离为组件的工作通常是简单的,只需把他拆成精确对应代表你的数据模型的每一块的组件. + +![Component diagram](/react/img/blog/thinking-in-react-components.png) + +在这里你会看到,在我们简单的APP里有五个组件.我用斜体表示每个组件的数据. + + 1. **`FilterableProductTable` (orange):** 包含示例的整体 + 2. **`SearchBar` (blue):** 接受所有 *用户输入* + 3. **`ProductTable` (green):** 基于 *用户输入* 显示和过滤 *数据集合(data collection)* + 4. **`ProductCategoryRow` (turquoise):** 为每个 *分类* 显示一个列表头 + 5. **`ProductRow` (red):** 为每个 *产品* 显示一个行 + +如果你看着 `ProductTable`,你会看到表的头(包含了 "Name" 和 "Price" 标签) 不是独立的组件.这是一个个人喜好问题,并且无论哪种方式都有争论.对于这个例子,我把它留做 `ProductTable` 的一部分,因为它是 *data collection*渲染的一部分,而 *data collection*渲染是 `ProductTable` 的职责.然而,当列表头增长到复杂的时候(例如 如果我们添加排序的功能性),那么无疑的使它成为独立的 `ProductTableHeader` 组件是有意义的. + +既然现在我们已经识别出了我们模型中的组件,让我们把他们安排到一个层级中.这很容易. 在模型中出现在其他组件中的组件 同样应该在层级中表现为一个子级: + + * `FilterableProductTable` + * `SearchBar` + * `ProductTable` + * `ProductCategoryRow` + * `ProductRow` + +## Step 2: 用React创建一个静态版本 + + + +既然你已经有了你的组件层级,时候实现你的app了.简单的方式是构建一个版本,它采取你的数据模型并渲染UI但是没有互动性.最好是分离这些过程,因为构建一个静态版本需要无脑的打很多字,然而添加互动性需要很多思考但不需要打太多字.我们会看到是为什么: + +要构建一个渲染你的数据模型的 app 的静态版本,你会想构建一个重用其他组件并利用 *props* 传递数据的组件. *props* 是一种从父级传递数据到子级的方式.如果你对 *state* 的观念很熟悉, **绝不要用state** 来构建这个静态版本.State 仅仅是为互动预留的,即,随时间变化的数据. 因为这是一个静态版本的app,你不需要它. + +你可以自顶向下或自底向上的构建.也就是说,你可以既从较高的层级(如 从 `FilterableProductTable` 开始) 也可以从较低的层级(`ProductRow`)开始构建组件. 在较简单的例子里,通常自顶向下要容易一些,然而在更大的项目上 自底向上更容易,并且伴随着构建写测试较容易. + +在这一步的最后,你会有一个渲染你数据模型的可重用的组件库. 这些组件将只含有 `render()` 方法因为这是一个你的app的静态版本. 在层级顶端的组件 (`FilterableProductTable`) 将会接受你的数据模型作为一个prop.如果你对你的底层数据模型做了变化并且再次调用 `React.render()` ,UI将会更新.很容易看到你的UI是如何更新以及哪里来作出变动,因为这里没有任何复杂的事情发生. React的 **单向数据流** (也被称为 *单向绑定* )保持了每个东西模块化和快速. + +如果你执行这步需要帮助,请参考 [React docs](/react/docs/) . + +### 小插曲: props vs state + +在React里有两种数据 "模型":props和state.明白这二者之间的区别是很重要的;如果你不是很确定不同是什么,请概览[React官方文档](/react/docs/interactivity-and-dynamic-uis-zh-CN.html) + +## 第三步:确定最小(但完备)的 UI state 的表达 + +要让你的UI互动,你需要能够触发变化到你的底层数据模型上.React用 **state** 来让此变得容易. + +要正确的构建你的app,你首先需要思考你的app需要的可变state的最小组. 这里的关键是 DRY: *Don't Repeat Yourself(不要重复自己)*.想出哪些是你的应用需要的绝对最小 state 表达,并按需计算其他任何数据.例如,如果你要构建一个 TODO list,只要保持一个 TODO 项的数组;不要为了计数保持一个单独的 state 变量.作为替代,当你想渲染 TODO 数量时,简单的采用TODO项目数组的长度. + +考虑我们例子应用中数据的所有块.我们有: + + * 原始的产品列表 + * 用户输入的搜索文本 + * checkbox的值 + * 产品的滤过的列表 + +让我们遍历每一个并指出哪一个是state.简单的问三个关于每个数据块的问题: + + 1. 它是通过props从父级传递来的吗?如果是,它可能不是state. + 2. 它随时间变化吗?如果不是,它可能不是state. + 3. 你能基于其他任何组件里的 state 或者 props 计算出它吗?如果是,它可能不是state. + +原始的产品列表被以 props 传入,所以它不是 state. 搜索文本和 checkbox 看起来是state 因为他们随时间变化并且不能从任何东西计算出.最后,产品滤过的列表不是state因为它可以联合原始列表与搜索文本及 checkbox 的值计算出. + +所以最后,我们的 state 是: + + * 用户输入的搜索文本 + * checkbox 的值 + +## 第四步:确定你的 state 应该存在于哪里 + + + +好,我们已经确定了app state 的最小组是什么.接下来,我们需要确定哪个组件变动,或者 *拥有*,这 个state. + +记住:React都是关于单向数据流下组件层级的.可能不能立刻明白哪些组件应该拥有哪些 state. **这常常对于新手在理解上是最具挑战的一部分,** 所以跟着这几步来弄明白它: + +对于你的应用里每一个数据块: + + * 基于 state 确定每个绘制某些东西的组件 + * 找到一个共同拥有者的组件(在层级中所有组件之上需要这个state的单个组件) + * 要么是共同拥有者,要么是其他在层级里更高级的组件应该拥有这个state + * 如果你不能找到一个组件,它拥有这个state有意义,创建一个新的组件简单的为了持有此state,并把它在层级里添加比共同拥有者组件更高的层级上. + +让我们过一遍这个用于我们应用的策略: + + * `ProductTable` 需要基于 state 过滤产品列表,`SearchBar` 需要显示搜索文本和选择状态. + * 共同拥有者组件是`FilterableProductTable` . + * 对于过滤文本和选择值存在于 `FilterableProductTable` 观念上是有意义的. + +酷,我们已经决定了我们的state存在于 `FilterableProductTable` .首先,添加一个 `getInitialState()` 方法到 `FilterableProductTable` ,返回 `{filterText: '', inStockOnly: false}` 来反映你的应用的初始状态. 然后,传递`filterText` 和 `inStockOnly` 给 `ProductTable` 和`SearchBar` 作为prop.最后,使用这些prop来过滤 `ProductTable` 中的行和设置`SearchBar`的表单项的值. + +你可以开始看看你的应用将有怎样的行为: 设置 `filterText` 为 `"ball"` 并刷新你的app.你将看到数据表被正确更新了. + +## 第五步:添加反向数据流 + + + +到目前为止,我们已经构建了一个应用, 它以props 和 state 沿着层级向下流动的功能正确渲染。现在是时候支持数据的另一种流动了:在层级深处的表单组件需要更新 `FilterableProductTable` 里的state. + +React 让数据显式流动,使理解你的应用如何工作变得简单,但是相对于传统的双向数据绑定,确实需要打多一些的字。 React 提供了一个叫做 `ReactLink` 的插件来使这种模式和双向数据绑定一样方便,但是考虑到这篇文章的目的,我们将会保持所有东西都直截了当。 + +如果你尝试在当前版本的示例中输入或者选中复选框,你会发现 React 忽略了你的输入。这是有意的,因为已经设置了 `input` 的 `value` prop 总是与 `FilterableProductTable` 传递过来的 `state` 一致。 + +让我们思考下我们希望发生什么。我们想确保每当用户改变了表单,我们就更新 state 来反映用户的输入。由于组件应该只更新自己拥有的 state , `FilterableProductTable` 将会传递一个回调函数给 `SearchBar` ,此函数每当 state 应被改变时就会击发。我们可以使用 input 的 `onChange` 事件来接受它的通知。 `FilterableProductTable` 传递的回调函数将会调用 `setState()` ,然后应用将会被更新。 + +虽然这听起来复杂,但是实际上只是数行代码。并且数据在应用中从始至终如何流动是非常明确的。 + +## 好了,就是这样 + +希望,这给了你一个怎样思考用React构建组件和应用的概念。虽然可能比你通常要输入更多的代码,记住,读代码的时间远比写代码的时间多,并且阅读这种模块化的,显式的代码是极端容易的。当你开始构建大型组件库时,你会非常感激这种清晰性和模块化,并且随着代码的重用,你的代码行数将会开始缩减. :)。 diff --git a/docs/tutorial.zh-CN.md b/docs/tutorial.zh-CN.md new file mode 100644 index 00000000..80fc0e54 --- /dev/null +++ b/docs/tutorial.zh-CN.md @@ -0,0 +1,712 @@ +--- +id: tutorial-zh-CN +title: 教程 +prev: getting-started-zh-CN.html +next: thinking-in-react-zh-CN.html +--- + +我们将建立一个你可以放进博客的简单却真实的评论框,一个 Disqus、LiveFyre 或 Facebook comments 提供的实时评论的基础版本。 + +我们将提供: + +* 一个所有评论的视图 +* 一个用于提交评论的表单 +* 为你提供制定后台的挂钩(Hooks) + +同时也会有一些简洁的功能: + +* **优化的评论:** 评论在它们保存到服务器之前就显示在列表里,所以感觉很快。 +* **实时更新:** 其他用户的评论被实时浮现到评论中。 +* **Markdown格式化:** 用户可以用Markdown格式化它们的文字。 + +### 想要跳过所有内容,只查看源代码? + +[全在 GitHub .](https://github.com/reactjs/react-tutorial) + +### 运行服务器 + +为了开始本教程,我们将要需要一个运行着的服务器。这将是我们纯粹用来获取和保存数据的伺服终端。为了让这尽可能的容易,我们已经用许多不同的语言编写了简单的服务器,它正好完成我们需要的事。 **你可以[查看源代码](https://github.com/reactjs/react-tutorial/) 或者 [下载 zip 文件](https://github.com/reactjs/react-tutorial/archive/master.zip) 包括了所有你开始学习需要的东西** + +为了简单起见,我们将要运行的服务器使用 `JSON` 文件作为数据库。你不会在生产环境运行这个,但是它让我们更容易模拟使用一个API时你可能会做的事。一旦你启动服务器,它将会支持我们的API终端,同时也将伺服我们需要的静态页面。 + +### 开始 + +对于此教程,我们将使它尽可能的容易。被包括在上面讨论的服务器包里的是一个我们将在其中工作的 HTML 文件。在你最喜欢的编辑器里打开 `public/index.html`。它应该看起来像这样 (可能有一些小的不同,稍后我们将添加一个额外的 ` + + + + +
+ + + + +``` + +在本教程剩余的部分,我们将在此 script 标签中编写我们的 JavaScript 代码。我们没有任何高级的实时加载所以在保存以后你需要刷新你的浏览器来观察更新。通过在浏览器打开 `http://localhost:3000` 关注你的进展。当你没有任何修改第一次加载时,你将看到我们将要准备建立的已经完成的产品。当你准备开始工作,请删除前面的 ` + + + + +``` + +然后,让我们转换评论文本为 Markdown 并输出它: + +```javascript{9} +// tutorial6.js +var Comment = React.createClass({ + render: function() { + return ( +
+

+ {this.props.author} +

+ {marked(this.props.children.toString())} +
+ ); + } +}); +``` + +我们在这里唯一做的就是调用 marked 库。我们需要把 从 React 的包裹文本来的 `this.props.children` 转换成 marked 能理解的原始字符串,所以我们显示地调用了`toString()`。 + +但是这里有一个问题!我们渲染的评论在浏览器里看起来像这样: "`

`This is ``another`` comment`

`" 。我们想让这些标签真正地渲染为 HTML。 + +那是 React 在保护你免受 [XSS 攻击](https://en.wikipedia.org/wiki/Cross-site_scripting)。有一个方法解决这个问题,但是框架会警告你别使用这种方法: + +```javascript{4,10} +// tutorial7.js +var Comment = React.createClass({ + rawMarkup: function() { + var rawMarkup = marked(this.props.children.toString(), {sanitize: true}); + return { __html: rawMarkup }; + }, + + render: function() { + return ( +
+

+ {this.props.author} +

+ +
+ ); + } +}); +``` + +这是一个特殊的 API,故意让插入原始的 HTML 变得困难,但是对于 marked 我们将利用这个后门。 + +**记住:** 使用这个功能你会依赖于 marked 是安全的。既然如此,我们传递 `sanitize: true` 告诉 marked escape 源码里任何的 HTML 标记,而不是直接不变的让他们通过。 + +### 挂钩数据模型 + +到目前为止我们已经完成了在源码里直接插入评论。作为替代,让我们渲染一团 JSON 数据到评论列表里。最终数据将会来自服务器,但是现在,写在你的源代码中: + +```javascript +// tutorial8.js +var data = [ + {author: "Pete Hunt", text: "This is one comment"}, + {author: "Jordan Walke", text: "This is *another* comment"} +]; +``` + +我们需要以一种模块化的方式将这个数据传入到 `CommentList`。修改 `CommentBox` 和 `React.render()` 方法,以通过 props 传入数据到 `CommentList`: + +```javascript{7,15} +// tutorial9.js +var CommentBox = React.createClass({ + render: function() { + return ( +
+

Comments

+ + +
+ ); + } +}); + +React.render( + , + document.getElementById('content') +); +``` + +既然现在数据在 `CommentList` 中可用了,让我们动态地渲染评论: + +```javascript{4-10,13} +// tutorial10.js +var CommentList = React.createClass({ + render: function() { + var commentNodes = this.props.data.map(function (comment) { + return ( + + {comment.text} + + ); + }); + return ( +
+ {commentNodes} +
+ ); + } +}); +``` + +就是这样! + +### 从服务器获取数据 + +让我们用一些来自服务器的动态数据替换硬编码的数据。我们将移除数据的prop,用获取数据的URL来替换它: + +```javascript{3} +// tutorial11.js +React.render( + , + document.getElementById('content') +); +``` + +这个组件不同于和前面的组件,因为它必须重新渲染自己。该组件将不会有任何数据,直到请求从服务器返回,此时该组件或许需要渲染一些新的评论。 + +注意: 此代码在这个阶段不会工作。 + +### Reactive state + +迄今为止,基于它自己的props,每个组件都渲染了自己一次。`props` 是不可变的:它们从父级传来并被父级“拥有”。为了实现交互,我们给组件引进了可变的 **state**。`this.state` 是组件私有的,可以通过调用 `this.setState()` 改变它。每当state更新,组件就重新渲染自己。 + +`render()` 方法被声明为一个带有 `this.props` 和 `this.state` 的函数。框架保证了 UI 总是与输入一致。 + +当服务器获取数据时,我们将会改变我们已有的评论数据。让我们给 `CommentBox` 组件添加一组评论数据作为它的状态: + +```javascript{3-5,10} +// tutorial12.js +var CommentBox = React.createClass({ + getInitialState: function() { + return {data: []}; + }, + render: function() { + return ( +
+

Comments

+ + +
+ ); + } +}); +``` + +`getInitialState()` 在生命周期里只执行一次,并设置组件的初始状态。 + +#### 更新状态 +当组件第一次创建时,我们想从服务器获取一些 JSON 并且更新状态以反映最新的数据。我们将用 jQuery 来发送一个异步请求到我们刚才启动的服务器以获取我们需要的数据。看起来像这样: + +```json +[ + {"author": "Pete Hunt", "text": "This is one comment"}, + {"author": "Jordan Walke", "text": "This is *another* comment"} +] +``` + +```javascript{6-18} +// tutorial13.js +var CommentBox = React.createClass({ + getInitialState: function() { + return {data: []}; + }, + componentDidMount: function() { + $.ajax({ + url: this.props.url, + dataType: 'json', + cache: false, + success: function(data) { + this.setState({data: data}); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, + render: function() { + return ( +
+

Comments

+ + +
+ ); + } +}); +``` + +这里, `componentDidMount` 是一个当组件被渲染时被React自动调用的方法。动态更新的关键是对 `this.setState()` 的调用。我们用新的从服务器来的替换掉旧的评论组,然后UI自动更新自己。因为这种反应性,仅是一个微小的变化就添加了实时更新。我们这里将用简单的轮询,但是你可以容易的使用 WebSockets 或者其他技术。 + +```javascript{3,15,20-21,35} +// tutorial14.js +var CommentBox = React.createClass({ + loadCommentsFromServer: function() { + $.ajax({ + url: this.props.url, + dataType: 'json', + cache: false, + success: function(data) { + this.setState({data: data}); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, + getInitialState: function() { + return {data: []}; + }, + componentDidMount: function() { + this.loadCommentsFromServer(); + setInterval(this.loadCommentsFromServer, this.props.pollInterval); + }, + render: function() { + return ( +
+

Comments

+ + +
+ ); + } +}); + +React.render( + , + document.getElementById('content') +); + +``` + +我们在这里做的全部事情是把 AJAX 调用移动到独立的方法里,然后在组件第一次加载时及其后每2秒 调用它。试着在你的浏览器里运行它并且改变 `comments.json` 文件(在你的服务器的相同目录);2秒内,变化将会显现! + +### 添加新评论 + +现在是时候建立表单了,我们的 `CommentForm` 组件应该询问用户他们的名字和评论文本然后发送一个请求到服务器来保存评论. + +```javascript{5-9} +// tutorial15.js +var CommentForm = React.createClass({ + render: function() { + return ( +
+ + + +
+ ); + } +}); +``` + +让我们做一个交互式的表单。当用户提交表单时,我们应该清空它,提交一个请求给服务器,和刷新评论列表。要开始,让我们监听表单的提交事件并清空它。 + +```javascript{3-14,16-19} +// tutorial16.js +var CommentForm = React.createClass({ + handleSubmit: function(e) { + e.preventDefault(); + var author = React.findDOMNode(this.refs.author).value.trim(); + var text = React.findDOMNode(this.refs.text).value.trim(); + if (!text || !author) { + return; + } + // TODO: send request to the server + React.findDOMNode(this.refs.author).value = ''; + React.findDOMNode(this.refs.text).value = ''; + return; + }, + render: function() { + return ( +
+ + + +
+ ); + } +}); +``` + +##### 事件 + +React使用驼峰命名规范(camelCase)给组件绑定事件处理器。我们给表单绑定一个`onSubmit`处理器,它在表单提交了合法的输入后清空表单字段。 + +在事件中调用`preventDefault()`来阻止浏览器提交表单的默认行为。 + +##### Refs + +我们利用`ref`属性给子组件赋予名字,`this.refs`-引用组件。我们可以在组件上调用 `React.findDOMNode(component)` 获取原生的浏览器DOM元素。 + +##### 回调函数作为属性 + +当用户提交评论时,我们需要刷新评论列表来包含这条新评论。在`CommentBox`中完成所有逻辑是有道理的,因为`CommentBox` 拥有代表了评论列表的状态(state)。 + +我们需要从子组件传回数据到它的父组件。我们在父组件的`render`方法中以传递一个新的回调函数(`handleCommentSubmit`)到子组件完成这件事,绑定它到子组件的 `onCommentSubmit` 事件上。无论事件什么时候触发,回调函数都将被调用: + +```javascript{16-18,31} +// tutorial17.js +var CommentBox = React.createClass({ + loadCommentsFromServer: function() { + $.ajax({ + url: this.props.url, + dataType: 'json', + cache: false, + success: function(data) { + this.setState({data: data}); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, + handleCommentSubmit: function(comment) { + // TODO: submit to the server and refresh the list + }, + getInitialState: function() { + return {data: []}; + }, + componentDidMount: function() { + this.loadCommentsFromServer(); + setInterval(this.loadCommentsFromServer, this.props.pollInterval); + }, + render: function() { + return ( +
+

Comments

+ + +
+ ); + } +}); +``` + +当用户提交表单时,让我们从 `CommentForm` 调用这个回调函数: + +```javascript{10} +// tutorial18.js +var CommentForm = React.createClass({ + handleSubmit: function(e) { + e.preventDefault(); + var author = React.findDOMNode(this.refs.author).value.trim(); + var text = React.findDOMNode(this.refs.text).value.trim(); + if (!text || !author) { + return; + } + this.props.onCommentSubmit({author: author, text: text}); + React.findDOMNode(this.refs.author).value = ''; + React.findDOMNode(this.refs.text).value = ''; + return; + }, + render: function() { + return ( +
+ + + +
+ ); + } +}); +``` + +既然现在回调函数已经就绪,我们所需要做的就是提交到服务器然后刷新列表: + +```javascript{17-28} +// tutorial19.js +var CommentBox = React.createClass({ + loadCommentsFromServer: function() { + $.ajax({ + url: this.props.url, + dataType: 'json', + cache: false, + success: function(data) { + this.setState({data: data}); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, + handleCommentSubmit: function(comment) { + $.ajax({ + url: this.props.url, + dataType: 'json', + type: 'POST', + data: comment, + success: function(data) { + this.setState({data: data}); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, + getInitialState: function() { + return {data: []}; + }, + componentDidMount: function() { + this.loadCommentsFromServer(); + setInterval(this.loadCommentsFromServer, this.props.pollInterval); + }, + render: function() { + return ( +
+

Comments

+ + +
+ ); + } +}); +``` + +### 优化: 优化的更新 + +我们的应用现在已经功能完备,但是它感觉很慢,因为在评论出现在列表前必须等待请求完成。我们可以优化添加这条评论到列表以使应用感觉更快。 + +```javascript{17-19} +// tutorial20.js +var CommentBox = React.createClass({ + loadCommentsFromServer: function() { + $.ajax({ + url: this.props.url, + dataType: 'json', + cache: false, + success: function(data) { + this.setState({data: data}); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, + handleCommentSubmit: function(comment) { + var comments = this.state.data; + var newComments = comments.concat([comment]); + this.setState({data: newComments}); + $.ajax({ + url: this.props.url, + dataType: 'json', + type: 'POST', + data: comment, + success: function(data) { + this.setState({data: data}); + }.bind(this), + error: function(xhr, status, err) { + console.error(this.props.url, status, err.toString()); + }.bind(this) + }); + }, + getInitialState: function() { + return {data: []}; + }, + componentDidMount: function() { + this.loadCommentsFromServer(); + setInterval(this.loadCommentsFromServer, this.props.pollInterval); + }, + render: function() { + return ( +
+

Comments

+ + +
+ ); + } +}); +``` + +### 祝贺! + +你刚刚通过几个简单的步骤建立了一个评论框。学习更多关于[为什么使用 React](/react/docs/why-react-zh-CN.html), 或者深入 [API 参考](/react/docs/top-level-api.html) 开始钻研!祝你好运!