随书代码

1 React 新的前端思维方式

npm i create-react-app -g
create-react-app first_react_app
npm start//执行eject则会弹出原本的scripts配置,不可逆

react的理念

UI = render(data)

2 设计高质量的React组件

当 prop 的值不是字符串类型时,在 JSX 中必须用花括号 {} 包住,
所以 style 的值有两层花括号,外层花括号代表是 JSX 的语法,内层的花括号代表这是一个对象常量。

可以使用 babel-react-optimize 在发布时去除开发中的propTypes。

直接使用this.state = xxx 而不是使用setState,可能会导致渲染异常。

在constructor里初始化state,替代了过去的getInitialState。

render函数并不做实际渲染动作,只是返回一个JSX描述的结构,最终由React操作渲染。
如果不需要渲染可以让render return null || false 。
不要在render里执行setState。

componentWillMount进行setState无效,和componentDidMount对称。随着组件render前调用。
componentWillMount可以在服务端被调用。

componentDidMount 并不是紧随着组件render后调用。而是当所有子组件都render后才会一起进行componentDidMount。
产生真实dom。

装载生命周期:
constructor -> getInitialState(deprecated) -> getDefaultProps -> componentWillMount -> render -> componentDidMount

更新生命周期:
componentWillReceiveProps -> shouldComponentUpdate(只有为true才会往下执行) -> componentWillUpdate -> render -> componentDidUpdate

只要父组件的render被调用,render函数里的子组件就会进行更新,不管父组件传给子组件的props有没有改变,都会触发componentWillReceiveProps。
而子组件内部的setState不会触发componentWillReceiveProps。
当对子组件传入function时,可能会触发子组件无谓的刷新?
componentWillReceiveProps可以做两次props的比较,拒绝无谓的刷新,算一点小优化。

render决定渲染什么,shouldComponentUpdate决定渲不渲染,默认为true。可以通过手动比较nextProps,nextState和this.props与this.state的值来决定是否渲染。
由于setState不是立即state值,在shouldComponentUpdate里取得可能不是this.setState()后的state值。

componentDidUpdate的作用在于一些原生的绑定事件在dom更新后在这里可以重新进行绑定。

如果在组件内自建了原生DOM,可能需要在componentWillUnmount里进行手动销毁防止内存泄漏。

3 从Flux到Redux

Flux

action是纯粹的数据对象,type定义action类型,由type触发相应的action构造函数。

Store也是对象,存储对应状态,接收Dispatcher派发的动作,根据动作来决定是否要更新应用状态。

不应该修改通过Store得到的数据,也就是Immutable的由来。

当一个动作被派发,Dispatcher就是简单地把所有注册的回调函数全部调用了一遍,至于这个动作是不是对方关心的,Flux 的Dispatcher不关心,要求每个回调函数去鉴别。

Flux的架构下,应用的状态被放在了store中,React组件中只是扮演View的作用,被动根据Store的状态来渲染,只是Store 的一个映射,不会主动变化数据,而是通过触发action来改变store,数据处理的工作不在view里。
正常组件里,操作可能是触发了setState,而Flux中只是派发了一个动作,这个动作会发送给所有Store对象,根据type改变Store对象,再而改变view本身。
这里的Store,不是一个数据的概念,而是一个大对象的概念,大对象里的某些Store,存储了数据。

缺点:1.Store之间依赖 2.难以进行服务端渲染(因为Dispatcher和Store都是全局唯一的一个) 3. Sotre混杂了逻辑和状态

Redux

1.唯一数据源:
不像Flux,redux的数据源只存在一个唯一的store上。

2.保持状态只读:
不能直接去修改状态。要修改store的状态,必须通过派发一个action对象来完成,这点和Flux一致。
UI=redner(data)

3.数据改变只能通过纯函数完成
这里的纯函数也就是reducer,redux的red也就是reducer。(reducer+flux)。
reducer(state,action)
第一个参数state为当前的状态,
第二个参数action是接收到的action对象,
reducer做的就是根据action和state返回一个新的对象,
reducer是一个纯函数,无副作用的,不能修改action和state。
reducer只负责计算状态,不负责存储状态。

redux中每个action构造函数都返回一个action对象。
而flux中action不返回什么,
而是把构造的动作函数立即通过调用dispatcher的dispatch函数派发出去。

redux中相比flux没有Dispatcher,而是在store上有一个dispatch函数。

flux中的state是由Store管理的,而redux中state是由reducer管理的,
reducer只关心如何更新state。
不能(应该)直接修改reducer里的state,而是通过返回新的state的复制的方式来修改,
因为reducer是纯函数且不应该产生副作用。

Store.getState()获取store存储的所有状态。
Store.subscribe()通过subscribe监听store的变化,变化则触发里面的方法。
Store.dispatch()派发action。

容器组件与傻瓜组件
傻瓜组件只负责渲染,没有state,只有传递下来不让他改变的props数据。
容器组件负责傻瓜组件需要的渲染数据的处理。
export出去的也是容器组件。

function Counter({caption, onIncrement, onDecrement, value}){return (<div><button style={buttonStyle} onClick={onIncrement}>+</button><button style={buttonStyle} onClick={onDecrement}>-</button><span>{caption} count: {value}</span></div>);
}
constructor(props, context) {super(props, context);
}
//等效
constructor() {super(...arguments);
}

可以通过context实现Provider

import { PropTypes, Component } from 'react';
class Provider extends Component {getChildContext() {return {store: this.props.store};}render() {return this.props.children;}
}
Provider.propTypes = {store: PropTypes.object.isRequired
}
Provider.childContextTypes = {store: PropTypes.object
};
export default Provider;

