UP | HOME

react-up-and-running

Table of Contents

Chaper 1. Hello World

Setup

  • react当然是可以使用npm来安装,或者是使用cdn的版本,这里我们使用的是把源代码下 载下来,因为本身react的代码量并不大,注意我们本书使用的是0.14.7版本(可以去 github下载历史版本).
  • 将下载下来的zip文件打包后,放入到我们project的react目录,我们会使用react目录 下面的build目录
    $ tree react/build/
    react/build/
    |-- react-dom-server.js
    |-- react-dom-server.min.js
    |-- react-dom.js
    |-- react-dom.min.js
    |-- react-with-addons.js
    |-- react-with-addons.min.js
    |-- react.js
    `-- react.min.js
    
    0 directories, 8 files
    
  • 值得注意的是react只不过是一个js,它并没有明确要求你一定要在哪个目录

Hello React World

  • 先看我们的第一个例子01_01_hello.html
    <html>
        <head>
            <title> Hello React</title>
            <meta charset="utf-8">
        </head>
        <body>
            <div id="app">
                <!-- my app renders here -->
            </div>
            <script src="react/build/react.js"></script>
            <script src="react/build/react-dom.js"></script>
            <script>
             // my app's code
            </script>
        </body>
    </html>
    
  • 这个例子只有两点需要注意:
    1. 你使用了<script>调用了两个js文件react.js和react-dom.js
    2. 设置了一个id为"app"的div,以后应用会加载到这个位置
  • 我们使用下面的代码替代`//my app's code`来看看最终的结果
    ReactDOM.render(
        React.DOM.h1(null, "Hello World!"),
        document.getElementById("app")
    );
    
  • 最终的结果就是大家会看到一个"Hello World"打印在页面上(在id为app的div里面), 从这个例子也可以看出'id为app的div'就是一个place holder

What Just Happened?

  • 第一个例子如下几个地方与众不同:
    • 我们使用了React object(好像只是通过script src调用),其method并不多
    • 我们使用了ReactDOM object,其method也不多,最著名的就是render(),原来这些 method都是在React object里面的,但是从0.14版本开始分开.因为render不仅仅是 DOM可以做,其他的Android, iOS也是可以做的
    • 我们遇到了component的概念,UI其实就是把不同的component结合起来.以后复杂的 项目中,你会创建自己的component.但是在这个例子里面,我们使用的是react为我们 封装好的component. React.DOM.h1():顾名思义,就是封装<h1>的一个component
    • 最后,你看到了document.getElementByIa("app")这段熟悉的代码,你其实是通过这 段代码,告诉react你的应用应该加载到哪里.

React.DOM.*

  • react其实为我们"封装了"所有的html element,把他们都放到了React.DOM.*里面,我们 可以使用ES6的最新feature来查看一个object所有的key
    > var React = require('react');
    undefined
    > Object.keys(React.DOM)
    [ 'a',
      'abbr',
      'address',
      'area',
      'article',
      'aside',
      'audio',
      'b',
      'form',
      'h1',
      'h2',
      'h3',
      'h4',
      'h5',
      'h6',
     ]
    
  • 好了,我们回到我们的代码,我们的代码里面是如下这么一句
    React.DOM.h1(null, "Hello World!")
    
  • 这一句如果在node repl里面看的话,其实就是生成一个object,这个object有一些key
    > React.DOM.h1(null, 'Hello World!')
    { '$$typeof': Symbol(react.element),
      type: 'h1',
      key: null,
      ref: null,
      props: { children: 'Hello World!' },
      _owner: null,
      _store: {} }
    
  • 第一个参数null是说你希望自己的DOM有什么attribute(注意在这里因为不是真实的DOM, 所以设置是是props: virtual DOM里面的attribute的别称),我们可以自己设置一个 props如下
    > React.DOM.h1({id: "my-heading"}, 'Hello World!')
    { '$$typeof': Symbol(react.element),
      type: 'h1',
      key: null,
      ref: null,
      props: { id: 'my-heading', children: 'Hello World!' },
      _owner: null,
      _store: {} }
    
  • 我们把这句设置到代码里面以后,会得到如下的html结果
    <h1 id="my-heading" data-reactid=".0">Hello World!</h1>
    
  • render()的第二个参数是一个child componnet,最简单的情况就是一个text child,但 其实也是可以有object作为其child的
    > React.DOM.h1({id: "my-heading"}, React.DOM.span(null, "Hello"), " World!")
    { '$$typeof': Symbol(react.element),
      type: 'h1',
      key: null,
      ref: null,
      props: { id: 'my-heading', children: [ [Object], ' World!' ] },
      _owner: null,
      _store: {} }
    
  • 一个更加复杂的例子如下
    React.DOM.h1(
        {id: "my-heading"},
        React.DOM.span(null,
                      React.DOM.em(null, "Hell"),
                      "o"),
        " world!"
    ),
    
  • 其得到的html文件如下
    <div id="app">
      <h1 id="my-heading" data-reactid=".0">
        <span data-reactid=".0.0">
          <em data-reactid=".0.0.0">Hell</em>
          <span data-reactid=".0.0.1">o</span>
        </span>
        <span data-reactid=".0.1"> world!
        </span>
      </h1>
    </div>
    
  • 当然了,我们代码如果这样嵌套下去肯定是非常难看的,react为此为我们专门设计了jsx 格式,使用jsx是话,上面的代码就可以轻松的转换为如下非常易读的代码.(虽然易读, 但是不利于我们掌握原理, 再理解原理之后,再使用这种语法)
    ReactDOM.render(
        <h1 id="my-heading">
        <span><em>Hell</em>o</span> world!
        </h1>,
        document.getElementById("app")
    );
    

Special DOM Attributes

  • 我们上面的代码,使用{attrName: value}作为render的第一个参数,从而设置DOM的attribute 这里就存在一个问题:我们在使用js代码来设置html,而html里面有几个attribute在js 里面是关键字!所以我们要为这几个attribute设置别名:
    • class ==> className
    • for ==> htmlFor
  • 所以我们的代码应该是这样的
    React.DOM.h1(
        {
            className: "pretty",
            htmlFor: "me",
        },
        "Hello World!"
    );
    
    
    //////////////////////////////
    // NOT work version         //
    // React.DOM.h1(            //
    //     {                    //
    //         class: "pretty", //
    //         for: "me",       //
    //     },                   //
    //     "Hello World!"       //
    // );                       //
    //////////////////////////////
    
  • style倒不是js的关键字,但是我们也不能使用style:'xxx'的设置方法,因为这样会引 入cross-site scripting(XSS)攻击.react给出的解决方案是使用js object来设置,需 要注意的是,我们这里使用的是fontFamily(js用法),而不是font-family(css用法)
    React.DOM.h1(
        {
            style: {
                background: "black",
                color: "white",
                fontFamily: "Verdana",
            }
        },
        "Hello World!"
    );
    
    
    ////////////////////////////////////////////////////////////////////////////
    // // CounterExample                                                      //
    // React.DOM.h1(                                                          //
    //     {                                                                  //
    //         style: "background:black; color: white; font-family: Verdana", //
    //     },                                                                 //
    //     "Hello World!"                                                     //
    // );                                                                     //
    ////////////////////////////////////////////////////////////////////////////
    

React DevTools Browser Extension

  • 为chrome和firefox准备的react调试工具

Next: Custom Components

  • 总结一下本章内容:
    • 使用<script>来安装reactjs
    • 使用render函数来把component放到你想要的位置:ReactDOM.render(reactWhat, domWhere)
    • reactWhat部分可以使用内置的常规DOM element: React.DOM.div(attributes, childrdn)

Chapter 2. The Lif of a Component

  • 上一章学习了ready-made DOM component,这一章学习自己创造的component

Bare Minimum

  • 创建一个新componet的API如下
    var MyComponent = React.createClass({
        /* spec */
    });
    
  • 其中的spec是一个JS object, 这个object必须内部含有一个叫做render()的函数, 大概的一个例子如下
    var Component = React.createClass({
        render: function() {
            return React.DOM.span(null, "I'm so custom");
        }
    });
    
  • 大家也看到了,这个叫做render()的函数还必须返回一个React component(内置的),这 个看起来就有点java里面的interface的感觉了:所有的自定义component必须继承一个 interface,这个interface里面就一个函数render,其返回值是内置component
  • 自定义的component使用稍有不同,其不能只用内置的那种newClass的风格,而是使用React createElement
    ReactDOM.render(
        React.createElement(Component),
        document.getElementById("app")
    );
    
  • 值得注意的是createElement其实是创建React Object的唯一方式, React.DOM.xx其实 只不过是一种wraper,所以内置的component也可以使用如下的代码来创建.
    ReactDOM.render(
        React.createElement("span", null, "Hello"),
        document.getElementById("app")
    );
    
  • 我们可以看到不同:createElement接受的不再是一个js function,而是几个string作 为参数

Properties

  • 你的component可以从parent那里获得一些配置,获得的渠道就是this.props. 因为this.prop 可以在调用createElement的时候指定,也就是说可以在使用的时候"注入":
    • 创建的方法
      var Component = React.createClass({
          render: function(){
              return React.DOM.span(null, "My name is " + this.props.name);
          }
      });
      
    • '注入'的方法
      ReactDOM.render(
          React.createElement(Component, {
              name: "Bob",
          }),
          document.getElementById("app")
      );
      
  • 我们要把this.props看成是一个read-only的,在parent和child之间传递configuration 的变量.

propTypes

  • 我们前面已经做了两个铺垫:
    • 自己定义的component object 必须有render函数,但是可以有其他的函数或者变量
    • render函数里面可以使用this.props.xxx来获得parent给予的配置, 其实这个 props.xxx里面的xxx就是component object的变量
  • 好了,我们的render的时候,使用了this.props.name是不是看起来有点突兀:万一parent 没有设置name,或者出现typo写成了nama怎么办?这个时候,页面上会出现如下text
    My name is undefined
    
  • 所以,除了render这个必须有的函数以外,如果我们render一定要用到某些configuration, 我们怎样才能提醒parent,让它记得总要设置这个configuration
  • react给出的解决方案是设置'自定component'的proTypes variable,如下
    var Component = React.createClass({
        propTypes: {
            name: React.PropTypes.string.isRequired,
        },
        render: function(){
            return React.DOM.sapn(null, "My name is " + this.props.name);
        }
    });
    
  • propType是为了更好的使用体验,所以它是optional的.其更好的体验体现在如下两个 方面:
    • 在propType里面写出render到底需要哪些变量,这样,就不需要每次都去render()里 面查找到底要哪些properties
    • react还可以对你"明确"表示需要的property value进行validation,比如我们render 里面的要求name,但是你并没有在createElement的时候,没有设置相应的property, 那么就会在chrome里面的console里面进行warning:
      1. 如果你没设置
        react.js:19368 Warning: Failed propType: Required prop `name`
        was not specified in `Constructor`.
        
      2. 如果你设置了name=123,integer而不是string
        react.js:19368 Warning: Failed propType: Invalid prop `name` of
        type `number` supplied to `Constructor`, expected `string`.
        
  • propTypes是optional的,所以并不一定每一个property都需要设置.best practice就 是
           为重要的property设置propTypes,或者某个property有问题,在debug 的时候,
           为这个property设置propTypes
    
  • 常见的propType类型有如下
    > Object.keys(React.PropTypes)
    [ 'array',
      'bool',
      'func',
      'number',
      'object',
      'string',
      'symbol',
      'any',
      'arrayOf',
      'element',
      'instanceOf',
      'node',
      'objectOf',
      'oneOf',
      'oneOfType',
      'shape' ]
    

