dva

484 阅读7分钟

###简介

dva是阿里做的基于 redux、redux-saga 和 react-router 的轻量级前端框架

概念

API

  • app = dva(opts) 创建应用,返回 dva 实例
  • app.use(hooks) 配置 hooks 或者注册插件,中间件
  • app.model(model) 注册 model 模型
  • app.router(({ history, app }) => RouterConfig) 注册路由表
  • app.start(selector?) 启动应用。selector 可选

他基于redux、redux-saga 和 react-router做了简单的封装,其实并没有什么新的东西

下面我们间的应用一下

首先需要安装 npm i dva -s

src/index.js

//connect react-redux
import React, { Component } from 'react';
import dva,{connect} from 'dva';
let app = dva();
// combineReducers({
//     counter:counter
// });
//状态state是合并后的状态树
//配置模型
app.model({
    //命令空间,它就是以前的combineReducers里面的key
    namespace: 'counter',
    state: {
        number: 0
    },
    //这里可以定义子reducer,它可以而且只能由它来修改状态
    reducers: {
        //这个名字是有意义的,如果派发一个counter/add,就会执行此reducer
        add(state,action){
            return {number: state.number + 1}
        },
        minus(state, action) {
            return { number: state.number - 1 };
            //...state, current: state.current - 1 
        }
    },
});
//在dva里面所有组件没有状态,都是参数,基本都用函数声明
function Counter({number,add}) {
    return (
        <div>
            <p>{number}</p>
            <button onClick={add}>+</button>
        </div>
    )
}
let actions = {
    add() {
        //dispatch({ type: 'counter/add' });
        return { type: 'counter/add' };
    }
}
let mapStateToProps = state => state.counter;
let ConnectedCounter = connect(
    mapStateToProps,
    actions
)(Counter);
//定义路由
app.router(({ history, app }) => (
    <ConnectedCounter />
));
app.start('#root');

eg2 记录一个当前值和最大值 记录一S能点多少次

src/index.js

//connect react-redux
import React, { Component } from 'react';
import dva,{connect} from 'dva';
let app = dva();
//配置模型
app.model({
    namespace: 'counter',
    state: {
        highest: 0,//最大值
        current: 0//当前值
    },
    reducers: {
        increment(state,action){
            let newCurrent = state.current + 1;
            return {
                ...state,
                current : newCurrent,
                // 新值大雨老得记录最大值就用新值,反之就用最大值
                highest : newCurrent > state.highest ? newCurrent : state.highest
            }
        },
        minus(state, action) {
            //一秒之后自动减1
            return { ...state,current: state.current - 1 };
            //...state, current: state.current - 1 
        }
    },
    //这个对象里放的是副作用,放的是generator
    // effects = redux-saga/effects
    effects: {
        *increment(action, { call, put }) {
            //call表示调用一个异步任务
            yield call(delay, 1000);
            //在model里派发动作的话是不需要加counter或者说namespace前缀的
            yield put({ type: 'minus' });
        }
    }
});
function delay(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, ms);
    });
}
//在dva里面所有组件没有状态,都是参数,基本都用函数声明
function Counter({highest, current, add}) {
    return (
        <div style={{ border: '1px solid red', textAlign: 'center', width: 200, height: 200, margin: '20px auto' }}>
            <p>最大值:{highest}</p>
            <p>当前值:{current}</p>
            <button onClick={add}>+</button>
        </div>
    )
}
let actions = {
    add() {
        return { type: 'counter/increment' };
    }
}
let mapStateToProps = state => state.counter;
let ConnectedCounter = connect(
    mapStateToProps,
    actions
)(Counter);
//定义路由
app.router(({ history, app }) => (
    <ConnectedCounter />
));
app.start('#root');

dva生成项目

dva可以创建新项目 dva new name --demo demo是自己版本,不加是正式版本

src/index.js