react-redux

connect接受两个参数mapStateToProps,mapDispatchToprops。
connect可部分替代容器组件的功能,如果props不是特别复杂,可以直接用
connect替代容器组件。
mapStateToProps将需要的state转化为props传递给组件,
mapDispatchToprops将dispatch转化为props传递给组件。

4 模块化React和Redux应用

开始一个新的应用,应先考虑3件事:

  1. 代码文件的组织结构
  2. 确定模块的边界
  3. Store的状态树设计

文件的组织结构

沿袭过去的MVC思想,按角色组织

  • reducer目录包含所有Redux和reducer,
  • actions目录包含所有action构造函数,
  • components目录包含所有的傻瓜组件,
  • containers目录包含所有的容器组件。

缺点是当项目大了以后不好扩展而且需要在不同目录间切换。

redux应用适合按功能组织,也就是将一个组件相关的redux文件都放在一起。

模块接口

在最理想的情况下,我们应该通过增加代码就能增加系统的功能,而不是通过对现有代码的修改来增加功能。

低耦合:不同功能模块之间的依赖关系应该简单而且清晰。
高内聚性:一个模块应该把自己的功能封装好,不让外界太依赖内部的结构,这样不会因为内部的变化而影响外部模块的功能。

状态树的设计

几个原则:

  • 一个模块控制一个状态节点
  • 避免冗余数据
  • 树形结构扁平

使用symbol替代actiontype里的字符串?

actionTypes里的名字必须保证唯一性。

export const toggleTodo = (id) => ({type: TOGGLE_TODO,id: id
});

使用combineReducers合并reducer。

不要把ref带入redux中。

const mapDispatchToProps = (dispatch) => {return {onToggleTodo: (id) => {dispatch(toggleTodo(id));},onRemoveTodo: (id) => {dispatch(removeTodo(id));}};
};//使用Store的bindActionCreators
const mapDispatchToProps = (dispatch) => bindActionCreators({onToggleTodo: toggleTodo,onRemoveTodo: removeTodo}, dispatch);//更简化?
//让其只是prop到action的映射?
const mapDispatchToProps = {onToggleTodo: toggleTodo,onRemoveTodo: removeTodo
};

state 与 redux 里props的界限?
是否所有状态都需要传入store中,如每次input变化的值?

要使用chrome React Perf插件,需要在Store加入这段代码:

import Perf from 'react-addons-perf'const win = window;
win.Perf = Perf

开发环境可以使用redux-immutable-state-invariant提醒防止误改state。
在Store加入这段代码:

const middlewares = [];
if (process.env.NODE_ENV !== 'production') {middlewares.push(require('redux-immutable-state-invariant')());
}
const storeEnhancers = compose(applyMiddleware(...middlewares),(win && win.devToolsExtension) ? win.devToolsExtension() : (f) => f,
);export default createStore(reducer, {}, storeEnhancers);
//export default createStore(reducer, initialState, storeEnhancers);

就可以使用redux-immutable-state-invariant和redux devtools了。

Store Enhancer可能有多个,用于将createStore函数产生的store具有更强的功能。
使用compose将多个storeEnhancers组合在一起。
使用applyMiddleware将多个中间件组合在一起。

5 React 组件的性能优化

我们应该忘记忽略很小的性能优化,可以说97%的情况下,过早的优化是万恶之源,而我们应该关心对性能影响最关键的那另外3%的代码。

使用shouldComponentUpdate手动比较值得变化绝对是否返回true进行渲染。

react产生的shouldComponentUpdatehe 和react-redux产生的shouldComponentUpdate是不一样的。
但他们都是浅层比较("shallow compare")。
当面对props是对象(函数也是对象)的时候,会比较不出来。

//bad
<Foo style={{color:"red"}}/}>//good
const fooStyle = {color:"red"};
<Foo style={fooStyle}/}>

也就是尽量少用匿名函数/对象的方式,而是用一个变量代替。

const mapDispatchToProps = (dispatch, ownProps) => {const {id} = ownProps;return {onToggle: () => dispatch(toggleTodo(id)),onRemove: () => dispatch(removeTodo(id))}
};

在redux里做掉,至于action是在父组件还是在本组件内都是可以的。

key是为提升比较算法性能,所以key应该保持稳定性,用会变化的数组的index是不对的,尽量保持其稳定,最好是props传递过来的值。如果一定要用数组的index,应该保证数组是不变的或者只是push变化的数组。

除了从渲染角度,还可以从数据获取的角度来优化性能。

reselect:
只要相关状态没有改变,那就直接用上一次的缓存结果。
先比较第一层,如果相等就不会继续往下比较。
同样是浅比较,但是因为我们要求reducer每次返回的是新对象,所以也可以比较到。

const selectVisibleTodos = (todos, filter) => {switch (filter) {case FilterTypes.ALL:return todos;case FilterTypes.COMPLETED:return todos.filter(item => item.completed);case FilterTypes.UNCOMPLETED:return todos.filter(item => !item.completed);default:throw new Error('unsupported filter');}
}const mapStateToProps = (state) => {return {todos: selectVisibleTodos(state.todos, state.filter)};
}//使用reselect
const getFilter = (state) => state.filter;
const getTodos = (state) => state.todos;
export const selectVisibleTodos = createSelector([getFilter, getTodos],(filter, todos) => {switch (filter) {case FilterTypes.ALL:return todos;case FilterTypes.COMPLETED:return todos.filter(item => item.completed);case FilterTypes.UNCOMPLETED:return todos.filter(item => !item.completed);default:throw new Error('unsupported filter');}}
);
const mapStateToProps = (state) => {return {todos: selectVisibleTodos(state)};
}

