[react] 《深入浅出React和Redux》 读书笔记
随书代码
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件事:
- 代码文件的组织结构
- 确定模块的边界
- 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请求:
- 禁止dom操作
- 在过去的xhr中有abort()方法。
而原生的fetch是没有的,但是一般使用的fetch其实是在xhr基础上封装的,可以在fetch上暴露出abort方法。 - 在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个部分:
- 应用架构,如backbone,react。
- 模板库。
- 服务端的api支持。
前端渲染的主要问题在于首屏渲染。
TTFP(Time To First Paint):从网页HTTP请求发出,到用户可以看到第一个有意义的内容渲染出来的时间差。
TTI(Time To Interactive):从网页HTTP请求发出,到用户可以对网页内容进行交互的时间。
前端渲染没有任何缓存(首屏渲染)的情况下,需要等待3个HTTP请求
- 向服务器获取HTML
- 获取js文件
- 访问api服务获取数据
PWA(Progressive Web App)利用Manifest和Service Worker可以进一步优化,该书没有讲述,可以另外进行探索。
服务端渲染的优点:
- 在html请求时就可以返回有内容的html,所以首次渲染TTFP相比前端渲染会短一些。
- 更利于搜索引擎(SEO)优化.
- 大多数情况下服务器上(api)获取数据的速度更快。
服务端渲染的缺点:
- 可能会产生过大的html,导致首屏一样不快
- 服务器压力更大
- 主流的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-2
<深入浅出图神经网络>读书笔记 1-2 第1章 图的概述 第2章 神经网络基础 2.1 机器学习基本概念 2.2 神经网络 2.4 训练神经网络 第1章 图的概述 图神经网络(Graph ...
- 《深入浅出设计模式-中文版》读书笔记-工厂模式(五)
今天给大家带来的是:工厂模式. 我们在代码中创建一个对象,我们会很自然的想到new.其实,除了new意外,我们还有很多的方式可以创建对象.不说复杂的模式,就说简单的语法,其实.NET框架中还有下面的方 ...
- 《深入浅出图神经网络》读书笔记(5.图信号处理与图卷积神经网络)
文章目录 5.图信号处理与图卷积神经网络 5.1 矩阵乘法 5.2 图信号与图的拉普拉斯矩阵 5.3 图傅里叶变换 5.4 图滤波器 5.4.1 空域角度 5.4.2 频域角度 5.5 图卷积神经网络 ...
- 《文本上的算法——深入浅出自然语言处理》读书笔记:第6章 搜索引擎是什么玩意儿
目录 第6章 搜索引擎是什么玩意儿 6.1 搜索引擎原理 6.2 搜索引擎架构 6.3 搜索引擎核心模块 6.4 搜索广告 第6章 搜索引擎是什么玩意儿 6.1 搜索引擎原理 假设Q为用户要查询的关键 ...
- 《深入浅出图神经网络》读书笔记(8. 图分类)
文章目录 8. 图分类 8.1 基于全局池化的图分类 8.2 基于层次化池化的图分类 8.2.1 基于图坍缩的池化机制 1.图坍缩 2.DIFFPOOL 3.EigenPooling 8.2.2 基于 ...
- 深入浅出mysql基础篇读书笔记
版本8.0.25 1.p19 int(2)已经不推荐了 2.p28 无序排列改为按原表顺序排列会更好,无序也没错,但是容易被误解为随机排列(类似HashMap),但其实是按照原表顺序排列 3.p29 ...
- 实操《深入浅出React和Redux》第二期—Flux
2019独角兽企业重金招聘Python工程师标准>>> 此书讲得蛮详细, 从Flux一步一步过渡到Redux. 写过的代码舍不得扔, 立此存照吧. 我有几张阿里云幸运券分享给你,用券 ...
- 实操《深入浅出React和Redux》第一期
上次的书看得差不多了,但实践越来越麻烦. 于是重新开一本书,这次,人家用了create-react-app. 实践方面就容易开展啦.:) 主要文件内容也少多啦. index.js import Rea ...
- 实操《深入浅出React和Redux》第四期--react-redux
入到第四期, 代码大大简化, 但如果没有前面的演化过程, 一定让人蒙圈~~ 三个主要文件: index.js import React from 'react'; import ReactDOM fr ...
最新文章
- Unix数据中心主宠儿
- 【网址收藏】达内Django视频笔记收藏
- [GAE教程]初识 Google App Engine
- JVM—内存模型JMM
- 【数学基础】参数估计之贝叶斯估计
- Springboot里输出的html里包含script标签页会怎样
- 华为手机丢失定位网站_手机端网站优化要从网站的设计和定位开始入手
- Linux下压缩包生成与解压命令以及进度
- WPF下递归生成树形数据绑定到TreeView上
- nginx配置详解1
- push指令的执行过程
- 设计模式解密(7)- 代理模式
- [转]oracle分页用两层循环还是三层循环?
- Java程序员排行前10的错误,请注意!
- html-前端调用MD5对密码进行加密
- 服务器上文件添加可信任,如何将服务器配置为受信任以进行委派
- Python shapefile转GeoJson的两种方式
- win10隐藏任务栏_推荐我使用的一个任务栏软件:7+ Taskbar Tweaker
- 已知ip地址如何算默认网关
- DVB-S/S2天线及信号相关知识