react-up-and-running
Table of Contents
- Chaper 1. Hello World
- Chapter 2. The Lif of a Component
- Bare Minimum
- Properties
- propTypes
- State
- A Stateful Textarea Component
- A Note on DOM Events
- Props Versus State
- Props in Initial State: An Anti-Pattern
- Accessing the Component from the Outside
- Changing Properties Mid-Flight
- Lifecycle Methods
- Lifecycle Example: Log It All
- Lifecycle Example: Use a Mixin
- Lifecycle Example: Using a Child Component
- Performance Win: Prevent Component Updates
- PureRenderMixin
- Chapter 3. Excel: A Fancy Table Component
- Chapter 4. JSX
- Chapter 5. Setting Up for App Development
- Chapter 6. Building an App
- Chapter 7. Lint, Flow, Test, Repeat
- Chapter 8. Flux
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>
- 这个例子只有两点需要注意:
- 你使用了<script>调用了两个js文件react.js和react-dom.js
- 设置了一个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:
- 如果你没设置
react.js:19368 Warning: Failed propType: Required prop `name` was not specified in `Constructor`.
- 如果你设置了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) ); }
- 首先创建initial state: state就是data,而我们这个component只有一个data,那就
是text. 我们可以通过this.state.text来访问这个text.而下面的代码是我们通过
拷贝props.text来初始化自己
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!" }
- 比如可以改动state(注意,此种做法并不推荐,我们更希望react去查询而不是更改核
心state)
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 ) ), },
- 首先我们使用getInitialState来从props初始化data
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, });
- 首先要知道安装哪一行来排序, 通过event targe来获取其cellIndex
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, }}); }
- 在tbody里面加上这个handler
- 我们的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, }) );
- 首先获取parent的edit变量
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, }))); },
- toolbar很简单,就是一个按钮
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 » </h2>
- 你也可以通过加上{}把这段代码"解析成JS". 这就不是普通的HTML啦,字符串也不会解
析成特殊的格式. 如果你想解析成特殊格式,那么就请使用unocode.比如在这里就是使
用\u00bb来代替»
<h2> { "More info »"} </h2>
Anti-XSS
- 为了防止XSS攻击,react会把所有的字符串转义,也就是说类似"»"这样的代码, 是无论如何都不会起到作用的,还是使用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>
- 原来老的办法,当然是一个一个的赋值,有很多多余的代码,attribute越多,多余代码越多
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> } });
- 比如你有一个FancyLink commponent, 这个component有很多<a>类似的attribute
下面是别人使用你的component的例子
- 注意上面例子里面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
- 导入使用import
- 我们甚至不再需要原来的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
- app.js的代码如下
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
- transpiler js, 使用babel cli:
- 注意,我们的代码都会在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;
- 首先安装这个package,需要命令行,所以
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)} > ☆ </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');
- collect subscription
Using the Store in <Whinepad>
- 在Flux里面, <Whinepad> component也是非常simpl的,因为:
- 大部分的功能都放入了CRUDActions里面
- 我们连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]