2018-09-29

dva


基于 redux、redux-saga 和 react-router 的轻量级前端框架。(Inspired by elm and choo)

2. API [#](#t12. API)

  • app = dva(opts) 创建应用,返回 dva 实例

  • app.use(hooks) 配置 hooks 或者注册插件

  • app.model(model) 注册 model

  • app.router(({ history, app }) => RouterConfig) 注册路由表

  • app.start(selector?) 启动应用。selector 可选

    import React from 'react';
    import dva, { connect } from 'dva';
    
    // 1. Initialize
    const app = dva();
    
    // 2. Model
    app.model({
      namespace: 'count',
      state: 0,
      reducers: {
        add  (count) { return count + 1 },
        minus(count) { return count - 1 },
      },
    });
    
    // 3. View
    const App = connect(({ count }) => ({
      count
    }))(function(props) {
      return (
        <div>
          <h2>{ props.count }</h2>
          <button key="add" onClick={() => { props.dispatch({type: 'count/add'})}}>+</button>
          <button key="minus" onClick={() => { props.dispatch({type: 'count/minus'})}}>-</button>
        </div>
      );
    });
    
    // 4. Router
    app.router(() => <App />);
    
    // 5. Start
    app.start('#root');
    

3. Model [#](#t23. Model)

model 是 dva 中最重要的概念

import React,{Fragment} from 'react';
import dva,{connect} from 'dva';
import {Route,Router} from 'dva/router';
const app=dva();
app.model({
    namespace: 'todos',
    state: {
        list: [],
        filter:'all'
    },
    reducers: {
        loaded(state,{payload:list}) {
            return {
                ...state,list:list
            }
        },
        added(state,{payload:todo}) {
            return {
                ...state,list: [...state.list,{...todo,id:Date.now(),completed:false}]
            }
        },
        toggled(state,{payload:id}) {
            return {
                ...state,
                list: state.list.map(item => {
                    if (item.id===id) {
                        item.completed=!item.completed;
                    }
                    return item;
                })
            }
        }
    },
    effects: {
        *load({},{put,call}) {
            const list=[{id:1,text:'1',completed:false},{id:2,text:'2',completed:false}];
            yield put({type:'loaded',payload:list});
        },
        *add({payload},{put,call}) {
            yield put({type:'added',payload});
        },
        *toggle({payload},{put,call}) {
            yield put({type:'toggled',payload});
        }
    },
    subscriptions: {
        setup({history,dispatch}) {
            history.listen(({pathname}) => {
                if (pathname=='/todos') {
                    dispatch({type:'load'});
                }
            });
        }
    }
});
const Home=() => <div>Home</div>
let Todos=({list,filter,toggle,add}) => (
        <div>
        <input ref={input => this.text=input} />
        <button onClick={() => {add(this.text.value);this.text.value = ''}}>+</button>
            <ul>
                {list.map(item => (
                <li>
                    <input type="checkbox" checked={item.completed} onChange={toggle.bind(null,item.id)}/>
                    {item.text}
                    </li>
                ))}
            </ul>
        </div>
)
let actions={
    add(text) {
        return {type:'todos/add',payload:{text}}
    },
    toggle(id) {
        return {type:'todos/toggle',payload:id}
    }
}
Todos=connect(
    state => ({
        list: state.todos.list.filter(todo => {
            switch (state.filter) {
                case 'completed':
                    return todo.completed;
                case 'uncompleted':
                    return !todo.completed
                default:
                    return true;
            }
        }),
        filter:state.filter
    }),
    actions
)(Todos);
app.router(({history,app}) => (
    <Router history={history}>
        <Fragment>
            <Route exact path="/" component={Home} />
            <Route path="/todos" component={Todos} />
        </Fragment>
    </Router>
));
app.start('#root');
  • namespace model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串
  • state 初始值
  • reducers 以 key/value 格式定义 reducer。用于处理同步操作,唯一可以修改 state 的地方。由 action 触发。
  • effects 以 key/value 格式定义 effect。用于处理异步操作和业务逻辑,不直接修改 state。由 action 触发,可以触发 action,可以和服务器交互,可以获取全局 state 的数据等等。
  • subscriptions 以 key/value 格式定义 subscription。subscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。在 app.start() 时被执行,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。

3. 测试鼠标点击速度的的应用 [#](#t33. 测试鼠标点击速度的的应用)

这是一个测试鼠标点击速度的 App,记录 1 秒内用户能最多点几次。顶部的 Highest Record 纪录最高速度;中间的是当前速度,给予即时反馈,让用户更有参与感;下方是供点击的按钮。

3.1 安装dva-cli [#](#t43.1 安装dva-cli)

npm install -g dva-cli
dva -v
dva-h

3.2 创建新应用 [#](#t53.2 创建新应用)

dva new counterApp --demo
cd counterApp
npm start

3.3 定义模型 [#](#t63.3 定义模型)

app.model({
  namespace: 'count',
  state: {
    record: 0,
    current: 0
  }
});
  • namespace 是 model state 在全局 state 所用的 key
  • state 是默认状态
  • state 里的 record 表示 highest record
  • current 表示当前速度

3.4 完成Component [#](#t73.4 完成Component)

CountApp\index.js

import styles from './index.less';
import { connect } from 'dva';
const CountApp = ({ record, current, dispatch }) => {
    return (
        <div className={styles.normal}>
            <div className={styles.record}>最高分:{record}</div>
            <div className={styles.current}>当前分:{current}</div>
            <div className={styles.button}>
                <button onClick={() => dispatch({ type: 'count/add' })}>+</button>
            </div>
        </div>
    )
}
export default connect(state => state.count)(CountApp);

CountApp\index.less

.normal{
    width:250px;
    border:1px solid #CCC;
    box-shadow: 1px 1px 1px 2px #CCC,-1px -1px 1px 2px #CCC;
    margin:50px auto;
    padding:50px;
    .record{
        font-size:18px;
        color:#CCC;
        text-align: left;
        border-bottom: 1px solid #CCC;
    }
    .current{
        height:150px;
        line-height: 150px;
        text-align: center;
    }
    .button{
        text-align: center;
        button{
            width:100px;
            height:50px;
            background-color: #CCC;
            color:#FFF;
        }
    }
}

3.5 更新state [#](#t83.5 更新state)

  • 更新 state 是通过 reducers 处理的

  • reducer 是唯一可以更新 state 的地方,这个唯一性让我们的 App 更具可预测性,所有的数据修改都有据可查。reducer 是 pure function,他接收参数 state 和 action,返回新的 state,通过语句表达即 (state, action) => newState。

    请注意,这里的 add 和 minus 两个action,在 count model 的定义中是不需要加 namespace 前缀的,但是在自身模型以外是需要加 model 的 namespace

    app.model({
      namespace: 'count',
      state: {
        record: 0,
        current: 0
      },
      reducers: {
        add(state) {
          const newCurrent = state.current + 1;
          return {
            ...state,
            record: newCurrent > state.record ? newCurrent : state.record,
            current: newCurrent
          }
        },
        minus(state) {
          return { ...state, current: state.current - 1 };
        }
      }
    });
    

3.6 异步处理 [#](#t93.6 异步处理)

  • dva 通过对 model 增加 effects 属性来处理 side effect(异步任务)

  • 这是基于 redux-saga 实现的,语法为 generator

  • 当用户点 + 按钮,数值加 1 之后,会额外触发一个 side effect,即延迟 1 秒之后数值减1 。

    effects: {
      *add(action, { call, put }) {
        yield call(delay, 1000);
        yield put({ type: 'minus' });
      }
    },
    
  • call 和 put 都是 redux-saga 的 effects,call 表示调用异步函数,put 表示 dispatch action,其他的还有 select, take, fork, cancel 等,详见 redux-saga

3.7 订阅键盘事件 [#](#t103.7 订阅键盘事件)

ubscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。

  • keymaster

    subscriptions: {
        keyboard({ dispatch }) {
          key('space', () => dispatch({ type: 'add' }));
        }
      }
    

3.8 构建应用 [#](#t113.8 构建应用)

$ npm run build

4. 参考 [#](#t124. 参考)

Gitalking ...

Markdown is supported

Be the first guy leaving a comment!