05月22, 2017

redux笔记

近期后端有一个项目,要添加一些功能和页面,以及对之前UI的一些优化。

就方案来说,一般是两种:

1、在之前代码上重构,唯一的缺点,就是需要前端有一个JAVA环境,因为代码是放在后端目录下的。 2、整个推翻重来,前后各自的目录分开。

就1而言,我觉得在不考虑低版本浏览器的情况下,可能使用vue + elementUI会是一个不错的选择。

在一番PK之后,最终选择了2的方案,于是乎开始纠结用Vue还是React。由于部门已经用了太多的技术栈(Backbone、bootstrap、jquery、React),最终还是决定用React,理由是再学一门,将来维护起来心累。

对我而言,这也是一个挑战,毕竟我之前只是做了react的边角代码,并没有深入到里面去,比如说像redux,并没有使用过。

当然redux坑还是蛮多的,比如说:

由于Redux的理念非常精简,没有追求大而全,这份架构上的优雅却在某种程度上伤害了使用体验:不能开箱即用,甚至是异步这种最常见的场景也要借助社区方案。

于是乎业内会有一些其他的方案出来,像dva之类的,不过我个人还是建议先踩一下redux吧,这样一来,对自身的成长也是一个帮助。

我现在学一门技术,会先到阮一峰的博客去翻一下。老实说,他写的入门教程还是相当不错的:

看完上面三篇,会大概对redux有了一个概念,下面要做的就是实践了,我们来简单做一个登录成功将用户名保存到store的功能。

第一步:写一个reducer

// 用户名
const initialState = {
    name: ""
};

export default function (state = initialState, action) {
    switch (action.type) {
        case 'SET_USER_NAME':
            return {
                ...state,
                name: action.payload
            }
        default:
            return state
    }
};

这里的代码约束了dispatch的格式必须如下:

{
       type: "xxxx",
       payload: "xxx"
}

当然payload不一定是数组,也可以是对象。

第二步:使用reducer

import reducers from 'REDUCERS'; 

const store = createStore(
    reducers,
    applyMiddleware(logger)
);

class Root extends React.Component {
    render() {
        return (
            <Provider store={store}>
                <Router history={browserHistory}>
                    {routers}
                </Router>
            </Provider>
        );
    }
}

第三步:dispatch action

class Login extends React.Component {
    handleSubmit = (e) => {
        let _this = this;
        e.preventDefault();
        this.props.form.validateFields((err, values) => {
            if (!err) {
                requestLogin(values).then((res) => {
                    _this.props.dispatch({ type: "SET_USER_NAME", payload: "xxxx" });
                })
            }
        });
    };
    render() {
        const { getFieldDecorator } = this.props.form;
        return (
            <div className="login">
                <div className="login-form" >
                    <Form onSubmit={this.handleSubmit} style={{maxWidth: '300px'}}>
                        <FormItem>
                            {getFieldDecorator('name', {
                                rules: [{ required: true, message: '请输入用户名!' }],
                            })(
                                <Input prefix={<Icon type="user" style={{ fontSize: 13 }} />} placeholder="用户名" />
                            )}
                        </FormItem>
                        <FormItem>
                            {getFieldDecorator('password', {
                                rules: [{ required: true, message: '请输入密码!' }],
                            })(
                                <Input prefix={<Icon type="lock" style={{ fontSize: 13 }} />} type="password" placeholder="密码" />
                            )}
                        </FormItem>
                        <FormItem>
                            {getFieldDecorator('remember', {
                                valuePropName: 'checked',
                                initialValue: true
                            })(
                                <Checkbox>记住我</Checkbox>
                            )}
                            <Button type="primary" htmlType="submit" className="login-form-button" style={{width: '100%'}}>
                                登录
                            </Button>
                        </FormItem>
                    </Form>
                </div>
            </div>

        );
    }
}

export default connect()(Form.create()(Login));

这里需要用到connect,然后我们就可以拿到props里面的dispatch,通过它来发起一个action。

第四步:其他组件拿到store里面的值

function select(state) {
    const { name } = state.userInfo
    return {
        name
    }
}

@connect(select)
class Main extends Component {
       render() {
             return (
                    <div>
                          this.props.name
                    </div>
              );
    }
}

这里借助connect的第一个参数,将state转成props往下传递。

思考

问题1、每次dispatch都是写一个json对象,然后type写的到处都是,有没有办法维护?

问题2、异步action怎么处理?(redux-thunk,但书写很累)

redux-actions

redux-action

看介绍:Flux Standard Action utilities for Redux,用它就能帮助我们解决上面的问题1。代码如下:

