基于Redux架构的单页应用开发总结(三)

写在前面

这篇具体讲讲Redux使用的一些技巧

React式编程思维

在没有遁入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的设计是基于组件化的,每个组件通过生命周期维护统一的statestate改变,组件便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 = [
            '<div class="mask" id="J_mask">',
                '<div class="modal-box">',
                    '<h2>',
                        title,
                    '</h2>',
                    '<p>',
                        content,
                    '</p>',
                    '<div class="mask-btns">',
                        '<span id="J_cancel">取消</span>',
                        '<span id="J_confirm">确定</span>',
                    '</div>',
                '</div>',
            '</div>',
        ].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 (
             <div className={this.state.jsMask}>
                <div className="modal-box" style={this.props.style}>
                    <div className="header">
                        <h3>{ this.props.title }</h3>
                        <span className="icon-remove closed-mask" onClick={()=>this.close()}></span>
                    </div>
                    <div className="content">
                        { this.props.children }
                    </div>
                    <div className="mask-btns">
                        <span className="btn-full-danger" onClick={()=>this.confirm()}>{ this.props.confirmText || '确定' }</span>
                        { this.props.showCancel && (<span className="btn-border-danger" onClick={()=>this.close()}>取消</span>) }
                    </div>
                </div>
             </div>
         );
     }
}
export default Modal

然后在containerrender()函数里通过标签的方式引入,并通过点击触发。

import {React, component} from 'react'; 
import Modal from 'Modal';

class App extends Component {
    render() {
       <div>
            <button onClick = {()=> {this.refs.modal.show()}}
            <Modal title={"提示"} 
                   style={{width: 420, height: 200}}
                   ref={(ref)=> this.modal = ref} 
                   onConfirm={()=>this.onModalConfirm()}>
                   <p className="tips">Hello world!</p>
            </Modal>
       </div>
    }
}

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 (<div />)
        }
    }
}

这样我们在containerrender()函数里就可以调用它进行渲染优化

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,
    })
  }
})

然后在reducersindex.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 (
            <div className="wrapper" onKeyPress={()=>this.onEnter()}>
                <div className="containers">
                    <div className="logo"></div>
                    <div className="content">
                        <div className="header">会员登录</div>
                        <div className="mainer">
                            <div className="input-group">
                                <input ref="J_username" type="text" placeholder="手机号码" className="input" />
                                <label className="check-info" ref="J_username-check"></label>
                            </div>
                            <div className="input-group">
                                <input ref="J_password" type="password" placeholder="登录密码" className="input" />
                                <label className="check-info" ref="J_password-check"></label>
                            </div>
                            <div className="input-group">
                                <span ref="J_login" onClick={()=>this.login()} className="login-btn">登录</span>
                                <span className="login-info">
                                    <a ref="J_register" href="#/register" className="register">免费注册</a> |
                                    <a ref="J_forget" href="#/password" className="forget">忘记密码 ?</a>
                                </span>
                            </div>
                            <div className="form-error">
                                { user.getIn(['login', 'error', 'message']) }
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        )
    }
}

// 下面是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取得了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值