thinking-in-react

关于React的一些思考

作者: Pete Hunt

查看原文

在我看来,React是使用javascript构建大型快速应用的首选途径. 它在Instagram和facebook上都运行的很好.

React的一个重要部分就是,如何去思考你要构建的应用.在这篇文章里,我会带领你一步一步思考如何使用React来实现一个搜索产品列表的功能.

从一个原型开始

想象我们已经有一个JSON格式的数据文件以及一个设计师给出的原型.显然我们的设计师水平很烂,因为这个原型看起来是这样的:

Mockup

我们的JSON文件返回的数据是这样的:

[
  {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"}
];

第一步: 把视图拆分成层级组件

首先你需要画一个盒子模型草图,这个盒子模型包括了原型里的每个组件和它的子组件,然后给每个组件取名字.如果你和设计师协作,他们可能已经帮你完成了取名这件事,直接去问他们就行.ps图层名可能就是最后React组件的名字.

但你怎么知道哪些应该是一个组件呢? 只需要通过判断你是否需要新建一个对象或者函数. 这个技术秉持各自负责原则,就是说,从理念上讲,一个组件只做一件事.如果它后面需要扩展,就应该被解耦成多个子组件.

由于大多数时候你展示给用户的数据模型都是基于JSON格式的,你会发现,一旦你的模型建立正确,你的视图(连同你的组件结构)会很好匹配它.这是因为视图和数据模型倾向于依附同一个结构.这就意味着,把视图拆分成组件是很简单的,只需要按照每个组件匹配一个数据模型来拆分就可以了.

Mockup

你可以看到,这个简单的应用里有5个组件.我已经用粗体字标明了每个组件代表了哪个数据模型.

  1. FilterableProductTable (橙色): 包含了整个栗子
  2. SearchBar (深蓝): 接收用户输入的内容
  3. ProductTable (绿色): 过滤并展示基于用户输入搜索条件的匹配数据.
  4. ProductCategoryRow (亮蓝): 展示每个分类的标题.
  5. ProductRow (红色): 将每个产品一行一行显示出来.

看一下 ProductTable, 你会发现,表格的标题部分(包括Name和Price两项)并不是一个单独的组件.这是由个人偏好决定的,也有争议说不应该这样处理.在这个例子里,我把它作为 ProductTable 的一部分,因为它也是渲染搜索结果数据的一部分,这个事情应该由 ProductTable 负责.但是,如果这个标题变得更复杂(比如我们要添加排序功能),显然就应该把它单独做成一个 ProductTableHeader 组件

现在,我们已经给原型定义好了组件,让我们把它整理成一个层级关系.这很简单. 被包含在其他组件里的组件应该出现在层级图的子层:

hierarchy

第二步: 使用React构建一个静态版本

代码在jsfiddle上:查看代码

现在你已经有了组件的层级关系,就可以开始你的应用了.最简单的方法是构建一个不带有任何交互,仅使用数据模型来渲染出视图的版本.最好能够把把这个过程进行解耦,因为构建静态版本需要打很多不用动脑筋的代码,而添加交互不需要写很多代码,却很费脑子.我们来看下这是为什么.

构建一个静态渲染数据模型的应用,你的组件里需要重用其他组件,然后通过 props 来传递数据.props用于从父组件向子组件传递数据. 如果你听说过’状态’这个概念,那么记住,不要在静态的版本里使用状态.状态是留给交互使用的,就是说,在一段时间后会改变的数据. 由于现在做的是静态版的应用,所以你用不到它.

你的思考方式可以是从上到下,也可以是从下到上. 就是说,你可以从层级的最高层的组件开始做(比如从FilterableProductTable开始)或者最底层的那个(ProductRow).一般来说,在简单的情况下,从上到下会比较简单,在大型的项目里,从下到上的构建并编写测试会比较简单.

在这一步的最后,你会得到一个可重用的组件库,用于渲染数据模型.组件只有 render() 方法,因为这是一个静态的版本.数据模型会被放在最外层的组件(FilterableProductTable)的prop属性里.如果你修改了底层的数据模型,然后再次调用 ReactDOM.render() 方法,视图会被更新.你可以很容易的看到视图是怎么被更新的,哪里发生了变化.因为没有什么复杂的事情发生.React的单向数据流(或称单向数据绑定)保证了页面的模块化和速度.

如果你在这一步有什么问题,请参考React文档.

第三步: 定义视图的状态,要做到最小化但却完整.

要让视图实现交互,你需要触发底层数据模型发生改变.React通过状态来简单的实现了它.

为了正确构建你的应用,首先要思考的是,应用有哪些变化的状态,他们的最小单位应该是什么.这里的关键是 DRY(Don’t Repeat Yourself) 原则: 不要自己重复自己.

找出应用里所需要的最小单位的状态,而其他需要的东西,可以通过算法来实现. 举个栗子,如果你要做一个TODO列表,只需要有一个数组格式的TODOs的列表;不要给数量单独赋予一个状态,而是通过获取TODO数组的长度来得到TODOs的数量.

考虑这个例子里的每条数据:

  • 商品的原始列表
  • 用户输入的搜索词
  • checkbox是否被选中
  • 过滤出的商品列表

让我们过一遍,找出哪些应该是状态.每条数据都问三个问题:

  1. 它是不是通过父组件的props传入的? 如果是,那它很可能不是一个状态.
  2. 它过一段时间会改变么? 如果不会,那它很可能不是一个状态.
  3. 你可以通过计算其它状态和组件的props属性来得到它么? 如果是,那它就不是状态.

商品原始列表是通过props属性传入的,所以它不是状态.搜索词和checkbox应该是状态,因为他们会发生改变,而且不能通过其它东西计算出结果.最后,过滤出的商品列表不是状态,因为它可以通过商品原始列表,搜索词,checkbox是否选中来计算出.

所以最后,我们的状态应该是:

  • 用户输入的搜索内容
  • checkbox的值

第四步: 确定状态应该放在哪里

代码在jsfiddle上:查看代码

很好,现在我们已经确定了应用的最小单位状态.接下来,我们需要确认哪些组件是变化的,或者说,哪些组件拥有了这些状态.

注意: React 是沿着组件的层级向下使用单向数据绑定的.所以不能立时片刻的搞清楚哪些组件应该拥有哪些状态.这通常也是对初学者来说理解起来最有挑战的部分,可以参考下面的步骤来找出它:

对于应用中的每个状态:

  • 找出哪些组件渲染时需要用到那个状态
  • 找出一个公共组件 (就是包含了所有1里找到的组件的父级组件)
  • 可以是2里找到的公共组件,也可以是这个公共组件的父组件
  • 如果找不到这样一个应该拥有状态的组件,那就自己创建一个,这个被创建的组件仅仅用于存放状态.把这个组件放在公共组件的外层.

让我们在这个应用里使用以上的策略:

  • ProductTable 组件需要通过状态来过滤产品列表, SearchBar 需要展示搜索内容和checked状态.
  • 这两个组件的公共组件是 FilterableProductTable
  • 把搜索内容和checked值放在 FilterableProductTable 下,从概念上也是合理的.

很好,现在我们已经决定了把状态放在 FilterableProductTable 组件上. 首先,给 FilterableProductTable 添加一个 getInitialState() 方法,返回 {filter: '', inStockOnly: false} 来作为状态的初始值. 然后,把 filterTextinStockOnly 添加到 ProductTableSearchBar 组件的prop属性里. 最后,通过这些属性,在 ProductTable 组件里过滤出内容,在 SearchBar 组件里设置表单控件的值.

现在可以开始看到应用是怎么工作的了: 把 filterText 的值设置为 ‘ball’ ,然后刷新应用,你会看到数据列表被正确的更新了.

第五步: 添加反向数据流

代码在jsfiddle上:查看代码

目前为止,我们已经构建了一个通过props和状态,从上到下正确渲染出组件层级的应用了.是时候支持反向的数据流了:阶层底层的表单组件需要更新 FilterableProductTable 的状态.

React数据流很清晰,很好理解程序是怎么运行的,但是这比传统的双向数据绑定要多写一些代码.
React 还提供了一种名为 ReactLink 的扩展,它可以使得这种模式和双向数据绑定一样的方便,但是这篇文章的目的不是这个,所以我们还是让数据流保持清晰.

如果你尝试在这个版本的例子里输入内容或者点击checkbox,你会看到React忽略了你的输入.这是故意的,因为我们把 inputvalue 属性设置成了 state 的值,而 state 值是从 FilterableProductTable 传入的.

让我们考虑一下我们希望实现什么效果.我们希望确保每当用户修改表单内容的时候,把状态更新为用户的输入内容.由于组件应该只更新自己的状态,所以 FilterableProductTable 应该传递一个回调给 SearchBar ,每当状态需要更新的时候,触发这个回调.我们在 inputs 上添加 onChange 事件来监听它.所有通过 FilterableProductTable 传入的回调都会调用 setState() 方法来更新应用的状态.

虽然这听起来有点复杂,但其实只有几行代码.而且数据的整个流向过程十分清晰.

以上就是本文的内容

希望这篇文章能够给你一些启发,如何使用React来构建组件和应用.虽然它可能会让你比以前多写一些代码,但你要知道,读代码的时间要远远多于写代码的时间,而这样模块化,清晰的代码可读性是很强的.当你开始构建大型的组件库的时候,你会感谢它的清晰和模块化,另外,由于代码的可重用性很高,越写到后面你需要写代码也就越少了.


end

react-教程

教程

我们会创建一个简单却真实的评论列表,你可以把它用在博客里,就像Disqus,LiveFyre,FaceBook里的实时评论功能一样,但它只是一个基础的版本.

我们会提供:

  • 一个包含所有评论的视图
  • 一个用于提交评论的表单
  • 可以自定义后端接口的钩子

它还有一些新特性:

  • 预显示评论: 新增评论会在被保存到服务器之前就显示在页面里,以获得更快的体验.
  • 实时更新: 其他用户的评论会被实时的输出到评论视图里.
  • 支持Markdown格式: 用户可以使用markdown语法来书写评论内容.

想要跳过本文直接看源码?

源码都放在了Github上

启动服务端

要开始这个教程,我们首先要启动一个服务端.我们只需要一个后端的API用来获取和保存数据.为了尽可能简化它,我们用脚本语言创建了一个简单的服务端,它刚好满足我们的需要.你可以查看源码或者下载zip压缩文件,这里面包含了启动所需要的一切程序.

为了更简化,我们要运行的服务端使用了一个 JSON 格式的文件来作为数据库.在开发环境不可能这样做,但能让这个API更简单的模拟你的操作.一旦启动服务端,它就可以支持我们的后端API,满足静态页面的生成需要.

开始

因为这只是个教程,所以我们尽可能的简化它. 在刚才讨论的服务端程序包里,有一个HTML页面,它就是我们要做的页面. 在你喜欢的编辑器里打开 public/index.html 页面. 它看起来应该像这样:

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>React Tutorial</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-dom.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/babel" src="scripts/example.js"></script>
    <script type="text/babel">
      // To get started with this tutorial running your own code, simply remove
      // the script tag loading scripts/example.js and start writing code here.
    </script>
  </body>
</html>

教程的剩下部分,会教你如何在script标签里写自己的javascript代码.我们没有使用高级的自动刷新功能,所以每次保存以后需要手动刷新浏览器来查看更新.启动服务器以后,在浏览器里打开 http://localhost:3000 来查看程序. 当你第一次打开这个它,还没有做过任何改动的时候,你可以看到完整的项目,这就是我们等下要做的.准备要开始学习,只需要把前面的 script 标签给删掉,然后就可以继续了.

注意: 为了等下方便发送ajax请求,我们在这里引入了jQuery,但React并不是强制使用jQuery才能工作的.

由于直接在html文件的script标签里写react的话,intelliJ不能识别JSX语法,所以最好还是写在外链的js文件里.

你的第一个组件

React都是模块化的, 由可编辑的组件组成. 我们的这个评论框例子由以下这些组件构成:

- CommentBox
  - CommentList
    - Comment
  - CommentForm

让我们创建一个 CommentBox 组件,它只是一个简单的

:

// tutorial1.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        Hello, world! I am a CommentBox.
      </div>
    );
  }
});
ReactDOM.render(
  <CommentBox />,
  document.getElementById('content')
);