状态书的设计应该尽量范式化(Normalized),
所谓范式化就是遵照关系型数据库的设计原则,减少冗余数据。
用数据库表设计的思路来看就是,范式化是多分表,然后多关联查,少冗余数据。而反范式化就是一表多数据,少关联查。
因为使用了reselect(等),推荐范式化

6 React高阶组件

重复是优秀系统设计的大敌

高阶组件可以类比高阶函数。
一个高阶组件就是一个函数,这个函数接受一个组件作为输入,然后返回一个新的组件作为结果,新的组件拥有输入组件所不具有的功能。
react里高阶组件是一个组件类(class)而不是一个组件实例,也可以是一个无状态组件的函数。

一般用代理方式比继承方式多。

使用的思路:
例如可以用于提取复用的shouldComponentUpdate,减少重复代码。
mixins的新实现?
不修改原本组件的代码。

代理方式

返回的新组件类直接继承自React.Component类。
新组件扮演的角色是传入参数组件的一个“代理”,在新组件的render函数中,把被包裹组件渲染出来,除了高阶组件让自己要做的工作,其余功能全都转手给了被包裹的组件。
如果高阶组件不涉及到除render之外的生命周期函数,也可以是无状态纯函数。

可以实现

操纵props

const addNewProps = (WrappedComponent, newProps) => {return class WrappingComponent extends React.Component {render() {return <WrappedComponent {...this.props} {...newProps} />}}
}

访问ref

const refsHOC = (WrappedComponent) => {return class HOCComponent extends React.Component {constructor() {super(...arguments);this.linkRef = this.linkRef.bind(this);}linkRef(wrappedInstance) {this._root = wrappedInstance;}render() {const props = {...this.props, ref: this.linkRef};return <WrappedComponent {...props}/>;}};
};

抽取状态

简易版connect