import React, { Component } from 'react';
import dva, { connect } from 'dva';
import { Router, Route, Switch, Link } from 'dva/router';
let todos = [{ id: 1, text: '1', completed: false }, { id: 2, text: '2', completed: true }];
let api = {
  load() {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(todos);
      }, 1000);
    });
  },
  add(todo) {
    return new Promise((resolve) => {
      setTimeout(() => {
        todos = [...todos, todo];
        resolve(todos);
      }, 1000);
    });
  },
  toggle(id) {
    return new Promise((resolve) => {
      setTimeout(() => { 
        todos = todos.map(todo => {
          if (todo.id === id) {
            todo.completed = !todo.completed;
          }
          return todo;
        });
        resolve(todos);
      }, 1000);
    });
  },
  del(id) {
    return new Promise((resolve) => {
      setTimeout(() => {
        todos = todos.filter(item => item.id != id);
        resolve(todos);
      }, 1000);
    });
  }
}
//路由器,规则,初始化
// 1. Initialize
const app = dva();
app.model({
  namespace:'filter',
  
});
// 2. Model
app.model({
  namespace: 'todos',
  state: {
    list: [],//todos的数据
    filter: 'all'//过滤的类型
  },
  reducers: {
    loaded(state, { list }) {
      return {
        ...state,
        list
      }
    },
    changeFilter(state, { filter }) {
      return {
        ...state,
        filter
      }
    }
  },
  effects: {
    *load(action, { put, call, select }) {
      let list = yield call(api.load);
      yield put({ type: 'loaded', list });
    },
    //切换ID对应的todo的完成状态
    *toggle({ id }, { put, call, select }) {
      let list = yield call(api.toggle, id);
      yield put({ type: 'loaded', list });
    },
    *del({ id }, { put, call }) {
      let list = yield call(api.del, id);
      yield put({ type: 'loaded', list });
    },
    *add({ todo }, { call, put }) {
      let list = yield call(api.add, todo);
      yield put({ type: 'loaded', list });
    }
  },
  //监听当url切换到todo的时候,当切过来的时候调用后台接口一步家在数据
  subscriptions: {
    //history操作历史  dispatch 派发动作
    setup({ history, dispatch }) {
      //当函数发生变化的时候会执行此回调函数,并传入location
      history.listen(({ pathname }) => {
        if (pathname == '/todos') {
          //如果要执行一步任务的话肯定要把动作派发给effects
          dispatch({ type: 'load' });
        }
      });
    }
  }
});

class Todos extends Component{
  render() {
    let { list, toggle, add, changeFilter, filter, del } = this.props;
    return (
      <div>
        <input type="text" ref={input => this.text = input} />
        <button onClick={() => { add(this.text.value); this.text.value = '' }}>增加</button>      
        <ul>
          {
            list.map(todo => (
              <li>
                <input type="checkbox"
                onChange={() => toggle(todo.id)} 
                checked={todo.completed} />
                <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>{todo.text}</span>
                <button onClick={del.bind(this, todo.id)}>删除</button>
              </li>
            ))
          }
        </ul>
        <div>
          <button style={{ color: filter == 'uncompleted' && 'red' }} onClick={() => changeFilter('uncompleted')}>未完成</button>
          <button style={{ color: filter == 'completed' && 'red' }} onClick={() => changeFilter('completed')}>已完成</button>
          <button style={{ color: filter == 'all' && 'red' }} onClick={() => changeFilter('all')}>全部</button>
        </div>
      </div>
    )
  }
}
let actions = {
  toggle(id) {
    return { type: 'todos/toggle', id };
  },
  add(text) {
    return { type: 'todos/add', todo: { id: Date.now(), text, completed: false } };
  },
  changeFilter(filter) {
    return { type: 'todos/changeFilter', filter };
  },
  del(id) {
    return { type: 'todos/del', id };
  }
}
let WrappedTodos = connect(
  state => ({
    filter: state.todos.filter,
    list: state.todos.list.filter(item => {
      switch (state.todos.filter) {
        case 'completed':
        return item.completed;
        case 'uncompleted':
          return !item.completed;
        default:
          return true;
      }
    })
  }),
  actions
)(Todos);
// 3. Router
const HomePage = () => <div>Hello Dva.</div>;
  //history是hashHistory和browserHistory的封装,是用来跳转路径的history.push() history.li
app.router(({ history }) =>
  <Router history={history}>
  <div>
    <Link to="/">首页</Link>
    <Link to="/todos">todos</Link>
    <Switch>
      <Route exact path="/" exact component={HomePage} />
      <Route path="/todos" exact component={WrappedTodos} />
    </Switch>
  </div>
  </Router>
);

// 4. Start
app.start('#root');

modal可以重复调用,我们把一个状态拆分出来