注意,原生的HTML元素是以小写字母开头的,而自定义的React类名应该以大写字母开头.

JSX 语法

首先要注意的是javascript里类似于XML的语法.它有一个简单的预编译机制,会把这个语法糖转换成纯javascript:

// tutorial1-raw.js
var CommentBox = React.createClass({displayName: 'CommentBox',
  render: function() {
    return (
      React.createElement('div', {className: "commentBox"},
        "Hello, world! I am a CommentBox."
      )
    );
  }
});
ReactDOM.render(
  React.createElement(CommentBox, null),
  document.getElementById('content')
);

是否要使用JSX语法是可选的,但是使用它会比不使用要更简单.了解更多关于 JSX语法

继续下一步

我们把一些方法放在一个Javscript对象里,然后把它传给 React.createClass() ,以创建一个React组件.这些方法里最重要的就是 render 方法,它返回了React组件树,最后会渲染成HTML.

这里的 <div> 标签并不是真正的DOM元素;他们是React div 组件的实例.你可以想象成他们是某种标记或数据,React知道要如何去处理他们.React是安全的,我们并不是生成HTML字符串,所以默认已经阻止了XSS攻击.

你不需要返回所有的HTML.你可以返回一个你(或别人)创建的组件树.这就使得React是可重用的.可重用化是构建可维护的前端的一个重要原则.