const doNothing = () => ({});function connect(mapStateToProps=doNothing, mapDispatchToProps=doNothing) {function getDisplayName(WrappedComponent) {return WrappedComponent.displayName ||WrappedComponent.name ||'Component';}return function(WrappedComponent) {class HOCComponent extends React.Component {constructor() {super(...arguments);this.onChange = this.onChange.bind(this);this.store = {};}/*//TODO: make a workable shouldComponentUpdateshouldComponentUpdate(nextProps, nextState) {for (const propType in nextProps) {if (nextProps.hasOwnProperty(propType)) {if (nextProps[propType] === this.props[propType]) {return true;}}}for (const propType in this.props) {if (this.props.hasOwnProperty(propType)) {if (nextProps[propType] === this.props[propType]) {return true;}}}return false;}*/componentDidMount() {this.context.store.subscribe(this.onChange);}componentWillUnmount() {this.context.store.unsubscribe(this.onChange);}onChange() {this.setState({});}render() {const store = this.context.store;const newProps = {...this.props,...mapStateToProps(store.getState(), this.props),...mapDispatchToProps(store.dispatch, this.props)}return <WrappedComponent {...newProps} />;}};//借助context的一种provider实现HOCComponent.contextTypes = {store: React.PropTypes.object}HOCComponent.displayName = `Connect(${getDisplayName(WrappedComponent)})`;return HOCComponent;};
}

防止高阶组件失去名字不好debug

  function getDisplayName(WrappedComponent) {return WrappedComponent.displayName ||WrappedComponent.name ||'Component';}HOCComponent.displayName = `Connect(${getDisplayName(WrappedComponent)})`;

包装组件

//。。。
const styleHOC = (WrappedComponent, style) => {return class HOCComponent extends React.Component {render() {return (<div style={style}><WrappedComponent {...this.props}/></div>);}};
};

继承方式

采用继承关系关联作为参数的组件和返回的组件。
代理方式下是不同的组件,每个组件都经历完整的生命周期.
继承方式共同使用一个。

function removeUserProp(WrappedComponent) {return class NewComponent extends WrappedComponent {render() {//不推荐的用法const {user, ...otherProps} = this.props;this.props = otherProps;return super.render();}};
}/*
function removeUserProp(WrappedComponent) {return class NewComponent extends WrappedComponent {render() {const elements = super.render();const {user, ...otherProps} = this.props;console.log('##', elements);return React.cloneElement(elements, otherProps, elements.props.children);}};
}
*/

操纵props

一般不这么用
一定要用页不推荐上面修改props的方法

const modifyPropsHOC = (WrappedComponent) => {return class NewComponent extends WrappedComponent {render() {const elements = super.render();const newStyle = {color: (elements && elements.type === 'div') ? 'red' : 'green'}const newProps = {...this.props, style: newStyle};return React.cloneElement(elements, newProps, elements.props.children);}};
};

操作生命周期

一般这么用
校验登录

const onlyForLoggedinHOC = (WrappedComponent) => {return class NewComponent extends WrappedComponent {render() {if (this.props.loggedIn) {return super.render();} else {return null;}}}
}

重写shouldCompoentUpdate
useCache???

const cacheHOC = (WrappedComponent) => {return class NewComponent extends WrappedComponent {shouldComponentUpdate(nextProps, nextState) {return !nextProps.useCache;}}
}

以函数为子组件 (现在叫做 render props)

高阶组件并不是唯一可用于提高React组件代码重用的方法。
高阶组件的缺点在于对元组件的props有固化的要求。

在‘以函数为子组件’的模式下,他是一个真正的组件,要求必须有子组件,而且子组件必须是一个函数。
这种模式的缺点在于比较难优化,实际实现中在react-motion中大量使用并没有发现明显的性能问题。如果对性能有硬(强迫症)性需求,可以考虑少使用匿名函数,在shouldComponentUpdate里加入对children的比较。

登录信息可以这样:

class AddUserProp extends React.Component {render() {const user = loggedinUser;return this.props.children(user)}
}

大概是这样用的

<AddUserProp>
{(user) => <div>{user}</div>}
</AddUserProp>

用这种模式实现一个倒计时

class CountDown extends React.Component {constructor() {super(...arguments);this.state = {count: this.props.startCount};}shouldComponentUpdate(nextProps, nextState) {return nextState.count !== this.state.count;}componentDidMount() {this.intervalHandle = setInterval(() => {const newCount = this.state.count - 1;if (newCount >= 0) {this.setState({count: newCount});} else {window.clearInterval(this.intervalHandle);this.intervalHandle = null;}}, 1000);}componentWillUnmount() {if (this.intervalHandle) {window.clearInterval(this.intervalHandle);this.intervalHandle = null;}}render() {return this.props.children(this.state.count);}
}CountDown.propTypes = {children: React.PropTypes.func.isRequired,startCount: React.PropTypes.number.isRequired
}

大概是这么用的

<CountDown startCount={10}>
{(count) => <div>{ count > 0 ? count : '新年快乐'}</div>
}
</CountDown>

7 Redux和服务器通信

解决跨域访问api的一个方式是通过代理(proxy)。
跨域访问api的限制是针对浏览器的行为,服务器对于任何域名下的api访问不受限制。
可以通过webpack的proxy实现简易的代理功能?

一般在componentDidMount里进行ajax请求。

 componentDidMount() {const apiUrl = `/data/cityinfo/${cityCode}.html`;fetch(apiUrl).then((response) => {if (response.status !== 200) {throw new Error('Fail to get response with status ' + response.status);}response.json()//要先解.then((responseJson) => {this.setState({weather: responseJson.weatherinfo});}).catch((error) => {this.setState({weather: null});});}).catch((error) => {this.setState({weather: null});});}

thunk表示辅助调用另一个子程序的子程序。

const f = (x) =>{renturn x() + 5;
}const g = () =>{renturn 3 + 4;
}f(g)//结果是 (3+4)+5 = 12

redux-thunk 的工作是检查对象是不是函数,如果不是函数就放行,完成普通action对象的生命周期,如果发现action对象是函数,就执行这个函数,并把Store的dispatch函数和getState函数作为参数传递到函数中去,处理过程到此为主,不会让这个异步action对象继续往前派发到reducer函数。

异步action并不是纯函数

export const fetchWeather = (cityCode) => {return (dispatch) => {const apiUrl = `/data/cityinfo/${cityCode}.html`;dispatch(fetchWeatherStarted())return fetch(apiUrl).then((response) => {if (response.status !== 200) {throw new Error('Fail to get response with status ' + response.status);}response.json().then((responseJson) => {dispatch(fetchWeatherSuccess(responseJson.weatherinfo));}).catch((error) => {dispatch(fetchWeatherFailure(error));});}).catch((error) => {dispatch(fetchWeatherFailure(error));})};
}

取消上一次的ajax请求:

  1. 禁止dom操作
  2. 在过去的xhr中有abort()方法。
    而原生的fetch是没有的,但是一般使用的fetch其实是在xhr基础上封装的,可以在fetch上暴露出abort方法。
  3. 在thunk中加入某种计数变量
export const fetchWeather = (cityCode) => {return (dispatch) => {const apiUrl = `/data/cityinfo/${cityCode}.html`;const seqId = ++ nextSeqId;const dispatchIfValid = (action) => {if (seqId === nextSeqId) {return dispatch(action);}}dispatchIfValid(fetchWeatherStarted())fetch(apiUrl).then((response) => {if (response.status !== 200) {throw new Error('Fail to get response with status ' + response.status);}response.json().then((responseJson) => {dispatchIfValid(fetchWeatherSuccess(responseJson.weatherinfo));}).catch((error) => {dispatchIfValid(fetchWeatherFailure(error));});}).catch((error) => {dispatchIfValid(fetchWeatherFailure(error));})};
}

8 单元测试

如果程序的结构是足够简单的,单元测试并不是必须的。

单元测试框架一般用:
Mocha + Chai

Jest

辅助工具一般用:
Enzyme(测试react组件)
sinon.js(模拟各种请求,特别是网络)
redux-mock-store(mock reduxt store)

测试一般的action

export const addTodo = (text) => ({type: ADD_TODO,completed: false,id: nextTodoId ++,text: text
});//test
describe('todos/actions', () => {describe('addTodo', () => {const addTodo = actions.addTodoit('should create an action to add todo', () => {const text = 'first todo';const action = addTodo(text);expect(action.text).toBe(text);expect(action.completed).toBe(false);expect(action.type).toBe(actionTypes.ADD_TODO);});it('should have different id for different actions', () => {const text = 'first todo';const action1 = addTodo(text);const action2 = addTodo(text);expect(action1.id !== action2.id).toBe(true);});});
});

使用mock store测试异步action

import thunk from 'redux-thunk';
import {stub} from 'sinon';
import configureStore from 'redux-mock-store';import * as actions from '../../src/weather/actions.js';
import * as actionTypes from '../../src/weather/actionTypes.js';const middlewares = [thunk];
const createMockStore = configureStore(middlewares);describe('weather/actions', () => {describe('fetchWeather', () => {let stubbedFetch;const store = createMockStore();beforeEach(() => {stubbedFetch = stub(global, 'fetch');//fake fetch});afterEach(() => {stubbedFetch.restore();});it('should dispatch fetchWeatherSuccess action type on fetch success', () => {const mockResponse= Promise.resolve({status: 200,json: () => Promise.resolve({weatherinfo: {}})});// fake responsestubbedFetch.returns(mockResponse);return store.dispatch(actions.fetchWeather(1)).then(() => {const dispatchedActions = store.getActions();//redux-mock-store 的apiexpect(dispatchedActions.length).toBe(2);expect(dispatchedActions[0].type).toBe(actionTypes.FETCH_STARTED);expect(dispatchedActions[1].type).toBe(actionTypes.FETCH_SUCCESS);});});});
});

测试reducer

export default (state = {status: Status.LOADING}, action) => {switch(action.type) {case FETCH_STARTED: {return {status: Status.LOADING};}case FETCH_SUCCESS: {return {...state, status: Status.SUCCESS, ...action.result};}case FETCH_FAILURE: {return {status: Status.FAILURE};}default: {return state;}}
}//test
describe('weather/reducer', () => {it('should return loading status', () => {const action = actions.fetchWeatherStarted();const newState = reducer({}, action);expect(newState.status).toBe(Status.LOADING);});
});

使用Enzyme的
shallow方法测试无状态React组件

const Filters = () => {return (<p className="filters"><Link filter={FilterTypes.ALL}> {FilterTypes.ALL} </Link><Link filter={FilterTypes.COMPLETED}> {FilterTypes.COMPLETED} </Link><Link filter={FilterTypes.UNCOMPLETED}> {FilterTypes.UNCOMPLETED} </Link></p>);
};//test
describe('filters', () => {it('should render three link', () => {const wrapper = shallow(<Filters />);expect(wrapper.contains(<Link filter={FilterTypes.ALL}> {FilterTypes.ALL} </Link>)).toBe(true);expect(wrapper.contains(<Link filter={FilterTypes.COMPLETED}> {FilterTypes.COMPLETED} </Link>)).toBe(true);expect(wrapper.contains(<Link filter={FilterTypes.UNCOMPLETED}> {FilterTypes.UNCOMPLETED} </Link>)).toBe(true);});
});

测试被连接(connnect)的react组件

const TodoList = ({todos, onClickTodo}) => {return (<ul className="todo-list">{todos.map((item) => (<TodoItemkey={item.id}id={item.id}text={item.text}completed={item.completed}/>))}</ul>);
};const mapStateToProps = (state) => {return {todos: selectVisibleTodos(state)};
}export default connect(mapStateToProps)(TodoList);//test
import {mount} from 'enzyme';
describe('todos', () => {it('should add new todo-item on addTodo action', () => {const store = createStore(combineReducers({todos: todosReducer,filter: filterReducer}), {todos: [],filter: FilterTypes.ALL});//创造的临时Storeconst subject = (<Provider store={store}><TodoList /></Provider>);const wrapper = mount(subject);//加入react contextstore.dispatch(actions.addTodo('write more test'));expect(wrapper.find('.text').text()).toEqual('write more test');});
});

这里的provide需要这样构建是因为TodoItem也是connect到store的组件。

9 扩展Redux

如果用express中间件类比redux的中间件,那么
如果把redux和express都看作一个对请求的处理框架,
redux中的action对象对应与express中的客户端请求,
所有的中间件就组成处理请求的'管道'。

在redux中,中间件处理的是action对象,而派发action对象的就是store上的dispatch函数,在action对象进入reducer之前,会经历中间件的管道。

一个空的中间件

function doNothingMiddleware({dispatch,getState}){return function(next){return function(action){return next(action);//下一个中间件继续处理}}
}//也可以这么写
let doNothingMiddleware = ({dispatch,getState}) => next => action => next(action)

redux-thunk源码

function createThunkMiddleware(extraArgument){return ({dispatch,getState}) => next =>action=>{if(typeof action === 'function'){return action(dispatch,getState,extraArgument);}return next(action);}
}
const thunk = createThunkMiddleware();
export default thunk;

单个Enhancer

const configureStore = applyMiddleware(thunkMiddleware)(createStore);
const store = configureStore(reducer,initialState);

有多个Enhancer的情况

const middlewares = [thunkMiddleware];const storeEnhancers = compose(applyMiddleware(...middlewares),(win && win.devToolsExtension) ? win.devToolsExtension() : (f) => f,
);export default createStore(reducer, {}, storeEnhancers);

替换thunk,利用promise处理异步action
一个简易的promise中间件

function isPromise(obj) {return obj && typeof obj.then === 'function';
}/*
export default function promiseMiddleware({dispatch}) {return next => action => {return isPromise(action) ? action.then(dispatch) : next(action);}
}
*/export default function promiseMiddleware({dispatch}) {return function(next) {return function(action) {return isPromise(action) ? action.then(dispatch) : next(action);}}
}

改进的promise中间件

function isPromise(obj) {return obj && typeof obj.then === 'function';
}export default function promiseMiddleware({dispatch}) {return (next) => (action) => {const {types, promise, ...rest} = action;if (!isPromise(promise) || !(action.types && action.types.length === 3)) {return next(action);}const [PENDING, DONE, FAIL] = types;dispatch({...rest, type: PENDING});return action.promise.then((result) => dispatch({...rest, result, type: DONE}),(error) => dispatch({...rest, error, type: FAIL}));};
}

promise中间件用的话就这样用,可以省去几个样板action

export const fetchWeather = (cityCode) => {const apiUrl = `/data/cityinfo/${cityCode}.html`;return {promise: fetch(apiUrl).then(response => {if (response.status !== 200) {throw new Error('Fail to get response with status ' + response.status);}return response.json().then(responseJson => responseJson.weatherinfo);}),types: [FETCH_STARTED, FETCH_SUCCESS, FETCH_FAILURE]};
}

applyMiddleware函数可以接受任意个参数的中间件,每个通过dispatch函数派发的动作组件按照在applyMiddleware中的先后顺序传递给各个中间件。

一个中间件如果产生新的action对象,正确的方式是使用dispatch函数派发,而不是使用next函数。

中间件可以用来增强Redux Store的dispatch方法。
Store Enhancer可以用来增强Redux Store。

一个空的Store Enhancer

const doNothingEnhancer = (createStore) =>(reducer,preloadedState,enhancer)=>{const store = createStore(reducer,preloadedState,enhancer);return store;
}

一个store对象包含:

  • dispatch
  • subscribe
  • getState
  • replaceReducer

对dispatch调用的日志

const logEnhancer = (createStore) =>(reducer,preloadedState,enhancer)=>{const store = createStore(reducer,preloadedState,enhancer);const originalDispatch = store.dispatch;store.dispatch = (action) =>{console.log('dispatch action:',action);originalDispatch(action);}return store;
}

不清除过去store新界面创建该页面(临时?)store的enhancer

const RESET_ACTION_TYPE = '@@RESET';const resetReducerCreator = (reducer, resetState) => (state, action) => {if (action.type === RESET_ACTION_TYPE) {return resetState;} else {return reducer(state, action);}
};const reset = (createStore) => (reducer, preloadedState, enhancer) => {const store = createStore(reducer, preloadedState, enhancer);const reset = (resetReducer, resetState) => {const newReducer = resetReducerCreator(resetReducer, resetState);store.replaceReducer(newReducer);store.dispatch({type: RESET_ACTION_TYPE, state: resetState});};return {...store,reset};
};export default reset;//大概是这么用的it('reset', () => {it('should reset state and reducer', () => {const newReducer = (state, action) =>({newPayload: action.payload});const newState = {newPayload: 'new'};store.reset(newReducer, newState);expect(store.getState()).toEqual(newState);store.dispatch({type: 'any', payload: 'changed'});expect(store.getState()).toEqual({newPayload: 'changed'});});});

10 动画

css3方式的缺点是时间和速度曲线不合理,过程可能是一闪而逝的,捕捉不到中间状态。优点是性能好。
js方式的缺点是性能差,优点是灵活度高。

60帧 = 60fps(Frame Per Second)
1000ms/60 = 16ms

模拟requestAnimationFrame实现

var lastTimeStamp = new Date().getTime();
function raf(fn) {var currTimeStamp = new Date().getTime();var delay  = Math.max(0, 16 - (currTimeStamp - lastTimeStamp));var handle = setTimeout(function(){fn(currTimeStamp);}, delay);lastTimeStamp = currTimeStamp;return handle;
}var left = 0;
var animatedElement = document.getElementById("sample");
var startTimestamp = new Date().getTime();
function render(timestamp) {left += (timestamp - startTimestamp) / 16;animatedElement.style.left = left + 'px';if (left < 400) {raf(render);}
}raf(render);

ReactCssTransitionGroup

ReactCssTransitionGroup是通过css实现的。
一般用在组件的装载和卸载动画中。

transitionName,
enter代表‘装载’开始时的状态,
leave代表‘卸载’开始时的状态,
active代表动画结束时的状态。

假设transitionName为sanmple,那么相关类名就是:
sample-enter
sample-enter-active
sample-leave
sample-leave-active
sample-appear
sample-appear-active

transitionEnterTimeout‘装载’动画持续时间,
transitionLeaveTImeout‘卸载’动画持续时间,
transitionAppearTImeout初次‘装载’动画持续时间,
他们的持续时间应该与css里的transition-duration保持一致,
这种重复设置是ReactCssTransitionGroup的一个缺点。

只有在该组件已经加载(真实渲染?)了,该ReactCssTransitionGroup动画才有效。
在初次加载时如果也需要动画可以手动将transitionAppear={true},大多数情况下是不需要的。

React-Motion

友好的API比性能更重要

React-Motion是通过js的方式来控制动画的。
相比ReactCssTransitionGroup不需要css,ReactCssTransitionGroup包裹子元素集合,React-Motion包裹return的根节点,用的是‘以函数为子组件’的模式。
willEnter对应装载,willLeave对应卸载,defaultStyles对应appear。

11 多页面应用

路由

React-Router按照Route在代码中的先后顺序决定匹配的顺序。
(这本书用的是3.x)

当react-router和react-redux同存时

const createElement = (Component, props) => {return (<Provider store={store}><Component {...props} /></Provider>);
};
//App
const App = ({children}) => {return (<div><TopMenu /><div>{children}</div></div>);
};//Routes
const Routes = () => (<Router history={history} createElement={createElement}><Route path="/" component={App}><IndexRoute component={Home} /><Route path="home" component={Home} /><Route path="about" component={About} /><Route path="*" component={NotFound} /></Route></Router>
);

IndexRoute是默认路由。

这里createElement每次都会调用,如果觉得有性能问题,也在Routes里去掉createElement然后这样用。

ReactDOM.render(<Routes />,document.getElementById('root')
);//改为
ReactDOM.render(<Provider store={store} ><Routes />,</Provider>document.getElementById('root')
);

如果希望将url的变化也统一到redux里,需要加入react-router-redux,
但是他的使用实际上是违背唯一数据源的。

//routes
import {Router, Route, IndexRoute, browserHistory} from 'react-router';
import {syncHistoryWithStore} from 'react-router-redux';const history = syncHistoryWithStore(browserHistory, store);//store
import {routerReducer} from 'react-router-redux';const reducer = combineReducers({routing: routerReducer
});

现在当url变化时,会向store派发action。

代码分片

利用webpack。

使用CommonsChunkPlugin,配置output的chunkFilename用于分片各页面组件间的公共组件。

chunkFilename: 'dist/js/[name].[chunkhash:8].chunk.js'new webpack.optimize.CommonsChunkPlugin('common', 'dist/js/common.[chunkhash:8].js')

开发环境可以不用[chunkhash]。

使用require.ensure和react-router的getComponent异步加载组件。也可以尝试使用新的import(),写法略有差异。

const getHomePage = (nextState, callback) => {require.ensure([], function(require) {callback(null, require('./pages/Home.js').default);}, 'home');
};//routes
const Routes = () => (<Router history={history} createElement={createElement}><Route path="/" component={App}><IndexRoute getComponent={getHomePage} /><Route path="home" getComponent={getHomePage} /><Route path="counter" getComponent={getCounterPage} /><Route path="about" getComponent={getAboutPage} /><Route path="*" getComponent={getNotFoundPage} /></Route></Router>
);

需要注意的是因为webpack打包过程是对代码静态扫描的过程,所以这里不能对getHomePage这种样板代码进行方法提取。
require里参数不能是变量。

因为代码分片后,redux的reducer和state有中断,所以需要一个store enhancer来弥补这个问题,替换当前store上的state和reducer。

const getCounterPage = (nextState, callback) => {require.ensure([], function(require) {const {page, reducer, stateKey, initialState} = require('./pages/CounterPage.js');const state = store.getState();store.reset(combineReducers({...store._reducers,counter: reducer//将新的recuer合入}), {...state,[stateKey]: initialState //state的处理});callback(null, page);}, 'counter');
};

这章整体讲的略为粗略,也可能是涉及的东西比较复杂。如果想更好的掌握需要自己去扩展更多的知识。

12 同构(isomorphic)

服务器端渲染 vs 浏览器端渲染

一般一个浏览器(前端)渲染需要3个部分:

  1. 应用架构,如backbone,react。
  2. 模板库。
  3. 服务端的api支持。

前端渲染的主要问题在于首屏渲染。

TTFP(Time To First Paint):从网页HTTP请求发出,到用户可以看到第一个有意义的内容渲染出来的时间差。
TTI(Time To Interactive):从网页HTTP请求发出,到用户可以对网页内容进行交互的时间。

前端渲染没有任何缓存(首屏渲染)的情况下,需要等待3个HTTP请求

  1. 向服务器获取HTML
  2. 获取js文件
  3. 访问api服务获取数据

PWA(Progressive Web App)利用Manifest和Service Worker可以进一步优化,该书没有讲述,可以另外进行探索。

服务端渲染的优点:

  1. 在html请求时就可以返回有内容的html,所以首次渲染TTFP相比前端渲染会短一些。
  2. 更利于搜索引擎(SEO)优化.
  3. 大多数情况下服务器上(api)获取数据的速度更快。

服务端渲染的缺点:

  1. 可能会产生过大的html,导致首屏一样不快
  2. 服务器压力更大
  3. 主流的react,vue框架对服务端渲染支持并不太好

构建渲染动态内容服务器

react的热加载除了webpack-dev-middleware和webpack-hot-middleware,还需要一个babel装载器react-hot-loader。

同构的简单思路就是,原本前端渲染是始终返回html挂载文件本身,现在是server始终render模板文件,但同时又加载前端运行的文件。

react 同构

react的render在客户端时,最终产出的是dom元素。
而在服务器端,最终产出的是字符串,
因为返回给浏览器的就是html字符串,所以服务端渲染不需要指定容器元素。

renderToString函数返回结果就是一个html字符串。

为了避免不必要的dom操作,服务端渲染react组件时会计算所生成html的校验和,并存放在根节点的属性 data-react-checksum中。
只有检验不过时,前端渲染才会覆盖掉服务端渲染产生的html。

所以实现同构很重要的一条,就是一定要保证前后端渲染的结果一致。

脱水(Dehydrate)和注水(Rehydrate)

服务端渲染产生了html,但是在交给浏览器的网页中不光要有html,还需要有“脱水数据”,也就是在服务器渲染过程中给react组件的输入数据,这样,当浏览器端渲染时,可以直接根据“脱水数据”来渲染react组件,这个过程叫做“注水”。
使用脱水数据可以避免没必要的api请求,保证了两端渲染的结果一致,这样不会产生网页内容的闪动。

使用脱水数据需要注意防止跨站脚本攻击(XSS)。

脱水数据一般来自redux 的store。

脱水数据不能过大,如果过大就会影响性能,让服务端渲染失去意义。

在服务器端使用redux必须对每个请求都创造一个新的store,这是和浏览器渲染的最大区别。

有时候需要在前端和后端中都请求某一个接口。

后端渲染不存在所谓分片(懒加载),全都是直接导入。

可能需要额外的特殊处理字符串的方法,防止脱水数据中的不安全字符。

function safeJSONstringify(obj) {return JSON.stringify(obj).replace(/<\/script/g, '<\\/script').replace(/<!--/g, '<\\!--');
}

前后端渲染都需要用react-router的match函数了现在。



react的同构与vue的同构

都有一个服务端渲染的方法
vue是vue-server-renderer提供的,
react是react-dom/server提供的。

vue需要客户端和服务端各打一次包,react只需要一次。
vue还需要在webpack里加入'vue-server-renderer/client-plugin'和'vue-server-renderer/server-plugin',而react只需要'webpack-manifest-plugin'.
这里很大可能是vue的该插件做了其他整合和优化。

react对redux的处理比较复杂,特别是对懒加载和ssr,而vue也应该是整合过了比较简单。

整体而言两者各有异同,可能vue本身并没有比react更适合ssr,但是奈何有个尤雨溪做掉了很多脏活。。。。

转载于:https://www.cnblogs.com/qingmingsang/articles/8798555.html

[react] 《深入浅出React和Redux》 读书笔记相关推荐

  1. 《深入浅出图神经网络》读书笔记 1-2

    <深入浅出图神经网络>读书笔记 1-2 第1章 图的概述 第2章 神经网络基础 2.1 机器学习基本概念 2.2 神经网络 2.4 训练神经网络 第1章 图的概述 图神经网络(Graph ...

  2. 《深入浅出设计模式-中文版》读书笔记-工厂模式(五)

    今天给大家带来的是:工厂模式. 我们在代码中创建一个对象,我们会很自然的想到new.其实,除了new意外,我们还有很多的方式可以创建对象.不说复杂的模式,就说简单的语法,其实.NET框架中还有下面的方 ...

  3. 《深入浅出图神经网络》读书笔记(5.图信号处理与图卷积神经网络)

    文章目录 5.图信号处理与图卷积神经网络 5.1 矩阵乘法 5.2 图信号与图的拉普拉斯矩阵 5.3 图傅里叶变换 5.4 图滤波器 5.4.1 空域角度 5.4.2 频域角度 5.5 图卷积神经网络 ...

  4. 《文本上的算法——深入浅出自然语言处理》读书笔记:第6章 搜索引擎是什么玩意儿

    目录 第6章 搜索引擎是什么玩意儿 6.1 搜索引擎原理 6.2 搜索引擎架构 6.3 搜索引擎核心模块 6.4 搜索广告 第6章 搜索引擎是什么玩意儿 6.1 搜索引擎原理 假设Q为用户要查询的关键 ...

  5. 《深入浅出图神经网络》读书笔记(8. 图分类)

    文章目录 8. 图分类 8.1 基于全局池化的图分类 8.2 基于层次化池化的图分类 8.2.1 基于图坍缩的池化机制 1.图坍缩 2.DIFFPOOL 3.EigenPooling 8.2.2 基于 ...

  6. 深入浅出mysql基础篇读书笔记

    版本8.0.25 1.p19 int(2)已经不推荐了 2.p28 无序排列改为按原表顺序排列会更好,无序也没错,但是容易被误解为随机排列(类似HashMap),但其实是按照原表顺序排列 3.p29 ...

  7. 实操《深入浅出React和Redux》第二期—Flux

    2019独角兽企业重金招聘Python工程师标准>>> 此书讲得蛮详细, 从Flux一步一步过渡到Redux. 写过的代码舍不得扔, 立此存照吧. 我有几张阿里云幸运券分享给你,用券 ...

  8. 实操《深入浅出React和Redux》第一期

    上次的书看得差不多了,但实践越来越麻烦. 于是重新开一本书,这次,人家用了create-react-app. 实践方面就容易开展啦.:) 主要文件内容也少多啦. index.js import Rea ...

  9. 实操《深入浅出React和Redux》第四期--react-redux

    入到第四期, 代码大大简化, 但如果没有前面的演化过程, 一定让人蒙圈~~ 三个主要文件: index.js import React from 'react'; import ReactDOM fr ...

最新文章

  1. Unix数据中心主宠儿
  2. 【网址收藏】达内Django视频笔记收藏
  3. [GAE教程]初识 Google App Engine
  4. JVM—内存模型JMM
  5. 【数学基础】参数估计之贝叶斯估计
  6. Springboot里输出的html里包含script标签页会怎样
  7. 华为手机丢失定位网站_手机端网站优化要从网站的设计和定位开始入手
  8. Linux下压缩包生成与解压命令以及进度
  9. WPF下递归生成树形数据绑定到TreeView上
  10. nginx配置详解1
  11. push指令的执行过程
  12. 设计模式解密(7)- 代理模式
  13. [转]oracle分页用两层循环还是三层循环?
  14. Java程序员排行前10的错误,请注意!
  15. html-前端调用MD5对密码进行加密
  16. 服务器上文件添加可信任,如何将服务器配置为受信任以进行委派
  17. Python shapefile转GeoJson的两种方式
  18. win10隐藏任务栏_推荐我使用的一个任务栏软件:7+ Taskbar Tweaker
  19. 已知ip地址如何算默认网关
  20. DVB-S/S2天线及信号相关知识

热门文章

  1. 浙江大学 计算机学院 秦青青,军训倒计时 | 说给辅导员听的“悄悄话”
  2. ibm服务器和小型机型号,IBM小型机各个型号之间的区别.doc
  3. 最新版Eclipse免安装下载地址
  4. 用Ada提高安全性、保密性和可靠性
  5. Flash场景之间相互跳转的实现方法
  6. mysql查询逗号隔开的字段
  7. 无线传感网络 --ZigBee3-1 系统睡眠唤醒
  8. 如何在 K3s 中使用网络策略
  9. 《我也能做CTO之程序员职业规划》之十五: 智商
  10. 在政策的加持下,助力服装纺织企业发展