import ...
let todos...
let api = ...
const app = dva();
app.model({
  namespace: 'filter',
  state: 'all',
  reducers: {
    changeFilter(state, { filter }) {
      return filter;
    }
  }
});
// 2. Model
app.model({
  namespace: 'todos',
  state: {
    list: [],//todos的数据
    // filter: 'all'//过滤的类型
  },
  reducers: {
    loaded...
    // changeFilter(state, { filter }) {
    //   return {
    //     ...state,
    //     filter
    //   }
    // }
  },
  effects: ...
  //监听当url切换到todo的时候,当切过来的时候调用后台接口一步家在数据
  subscriptions: ...
}

class Todos ...
let actions = {
  changeFilter(filter) {
    // return { type: 'todos/changeFilter', filter };
    return { type: 'filter/changeFilter', filter };
  }
}
let WrappedTodos = connect(
  state => ({
    filter: state.filter,
    list: state.todos.list.filter(item => {
      switch (state.filter) {
        ...
      }
    })
  }),
  actions
)(Todos);
...
app.start('#root');

结合immutable

import React, { Component } from 'react';
import dva, { connect } from 'dva';
import { Router, Route, Switch, Link } from 'dva/router';
import immutable, { Map, fromJS } from 'immutable';

let todos = [{ id: 1, text: '1', completed: false }, { id: 2, text: '2', completed: true }];
let api = {
  load() {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(todos);
      }, 1000);
    });
  },
  add(todo) {
    return new Promise((resolve) => {
      setTimeout(() => {
        todos = [...todos, todo];
        resolve(todos);
      }, 1000);
    });
  },
  toggle(id) {
    return new Promise((resolve) => {
      setTimeout(() => { 
        todos = todos.map(todo => {
          if (todo.id === id) {
            todo.completed = !todo.completed;
          }
          return todo;
        });
        resolve(todos);
      }, 1000);
    });
  },
  del(id) {
    return new Promise((resolve) => {
      setTimeout(() => {
        todos = todos.filter(item => item.id != id);
        resolve(todos);
      }, 1000);
    });
  }
}
//路由器,规则,初始化
// 1. Initialize
const app = dva();
app.model({
  namespace: 'filter',
  state: 'all',
  reducers: {
    changeFilter(state, { filter }) {
      return filter;
    }
  }
});
// 2. Model
app.model({
  namespace: 'todos',
  //state对象可以包一下
  state: fromJS({list: []}),
  reducers: {
    loaded(state, { list }) {
      // return {...state, list}
      return fromJS({ list });
      // return Map({ list: Map(list)});
    },
    // changeFilter(state, { filter }) {
    //   return {
    //     ...state,
    //     filter
    //   }
    // }
  },
  effects: {
    *load(action, { put, call, select }) {
      let list = yield call(api.load);
      yield put({ type: 'loaded', list });
    },
    //切换ID对应的todo的完成状态
    *toggle({ id }, { put, call, select }) {
      let list = yield call(api.toggle, id);
      yield put({ type: 'loaded', list });
    },
    *del({ id }, { put, call }) {
      let list = yield call(api.del, id);
      yield put({ type: 'loaded', list });
    },
    *add({ todo }, { call, put }) {
      let list = yield call(api.add, todo);
      yield put({ type: 'loaded', list });
    }
  },
  //监听当url切换到todo的时候,当切过来的时候调用后台接口一步家在数据
  subscriptions: {
    //实现钩子函数的作用
    //history操作历史  dispatch 派发动作
    setup({ history, dispatch }) {
      //当函数发生变化的时候会执行此回调函数,并传入location
      history.listen(({ pathname }) => {
        if (pathname == '/todos') {
          //如果要执行一步任务的话肯定要把动作派发给effects
          dispatch({ type: 'load' });
        }
      });
    }
  }
});

class Todos extends Component{
  render() {
    let { list, toggle, add, changeFilter, filter, del } = this.props;
    return (
      <div>
        <input type="text" ref={input => this.text = input} />
        <button onClick={() => { add(this.text.value); this.text.value = '' }}>增加</button>      
        <ul>
          {
            list.map(todo => (
              <li>
                <input type="checkbox"
                onChange={() => toggle(todo.id)} 
                checked={todo.completed} />
                <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>{todo.get('text')}</span>
                <button onClick={del.bind(this, todo.get('id'))}>删除</button>
              </li>
            ))
          }
        </ul>
        <div>
          <button style={{ color: filter == 'uncompleted' && 'red' }} onClick={() => changeFilter('uncompleted')}>未完成</button>
          <button style={{ color: filter == 'completed' && 'red' }} onClick={() => changeFilter('completed')}>已完成</button>
          <button style={{ color: filter == 'all' && 'red' }} onClick={() => changeFilter('all')}>全部</button>
        </div>
      </div>
    )
  }
}
let actions = {
  toggle(id) {
    return { type: 'todos/toggle', id };
  },
  add(text) {
    return { type: 'todos/add', todo: { id: Date.now(), text, completed: false } };
  },
  changeFilter(filter) {
    // return { type: 'todos/changeFilter', filter };
    return { type: 'filter/changeFilter', filter };
  },
  del(id) {
    return { type: 'todos/del', id };
  }
}
let WrappedTodos = connect(
  state => ({
    filter: state.filter,
    list: state.todos.get('list').filter(item => {
      switch (state.filter) {
        case 'completed':
        return item.completed;
        case 'uncompleted':
          return !item.completed;
        default:
          return true;
      }
    })
  }),
  actions
)(Todos);
// 3. Router
const HomePage = () => <div>Hello Dva.</div>;
  //history是hashHistory和browserHistory的封装,是用来跳转路径的history.push() history.li
app.router(({ history }) =>
  <Router history={history}>
  <div>
    <Link to="/">首页</Link>
    <Link to="/todos">todos</Link>
    <Switch>
      <Route exact path="/" exact component={HomePage} />
      <Route path="/todos" exact component={WrappedTodos} />
    </Switch>
  </div>
  </Router>
);

// 4. Start
app.start('#root');