ReactDOM.render() 实例化了根组件,开始构造组件内容,然后把内容注入到一个既存的原生DOM元素里.这个DOM元素可以通过第二个参数传入.

ReactDOM 模块暴露的是DOM特有的方法,而 React 类有一些核心的工具是可以在不同的使用React的平台上共享的.(比如 React Native)

很重要的一点是,可以在这个教程里看到, ReactDOM.render 方法是被放在script的最后调用的. React.render 只有在合成的组件被定义完成后才能被调用.

创建组件

让我们继续创建 CommentListCommentForm 的骨架,也是简单的 <div>. 把这两个组件加到你的文件里,声明 CommentBox 组件和调用 ReactDOM.render 部分的代码不变:

// tutorial2.js
var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        Hello, world! I am a CommentList.
      </div>
    );
  }
});

var CommentForm = React.createClass({
  render: function() {
    return (
      <div className="commentForm">
        Hello, world! I am a CommentForm.
      </div>
    );
  }
});

接下来,更新 CommentBox 组件的代码,使用新的组件:

// tutorial3.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList />
        <CommentForm />
      </div>
    );
  }
});

注意我们是如何把HTML标签和我们创建的组件合在一起的.HTML组件只是普通的React组件,它和你自定义的组件只有一点不同.JSX编译机制会自动把HTML标签重写成 React.createElement(tagName) 表达式,其他都不变.这是为了防止污染全局命名空间.