Default Property Values

  • propType不是强制的,所以为了不让你的代码出现undefined,一个可行的办法是在你 的render里面设置如下的代码来保证,如果property没有设置的情况下怎么办
    var text = 'text' in this.props ? this.props.text : '';
    
  • 为了不写这种比较丑陋的逻辑代码,我们可以设置一个getDefaultProps() 函数来设置那些default value
    var Component = React.createClass({
        propTypes: {
            firstName: React.PropTypes.string.isRequired,
            middleName: React.PropTypes.string,
            familyName: React.PropTypes.string.isRequired,
            address: React.PropTypes.string,
        },
    
        getDefaultProps: function() {
            return {
                middleName: '',
                address: 'n/a',
            };
        },
    });
    

State

  • 此前我们的例子大部分都是static的(比如props就是read-only的),换句话说就是 stateless的,其实对于React来说,其最强的特性,就是对待那些变动的,也就是stateful 的情况.
  • React定义的state,其实就是你的Component的data,一旦你的data有变动,react会自动 的"找到"这些变动,并帮助你进行更新.所以你只需要关注如何更新data就可以了.
  • 在render函数里面,我们是通过this.props来获得props的,我们同样可以通过this.state 来获得state,但是需要注意的是,我们不要去"更改"this.state(这是React引擎做的事 情),我们要通过this.state的公开的API,也就是this.steState()来更新我的state

A Stateful Textarea Component

  • 我们来看一个例子,我们创建一个新的component,这里面的textarea的char的数字是动 态更新的,我们希望能够动态的现实这个数字
  • 我们最外层的函数还是差不多的:把一个custom的Component(现在名字变了,叫做 TextAreaCounter)显示到id为app的dev上面去
    ReactDOM.render(
        React.createElement(TextAreaCounter,{
            text: "Bob",
        }),
        document.getElementById("app")
    );
    
  • 下面我们来实现我们新的component,为了便于理解,我们首先实现一个stateless的版 本:
    var TextAreaCounter = React.createClass({
        propTypes: {
            text: React.PropTypes.string,
        },
    
        getDefaultProps: function() {
            return {
                text: '',
            };
        },
    
        render:function() {
            return React.DOM.div(
                null,
                React.DOM.textarea({
                    defaultValue: this.props.text,
                }),
                React.DOM.h3(null, this.props.text.length)
            );
        }
    });
    
  • 下一步,我们要把这个stateless的变成stateful的版本.其核心思想是
    让component维护一个data(state),并且使用这个data来render一个
    "最开始的版本",每当data(state)改变的时候,React"自动"调用render
    来更新UI
    
  • 总的结果,就是使用state来替代props,分了几个步骤:
    • 首先创建initial state: state就是data,而我们这个component只有一个data,那就 是text. 我们可以通过this.state.text来访问这个text.而下面的代码是我们通过 拷贝props.text来初始化自己
      getInitialState: function() {
          return {
              text: this.props.text,
          };
      },
      
    • 然后,当state变化的时候,我们需要触发一个function来进行更新,我们这里设置了 一个event listener,过一会我们会把它注册好.注意!我们要使用setState来进行 注册(里面的object会替代已有object),不要试图自己去动this.state!!
      _textChange: function(ev) {
          this.setState({
              text: ev.target.value,
          });
      },
      
    • 最后一步就是更新render(),让它使用this.state(而不再是this.props)来展示到最 终的页面上. 同时设置event listener
      render: function() {
          return React.DOM.div(
              null,
              React.DOM.textarea({
                  value: this.state.text,
                  onChange: this._textChange,
              }),
              React.DOM.h3(null, this.state.text.length)
          );
      }
      

A Note on DOM Events

  • 我们的event listener起作用的代码如下,这是react优化过的使用方法,作为对比,我 们看看原来是如何工作的
    onChange: this._textChange
    

Event Handling in the Olden Days

  • 传统的DOM世界里面,设置event handler的设置如下
    <div id="parent">
        <button id="ok">OK</button>
        <button id="cancel">Cancel</button>
    </div>
    
    <script>
     document.getElementById('parent').addEventListener('click', function(event) {
         var button = event.target;
         switch (button.id) {
             case 'ok':
                 console.log('OK!');
                 break;
             case 'cancel':
                 console.log('Cancel');
                 break;
             default:
                 new Error('Unexpected button ID');
    
         };
    
     });
    </script>
    
  • 但是老的设置有很多的缺点:
    • listener和UI之间距离太远
    • 使用switch,会有太多多余代码
    • 浏览器的兼容性会让代码比这个要多的多
  • 原来处理浏览器兼容性的问题,通常会加入一个event library.但是我们的React提供 了自己非常好的event处理系统,就不需要加入其它什么library啦

Event Handling in React

  • React使用了synthetic events系统来统一处理event,等于消除了所有浏览器之间的 不兼容.换句话说,你永远可以assume event.target是存在的,可用的.
  • 这也意味着"取消"event的函数也是所有浏览器都支持的
  • react的结构还会让UI和event listener设置在一起
  • react的event handler的命名有些讲究,要使用驼峰命名法比如我们前面的handler就 取名叫做_textChange
  • 还有一件事情值得说,传统的DOM里面的onChange event是等你输入完,然后离开这个 输入区域以后,才开始运行handler的.这其实不是人们expected的behavior,但是react 的onChange是真的在输入的同时就触发handler

Props Versus State

  • 到现在为止,你知道了,你可以通过read this.props和this.state来获取你想要的显示 内容,并把他们放到render()函数体里面去.现在我们就来比较一下两者的不同:
    • props是让outside world(component的用户)来configure我们的component是一种 ctor一般的存在
    • state则是内部的data,类似于OO编程里面的private data

Props in Initial State: An Anti-Pattern

  • 我们前面的例子中取名取的不太好,把props直接取名叫做text:这不符合用户的预期, 因为用户其实是想把parent设置给component的默认值给state,所以我们来重命名一下 就解决问题
    propTypes: {
        defaultValue: React.PropTypes.string
    },
    
    getInitialState: function() {
        return {
            text: this.props.defaultValue,
        };
    },
    

Accessing the Component from the Outside

  • react设计的初衷是要和所有已经存在的js代码和睦相处,在这种要求下面,我们必须能 够使用外部的代码来调用react
  • 最常见的和外界交流的办法,就是生成react的ref,然后让外部代码调用这个ref
    var myTextAreaCounter = ReactDOM.render(
        React.createElement(TextAreaCounter,{
            defaultValue: "Bob",
        }),
        document.getElementById("app")
    );
    
  • 在render()函数中可以使用this访问的部分,就可以使用myTextAreaCounter来访问了:
    • 比如可以改动state(注意,此种做法并不推荐,我们更希望react去查询而不是更改核 心state)
      myTextAreaCounter.setState({text: "Hello outside world!"});
      
    • 比如可以获得react活动的parent DOM node
      var reactAppNode = ReactDOM.findDOMNode(myTextAreaCounter);
      reactAppNode.parentNode === document.getElementById('app'); // true
      
    • 获取properties和state:
      myTextAreaCounter.props;        // Object { defualtValue: "Bob" }
      myTextAreaCounter.state;        // Object { text: "Hello outside world!" }
      

Changing Properties Mid-Flight

  • 前面在讲props和state的区别的时候,我们说了,props更多的是一种"外部"给我们的 configuration,而state则是private data
  • private data就该不被外部动,所以我们前面说使用如下方法在外部更改private data 的方法是不好的,会引发潜在bug的
    myTextAreaCounter.setState({text: "Hello outside world!"});
    
  • 所以,使用props来实现"外部"的configuration需求,是正当的.也是唯一的手段,但是 看看我们的代码,在render函数里面是没有涉及到Properties的,全部都是state
    render: function() {
        return React.DOM.div(
            null,
            React.DOM.textarea({
                value: this.state.text,
                onChange: this._textChange,
            }),
            React.DOM.h3(null, this.state.text.length)
        );
    }
    
  • 存在如此的render(),我们就算是像如下一样更改props,在UI上也是不会有结果的
    myTextAreaCounter = ReactDOM.render(
        React.createElement(
            TextAreaCounter,
            { defaultValue: "Hello", // previously known as "Bob"
            }),
        document.getElementById("app")
    );
    
  • 注意!!我们不能像传统的oo语言一样理解这里的代码,虽然我们使用了ReactDOM.render 重新创建了一次myTextAreaCounter,但是这并不是两个ref(传统oo语言这就意味着是 两个reference了,而且前一个被遮盖了),因为都是和id为app的div相关联,我们这里其 实两次都是对同一个组件进行的设置,react会自动寻找两次的diff进行升级.
  • 我们可以发现this.props改动了,但是UI却没有
    myTextAreaCounter.props;        // Object { defaultValue="Hello" }
    
  • react当然知道这种情况,为了能够"让外部的props注入同时改动state",你必须设置如 下的函数
    componentWillReceiveProps: function(newProps) {
        this.setState({
            text: newProps.defaultValue,
        });
    },
    

Lifecycle Methods

  • 前面的componentWillReceiveProps其实就是react提供的一种叫做lifecycle的函数: 你可以使用lifecycle函数来监听'你的component的改动'
  • 其他的lifecycle函数还有:
    • componentWillUpdte(): 每当你的render()被调用(通常有两种情况,state改动或者 props改动)'之前'的时候会执行,类似rails里面的before filter函数
    • componentDidUpdate(): render()被调用之后, DOM被调用之前!类似rails里面的 after filter函数
    • componentWillMount(): node被加入到DOM之前
    • componentDidMount(): node被加入到DOM之后
    • componentWillUnmount(): component被"移除"DOM之前
    • shouldCompnentUpdate(newProps, newState):在componentWillUpdate()调用之前 你可以主动调用shouldCompnentUpdate(newProps, newState), 通过比较newProps和 this.prop, 比较newState和this.state,如果发现没有改变,可以直接返回false,阻 止这次update

Lifecycle Example: Log It All

  • 为了查看什么时候lifecycle函数会起作用,我们在component里面调用每个lifecycle 函数,并且每个函数里面都打上log,如下
    var TextAreaCounter = React.createClass({
    
        _log: function(methodName, args) {
            console.log(methodName, args);
        },
        componentWillUpdate:  function() {this._log('componentWillUpdate',  arguments);},
        componentDidUpdate:   function() {this._log('componentDidUpdate',   arguments);},
        componentWillMount:   function() {this._log('componentWillMount',   arguments);},
        componentDidMount:    function() {this._log('componentDidMount',    arguments);},
        componentWillUnmount: function() {this._log('componentWillUnmount', arguments);},
        // ...
    });
    
  • 好了,写入代码,刷新页面,我们会发现如下两个函数首先被调用了
    componentWillMount
    componentDidMount
    
  • 而这两个以componentDidMount更为重要,因为在它内部,可以通过如下语句获得fresh new的新创建DOM
    ReactDOM.findDOMNode(this)
    
  • 我们在textarea里面type一个s,这个时候被触发的是下面两个函数
    componentWillUpdate
    componentDidUpdate
    
  • 先被调用的是componentWillUpdate,它有两个参数nextProps, nextState.比如nextProps 就是prop的future expected value.你可以看看这个是不是你想要的.然后决定是不是 进行一些操作
  • 更加有趣的是componentDidUpdate,这个相当于是一个"事后诸葛":比如我们要限制用 户输入的字符串的长度.我们在_textChange()里面当然可以设置,但是这个设置却不是 "完全保险"的,因为我们前面讲过,outside world获得了react object的ref以后,可以 使用componentDidUpdate,这里等于是每次"升级之后"检查"本次升级"是否是合格的, 不合格的话,我们可以退回(所以参数是oldValue)
    componentDidUpdate: function(oldProps, oldState) {
        if (this.state.text.length > 3) {
            this.replaceState(oldState);
        }
    },
    
  • 这里使用的是replaceState,我们可以把它想象成是http中的PUT(也就是所有属性都替 换),而原来的setState是http中的Patch(只更新需要的)