import { createAction } from 'redux-actions';

const SET_USER_NAME = 'SET_USER_NAME';

const setUserName = createAction(SET_USER_NAME);

export { setUserName };
// dispatch
this.props.dispatch(setUserName("xxxx"));

redux-thunk-actions

redux-thunk-actions,可以用来简化redux-thunk

使用redux-thunk,代码如下:

function myFetch() {
  // instead of an object, you can return a function
  return (dispatch) => {
    dispatch({type: 'MY_FETCH_START'});
    try {
      //we can do async and then dispatch more stuff
      await api.fetch();
    }
    catch(e) {
      return dispatch({type: 'MY_FETCH_FAIL', error: e});
    }
    dispatch({type: 'MY_FETCH_END'});
  }
}
dispatch(myFetch());

而使用redux-thunk-actions,代码只需要如下就行了:

let myFetch = createActionThunk('MY_FETCH', () => api.fetch());

它对应四个状态:MY_FETCH_STARTED、MY_FETCH_SUCCEEDED、MY_FETCH_FAILED、MY_FETCH_ENDED,但我个人觉得最后一个状态其实没啥卵用。

通用Loading及异常怎样处理?

按上面的写法,比如说处理loading的问题,有四个组件,就要维护4份loading,包括对异步的处理,其实是挺累的。

当然如果redux-thunk-actions提供出中间件的写法,就可以顺利解决上面的问题。

在翻各种资料时,让我找到了redux-action-tools

它的方案提供了中间件,代码如下:

import _ from 'lodash'
import { ASYNC_PHASES } from 'redux-action-tools'

function loadingMiddleWare({dispatch}) {
    return next => action => {
        const asyncPhase = _.get(action, 'meta.asyncPhase');
        const omitLoading = _.get(action, 'meta.omitLoading');

        if (!asyncPhase || omitLoading) return next(action);

        dispatch({
            type: asyncPhase === ASYNC_PHASES.START ? 'ASYNC_STARTED' : 'ASYNC_ENDED',
            payload: {
                action
            }
        })

        return next(action);
    }
}

export default loadingMiddleWare;

我们只要维护一个公共的reducer就行了:

// 公共
const initialState = {
    loading: false
};

export default function (state = initialState, action) {
    switch (action.type) {
        case 'ASYNC_STARTED':
            return {
                ...state,
                loading: true
            }
        case 'ASYNC_ENDED':
            return {
                ...state,
                loading: false
            }
        default:
            return state
    }
};

异常fail的情况:

const fetchAppList = createAsyncAction(APP_LIST, (todo, dispatch, getState) => {
    return new Promise((resolve, reject) => {
            setTimeout(()=>{
                   reject(new Error('我失败了'))
            }, 3000)
    })
})

使用reject而不是throw

代码会在catch中,dispatch一个action。

路由简单说明

react-router比较推荐使用browerHistory,而不是hashHistory。而且react-router3.x还有一个bug:

react-router3.x hashHistory render两次的bug,及解决方案

当然有人会说,升级到4嘛,其实我觉得每次升级,都得看一下它的changelog,不然被坑到了,都不知道。。

另外我喜欢上browerHistory的原因之一在于,它可以基于resful的URL,比如说列表第一页:

www.xxx.com/list/1
www.xxx.com/list/2
...

之前用query param的方式,实在太挫比了。。当然用browerHistory,nginx得要配置一下:

server {
    listen 80;
    server_name 127.0.0.1;
    index  index.html;
    root /Users/zhangpu/Documents/antd-admin/;
    location / {
        try_files $uri $uri/ /index.html;
    }
}

再将特定的,比如说api的接口转发出去即可。

其他

其实我上面举的login成功,将用户名dispatch到store里面,是有问题的。。因为当用户刷新浏览器后,用户名会丢失。。当然我只是举个例子而已,所以不要太较真。

另外,从代码可以看出,redux的dispatch是没有命名空间的。当然前期统一了命名方面,其实问题也不是太大。

噢对,在往下传递数据给component时(即container流向component),需要注意的是给component加一个“shouldPureComponentUpdate”。因为不管props的值是否发生变化(即前一次得到的值是2,后一次得到的值也是2),子组件都会更新(默认shouldComponentUpdate为true),所以需要简单判断一下是否相同,相同的话,就不做更新了。

感觉下来,redux其实也不难,难的是它实在太简单了,需要自己去总结一些方案,可以说痛并快乐着。

最后感谢前人总结的一些坑,可以让我们这些后来者迅速地进行开发。。

本文链接:www.my-fe.pub/post/about-redux.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。