使用属性

让我们来创建 Comment 组件,它依赖于父组件传给它的数据. 从父组件传入的数据可以通过子组件的’属性’来获取. 这些’属性’可以通过 this.props 来访问. 使用属性,我们可以获取到 CommentList 传给 Comment 的数据,然后生成元素.

// tutorial4.js
var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {this.props.children}
      </div>
    );
  }
});

在JSX语法里(可以是属性,也可以是子组件),通过把一段Javascript表达式放在花括号里,可以插入文本或者在组件树里插入React组件.指定名字的属性值可以通过 this.props 对象中的对应的key来获取.嵌套的元素可以通过 this.props.children 来获取.

组件的属性

现在我们定义了 Comment 组件,接下来我们想要给它传入作者名字和评论内容.这允许我们重用每个评论的代码.现在,在我们的 CommentList 组件里添加一些评论:

// tutorial5.js
var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        <Comment author="Pete Hunt">This is one comment</Comment>
        <Comment author="Jordan Walke">This is *another* comment</Comment>
      </div>
    );
  }
});

现在,我们通过 CommentList 组件向它的子组件 Comment 传递了一些数据.举个栗子,我们传入了 ‘Pete Hunt’(通过一个属性) 以及 ‘This is one comment’(通过一个像XML格式一样的子节点) 给第一条 Comment. 就像前面提到的, Comment组件可以通过 this.props.author 以及 this.props.children 来获取到这些属性.

添加Markdown支持

Markdown是一个简单的格式化文本的方法.举个栗子,用’*’包围的文字会变成强调部分(斜体).

在这个教程里,我们使用第三方的库 marked 来处理Markdown文本,把它们转换成原生HTML. 我们已经在页面里引入了这个库,可以直接用它了.让我们把内容通过Markdown转换然后输出它:

// tutorial6.js
var Comment = React.createClass({
  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        {marked(this.props.children.toString())}
      </div>
    );
  }
});

我们这里所做的仅仅是调用了marked库,我们需要把被React封装过的 this.props.children 文本转换成原始的字符串,这样marked才能识别.所以我们明确的调用 toString().

但这样做有个问题! 我们渲染出来的评论内容在浏览器里看上去是这样的: "<p>This is <em>another</em> comment</p>" .我们希望的是这些标签能够真正地被转换成HTML.

这是React保护你免受 XXS攻击.有一个方法可以解决这个问题,但是框架会给你一个警告,叫你不要这样做:

// tutorial7.js
var Comment = React.createClass({
  rawMarkup: function() {
    var rawMarkup = marked(this.props.children.toString(), {sanitize: true});
    return { __html: rawMarkup };
  },

  render: function() {
    return (
      <div className="comment">
        <h2 className="commentAuthor">
          {this.props.author}
        </h2>
        <span dangerouslySetInnerHTML={this.rawMarkup()} />
      </div>
    );
  }
});

这是一个特殊的API,有意让插入原始的HTML变得困难.但对于marked来说,刚好可以利用它的优势.

注意: 要使用这个API,你就必须仰赖于marked是安全的. 在这里,我们传入 sanitize:true 来告诉marked,把所有的HTML标记进行转义,而不是直接输出HTML.

解释一下:在你输入的内容是纯文字或者’*’等符号的时候,marked会把它转换成html文本,比如 <p>123<em>456</em></p> ,这个时候,你希望这些标签能变成HTML标签,而不是文字,所以你可以使用React的 dangerouslySetInnerHTML ,但是如果用户本身在内容里输入了html文本,比如 ‘‘ 这样,如果不在marked里面设置 sanitize:true,那么用户输入的这些html文本也会变成HTML标签,这样就不安全.所以需要开启 sanitize:true ,这样,首先marked会把用户输入的所有内容都转义成html文本,然后通过Markdown转换来把文本变成对应的HTML,最后React直接把HTML渲染.这样就确保了用户不能创建HTML插入,但是可以使用markdown所支持的语法.

连接数据模型

到目前为止,我们直接在源码里插入了评论内容.接下来,我们要用一堆JSON数据生成评论列表.到最后,数据应该从服务端获取,但现在我们还是把它写在源码里:

// tutorial8.js
var data = [
  {id: 1, author: "Pete Hunt", text: "This is one comment"},
  {id: 2, author: "Jordan Walke", text: "This is *another* comment"}
];

我们需要用模块化的方式把数据放到 CommentList 里. 修改 CommentBoxReactDOM.render() 方法,通过props把数据传入 CommentList:

// tutorial9.js
var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.props.data} />
        <CommentForm />
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox data={data} />,
  document.getElementById('content')
);

现在,数据就可以在 CommentList 里获取到了,让我们动态的生成评论:

// tutorial10.js
var CommentList = React.createClass({
  render: function() {
    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>
    );
  }
});

就系酱紫!

从服务端获取数据

让我们把硬编码的数据换成从服务端获取的动态数据.我们把data属性移除,换成一个获取数据的url属性:

// tutorial11.js
ReactDOM.render(
  <CommentBox url="/api/comments" />,
  document.getElementById('content')
);

这个组件和之前的组件不同,它必须重新渲染自己.这个组件在服务器没有返回数据之前是没有任何数据的,在得到数据的时候,他们需要渲染一些新的评论.

注意: 代码在这一步是不能运行的.

响应式状态

到目前为止,每个组件都会基于它的props属性渲染自己一次. props 是不可变的:他们是通过父组件传过来的,被父组件所’拥有’的.为了实现交互,我们引入可变的状态到组件里. this.state 是组件私有的,并且可以通过调用 this.setState() 来改变.当状态更新的时候,组件会重新渲染自己.

render() 方法用声明式的写法来使用 this.propsthis.state 功能. 所以它确保了视图总是和输入保持一致.

所谓的声明式,就像css类名一样,只需要声明一个类名,对应的样式修改了,元素的样式也就改变了,这个概念用到这里就是说,render()方法里的this.propsthis.state变化的时候,视图也会同步发生变化.

当服务器获取到数据的时候,评论的内容会变成新获取的数据.让我们添加一个评论的数组作为 CommentBox 组件的状态:

// tutorial12.js
var CommentBox = React.createClass({
  getInitialState: function() {
    return {data: []};
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

getInitialState() 方法在组件的生命周期里只会被执行一次,用于设置组件的初始状态.

更新状态

当组件第一次被创建后,我们想要通过GET请求从服务端去获取JSON数据,然后更新状态以映射最新获取的数据.我们使用jQuery向前面已经搭建好的服务器发送一个异步请求来获取我们需要的数据.数据已经被放在这个服务器端了(就是 comments.json 文件),所以当数据被获取后, this.state.data 看起来是这样的:

[
  {"author": "Pete Hunt", "text": "This is one comment"},
  {"author": "Jordan Walke", "text": "This is *another* comment"}
]


// 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 (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

这里, componentDidMount 方法会在组件第一次渲染完成后被React自动调用.实现动态更新的关键是 this.setState() 方法. 我们用从服务器获取的最新数据来替换原来的评论数组,然后视图会自动更新自己.由于它是响应式的,还需要加一个小改动来实现实时更新.我们这里只使用简单的轮询,你也可以很容易的使用WebSockets或者其他的技术.

// 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 (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

ReactDOM.render(
  <CommentBox url="/api/comments" pollInterval={2000} />,
  document.getElementById('content')
);

这里所做的只是把AJAX部分的代码放到一个单独的方法里,然后在组件第一次渲染完毕后执行一次,然后再每隔2s执行一次.试着在浏览器里运行它,然后修改 comment.json 文件(在你启动的那个项目文件夹里); 在2s以内,改变就会被呈现出来!

添加评论

现在可以开始创建表单了.我们的 CommentForm 组件需要用户输入名字和评论内容,然后向服务器发送请求来保存评论:

// tutorial15.js
var CommentForm = React.createClass({
  render: function() {
    return (
      <form className="commentForm">
        <input type="text" placeholder="Your name" />
        <input type="text" placeholder="Say something..." />
        <input type="submit" value="Post" />
      </form>
    );
  }
});

可控组件 (Controlled components)

对于传统的DOM元素, input 元素渲染后由浏览器管理它的状态(就是渲染出的value值).所以,真正的DOM元素的状态和对应的组件的状态就不一样了. 视图和组件的状态不同,这可不是理想的状态. 在React里,组件的状态应该永远和视图同步,而不仅仅是在初始化的时候.

因此,我们使用 this.state 在用户打字的时候保存他们输入的内容. 我们定义一个初始的 state ,它有两个属性 authortext ,一开始他们的值都是空字符串.在的 <input> 元素里,我们把 value 值设置成组件的 state ,然后给它们添加一个 onChange 事件处理句柄. 这类 value 被设置过的 <input> 元素,被成为可控元素.了解更多关于可控元素请查看 表单

// tutorial16.js
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});
  },
  render: function() {
    return (
      <form className="commentForm">
        <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>
    );
  }
});

事件

React使用驼峰命名法给组件添加事件处理句柄.我们给这里的两个 <input> 元素添加了 onChange 事件处理句柄.现在,当用户在这两个 <input> 元素里输入内容的时候, 对应的 onChange 回调就会触发,组件的 state 就会被改变. 然后, input 元素所渲染的value值会被更新,映射组件当前的 state 值.

提交表单

让我们实现表单的交互.当用户提交表单的时候,我们应该清空它,然后提交一个请求到服务器,然后刷新评论列表.首先,让我们监听表单提交事件,然后清空它.

// tutorial17.js
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;
    }
    // TODO: send request to the server
    this.setState({author: '', text: ''});
  },
  render: function() {
    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>
    );
  }
});

我们给表单添加一个 onSubmit 事件处理句柄,当里面的表单被提交,并且表单控件都通过验证时,清空表单.

在事件回调里执行 preventDefault() 来阻止浏览器的默认表单提交事件.

回调作为属性

当用户提交新评论的时候,我们需要更新评论列表来把新增的评论显示出来.应该把这个逻辑放在 CommentBox 里,因为 CommentBox 的状态对应的就是评论列表.

我们需要把数据从子组件再传回到它的父组件.通过在父组件的 render 方法里传递一个回调(handleCommentSubmit)给子组件,绑定到子组件的 handleCommentSubmit 事件上.每当事件触发,回调就会被执行:

// tutorial18.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 (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});

当用户提交表单的时候,在 CommentForm 里调用回调:

// tutorial19.js
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() {
    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>
    );
  }
});

现在回调已经正确了,接下来只需要把请求发送到服务器然后更新列表:

// 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) {
    $.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 (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});

优化:乐观性更新

现在这个应用的功能都已经完成了.但是需要等到评论成功发送到服务器以后才能看到列表更新,这样会比较慢,我们可以乐观的认为评论会提交成功,直接把它添加到列表里,这样可以让应用的体验更快一些.

// tutorial21.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;
    // 乐观地给评论添加一个id.将来这个id会被服务器端生成的id替换.
    // 在生产环境下你可能不会使用 Date.now() 作为id,而是使用更合适更健壮的id体系.
    comment.id = Date.now();
    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) {
        this.setState({data: comments});
        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 (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm onCommentSubmit={this.handleCommentSubmit} />
      </div>
    );
  }
});

恭喜!