Lifecycle Example: Use a Mixin

  • mixin这个概念在ruby里面也有,就是一系列的method和properties都设置在一个mixin 里面,然后你一旦引入这个mixin就等于引入了所有的函数和属性.所以我们这里先设计 一个mixin
    var logMixin = {
      _log: function(methodName, args) {
        console.log(this.name + '::' + methodName, args);
      },
      componentWillUpdate:  function() {this._log('componentWillUpdate',  arguments);},
      componentDidUpdate:   function() {this._log('componentDidUpdate',   arguments);},
      componentWillMount:   function() {this._log('componentWillMount',   arguments);},
      componentDidMount:    function() {this._log('componentDidMount',    arguments);},
      componentWillUnmount: function() {this._log('componentWillUnmount', arguments);},
    };
    
  • 当然了mixin在非react领域里面想使用,那就是放到一个for循环里面,把属性函数都加入到某个 object里面. react领域里面专门为mixin设计了一个快捷方法mixins,使用方法如下
    var MyComponent = React.createClass({
        mixins: [obj1, obj2, obj3],
        // ....
    };
    
  • 我们的使用logMixin的代码如下
    var TextAreaCounter = React.createClass({
        name: 'TextAreaCounter',
        mixins: [logMixin],
    });
    

Lifecycle Example: Using a Child Component

  • lifecycle里面的componentWillUnmount使用的范围,是当前component的child component被移除DOM的时候才会被触发的.所以我们要先设计一个child component
    var Counter = React.createClass({
        name: 'Counter',
        mixins: [logMixin],
        propTypes: {
            count: React.PropTypes.number.isRequired,
        },
        render: function() {
            return React.DOM.span(null, this.props.count);
        }
    });
    
  • 这个component没有自己的state,只是负责显示一下当前textarea的字符个数
  • 我们的render()会去判断当前的字符长度,根据不同的长度来决定是否创建一个child component来显示个数
    render: function() {
        var counter = null;
        if (this.state.text.length > 0) {
            counter = React.DOM.h3(
                null,
                React.createElement(Counter, {
                    count: this.state.text.length,
                })
            );
        }
    
        return React.DOM.div(
            null,
            React.DOM.textarea({
                value: this.state.text,
                onChange: this._textChange,
            }),
            counter
        );
    }
    
  • 当我们的textarea里面的字符串长度变成0的时候,child component就消失了.react diff以后,发现了某个component会unmount,就会触发这个component的child的函数 componentWillUnMount

Performance Win: Prevent Component Updates

  • shouldComponentUpdate(nextProps, nextState)这个lifecycle的method是和性能有 关的.它会在componentWillUpdate()之前被invoke,然后给你一个机会让你决定是不是 真的要去调用
  • 有一类的component再render()里面只使用this.props和this.state,并且不需要额外 的function call.这一类的component叫做pure component.
  • 对于这类component,我们可以在shouldComponentUpdate里面比较下前后,如果没有什 么改动的话,就直接return false,减少后面调用的消耗(不再需要调用render())
  • 还有一类component,连props和state都不使用,那么shouldComponentUpdate就直接返 回false
  • 下面我们来做一个新的实验,首先移除logMixin,只需要去记录component什么时候调用 render
    var Counter = React.createClass({
        name: 'Counter',
        propTypes: {
            count: React.PropTypes.number.isRequired,
        },
        render() {
            console.log(this.name + '::render()');
            return React.DOM.span(null, this.props.count);
        }
    });
    
  • TextAreaCounter也是一样的操作
    var TextAreaCouter = React.createClass({
        name: 'TextAreaCounter',
        // all other methods...
    
        render: function() {
            console.log(this.name + '::render()');
        }
    });
    
  • 这个时候,我们刷新页面,使用'LOL'来替代'BoB',然后我们会发现log如下
    // select all, paste the "LOL" string
    TextAreaCounter::render()
    Counter::render()
    
  • 也就是说,textarea的render首先被调用(这个是必须的,因为字符串换了么),然后 textarea的render又会在内部调用counter的render,所以会出现连个render都被调用 的情况
  • 但是静静分析以后发现,对counter的render()调用不是必须的:因为字符串长度没变嘛!
  • 这种情况下就是使用shouldComponentUpdate的情景啦,为counter设置这个函数,就也 保证parent的props.count不变的情况下,我们不需要更新(通过返回false,避免render() 的调用)
    shouldComponentUpdate: function(nextProps, nextState) {
        return nextProps.count != this.props.count;
    }
    
  • 结果就是如我们预期了
    // paste the string "LOL"
    TextAreaCounter::render()
    

PureRenderMixin

  • 上面这种逻辑:
    • 某个component只负责展示
    • 如果这个component其data没有改变的话,那么就不调用render
  • 这种'对比this.props和nextProps,同时对比this.state和nextState,发现没有改变就 不去更新render,直接在shouldComponentUpdate()里面返回false'的行为非常的常用 所以某些react addon把这个工作总结成了一个mixin叫做PureRenderMinxin,使用方法 如下,注意,这个不是react核心库里面的,需要引用react-with-addons.js
    <script src="react/build/react-with-addons.js"></script>
    <script src="react/build/react-dom.js"></script>
    <script>
    
      var Counter = React.createClass({
        name: 'Counter',
    
        mixins: [React.addons.PureRenderMixin],
    
        propTypes: {
          count: React.PropTypes.number.isRequired,
        },
    
        render: function() {
          console.log(this.name + '::render()');
          return React.DOM.span(null, this.props.count);
        }
      });
    

Chapter 3. Excel: A Fancy Table Component

  • 前面我们讲了如何创建自己的react component, 然后把自己的react component和 generic DOM component结合起来.
  • 然后就是如何设置properties, 如何管理state
  • 最后还讲了如何避免不必要的更新(调用render)操作
  • 这一章我们把我们的知识总结起来,创建一个更加有趣的component,一个微软Excel的v0.1 版本

Data First

  • table就是对数据的管理,我们的fancy table就得先包括一个数组的数据(同时还得有 一个数组的header).所以我们就总结了历史最佳畅销书,放到两个数组里面
    var headers = [
      "Book", "Author", "Language", "Published", "Sales"
    ];
    
    var data = [
      ["The Lord of the Rings", "J. R. R. Tolkien", "English", "1954-1955", "150 million"],
      ["Le Petit Prince (The Little Prince)", "Antoine de Saint-Exupéry", "French", "1943", "140 million"],
      ["Harry Potter and the Philosopher's Stone", "J. K. Rowling", "English", "1997", "107 million"],
      ["And Then There Were None", "Agatha Christie", "English", "1939", "100 million"],
      ["Dream of the Red Chamber", "Cao Xueqin", "Chinese", "1754-1791", "100 million"],
      ["The Hobbit", "J. R. R. Tolkien", "English", "1937", "100 million"],
      ["She: A History of Adventure", "H. Rider Haggard", "English", "1887", "100 million"],
    ];
    
  • 我们先来实现一个只带header的table
    var Excel = React.createClass({
        render: function() {
            return (
                React.DOM.table(
                    null,
                    React.DOM.thead(
                        null,
                        React.DOM.tr(
                            null,
                            this.props.headers.map(function(title) {
                                return React.DOM.th(null, title);
                            })
                        )
                    )
                )
            );
        }
    });
    
  • 这段代码里面有一个map函数,这个函数是ruby里面map的感觉:就是对某个数组进行重 新的"计算"然后返回一个新的数组,我们用ruby来演示一下
    headers.map {|title| React.DOM.th(title) }
    
  • 这种只用javascript的能力来构建UI的方法是react的魅力所在,我们不需要使用erb文 件了.
  • 这里需要说明一下, react里面允许你把最后一个参数作为数组传递,也可以分别传递 进去,这跟js的arguments有关,并不是要求的那么死. 如下两条完全相同
    // separate arguments
    React.DOM.ul(
        null,
        React.DOM.li(null, 'one'),
        React.DOM.li(null, 'two')
    );
    
    // array
    React.DOM.ul(
        null,
        [
            React.DOM.li(null, 'one'),
            React.DOM.li(null, 'two')
        ]
    );
    
  • 然后我们就可以把这个给展示出来了
    ReactDOM.render(
        React.createElement(Excel, {
            headers: headers
            initialData: data,
        }),
        document.getElementById("app")
    );
    

Debugging the Console Warning

  • 上面的展示会在console里面有warning
    react.js:18788 Warning: Each child in an array or iterator
    should have a unique "key" prop. Check the render method of
    `Constructor`.
    
  • 首先看这个warnning的'Check the render method of `Constructor`',这个Constructor 其实是指的我们的Excel,但是由于我们没给它名字,所以react不知道如何给予说明,我 们可以通过增加一个displayName,来让我们的react认识Excel. 注意在JSX里面是不需 要定义displayName的
    var Excel = React.createClass({
        displayName: 'Excel',
        render: function() {
        }
    };
    
  • 然后warnning就变成了
    react.js:18788 Warning: Each child in an array or iterator should
    have a unique "key" prop. Check the render method of `Excel`.
    
  • 然后我们再来看看,这个warning的意思,其实是说,我们在创建一个数组的component, 然后把这个数组都返回给react的ctor,react抱怨说,你给我的这个数组里面的每个 element我都需要track,所以请你给它们没人一个id,所以解决方案就是
    this.props.headers.map(function(title, idx) {
        return React.DOM.th({key: idx+10}, title);
    })
    
  • 这个就是react里面的超级map啦,用ruby来对比就是
    headers = ['one', 'two', 'three']
    
    p headers.each_with_index.map{|header, idx| [header, idx + 10]}
    
    ##################################################
    # <===================OUTPUT===================> #
    # [["one", 10], ["two", 11], ["three", 12]]      #
    ##################################################
    
  • 我的warning就没有了,然后table的数组成员都有了id, 这个id只有在数组内部唯一就可以啦
    <tr data-reactid=".0.0.0">
      <th data-reactid=".0.0.0.$10">Book</th>
      <th data-reactid=".0.0.0.$11">Author</th>
      <th data-reactid=".0.0.0.$12">Language</th>
      <th data-reactid=".0.0.0.$13">Published</th>
      <th data-reactid=".0.0.0.$14">Sales</th>
    </tr>
    
  • 下面我们来初始化table的content,和table的title不一样的是,我们的content是可能 变动的.可能变动的部分就不能使用props了,而要使用data:
    • 首先我们使用getInitialState来从props初始化data
      getInitialState: function() {
          return { data: this.props.initialData };
      },
      
    • 然后在原来的render function里面在thead平行的位置初始化tbody
      React.DOM.tbody(
          null,
          this.state.data.map( function(row, idx) {
              return (
                  React.DOM.tr({key: idx},
                               row.map( function(cell, idx) {
                                   return React.DOM.td({key: idx}, cell);
                               })
                  )
              );
          })
      )
      
    • 最后,为了"document"和validate的需要,我们写上propTypes
      propTypes: {
          headers: React.PropTypes.arrayOf(
              React.PropTypes.string
          ),
          initialData: React.PropTypes.arryOf(
              React.PropTypes.arrayOf(
                  React.PropTypes.string
              )
          ),
      },
      

Sorting

  • 多少次你在网上看到table的时候,都想把它们按照某列排序,幸运的是,在react里面, 这是非常容易做到的.因为你只需要sort你的data就可以了.然后所有的UI都会随之改 动,因为react发现了data的变化.自动调用render
  • 我们的把触发的位置设置到thead
    React.DOM.table(
        null,
        React.DOM.thead({onClick: this._sort},
                        // ...
    
  • 我们来实现我们的_sort:
    • 首先要知道安装哪一行来排序, 通过event targe来获取其cellIndex
      var column = e.target.cellIndex;
      
    • 我们更新的方法,不能是直接sort data,因为我们要使用setState(),所以我们要先 复制一遍state
      // copy the data
      var data = this.state.data.slice();
      
    • 下一步是js排序
      data.sort(function(a, b) {
          return a[column] > b[column] ? 1 : -1;
      });
      
    • 最后就是update啦
      this.setState({
          data: data,
      });
      

Sorting UI Cues

  • 我们下面来改进下我们的代码:
    • 显示出当前是按照哪一行进行的排序, 使用this.state.sortby
    • 可以按照逆序排序, 使用this.state.descending
  • 我们的初始化代码更改如下
    getInitialState: function() {
        return {
            data: this.props.initialData,
            sortby: null,
            descending: false,
        };
    },
    
  • 设置descending 变量来确认是使用当前的column,并且原来没有逆序
    var descending = this.state.sortby === column && !this.state.descending;
    
  • sort函数要改变了
    data.sort(function(a, b) {
        return descending
            ? (a[column] < b[column] ? 1 : -1)
        : (a[column] > b[column] ? 1: -1)
    });
    
  • 然后我们要在setState的时候,传入这些参数
    this.setState({
        data: data,
        sortby: column,
        descending: descending,
    });
    
  • 我们还要在我们的title里面加上一些提示,是上升还是下降的箭头来表示"排序安装当 前columen, 升还是降"
    this.props.headers.map(function(title, idex) {
        if (this.state.sortby === idx) {
            title += this.state.descending? '\u2191' : '\u2193'
        }
        return React.DOM.th({key:idex}, title);
    }, this)
    

Editing Data

  • 对Excel的下一步改造,就是让我们可以更改cell的content,可以:
    • 双击某个cell,然后text就变成input field了
    • 编辑这个input field
    • 点击回车,input field就消失了,只剩下更新好的新的text

Editable Cell

  • 首先建立一个event handler,来应对double-click:
    • 在tbody里面加上这个handler
      React.DOM.tbody({onDoubleClick: this._showEditor}, ....)
      
    • 然后我们来看看_showEditor的实现
      _showEditor: function(e) {
          this.setState(
              {edit: {
                  row: parseInt(e.target.dataset.row, 10),
                  cell: e.target.cellIndex,
              }});
      }
      
  • 我们的setState函数这次设置的是this.state的一个property: edit(注意这个不是 特殊property是需要我们自己设置的).
    • 这个edit在没有改动的时候,值是null
    • 这个edit在有改动的时候,会记住被改动的value的row和index,比如你点击了最左 上角,它的值就会是{row: 0, cell: 0}
    • 获取cell,也就是column的办法是分析e.target中的cellIndex. e.target也就是我 们双击的<td>
    • 获取row就比较麻烦,因为不是html里面有的值,所以我们要使用data-attribute,注 意:这里的data-attribute也不是什么内置的神器,而是我们在初始化table的时候, 给每个cell都初始化的值
      React.DOM.tbody({onDoubleClick: this._showEditor},
                      this.state.data.map(function(row, rowidx) {
                          return (
                              React.DOM.tr(
                                  {key: rowidx},
                                  row.map(function(cell, idx) {
                                      var content = cell;
                                      // TODO turn content into a input if 'idx'
                                      // and 'rowidx' match the one being edited
      
                                      return React.DOM.td({
                                          key: idx,
                                          'data-row': rowidx
                                      }, content);
                                  }, this)
                              )
                          );
                      }, this)
                     )
      
  • edit这个property并不是关键字,是需要我们自己去设置的,在getInitialState里面
    getInitialState: function() {
        return {
            data: this.pros.initialData,
            sortby: null,
            descending: false,
            edit: null,
        };
    }
    

Input Field Cell

  • 前面介绍data-attribute,的时候,我们把cell变成input的设置为TODO,我们这里就来 实现一下:
    • 首先获取parent的edit变量
      var edit = this.state.edit;
      
    • check变量edit是否被设置,而且设置的是不是我们当前的cell
      if (edit && edit.row === rowidx && edit.cell == idx) {
          // ...
      }
      
    • 如果经过了check,那么我们就把当前的cell变成input. 初始值通过defaultValue 设置,然后存储的时候,使用_save()函数
      content = React.DOM.form(
          {onSubmit: this._save},
          React.DOM.input({
              type: 'text',
              defaultValue: content,
          })
      );
      

Saving

  • 我们来实现下上面的_save()函数,按下Enter Key以后,会触发这个存储.首先我们要 禁止默认的存储行为(默认的存储行为会刷新页面)
    _save: function(e) {
        e.preventDefault();
        // ... do the save
    },
    
  • 之后,我们需要获取input field的ref
    var input = e.target.firstChild;
    
  • 拷贝里面的数据,我们要通过update,而非改动state的方式来更新数据
    var data = this.state.data.slice();
    
  • 更新过程是首先把数据放到一个变量里面
    data[this.state.edit.row][this.state.edit.cell] = input.value;
    
  • 使用setState更新
    this.setState(
      {
        edit: null,                  # done editing
        data: data,
      });
    

Conclusion and Virtual DOM diffs

  • 我们来总结下我们实现editing的过程:
    • 通过this.state.edit来记录当前更新的是哪个cell
    • 如果发现当前cell的坐标和edit里面设置的一样,那么就render一个input field
    • 使用input field里面的值来更新table content
  • 每当你使用setState()来设置新的data的时候, React都会自动调用component的render 函数, UI就会自动的更新!
  • React的牛逼之处在于它并不是render整个table,它`只`update一个cell!
  • React之所以可以做到这一点,是因为它创建了一种叫做virtual DOM的东西,每次render 函数被调用的时候, React会做一个virtual dom的diff.根据这个diff, React会计算 出更新所需要的最小的DOM opertion,然后把这个operation实施在真正的DOM上

Search

  • 下面我们来添加对Excel component的搜索功能,可以让用户来filter table的内容:
    • 首先要增加一个button来开启或者关闭搜索
    • 如果开启,那么搜索是以一个列为搜索单位的
    • 用户输入搜索字符串以后,只显示和这个搜索相关的内容

State and UI

  • 我们首先要把是否搜索开启设计成一个变量,存在getInitialState里面
    getInitialtate: function() {
        return {
            data: this.props.initialData,
            sortby: null,
            descending: false,
            edit: null,
            search: false,
        };
    },
    
  • render()的过程不免就会复杂起来了,我们为了方便理解,把render()函数分成两个部 分: 一部分是render table,另一部分是render toolbar
    render: function() {
        return (
            React.DOM.div(null,
                          this._renderToolbar(),
                          this._renderTable()
                         )
        );
    },
    
    
    _renderToolbar: function() {
        // TODO
    },
    
    _renderTable: function() {
        // TODO
    },
    
  • 也可以这么理解,新的render函数会返回两个<div> 一个是toolbar一个是table:
    • toolbar很简单,就是一个按钮
      _renderToolbar: function() {
          return React.DOM.button(
              {
                  onClick: this._toggleSearch,
                  className: 'toolbar',
              },
              'search'
          );
      },
      
    • table不是那么简单,要考虑search功能是否开启,来决定table的样子
      _renderSearch: function() {
          if (!this.state.search) {
              return null;
          }
      
          return (
              React.DOM.tr({onChange: this._search},
                           this.props.headers.map(function(_ignore, idx) {
                               return React.DOM.td( {key: idx},
                                                    React.DOM.input({
                                                        type: 'text',
                                                        'data-idx': idx,
                           })));
      },
      

Filtering Content

  • 我们的search,其实就是利用js里面的Array.proptype.filter()来过滤关键字,只返 回我们需要的array
  • 你需要把数组的成员存储一下,否则你会丢失数据的.我们存储的数据还可以让用户在 清除input以后,获得原来完整的输入
  • 当我们点击search button的时候,我们会触发_toggleSearch()函数,这个函数要做下 面的几件事情:
    • 设置this.state.search为true或者false
    • 当设置了search为true的时候,要"remembering" old data
    • 当设置了search为false的时候,要把old data revert
  • 全部代码如下
    _toggleSearch: function() {
        if (this.state.search) {
            this.setState({
                data: this._preSearchData,
                search: false,
            });
            this._preSearchDate = null;
        } else {
            this._preSearchDate = this.state.date;
            this.setState ({
                search: true,
            });
        }
    },
    
  • 最后就是实现_search()函数了,这个函数会在每次搜索input有改动的时候被调用,代码如下
    _search: function(e) {
        var needle = e.target.value.toLowerCase();
        if (!needle) {
            this.setState({data: this._preSearchData});
            return;
        }
    
        var idx = e.target.dataset.idx;
        var searchdta = this._preSearchData.filter(function(row) {
            return row[idx].toString().toLowerCase().indexOf(needl) > -1;
        });
        this.setState({data: searchdata});
    },
    
  • TODO

Chapter 4. JSX

  • 目前为止,我们学的都是使用如下两种方法创建component:
    • React.DOM.*
    • React.createElement()
  • 上面两种使用function创建component的缺点是括号太多,不容易对齐,react对这个问题 给出的解决方案是jsx

Hello JSX

  • 我们来回顾下第一章的Hello World的例子,使用function的方法创建如下
    <script src="react/build/react.js"></script>
    <script src="react/build/react-dom.js"></script>
    <script>
      ReactDOM.render(
        React.DOM.h1(
          {id: "my-heading"},
          React.DOM.span(null,
            React.DOM.em(null, "Hell"),
            "o"
          ),
          " world!"
        ),
        document.getElementById('app')
      );
    </script>
    
  • 而使用JSX的样子如下,变得非常的简洁
    ReactDOM.render(
    <h1 id="myheading">
        <span><em>Hell</em>o</span> world!
    </h1>,
    document.getElementById('app')
    );
    
  • 样子非常像html,所以很容易理解.但是问题在于jsx其实是一种wrapper,浏览器并不理 解这种格式,我们需要一种transform来把jsx转化成浏览器理解的pure js

Transpiling JSX

  • 所谓的transpilation就是把source code转换成如下的js代码,新的的js代码:
    • 和老的js在效果上要保证完全一致
    • 必须要被老的浏览器都能理解,所以不能使用ES6等特性
  • 这里我们要和polyfill区别一下,所谓polyfill是通过自己的努力让"某些"ES5 的特性能够工作在只支持ES3的浏览器里面,比如
    if (!Array.prototype.map) {
        Array.prototype.map = function() {
            // implement the method
        };
    }
    
    // usage
    typeof [].map === 'function';   // true
    
  • polyfill是一种纯的js解决方案(不会引入类似jsx这种新的技术),这个方案有其局限 性:只能向老的js里面增加函数,并不能做更多的事情:比如增加class关键字
  • 而transpiling则是另外一番景象,你可以放心的使用最新的规范(ES6),因为你知道最 后还会有一个代码翻译的过程,最终出来的代码是所有浏览器都可以理解的,最老规范 的版本

Babel

  • Babel是一个开源的js transpiler.支持把jsx转成老的浏览器都理解的js
  • 后面我们会介绍当下流行的build process,把React发布成可以使用的product级别的 代码,Babel在其中起了重要的作用
  • 我们当前先只把这个transpiler过程限定在client端,注意这只是为了理解代码,工业 级的代码并不这么写
  • 在0.14版本之前,react自带了一个transpiler的工具,但是现在已经默认使用Babel了

Client Side

  • 之前我们的代码中,都要include两个react js
    <script src="react/build/react.js"></script>
    <script src="react/build/react-dom.js"></script>
    
  • 而你现在需要加上babel js的支持
    <script src="react/build/react.js"></script>
    <script src="react/build/react-dom.js"></script>
    <script src="babel/browser.js"></script>
    
  • 为了让babel/browser.js能够找到jsx,你必须在script tag里面进行标注,这样 babel/brower.js才能找到需要自己的区域
    <script type="text/babel">
      ReactDOM.render(/*...*/)
    </script>
    

Javascript in JSX

  • 当我们创建UI的时候,总避免不了使用变量,循环,逻辑判断.一般来说,在rails里面,我 总是会在erb文件里面,使用ruby的语法来使用变量,循环,逻辑判断.
  • react当然不能引入任何另外的script语言,它就只用js来做"变量,循环,逻辑判断",当 然了你需要在jsx文件里面,使用{}来包裹你的js代码,下面就是前面一章Excel Component 使用jsx语法后的代码.
  • 注意,是在html里面需要用"变量,循环,逻辑判断"才用{}, 外面的代码本身是js的就不 用{}了
    var Excel = React.createClass({
    displayName: 'Excel',
    
    
    render: function() {
    var state = this.state;
    return (
    <table>
        <thead onClick={this._sort}>
            <tr>{
                this.props.headers.map(function(title, idx) {
                if (state.sortby === idx) {
                title += state.descending ? ' \u2191' : ' \u2193'
                }
                return <th key={idx}>{title}</th>;
                })
                }</tr>
        </thead>
        <tbody>
            {
            this.state.data.map(function(row, idx) {
            return (
            <tr key={idx}>{
                row.map(function(cell, idx) {
                return <td key={idx}>{cell}</td>;
                })
                }</tr>
            );
            })
            }
        </tbody>
    </table>
    );}
    })
    
  • 一个典型的应用就是在th里面使用{}引入title
    <th key={idx}> {title} </th>
    

Whitespce in JSX

  • 在jsx里面,空格的转换有些特点:
    • 和传统HTML一样的地方: 多个空格会被转换成一个空格
    • 但是回车会被"吞噬"掉

Comments in JSX

  • 不要使用single-line comment
  • 全部使用multiline comment,如下
    <h1>
      { /* multiline comment */ }
    </h1>
    

HTML Entities

  • 你可以在JSX里面如下使用HTML entities. 这就和普通的HTML没有任何的区别
    <h2>
      More info &raquo;
    </h2>
    
  • 你也可以通过加上{}把这段代码"解析成JS". 这就不是普通的HTML啦,字符串也不会解 析成特殊的格式. 如果你想解析成特殊格式,那么就请使用unocode.比如在这里就是使 用\u00bb来代替&raquo;
    <h2>
      { "More info &raquo;"}
    </h2>
    

Anti-XSS

  • 为了防止XSS攻击,react会把所有的字符串转义,也就是说类似"&raquo"这样的代码, 是无论如何都不会起到作用的,还是使用unicode的吧.

Spread Attributes

  • jsx反正要进行transpiler的,索性就借鉴了更多的最新js语法,比如ECMAScript6里面 的spread operator,这个特性对于定义properties非常有用
  • 比如你有一个js的attribute
    var attr = {
        href: 'http://example.org',
        target: '_blank',
    };
    
  • 你有两种方法来把这些attribute都赋值给一个<a> component:
    • 原来老的办法,当然是一个一个的赋值,有很多多余的代码,attribute越多,多余代码越多
      return (
      <a
          href={attr.href}
          target={attr.target}>
      </a>
      );
      
    • spread attribute引入的办法,非常简洁
      return <a {...attr} > Hello </a>
      

Parent-to-child Spread Attributes

  • spread attribute更重要的使用方法是从parent向child传递attribute:
    • 比如你有一个FancyLink commponent, 这个component有很多<a>类似的attribute 下面是别人使用你的component的例子
      <FancyLink
         href="http://example.org"
         style={ { color: "red" }}
         target="_blank"
         size="medium">
        Hello
      </FancyLink>
      
    • 你创建这个component的方法如下:this.props是从parent送来的attribute,我们要 一股脑的放到<a>里面
      var FancyLink = React.createClass({
          render: function() {
              // ...
              return <a {...this.props}> {this.props.children}</a>
          }
      });
      
  • 注意上面例子里面this.props.children的用法,"Hello"通过<FancyLink…>Hello </Francy>的办法引入,被付给this.props.children, 而调用的时候直接使用就可以 了.

Returning Multiple Nodes in JSX

  • 我们的render()函数每次只能返回一个html node,换句话说下面的代码是错误的
    var Example = React.createClass({
        render: function() {
            return (
                <span>
                    Hello
                </span>
                    <span>
                    World
                </span>
            );
        }
    });
    
  • 最简单的改动方法,是把它们放到一个div里面
    var Example = React.createClass({
        render: function() {
            return (
                <div>
                <span>
                    Hello
                </span>
                    <span>
                    World
                </span>
                </div>
            );
        }
    });
    
  • 注意,我们只是render()不能返回多个node,我们完完全全还可以在js代码里面自己创 建复杂的一个array,然后把这个数组render()出去.要注意两点:
    • 数组每个成员必须有key
    • 字符串(空或者不空)都不需要一个key
  • 下面就是一个返回array的例子
    var Example = React.createClass({
    render: function() {
        var greeting = [
            <span key="greet">Hello</span>,
            ' ',
            <span key="world"> World</span>
            '!'
        ];
    
        return (
            <div>
                {greeting}
            </div>
        );
        }
    });
    
  • 如果我们在<Example>`children`</Example>里面把`children`也设置成array,也可以 达到上面一样的效果
    var Example = React.createClass({
      render: function() {
        console.log(this.props.children.length); // 4
        return (
          <div>
            {this.props.children}
          </div>
        );
      }
    });
    
    ReactDOM.render(
      <Example>
        <span key="greet">Hello</span>
        {' '}
        <span key="world">World</span>
        !
      </Example>,
      document.getElementById('app')
    );
    

JSX Versus HTML Differences

  • JSX的特点是看起来像HTML,但是使用js包裹(通过{})的方法来实现`变量,循环,逻辑判断`
  • 共同点总是容易找到但是不同点却会容易引起误会,下面介绍几个不同点

No class, What for?

  • class和for都是ECMAScript里面的关键字,所以在jsx里面要使用className和htmlFor 来代替:
    • 错误版本
      var em = <em class="important" />;
      var label = <label for="thatInput" />;
      
    • 正确版本
      var em = <em className="important" />;
      var label = <label htmlFor="thatInput" />;
      

Style Is an Object

  • style不是是接受一个字符串,而是接受一个object, css的property不能使用dash-delimited, 而要使用camelCase:
    • 错误版本
      var em = <em style="font-Size: 2em; line-height: 1.6"/>;
      
    • 正确版本
      var styles = {
          fontSize: '2em',
          lineHeight: '1.6'
      };
      
      var em = <em style={style} />;
      
    • 高阶版本,有两个{},一个是为js的object,另外一个是为了jsx的js代码解析
      var em = <em style = { {fontSize: '2em', lineHeight: '1.6'} } />;
      

Closing Tags

  • 在HTML里面一些tag可以不关闭, JSX既然是JS Xml的简称,那么tag就必须关闭啦
    • 错误版本:
      var list = <ul><li>item</ul>
      
    • 正确版本
      var list = <ul><li>item</li></ul>
      

camelCase Attributes

  • JSX里面所有的attribute都是camelCase的:
    • 错误版本
      var a = <a onclick="reticulateSplines()" />;
      
    • 正确版本
      var a = <a onClick={reticulateSplines} />;
      
  • 例外是data-和aria-开头的attribute

JSX and Forms

  • html的form和jsx的form有很多不同,值得我们认真的写一下

onChange Handler

  • 在人们和form打交道的时候,人们关注的,是你输入字符的情况下,form的反应.换句话 说,我们和form交流的方式是form value的改动
  • 在react的世界里面,你使用onChange attribute来订阅这些改动,然后可以施加某些 event handler
  • 而其他所有的老的html里面的监听鼠标键盘等的事件,统统不需要关注了!!!

value Versus defaultValue

  • 在HTML里面,如果你给<input>提供了一个value attribute,这个attribute的行为是 割裂的!是HTML设计的缺陷,比如<input id="i" value="hello"/>你的在输入框里面 输入bye,那么就有
    i.value;                        // "bye"
    i.getAttribute('value');        // "hello"
    
  • 在react里面肯定不会有这种割裂的bug存在了,如果你想要找到原来默认的值,那么你 需要的是defaultValue这个属性

<textarea> Value

  • react里面的textarea和input有着一样的使用方法:
    • value是最新的内容
    • defaultValue是默认值
  • 注意HTML里面,你要在<textarea></textarea>里面来设置文本内容(因为html的属性 不能有换行符),但是jsx是javascript,它不会有这方便的顾虑,所以下面的代码是完 全可行的
    React.render(
        <textarea defaultValue="hello\nworld" />,
        document.getElementById('app1')
    );
    
  • 注意上面这个例子中的"hello\nworld"会被转义,因为它是raw string,如果你不想被 转义那么要加上个{}
    React.render(
        <textarea defaultValue={"hello\nworld"} />,
        document.getElementById('app2')
    );
    

<select> Value

  • 在HTML里面,select是必须使用option selected来表达当前谁被选中了
    <!-- old school HTML -->
    <select>
      <option value="stay"> Should I stay</option>
      <option value="move" selected> or should I go</option>
      <option value="trouble"> If I stay it will be trouble</option>
    </select>
    
  • react里面使用defaultValue,更加的reasonable
    <select defaultValue="move">
      <option value="stay"> Should I stay</option>
      <option value="move"> or should I go</option>
      <option value="trouble"> If I stay it will be trouble</option>
    </select>
    
  • 多选就是defaultValue是一个数组
    <select defaultValue={["stay", "move"]} multiple={true}>
      <option value="stay"> Should I stay</option>
      <option value="move"> or should I go</option>
      <option value="trouble"> If I stay it will be trouble</option>
    </select>
    

Excel Component in JSX

  • 使用jsx来实现excel的例子

Chapter 5. Setting Up for App Development

  • 这一章主要介绍公司开发的流程中,如何搭建SinglePageApplicatoin的开发环境
  • 其目的是使用最新的ES特性,但是通过transpiler来转换成老的浏览器也支持的js代码

Boilerplate App

  • 我们来创建一个boilerplate配置模板,其特点是支持JSX,以及所有已知的最新技术:
    • ES5
    • ES6
    • ES7

Files and Folders

  • 我们首先要有一个组织架构,大概如下
    .
    |-- css
    |-- images
    |-- js
    |   |-- build
    |   `-- source
    `-- scripts
    
    6 directories, 0 files
    
  • js文件夹被分成了build和souce,其实就是source里面是最新技术的js版本,被transpiler 以后,放到build文件夹里面
  • scripts是bash的一些常用脚步
  • 我们下面进一步细化css和js/souce,在这两个文件夹下面都加上一个components文件 夹,这个文件夹里面存储我们react最重要的组成部分component
  • component的js和css的文件名最好首字母大写(和react class名字一致)
  • 其他的不和react有关系的js和css文件就放到css和js/source里面,名字可以起的统 一一点就叫做app.js和app.css
  • 当然还要有承载所有文件的index.html
  • 现在,我们的结构如下
    .
    |-- css
    |   |-- app.css
    |   `-- components
    |       `-- Logo.css
    |-- images
    |-- index.html
    |-- js
    |   |-- build
    |   `-- source
    |       |-- app.js
    |       `-- components
    |           `-- Logo.js
    `-- scripts
    
    8 directories, 5 files
    

index.html

  • 我们的index.html承载了很多东西:
    • 所有的css文件都要被index包括,包括的方法是把所有的css文件整合在一起在一个 bundle.css文件里面
    • 同样的,所有的js文件整合在bundle.js里面,注意js的整合要比css麻烦,因为它需 要include dependency
    • index里面要有<div id="app">来放置我们的应用
  • 代码如下,注意,我们只引用了一个css文件和一个js文件
    <!DOCTYPE html>
    <html>
        <head>
            <title>App</title>
            <meta charset="utf-8">
            <link rel="stylesheet" type="text/css" href="bundle.css">
        </head>
        <body>
            <div id="app"></div>
            <script src="bundle.js"></script>
        </body>
    </html>
    

CSS

  • /css/app.css会包括全局的一些配置
    html {
      background: white;
      font: 16px Arial;
    }
    
  • /css/components/Logo.css就包括某个component的css配置啦
    .Logo {
      background-image: url('../../images/react-logo.svg');
      background-size: cover;
      display: inline-block;
      height: 50px;
      vertical-align: middle;
      width: 50px;
    }
    

Javascript

  • js/source/app.js是全局的js代码,是设置react componet在html里面布局的代码
    'use strict';
    
    import React from 'react';
    import ReactDOM from 'react-dom';
    import Logo from './components/Logo';
    
    ReactDOM.render(
      <h1>
        <Logo /> Welcome to The App!
      </h1>,
      document.getElementById('app')
    );
    
  • js/source/componenets/Logo.js是某个react component的代码
    import React from 'react';
    
    class Logo extends React.Component {
      render() {
        return <div className="Logo"/>;
      }
    }
    
    export default Logo
    

Javascript: Modernized

  • nodejs引入javascript的一个大的概念就是package,我们不能再使用React或者 ReactDOM作为我们的global variable就不再适合了,这个时候,你就需要module了, nodejs就提供了这个功能
  • nodejs最开始的时候,是使用CommonJS来作为我们的module,这是一种纯的js实现(因 为当时不能使用es标准来实现这件事情,话说回来是有了nodejs之后,es新标准才更快 的推广的,这个是先有鸡还是先有蛋的问题),CommonJS的方法是我们创建了一个object, 然后使用module.exports的方法来导出这个module
    var Logo = React.createClass({  /* ... */ });
    
    module.exports = Logo;
    
  • 需要注意的是,上面的代码用到了React,React已经不是全局变量了,它需要从其他地 方include来
    var React = require('react');
    
  • 总结起来就是:
    • declare requirement up top
    • export at the bottom
    • implement the "meat" between
  • ECMAScript后来再进一步,继承了上面的"三段式"但是使用更加先进的格式来declare 和export(因为ECMAScript是标准组织,它可以引入新的syntax, 当然我们要使用最新 的啦,因为肯定更先进,Babel会帮助我们转换成老的js的):
    • 导入使用import
      import React from 'react';
      
    • 导出使用export,注意最后没有分号
      export default Logo
      
  • 我们甚至不再需要原来的React.creatClass了,因为ECMAScript引入了class的概念
    class Logo extends React.Component {/*...*/}
    
  • 最新的class和原来的使用object作为class是有些不同的:
    • 新的class内部只能有function, 定义property需要定义在this里面
    • 不再需要render: function() {}, 只需要直接定义函数render()就可以了
    • 函数之间也不再需要','分割
  • 学习了新的格式,我们来看看各部分js代码:
    • app.js的代码如下
      'use strict';
      
      import React from 'react';
      import ReactDOM from 'react-dom';
      import Logo from './components/Logo';
      
      ReactDOM.render(
        <h1>
          <Logo /> Welcome to The App!
        </h1>,
        document.getElementById('app')
      );
      
    • Logo.js的代码如下
      import React from 'react';
      
      class Logo extends React.Component {
        render() {
          return <div className="Logo"/>;
        }
      }
      
      export default Logo
      

Install Prerequisites

  • 即便有了上面的代码,你还是不可能马上执行的,原因很简单:index.html里面的bundle.css 和bundle.js都并没有,所以你还得:
    • 创建bundle.css这个很简单只需要把多个css文件合并成一个就可以
    • 创建bundle.js这个有点麻烦,我们需要同时把所有的dependency都引入到我们的bundle.js 代码,引入依赖并且合并成一个文件的过程要通过一个叫做Browserify的nodejs插件来完成
    • 我们的代码还要transpiler, 这个过程需要Babel这个nodejs插件来完成

Node.js

  • 首先需要安装node.js,安装后打印下面的来判断安装是否成功
    npm --version
    

Browersify

  • 使用如下方法安装browersify,注意是安装到global的npm library
    npm install --global browersify
    

Babel

  • Babel因为要使用其命令行,所以其命令行也要安装到global的npm library
    npm install --global babel-cli
    

React, etc.

  • 剩下的我们还需要的library有:
    • react这个不用说了
    • react-dom, 从react分出来的
    • babel-preset-react,Babel对于JSX和React的支持
    • babel-preset-es2015, Babel对于最新ES规范的支持
  • 这些library不需要使用command-line的binary,所以安装到local就可以了
    npm install --save-dev react
    npm install --save-dev react-dom
    npm install --save-dev babel-preset-react
    npm install --save-dev babel-preset-es2015
    
  • 注意这里的–save-dev, 它和–save的区别就是:
    • –save-dev会把这个安装到devDependencies下面, 这个是我们开发模式下需要的, 不会被部署到服务器
    • –save会把这个安装到dependencies下面, 这个是我们生产环境需要的,会被部署 到服务器

Let's Build

  • 我们的build分成了如下几个部分:
    • transpiler js, 使用babel cli:
      babel --presets react,es2015 js/source -d js/build
      
    • package js,会从一个app.js出发,把所有的dependency都导入到一个js文件里面
      browserify js/build/app.js -o build.js
      
    • package css, 把所有的css文件合起来就好,再把'../../images'替换成'images'
      cat css/*/* css/*.css | sed 's/..\/..\/images/images/g' > bundle.css
      
  • 注意,我们的代码都会在root目录下面生成bundle.css和bundle.js两个文件,这也是我 们index.html里面写到的,但是正式工程可能会写入到一个out文件夹之类的
  • 每改动一个文件,就调用一遍上面的script是有点麻烦,型号我们可以使用nodejs听的watch package:
    • 首先安装这个package,需要命令行,所以
      npm install -g watch
      
    • 然后调用watch 使用某个script,同时检测某些文件夹,比如这里的js/sources和css
      watch "sh scripts/build.sh" js/sources css
      
    • build.sh的内容如下
      # js transform
      babel --presets react,es2015 js/source/ -d js/build
      # js package
      browserify js/build/app.js -o bundle.js
      # css package
      cat css/*/* css/*.css | sed 's/..\/..\/images/images/g' > bundle.css
      # done
      date; echo;
      

Deployment

  • 我们已经做了很多工作,部署就是很简单了,只需要把前面的工作稍微改动一下:
    • 创建一个deploy的文件夹
    • 把css和js文件build出来,然后做一下minification(开发的时候不需要,部署的时候 越小越好么)
    • 把html文件和image文件都拷贝到deploy文件夹下面
  • 部署的script如下
    # cleanup last version
    rm -rf __deployme
    mkdir __deployme
    
    # build
    sh scripts/build.sh
    
    # minify js
    uglify -s bundle.js -o __deployme/bundle.js
    # minify css
    cssshrink bundle.css > __deployme/bundle.css
    # copy html and images
    cp index.html __deployme/index.html
    cp -r images/ __deployme/images/
    
    # done
    date; echo;
    

Chapter 6. Building an App

  • 本章开始设计一个小的例子,创建一个评价葡萄酒的平台,叫做winepad

Whinepad v.0.0.1

  • 这个例子很简单,那就是把前面的Excel object全部都放入到app.js里面就可以了代码 大概是这个样子(用到了localStorage,可以暂时把它理解成大小为5MB的cookie)
    var headers = localStorage.getItem('headers');
    var data = localStorage.getItem('data');
    
    if (!headers) {
    headers = ['Title', 'Year', 'Rating', 'Comments'];
    data = [['Test', '2015', '3', 'meh']];
    }
    
    ReactDOM.render(
    <div>
        <h1>
            <Logo /> Welcome to Whinepad!
        </h1>
        <Excel headers={headers} initialData={data} />
    </div>,
    document.getElementById('pad')
    );
    

The Components

  • 重新使用Excel是一个非常好的开始,但是使用完以后,你会发现Excel整体作为一个component 实在是太大了,可以把Excel分成几个更小的component,然后可以发挥更大的作用
  • 除了Excel可以从自己内部分拆出来的component,我们还需要类似亚马逊的评分系统: 使用start来评分
  • 在React这种特别重视component的系统中,我们需要建立一个component测试系统,让一 个component能在比较"孤立"的状态下进行创作, 下面就是创建步骤

Setup

  • 首先把加入Excel成功的代码重新命名成whinepad

Discover

  • 复制index.html到discovery.html,我们的discovery.html是用来研究新component的 所以它不需要include太多js,我们这里只需要让它include特点的js,也就是discover-bundle.js
    <!DOCTYPE html>
    <html>
      <head>
        <title>Whinepad</title>
        <meta charset="utf-8">
        <link rel="stylesheet" type="text/css" href="bundle.css">
      </head>
      <body>
        <div id="pad"></div>
        <script src="discover-bundle.js"></script>
      </body>
    </html>
    
  • 我们还需要在script里面创建这个discover-bundel.js
    browserify js/build/discover.js -o discover-bundle.js
    
  • 我们discover.js里面也要有些代码,就把Logo先放进去
    import React from 'react';
    import ReactDOM from 'react-dom';
    import Logo from './components/Logo';
    
    
    ReactDOM.render(
      <div style={{padding: '20px'}}>
        <h1>Component discoverer</h1>
    
        <h2>Logo</h2>
        <div style={{display: 'inline-block', background: 'purple'}}><Logo/></div>
    
    
      </div>,
      document.getElementById('pad')
    );
    

Button Component

  • 先说button,是因为这是一个很简单的component,我们只准备加一个<a>到这个button 里面,所以其使用接口应该是.注意代码里面的()=> alert('ouch')是ES2015的arrow function
    import Button from './components/Button';
    
        <h2>Buttons</h2>
        <div> Button with onClick: <Button onClick={ () => alert('ouch')}> Click me </Button></div>
        <div> A link: <Button href="https://reactjs.com">Follow me</Button></div>
        <div> Custom clsss name: <Button className="custom"> I do nothing</Button></div>
    
  • css部分要放在/css/components/Button.css, 需要我们的,要能用的上,必须要求我 们的html 元素的className(本来是class)里面至少有一个Button
    .Button {
      background-color: #6f001b;
      border-radius: 28px;
      border: 0;
      box-shadow: 0px 1px 1px #d9d9d9;
      color: #fff;
      cursor: pointer;
      display: inline-block;
      font-size: 18px;
      font-weight: bold;
      padding: 5px 15px;
      text-decoration: none;
      transition-duration: 0.1s;
      transition-property: transform;
    }
    
    .Button:hover {
      transform: scale(1.1);
    }
    
  • 好了,我们要实现我们的button.js,注意,要让我们的Button至少有个className叫做 Button才能正常的使用我们的css,所以我们引入了一个叫做classNames的npm package 它所做的就是把parent通过props传递来的className加上我们必须拥有的'Button'返 回一个字符串给新创建的react component
    const cssclasses = classNames('Button', props.className);
    
  • 我们来看看整个Button.js的代码
    import classNames from 'classnames';
    import React, {PropTypes} from 'react';
    
    function Button(props) {
        const cssclasses = className('Button', props.className);
        return props.href
            ? <a {...props} className={cssclasses} />
            : <button {...props} className={cssclasses} />;
    }
    
    Button.propTypes = {
        href: PropTypes.string,
    };
    
    export default Button
    
  • 这段代码好多地方也非常难以理解, 比如Destructuring assignment,也就是这一句
    import React, {PropTypes} from 'react';
    
  • 其实是如下两句的简写
    import React from 'react';
    const PropTypes = React.PropTypes;
    
  • 下面是函数式的时间了,如果你的component比较小,你可以不用维护它的state,那么 你可以使用一个function来解决, 前面说过的()=>{}就起作用了
    const Button = props => {
        // ...
    };
    
  • 我们的代码如果使用了ternary operator的话,那么就是one-line的,连{}都可以省略
    const Button = props =>
      props.href
       ? <a {...props} className={classNames('Button', props.className)} />
       : <button {...props} className={classNames('Button', props.className)} />
    
  • 前面我们定义propTypes都是定义在object体内的
    var PropTypes = React.PropTypes;
    
    var Button = React.createClass({
        propTypes: {
            href: PropTypes.string
        },
        render: function() {
            /* render */
        }
    });
    
  • 而新的class内部是只有function的,所以要在外部设置static的成员property
    import React, {Component, PropTypes} from 'react';
    
    class Button extends Component {
        render() {
            /* render */
        }
    }
    
    Button.propTypes = {
        href: PropTypes.string,
    };
    

Forms

  • 我们下面创建自己的<FormInput> component,这个FormInput component可以根据type 的不同来决定"返回"怎样的更generic的component,比如<Suggest> input或者是<Rating> input

<Suggest>

  • 在输入的时候,自动补全用户的输入是一种互联网常见的技术,我们这里不去考量后台 数据,所以我们的suggest选择是有限的,通过一个数组传入
    <h2>Suggest</h2>
    <div><Suggest options={['eenie', 'meenie', 'miney', 'mo']} /></div>
    
  • 然后我们来实现这个js文件:js/source/components/Suggest.js
    import React, {Component, PropTypes} from 'react';
    
    class Suggest extends Component {
        getValue() {
            return this.refs.lowlevelinput.value;
        }
    
        render() {
            const randomid = Math.random().toString(16).substring(2);
            return (
                    <div>
                    <input list={randomid}
                           defaultValue={this.prop.defaultValue}
                           ref="lowlevelinput"
                           id={this.props.id} />
                    <datalist id={randomid}> {
                        this.props.options.map((item, idx) =>
                        <option value={item} key={idx} />
                        )
                        }
                    </datalist>
                    </div>
            );
        }
    }
    
    Suggest.propTyes = {
        options: PropTypes.arrayOf(PropTypes.tring),
    };
    
    export default Suggest
    
  • 我们看到这个<Suggest>也没有什么不同的,不过是对<input>和<datalist>的一个包装

<Rating> Component

  • 打分又是一个经常应用的component,我们对<Rating>的使用方法如下
    <h2>Rating</h2>
    <div>No initial value: <Rating /></div>
    <div>Initial Value 4: <Rating defaultValue={4} /></div>
    <div>This one goes to 11 <Rating max={11} /></div>
    <div>Read-only: <Rating readonly={true} detaulValue={3} /></div>
    
  • 好了,我们来看看实现的主体框架方法
    import classNames from 'classnames';
    import React, {Component, PropTypes} from 'react';
    
    class Rating extends Component {
        constructor(props) {
            super(props);
            this.state = {
                rating: props.defaultValue,
                tmpRating: props.defaultValue,
            };
            // More methods
        }
    
        Rating.propTypes = {
            defaultValue: PropTypes.number,
            readonly: PropTypes.bool,
            max: PropTypes.number,
        };
    
        Rating.defaultProps = {
            defaultValue: 0,
            max: 5,
        };
    
        export default Rating
    }
    
  • 我们需要一些helper函数
    getValue() {
        return this.state.rating;
    }
    
    setTemp(rating) {
        this.setState({tmpRating: rating});
    }
    
    setRating(rating) {
        this.setState({
            tmpRating: rating,
            rating: rating,
        });
    }
    
    reset() {
        this.setTemp(this.state.rating);
    }
    
    componentWillReceiveProps(nextProps) {
        this.setRating(nextProps.defaultValue);
    }
    
  • 最后是我们的render()函数的实现
    render() {
      const stars = [];
      for (let i = 1; i <= this.props.max; i++) {
        stars.push(
          <span
            className={i <= this.state.tmpRating ? 'RatingOn' : null}
            key={i}
            onClick={!this.props.readonly && this.setRating.bind(this, i)}
            onMouseOver={!this.props.readonly && this.setTemp.bind(this, i)}
          >
            &#9734;
          </span>);
      }
      return (
        <div
          className={classNames({
            'Rating': true,
            'RatingReadonly': this.props.readonly,
          })}
          onMouseOut={this.reset.bind(this)}
        >
          {stars}
          {this.props.readonly || !this.props.id
            ? null
            : <input
                type="hidden"
                id={this.props.id}
                value={this.state.rating} />
          }
        </div>
      );
    }
    
  • TODO

Chapter 7. Lint, Flow, Test, Repeat

  • 下一章会介绍Flux,是一种新的管理component之间通信的手段(它替代了原来的 onDataChange).所以我们要重新重构一下我们的代码,重构的过程,首先就要要求我们的 测试要完整,能够保证改动后的效果不变

package.json

  • 你已经知道了npm可以安装依赖,其实npm也可以把你的依赖分享在npmjs.com上面
  • 而你从npm获取依赖的整个过程,可以记录在你的root文件夹下,形成一个叫做package.json的文件
  • 我们首先创建一个新的空文件夹, 然后创建空文件package.json
    cd ~/reactbook/whinepad2
    touch package.json
    
  • 最简单的package.json内容就是package名字加上version信息
    {
        "name": "whinepad",
        "version": "2.0.0",
    }
    

Configure Babel

  • 我们的命令原来需要使用babel来transpiler文件,所以大概需要如下的命令
    babel --presets react,es2015 js/source -d js/build
    
  • 我们可以把参数配置到package.json里面(也就是–presets这个option)
    {
        "name": "whinepad",
        "version": "2.0.0",
        "babel": {
            "presets" : [
                "es2015",
                "react"
            ]
        },
    }
    
  • 这样已配置,命令行就可以如下书写了
    babel js/source -d js/build
    

scripts

  • NPM是允许你把在root运行的script放到package json里面的
    {
        "name": "whinepad",
        "version": "2.0.0",
        "babel": {
            "presets" : [
                "es2015",
                "react"
            ]
        },
        "script": {
            "watch": "watch \"sh scripts/build.sh\" js/source css/"
        }
    }
    
  • 这样你就可以使用npm run watch啦
  • 当然了你甚至可以去掉build.sh,而把所有的配置工作都放到package.json里面

ESLint

  • ESLint是用来检查你的代码是不是有"潜在"的错误,还能检查你的代码风格是不是统一

Setup

  • eslint本身只是知道最基本的js的规范,如果你想用jsx, es2015等最新的规范,你需要 安装插件给它
    npm i -g eslint eslint-plugin-react eslint-plugin-babel babel-eslint
    
  • 然后你就可以在package.json里面利用它了
    {
        "name": "whinepad",
        "version": "2.0.0",
        "babel": {
            "presets" : [
                "es2015",
                "react"
            ]
        },
        "script": {
            "watch": "watch \"sh scripts/build.sh\" js/source css/"
        },
        "eslintConfig": {
            "parser": "babel-eslint",
            "plugins": [
                "babel",
                "react"
            ],
        }
    }
    
  • 配置之后的使用就很简单了(因为不需要急着option)
    $ eslint js/source/app.js
    
  • 我们需要让这个命令没有一点问题的过,这很容易,因为现在没有太多规则,所以我们 要加上一点规则
    "eslintConfig": {
        // ....
        "extends": "eslint:recommended"
    }
    
  • 配置完这个规则以后,问题又来了,我们的eslint运行会报错
    $ eslint js/source/app.js
    
    /Users/hfeng/tmp/w2/js/source/app.js
       6:8   error  'React' is defined but never used     no-unused-vars
      12:26  error  'localStorage' is not defined         no-undef
      27:3   error  'document' is not defined             no-undef
    
    ✖ 3 problems (3 errors, 0 warnings)
    
  • 我们需要告诉eslint,我们是运行在brower里面的, 这些全局变量(document, localStorage)在brower里面都是可见的.
    "env": {
        "browser":true
    }
    
  • 另外的React错误,就要用下面的代码来清除
    "rules": {
        "react/jsx-uses-react": 1
    }
    
  • 可能还会有其他的问题,总之再加上rule就可以了

Flow

  • js是一种动态语言,类型检查不是必须的,但是flow提供了这种功能,从一定程度上会避免很多低级的错误

Setup

  • 我们使用如下命令来安装flow
    $ npm install -g flow-bin
    
  • 然后初始化一个.flowconfig文件
    $ flow init
    
  • 设置.flowconfig的ignore和include文件
    [ignore]
    .*/ract/node_modules/.*
    [include]
    node_modules/react
    node_modules/react-dom
    node_modules/classnames
    [libs]
    
    [options]
    

Running

  • 使用flow,或者指定一个文件来进行检查
    $ flow
    $ flow js/source/app.j
    

Signing Up for Typechecking

  • 必须在文件的最开头设置如下的comment,flow才会检查你
    /* @flow */
    
  • TODO

Chapter 8. Flux

  • 本章介绍flux,这是一种管理components直接通信,以及app内部的data flow通信的框架
  • 前面的章节中,你已经了解到了最简单的一种通信方式:parent发送properties给child, 然后监听child的change(通过onDataChange)
  • 这种最简单的数据传递存在一个问题,那就是properties太多了,你无法有效管理和测试
  • 还有一个问题就是随着你应用的扩大,你以后会遇到properties从parent到child,从child 到grandchild..等等,这就会导致代码的重复(不满足DRY)
  • Flux就是为了解决这些问题而诞生的, 它不是一个library,而且一种管理你app中data的 best practice

The Big Idea

  • 解决问题的核心理念是你的app就关于data的, data是你app的全部核心.而我们在flux 里面,会把data存放在Store里面
  • 你的React component(View)从Store里面读取数据,然后把数据render到Store里面去
  • app的用户会来perform Action(比如点击一下), 这个点击的过程(Action)会导致Store 里面的数据update, 然后会(自动)的导致View改动
  • 整个过程是如下的一个单项的data flow,这种模型更容易让人理解
               +------------+
               |  Action    |
               +------------+
               /            ^
              /              \
             /                \
            V                  \
    +----------+            +-----------+
    | Store    +----------->|  View     |
    +----------+            +-----------+
    

Another Look at Whinepad

  • Whinepad有一个top-lvel的component叫做<Whinepad>
    <Whinepad
        schema={schema}
        initialData={data} />
    
  • 而Whinepad又可以包含其他component比如Excel
    <Excel
       schema={this.props.schema}
       initialData={this.state.data}
       onDataChange={this._onExcelDataChange.build(this)} />
    
  • schema和initialData是两个初始化值,但是传递的时候,方法却不一样:
    • Whinepad传递了this.props.schema给child Excel的schema
    • Whinepad却传递了this.state.data给child Excel的initialData,我们知道这个state 是很有可能改变的,那问题来了,谁来保存这些改变,谁来告诉我,哪个数据更新更准确
    • 从前面的章节你可以看到<Whinepad>包含了最新的data,但是React作为一个View层, 让它来保存数据,不是一个明智的选择

The Store

  • 保存数据的部分,在flux里面叫做store,我们创建两个Flux module, Store和Action:
    touch js/source/flux/CRUDStore.js
    touch js/source/flux/CRUDActions.js
    
  • 我们先来看看CRUDStore,这是一个和react完全没关系的js代码,说白了,其实它是一个 简单的JavaSript Object
    let data;
    let schema;
    
    const CRUDStore = {
        getData(): Array<Object> {
            return data;
        },
    
        getSchema(): Array<Object> {
            return schema;
        },
    };
    
    export default CRUDStore
    
  • 我们可以看到Store保存了single source of truth,每个想要得到datat和schema的都 会得到满意的答案,因为给了借口getData和getSchema
  • Store也会允许data进行update(schema不会更新,它们是app生命周期内都不会改变的 constant)
    setData(newData: Array<Object>, commit: boolean = true) {
        data = newData;
        if (commit && 'localStorage' in window) {
            localStorage.setItem('data', JSON.stringify(newData));
        }
    
        emitter.emit('change');
    
    },
    
  • 这里,我们的setData函数更新了data,同时也会更新permanent storage(也就是数据库) 但是更新permanent storage的前提是commit这个boolean值是true,因为有些时候你是 不想更新permanent storage的,比如当搜索的时候,你会想显示API返回的搜索的结果, 但是并不想存储它们
  • 还有些辅助函数,比如返回data的长度,和返回一个record是否在我们的data里面
  • 我们还要在Store里面初始化自己
    init(initialSchema: Array<Object>) {
        schema = initialSchema;
        const storage = 'localStorage' in window
              ? localStorage.getItem('data')
              : null;
    
        if (!storage) {
            data = [{}];
            schema.forEach(item => data[0][item.id] = item.sample);
        } else {
            data = JSON.parse(storage);
        }
    }
    
  • 我们另外需要在app.js开头初始化store(调用init),然后我们的<Whinepad>就不再需 要带着properties啦,它会直接去调用CRUDStore.getData()和CRUDStore.getSchema()
    import CRUDStore from './flux/CRUDStore';
    import Whinepad from './components/Whinepad';
    import schema from './schema';
    
    CRUDStore.init(schema);
    
    
    ReactDOM.render(
            <div>
            <Whinepad />
            </div>
    );
    

Store Events

  • 记得emitter.emit('chagne');这句么,这句户是说让所有关系data'改动'的UI,现在 他们可以读取我们新的data值,然后更新UI了
  • 有很多方法来实现这种event suscription pattern: 其核心就是收集一系列的subscriber 在一个list里面,然后当event发生的时候,从头到尾便利这个list,调用每个subscriber 的callback函数
  • 我们使用fbemitter来做这个部分
    npm i --save-dev fbemitter
    
  • 初始化很简单
    import {EventEmitter} from 'fbemitter';
    
    let data;
    let schema;
    const emitter = new EventEmitter();
    
    // ...
    
  • 使用的时候就是两个部分啦:
    • collect subscription
      const CRUDStore = {
          // ...
          addListener(eventType: string, fn: Function) {
              emitter.addListener(eventType, fn)
          },
      };
      
    • notify subscriber,也就是我们前面的触发
      emitter.emit('change');
      

Using the Store in <Whinepad>

  • 在Flux里面, <Whinepad> component也是非常simpl的,因为:
    1. 大部分的功能都放入了CRUDActions里面
    2. 我们连this.state.data都不需要维护了,因为this.state.data的唯一作用就是传 递给<Excel>,而现在<Excel>可以自己去Store里面取了
  • 说了半天,Whinepad甚至可以不去和store有关系(让Excel有关系就可以了),但是为了 让我们的故事写下去,我们"强行"让Whine和store有点关系,那就是加个搜索框,来搜索 所有的record
  • 之前<Whinepad>的constructor()如下
    this.state = {
        data: props.initialData,
        addnew: false,
    };
    
  • 但是我们知道了,<Whinepad>不再需要data了,Excel可以自己去取,但是我们的search 需要显示总体有多少给record,所以我们"勉强"和store搭上了关系:要去取自己有多 少个record,所以
    import CRUDStore from '../flux/CRUDStore';
    
    class Whinepad extends Component {
        constructor() {
            super();
            this.state = {
                addnew: false,
                count: CRUDStore.getCount(),
            };
        }
    }
    
  • 光从Store获得初始化的count还不够,还需要注册listener,当count的值变动的时候 store能够调用我们的callback函数来updateUI
    constructor() {
        super();
        this.state = {
            addnew: false,
            count: CRUDStore.getCount(),
        };
        CRUDStore.addListener('change', () => {
            this.setState({
                count: CRUDStore.getCount(),
            })
        });
    }
    
  • 我们可以看到,我们的callback函数设置的是setState,也就是说,一旦count有变,那么 就自动的调用setState,那么render()也就自动会被调用,UI也就更新了,UI什么样?看 下面代码
    render() {
        return (
                <input placeholder=(this.state.count === 1
                                    ? 'Search 1 record...'
                                    : `Search ${this.state.count} records...`
                                   }
                />
        );
    }
    
  • 一个精细化的管理就是<Whinepad>来实现shouldComponentUpdate(),因为store的 emitter.emit被触发的原因,是任意一种change的改变,某些属性被edit也会触发 emitter.emit,但是这种情况下其实count是不变的,所以render()会白白计算一次diff 我们可以通过实现shouldComponentUpdate()来减轻react引擎的压力
    shouldComponentUpdate(newProps: Object, newState: State): boolean {
        return (
            newState.addnew !== this.state.addnew ||
                newState.count !== this.state.count
        );
    }
    
  • 好了<hine>不需要传递schema和data给Excel了,也不需要onDataChagne了,因为Store 都会处理,所以最后的render函数非常简单
    render(){
        return (
            // ...
                <div className="WhinepadDatagrid">
                <Excel />
                </div>
            // ...
        );
    }
    

Using the Store in <Excel>

  • Excel也同样不需要接受任何的参数来初始化自己,它可以去Store读取.同时Excel也 要注册event handler,以便store更新的情况下,自己能自动更改UI,新的Excel的 constructor如下
    constructor() {
        super();
        this.state = {
            data: CRUDStore.getData(),
            sortby: null,
            descending: false,
            edit: null,
            dialog: null,
        };
        this.schema = CRUDStore.getSchema();
        CRUDStore.addListender('change', () => {
            this.setState({
                date: CRUDStore.getData(),
            })
        });
    }
    

Using the Store in <Form>

  • form component也是相似的道理,只不过它没有注册store的event listener因为它不 关心
    import CRUDStore from '../flux/CRUDStore';
    
    // ...
    type Props = {
        readonly? : boolean,
        recordId: ?number,
    };
    
    class From extends Component {
        fields: Array<Object>;
        initialData: ?Object;
    
        constructor(props: Props) {
            super(props);
            this.fields = CRUDStore.getSchema();
            if ('recordId' in this.props) {
                this.initialData = CRUDStore.getRecord(this.props.recordId);
            }
        }
        // ...
    }
    
    export default Form
    

Actions

  • action指的是Store里面的data是怎么改动的,当user和View进行了一下互动以后,其实 它们是更新了store,更新了store以后,所有跟store"报道"过的感兴趣的UI,都会更新
  • 创建一个Action非常简单
    import CRUDStore from './CRUDStore';
    
    const CRUDActions = {
        // methods
    };
    
    export default CRUDActions
    

CRUD Actions

  • CRUD Action其实就是实现CRUD里面的CUD: create, update, delete. Read不需要实 现,因为在Store里面实现了
    const CRUDActions = {
        create(newRecord: Object) {
            let data = CRUDStore.getData();
            data.unshift(newRecord);
            CRUDStore.setData(data);
        },
    
        delete(recordId: number) {
            let data = CRUDStore.getData();
            data.splice(recordId, 1);
            CRUDStore.setData(data);
        },
    
        updateRecord(recordId: number, newRecord: Object) {
            let data = CRUDStore.getData();
            data[recordId] = newRecord;
            CRUDStore.setData(data);
        },
    
        updateField(recordId: number, key: string, value: string|number) {
            let data = CRUDStore.getData();
            data[recordId][key] = value;
            CRUDStore.setData(data);
        },
    };
    

Searching and Sorting

  • 搜索和排序(对于Excel很重要),放在Action或者Store里面都OK,我们在这里认为Store 里面的数据尽可能的单调一点作为getter和setter的合集,其他的函数在action里面实现
    const CRUDActions = {
    
      _preSearchData: null,
    
      startSearching() {
        this._preSearchData = CRUDStore.getData();
      },
    
      search(e: Event) {
        const target = ((e.target: any): HTMLInputElement);
        const needle: string = target.value.toLowerCase();
        if (!needle) {
          CRUDStore.setData(this._preSearchData);
          return;
        }
        const fields = CRUDStore.getSchema().map(item => item.id);
        if (!this._preSearchData) {
          return;
        }
        const searchdata = this._preSearchData.filter(row => {
          for (let f = 0; f < fields.length; f++) {
            if (row[fields[f]].toString().toLowerCase().indexOf(needle) > -1) {
              return true;
            }
          }
          return false;
        });
        CRUDStore.setData(searchdata, /* commit */ false);
      },
    
      _sortCallback(a: (string|number), b: (string|number), descending: boolean): number {
        let res: number = 0;
        if (typeof a === 'number' && typeof b === 'number') {
          res = a - b;
        } else {
          res = String(a).localeCompare(String(b));
        }
        return descending ? -1 * res : res;
      },
    
      sort(key: string, descending: boolean) {
        CRUDStore.setData(CRUDStore.getData().sort(
          (a, b) => this._sortCallback(a[key], b[key], descending)
        ));
      },
    };
    
    export default CRUDActions
    

Using the Actions in <Whinepad>

  • 我们再来看看在Action引入之后的<Whinepad>如何利用这些action, 整体上来说,就是 一个import啦
    import CRUDActions from '../flux/CRUDActions';
    
    class Whinepad extends Component { /* ... */}
    export default Whinepad
    
  • 如果要加一个record,Whinepad之前要负责保存和更新自己的this.state.data:
    _addNew(action: string) {
        if (action === 'dismiss') {
            this.setState({addnew: false});
        } else {
            let data = Array.from(this.state.data);
            data.unshift(this.refs.from.getData());
            this.setState({
                addnew: false,
                data: data,
            });
            shit._commitToStorage(data);
        }
    }
    
  • 现在,一切的一切都交给Store就可以了
    _addNew(action: string) {
        this.setState({addnew: false});
        if (action === 'confirm') {
            CRUDAcations.create(this.refs.form.getData());
        }
    }
    
  • search也一样,核心就是我们不在维护state,不在维护data,都交给store去处理

Flux Recap

  • 以上就是我们对react的改写,加入了flux的支持
  • 我们所做的其实是:
    • View通过Action更新同一个Store
    • 而这个Store改动后,会发event,调用View的call back来更新view
  • 这其实就是一个环
               +------------+
               |  Action    |
               +------------+
               /            ^
              /              \
             /                \
            V                  \
    +----------+            +-----------+
    | Store    +----------->|  View     |
    +----------+            +-----------+
    
  • 当然了如果我们在页面更改的时候,还有其他方法能够触动数据库,那么,我们还会创建 一种"额外"的action来触动store
                                       +------------+
                                       |  Action    |
                                       +------------+
                                       /            ^
                                      /              \
                                     /                \
                                    V                  \
    +------------+          +----------+            +-----------+
    |  Action    +--------->| Store    +----------->|  View     |
    +------------+          +----------+            +-----------+
    
  • 一旦有多个action的话,那么上面的问题就不那么简单了,为了数据安全的要求,你要有 以个dispatch而来协调多个action,于是就变成这样
                                +------------+
                                |  Action    |
                                +------------+
                                /            ^
                               /              \
                              /                \
                             V                  \
    +--------+     +----------+     +------+    +-----------+
    | Action +-----|Dispatcher|---->|Store +--->|  View     |
    +--------+     +----------+     +------+    +-----------+
    
  • 而上面的图更有可能随着项目的扩展除了Dispatcher都变成多份!!这种情况下就更需 要好的架构来处理

Immutable

  • 所谓immutable object,就是一旦创建不可改变的object,immutable object总是理解 起来简单,而且容易debug,也易于多线程的处理.
  • 在JS里面,你可以使用immutable package来实现immutable

Immutable Store Data

  • immutable提供了几种data structure,比如List, Stack, Map,我们先来看一个List 的例子, List和js内置的array很像
    import {EventEmitter} from 'fbemitter';
    import {List} from 'immutable';
    
    let data: List<Object>;
    let schema;
    
    const emitter = new EventEmitter();
    
    const CRUDStore = {
        init(initialSchema: Array<Object>) {
            schema = initialSchema;
    
            const storage = 'localStorage' in window
                  ? localStorage.getItem('data')
                  : null;
            if (!storage) {
                let initialRecord = {};
                schema.forEach(item => initialRecord[item.id] = item.sample);
                data = List([initialRecord]);
            } else {
                data = List(JSON.parse(storage));
            }
        },
        // ...
    };
    
  • 我们可以看到List是使用array来初始化的.但是有些许不同, 比如:
    • 没有data.length了,要使用data.count()
    • 不能使用[] operator比如data[recordId],而要使用data.get(recordId)
  • 既然是从immutable package来的容器,这个容器自然就是不能改变的啦!不能改变的 怎么使用?如果对java有所了解的同学就会知道,为了提供更好的并发性能,java里面的 的immutable每次改动都会生成一个新的对象

Immutable Data Manipulation

  • 我们来看看immutable list的使用方法
    let list = List([1, 2]);
    let newlist = list.push(3, 4);
    
    list.size;                      // 2
    newlist.size;                   // 4
    list.toArray();                 // Array [1, 2]
    newlist.toArray();              // Array [1, 2, 3, 4]