这篇具体讲讲Redux使用的一些技巧
在没有遁入React之前,我是一个DOM操作控,不论是jQuery
还是zepto
,我在页面交互的实现上用的最多的就是DOM操作,把复杂的交互一步一步通过选择器和事件委托绑定到document上,然后逐个连贯起来。
$(document).on('event', 'element', function(e){
e.preventDefault();
var that = this;
var parent = $(this).parent();
var siblings = $(this).siblings();
var children = $(this).children();
// .....
});
这是jQuery
式的编程思维,React
和它截然不同。React
的设计是基于组件化的,每个组件通过生命周期维护统一的state
,state
改变,组件便update
,重新触发render
,即重新渲染页面。而这个过程操作的其实是内存里的虚拟DOM
,而不是真正的DOM节点,加上其内部的差异更新算法,所以性能上比传统的DOM操作要好。
举个简单的例子:
现在要实现一个模态组件,如果用jQuery式的编程思维,很习惯这么写:
/**
* @desc 全局模态窗口
**/
var $ = window.$;
var modal = {
confirm: function(opts) {
var title = opts.title || '提示',
content = opts.content || '提示内容',
callback = opts.callback;
var newNode = [
'',
'',
''
,
title,
'',
''
,
content,
'',
'',
'取消',
'确定',
'',
'',
'',
].join('');
$('#J_mask').remove();
$('body').append(newNode);
$('#J_cancel').on('click', function() {
$('#J_mask').remove();
});
$('#J_confirm').on('click', function() {
if (typeof callback === 'function') {
callback();
}
$('#J_mask').remove();
});
}
};
module.exports = modal;
然后在页面的JavaScript里通过选择器触发模态和传递参数。
var Modal = require('modal');
var $ = window.$;
var app = (function() {
var init = function() {
eventBind();
};
var eventBind = function() {
$(document).on('click', '#btnShowModal', function() {
Modal.confirm({
title: '提示',
content: '你好!世界',
callback: function() {
console.log('Hello World');
}
});
});
};
init();
})();
如果采用React
式的编程思维,它应该是这样的:
/**
* @desc 全局模态组件 Component
* @author Jafeney
* @createDate 2016-05-17
* */
import React, { Component } from 'react'
import './index.less'
class Modal extends Component {
constructor() {
super()
this.state = {
jsMask: 'mask hidden'
}
}
show() {
this.setState({
jsMask: 'mask'
})
}
close() {
this.setState({
jsMask: 'mask hidden'
})
}
confirm() {
this.props.onConfirm && this.props.onConfirm()
}
render() {
return (
this.state.jsMask}>
"modal-box" style={this.props.style}>
"header">
{ this.props.title }
"icon-remove closed-mask" onClick={()=>this.close()}>
"content">
{ this.props.children }
"mask-btns">
"btn-full-danger" onClick={()=>this.confirm()}>{ this.props.confirmText || '确定' }
{ this.props.showCancel && ("btn-border-danger" onClick={()=>this.close()}>取消) }
);
}
}
export default Modal
然后在container
的render()
函数里通过标签的方式引入,并通过点击触发。
import {React, component} from 'react';
import Modal from 'Modal';
class App extends Component {
render() {
}
}
export default App
你会发现,上面的代码并没有刻意地操作某个DOM元素的样式,而是通过改变组件的state
去触发自身的渲染函数。换句话说,我们不需要写繁琐的DOM操作,而是靠改变组件的state
控制组件的交互和各种变化。这种思维方式的好处等你熟悉React
之后自然会明白,可以大大地减少后期的代码量。
前面提到组件的state
改变即触发render()
,React
内部虽然做了一些算法上的优化,但是我们可以结合Immutable
做进一步的渲染优化,让页面更新渲染速度变得更快。
/**
* @desc PureRender 优化渲染
**/
import React, { Component } from 'react'
import Immutable from 'immutable';
export default {
// 深度比较
deepCompare: (self, nextProps, nextState) => {
return !Immutable.is(self.props, nextProps) || !Immutable.is(self.state, nextState)
},
// 阻止没必要的渲染
loadDetection: (reducers=[])=> {
for (let r of reducers) {
if (!r.get('preload')) return ()
}
}
}
这样我们在container
的render()
函数里就可以调用它进行渲染优化
import React, { Component } from 'react'
import PureRenderMixin from '../../mixins/PureRender';
class App extends Component {
render() {
let { actions, account, accountLogs, bankBind } = this.props;
// 数据导入检测
let error = PureRenderMixin.loadDetection([account, accountLogs, bankBind])
// 如果和上次没有差异就阻止组件重新渲染
if (error) return error
return (
<div>
// something ...
div>
);
}
}
其实Redux
最大的作用就是有效减少代码量,把繁琐的操作通过 action ----> reducer ----> store
进行抽象,最后维护统一的state
。对于页面的全局模块,简单地封装成mixin
来调用还是不够的,比如全局的request
模块,下面介绍如何用Redux
进行改造。
首先在types.js
里进行声明:
// request
export const REQUEST_PEDDING = 'REQUEST_PEDDING';
export const REQUEST_DONE = 'REQUEST_DONE';
export const REQUEST_ERROR = 'REQUEST_ERROR';
export const REQUEST_CLEAN = 'REQUEST_CLEAN';
export const REQUEST_SUCCESS = 'REQUEST_SUCCESS';
然后编写action
:
/**
* @desc 网络请求模块的actions
**/
// fetch 需要使用 Promise 的 polyfill
import {
pendingTask, // The action key for modifying loading state
begin, // The action value if a "long" running task begun
end // The action value if a "long" running task ended
} from 'react-redux-spinner';
import 'babel-polyfill'
import fetch from 'isomorphic-fetch'
import Immutable from 'immutable'
import * as CONFIG from './config'; //请求的配置文件
import * as TYPES from './types';
export function request(route, params, dispatch, success=null, error=null, { method='GET', headers={}, body=null } = {}) {
dispatch({type: TYPES.REQUEST_PEDDING, [ pendingTask ]: begin})
// 处理query
const p = params ? '?' + Object.entries(params).map( (i)=> `${i[0]}=${encodeURI(i[1])}` ).join('&') : ''
const uri = `${ CONFIG.API_URI }${ route }${ p }`
let data = {method: method, headers: headers}
if (method!='GET') data.body = body
fetch(uri, data)
.then((response) => {
dispatch({type: TYPES.REQUEST_DONE, [ pendingTask ]: end})
return response.json()
})
.then((data) => {
if (String(data.code) == '0') {
if (method !== 'GET' ) dispatch({type: TYPES.REQUEST_SUCCESS});
success && success(data);
} else {
console.log(data.error)
dispatch({type: TYPES.REQUEST_ERROR, ...data})
error && error(data)
}
})
.catch((error) => {
console.warn(error)
})
}
export function requestClean() {
return { type: TYPES.REQUEST_CLEAN }
}
然后编写对应的reducer
操作state
:
import Immutable from 'immutable';
import * as TYPES from '../actions/types';
import { createReducer } from 'redux-immutablejs'
export default createReducer(Immutable.fromJS({status: null, error: null}), {
[TYPES.REQUEST_ERROR]: (state, action) => {
return state.merge({
status: 'error',
code: action.code,
error: Immutable.fromJS(action.error),
})
},
[TYPES.REQUEST_CLEAN]: (state, action) => {
return state.merge({
status: null,
error: null,
})
},
[TYPES.REQUEST_SUCCESS]: (state, action) => {
return state.merge({
status: 'success',
error: null,
})
}
})
然后在reducers
的index.js
里对外暴露接口
export request from './request'
为什么要做这一步呢?因为我们需要在configureStore.js
里利用combineReducers
对所有的reducer
进行进一步的结合处理:
import { createStore, combineReducers, compose, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import * as reducers from './reducers'
import { routerReducer, routerMiddleware } from 'react-router-redux'
import { pendingTasksReducer } from 'react-redux-spinner'
export default function configureStore(history, initialState) {
const reducer = combineReducers({
...reducers,
routing: routerReducer,
pendingTasks: pendingTasksReducer,
})
const store = createStore(
reducer,
initialState,
compose(
applyMiddleware(
thunkMiddleware,
routerMiddleware(history)
)
)
)
return store
}
接下来就可以在container
里使用了,比如登录模块:
/**
* @desc 登录模块 container
* @createDate 2016-05-16
* @author Jafeney<692270687@qq.com>
**/
import React, { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { replace } from 'react-router-redux'
import { login } from '../../actions/user'
import { requestClean } from '../../actions/request'
import CheckUserMixin from '../../mixins/CheckUser'
import PureRenderMixin from '../../mixins/PureRender'
import '../style.less';
class Login extends Component {
constructor() {
super()
}
shouldComponentUpdate(nextProps, nextState) {
// 如果已经登录不触发深度比较
if (nextProps.user.getIn(['login', 'status'])=='logged') {
this.toMain()
return true
}
return PureRenderMixin.deepCompare(this, nextProps, nextState)
}
// 检查登录态
componentDidMount() {
let { user } = this.props;
if (CheckUserMixin.isLogged(user)) this.toMain()
}
// 初始化页面
toMain() {
this.props.actions.replace('/')
this.props.actions.requestClean()
}
// 执行登录
login() {
const userName = this.refs['J_username'].value, password = this.refs['J_password'].value
if (userName && password) {
this.props.actions.login({username: userName, password: password})
}
}
// 绑定回车事件
onEnter(event) {
var e = event || window.event || arguments.callee.caller.arguments[0];
if(e && e.keyCode==13) { // enter 键
this.login()
}
}
render() {
let { user } = this.props
return (
"wrapper" onKeyPress={()=>this.onEnter()}>
"containers">
"logo">
"content">
"header">会员登录
"mainer">
"input-group">
"J_username" type="text" placeholder="手机号码" className="input" />
"input-group">
"J_password" type="password" placeholder="登录密码" className="input" />
"input-group">
"J_login" onClick={()=>this.login()} className="login-btn">登录
"login-info">
"J_register" href="#/register" className="register">免费注册 |
"J_forget" href="#/password" className="forget">忘记密码 ?
"form-error">
{ user.getIn(['login', 'error', 'message']) }
)
}
}
// 下面是redux的核心方法
function mapStateToProps(state) {
return {
user: state.user
}
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators({ login, requestClean, replace }, dispatch) }
}
export default connect(mapStateToProps, mapDispatchToProps)(Login)
注意:通过以上方式,在组件内部
actions
里挂载的方法就可以通过this.props
取得了。