你已经通过一些简单的步骤搭建了一个评论功能. 想要知道更多为什么要使用React? 深入了解API文档,开始使用吧! Good luck!


end

关于ajax缓存的两个真相

翻自 Ajax Caching: Two Important Facts

Ajax的调用和其它的HTTP请求一样,用于构建web页面. 然后由于它是动态的,人们常常忽略了缓存它的好处.

<< High Performance Web Sites >>一书的第14条说到:

让ajax请求可以被缓存

确保你的ajax请求遵循高性能指导方针,尤其是要有一个设置未来过期时间的Expires头.


这篇文章剩下的部分会包含两个重要的真相,这会帮助你理解并高效的使用Ajax请求的缓存.

真相1:Ajax的缓存和HTTP的缓存是一样的

现代浏览器的HTTP和缓存机制比Ajax的XMLHttpRequest对象要差很多,所以它不认识也不关心Ajax请求.它仅仅是遵循普通的HTTP缓存规则,通过服务器返回的响应头来进行缓存.

如果你已经对 HTTP缓存 有了解,那么你可以把HTTP缓存的知识用对Ajax缓存的理解上. 他们只有一点不同的,就是设置响应头的方式会和普通文件不一样.

下面这些响应头可以让你的Ajax可缓存:

  • Expires: 这一项应该被设置成未来的某个合适的时间点,时间点的设置取决于内容变动的频繁程度.举个栗子,如果请求的是个库存数量,那么Expires的值可以是10秒以后.如果请求的是一个相片,那么Expires的值就可以久一点,因为它不会经常变动.Expires头可以让浏览器在一段时间内重用本地缓存数据,从而避免任何不必要的与服务器数据交互.
  • Last-Modified: 设置这一项是一个很好的选择,通过它,浏览器在发送条件性GET请求的时候会使用请求头里的 If-Modified-Since 来检查本地缓存的内容.如果数据不需要更新,服务器会返回304响应状态.
  • Cache-Control: 在合适的情况下,这个值应该被设置为 Public ,这样所有的中间代理和缓存都可以被保存并且与其他用户共享内容.在火狐里,它还支持HTTPS请求的缓存

当然,如果你使用POST方式发送Ajax是不能缓存的,因为POST请求永远不会被缓存.如果你的Ajax请求会产生其他作用(比如银行账户之间的转账),请使用POST请求.

我们设置了一个demo(这个demo已经不能看了ヽ(≧□≦)ノ)来阐明这些头信息是如何工作的. 在HttpWatch里,你可以看到我们在响应头信息里设置了以上三个响应头

Ajax Caching Headers

如果你规律的点击 ‘Ajax Update’ 按钮,时间的改变会趋向于每隔一分钟一次.因为Expires响应头被设置为未来的一分钟.在下面这张截图里你可以看到:重复的点击更新按钮时,Ajax请求会读取浏览器本地的缓存而不会产生网络活动(发送和传输栏的值都是0)

Ajax Caching Headers

最后一次1:06.531时刻的点击发送的Ajax请求产生了网络数据传输,因为缓存的数据已经超过了一分钟. 服务器返回200响应状态表示获取到了一份新的数据.

猜测这个demo应该是一个按钮,每点击一次获取一次当前时间然后回现在页面上.

真相2:IE浏览器在Expires时间过期之前不会刷新通过Ajax获取的内容.

有些时候,Ajax在页面加载的时候就被用来填充页面的某些部分(比如一个价格列表).它并不是通过用户的某个事件(比如点击某个按钮)触发的,而是在页面加载的时候就通过javascript来发送的.就好像Ajax请求和那些嵌入资源(比如js和css)是一样的.

如果你开发这样的页面,在刷新它的时候,很可能想要更新嵌入的Ajax请求内容.对于嵌入资源(CSS文件,图片等),浏览器会通过用户刷新的方式是F5(刷新)还是Ctrl+F5(强制刷新)来自动发送下列不同类型的请求:

  1. F5(刷新): 如果请求内容带有 Last-Modified 响应头,那么浏览器会发送条件性更新请求. 它使用 If-Modified-Since 请求头进行比较,这样服务器就可以返回304状态来避免传输不必要的数据.
  2. Ctrl+F5(强制刷新): 告诉浏览器发送无条件更新请求,请求头的 Cache-Control 被设置为‘no-cache’.这告诉所有的中间代理和缓存:浏览器需要获取最新的版本,无论它是否已经被缓存.

Firefox把这个刷新的方式传播到了那些在页面加载的时候就发送的Ajax请求上,把这些Ajax请求当成嵌入资源来处理.下面是HttpWatch在火狐下的截图,显示了Ajax Caching demo(这个demo已经不能看了ヽ(≧□≦)ノ)刷新(F5)页面时Ajax请求的效果:

Refresh of Ajax Request in Firefox

火狐确保Ajax发起的请求是条件性的.在这个例子里,如果缓存数据不到10秒,服务器返回304,超过10秒,服务器返回200,重新传送数据.

在ie里,加载页面时就发起的Ajax请求被看做是和页面其他部分刷新毫无关系的,也不会被用户的刷新方式所左右.如果缓存的ajax数据没有过期,就不会有GET请求发送到服务器.它会直接从缓存里读取数据,从HttpWatch里看就是(Cache)结果.下面这个图是在ie下缓存没有过期的情况下按F5刷新:

IE Refresh of Ajax Request

就算是通过 Ctrl+F5 强制刷新,通过Ajax获取的数据也是从缓存里读取:

IE Refresh of Ajax Request

这就意味着,任何通过Ajax得到的内容如果没有过期,在ie下都不会被更新 - 即使你使用Ctrl+F5强制刷新. 唯一能确保你获取最新数据的方法就是手动清楚缓存. 可以使用HttpWatch的工具栏:

IE Forced Refresh

注意,Cache结果和304结果是不同的.Cache其实是200(cache),304就是304.Cache其实没有向服务器发送请求,可以从chrome里看到,它的耗时是0,response也是空.而304不同,
304请求是浏览器发起了一个条件性的请求,这个请求携带了 If-Modified-Since 请求头,如果这个文件在浏览器发送的这个时间之后没有修改过,服务器端就回返回一个304状态,告诉浏览器使用它本地的缓存内容.它没有Cache快,因为请求还是发送到了服务器端,只不过服务器端没有发送数据.
可以看下taobao首页,里面既有200(cache)也有304.可以查看他们的区别.


end

mac下安装mongodb

之前已经在win7下安装了mongodb,现在要在mac下再安装一遍.

一.下载安装:
首先是: 下载链接

解压它:

code-bunnydeMacBook-Air:Downloads code_bunny$ tar zxvf mongodb-osx-x86_64-3.0.5.tgz

mongodb是个绿色软件,不需要手动安装,所以它解压完即是安装完.接下来我们可以把它移到我希望的路径去并放在mongodb文件夹里:

code-bunnydeMacBook-Air:~ code_bunny$ mv /Users/code_bunny/Downloads/mongodb-osx-x86_64-3.0.5 mongodb

bin目录说明:

  • bsondump 导出BSON结构
  • mongo 客户端
  • mongod 服务端
  • mongodump 整体数据库二进制导出
  • mongoexport 导出易识别的json文档或csv文档
  • mongorestore 数据库整体导入
  • mongos 路由器(分片用)
  • mongofiles GridFS工具,内建的分布式文件系统
  • mongoimport 数据导入程序
  • mongotop 运维监控
  • mongooplog
  • mongoperf
  • mongostat

二.然后在里面创建两个文件夹,一个data文件夹用于存放数据,一个logs文件夹用于存放日志:

code-bunnydeMacBook-Air:mongodb code_bunny$ mkdir data
code-bunnydeMacBook-Air:mongodb code_bunny$ mkdir logs

不一定要建在mongodb的安装包下.可以是任意地方,下面会配置.


在logs文件夹下创建一个log文件:

code-bunnydeMacBook-Air:logs code_bunny$ touch mongo.log
code-bunnydeMacBook-Air:logs code_bunny$ chmod 777 mongo.log

三.启动服务端:

code-bunnydeMacBook-Air:mongodb code_bunny$ sudo ./bin/mongod --dbpath /Users/code_bunny/mongodb/data --logpath /Users/code_bunny/mongodb/logs/mongo.log --fork --port 27017

其中: –dbpath表示数据存放的路径,就是刚才建的data文件夹. –logpath表示日志存放位置,它必须是个log文件. –fork表示后台运行程序. –port是端口

显示这样一段东西,表示启动成功:

about to fork child process, waiting until server is ready for connections.
forked process: 6266
child process started successfully, parent exiting

四.启动客户端:

sudo ./bin/mongo

显示这样一段东西,表示启动成功:

MongoDB shell version: 3.0.5
connecting to: test
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
    http://docs.mongodb.org/
Questions? Try the support group
    http://groups.google.com/group/mongodb-user

end