1.dva介绍 #

dva是一个基于 reduxredux-saga 的轻量级前端框架,它大大简化了数据流管理的复杂度,并提供了一种更高效的方式来组织和管理代码。dva 的名字来源于 Dota 2 的一个英雄——D.Va。

以下是关于 dva 的详细介绍:

1. 主要特点 #

2. 核心概念 #

dva 是一个非常强大且易于上手的前端框架,它整合了 redux, redux-sagareact-router,使得开发者可以更加集中精力于业务逻辑,而不是数据流或路由配置的复杂性。

dva

2. model #

dva 是一个基于 redux、redux-saga 和 react-router 的轻量级框架,它简化了数据流管理的模型,并整合了多个库的功能,使得开发者可以使用一种更加统一和简洁的方式来构建应用程序。

其中,modeldva 的核心概念,表示存储的数据模型。dvamodel 封装了 Redux 中的 reducers、actions 以及对应的 side effects。

一个典型的 dva model 定义如下:

{
  namespace: 'example',       // 当前 Model 的名称。整个应用的 state 中,该 Model 的 state 会被放入 state.example。
  state: {},                  // 该 Model 当前的状态。数据保存在这里,起到了 `store` 的作用。
  reducers: {                 // Action 处理器,处理同步动作,用来算出最新的 State。
    delete(state, action) {
      return { ...state, ...action.payload };
    },
  },
  effects: {                  // Action 处理器,处理异步动作。
    *fetch(action, { call, put }) {
      const data = yield call(service.fetch, action.payload);
      yield put({ type: 'delete', payload: data });
    },
  },
  subscriptions: {           // 订阅源,它们的来源仅仅是当前的 global state。
    setup({ dispatch, history }) {
      history.listen(({ pathname }) => {
        if (pathname === '/users') {
          dispatch({ type: 'fetch' });
        }
      });
    },
  },
}

以下是 model 方法中各属性的详细解释:

  1. namespace:Model 的命名空间,它的值在全局 state 上是唯一的。
  2. state:Model 的初始值,在 store 中会以 namespace 为 key 存储。
  3. reducers:等同于 Redux 中的 reducer,接收 action 和当前的 state 作为参数,返回一个新的 state。注意,它们只能返回 state,不能处理其他的 effects。
  4. effects:基于 redux-saga 的 effect,用于处理异步操作和业务逻辑。它们内部可以用 yield 语法调用其他 effects、发起 action 请求、调用异步函数等。
  5. subscriptions:是一个对象,它的每个属性都是一个函数,这些函数可以接收 dispatchhistory 两个参数,用于监听数据源并根据需要 dispatch 相应的 action。数据源可以是当前的位置信息、Websocket 连接、keyboard 输入、geolocation 变化等。

使用 dva 的 model 方法,开发者可以更高效地组织和管理应用的状态和逻辑,而不需要深入理解 Redux 和 redux-saga 的复杂性。

2.1 index.js #

// 导入React库
import React from 'react';
// 导入dva库和connect方法
import dva, { connect } from 'dva';

// 创建一个dva应用实例
const app = dva();

// 为应用添加一个model
app.model({
    // model的命名空间
    namespace: 'counter',
    // model的初始状态
    state: { number: 0 },
    // 定义model中的reducers来处理state变化
    reducers: {
        // 定义一个add的reducer来增加计数值
        add(state) {
            // 返回更新后的状态值
            return { number: state.number + 1 };
        }
    }
});

// 定义Counter组件
function Counter(props) {
    // 返回组件的渲染内容
    return (
        <div>
            // 显示计数器的值
            <p>{props.number}</p>
            // 添加一个按钮,点击时派发一个“counter/add”的action
            <button onClick={() => props.dispatch({ type: "counter/add" })}>+</button>
        </div>
    )
}

// 使用connect方法连接Counter组件和Redux store
const ConnectedCounter = connect(
    // 将state映射到Counter组件的props
    (state) => state.counter
)(Counter);

// 定义应用的路由配置
app.router(() => <ConnectedCounter />);
// 启动dva应用并挂载到id为root的DOM元素上
app.start('#root');

3. 支持effects #

3.1 src\index.js #

src\index.js

import React from 'react';
import dva, { connect } from 'dva';
+function delay(ms) {
+    return new Promise((resolve) => {
+        setTimeout(function () {
+            resolve();
+        }, ms);
+    });
+}
const app = dva();
app.model({
    namespace: 'counter',
    state: { number: 0 },
    reducers: {
        add(state) {
            return { number: state.number + 1 };
        }
    },
+    effects: {
+        *asyncAdd(action, { call, put }) {
+            yield call(delay, 1000);
+            yield put({ type: 'add' });
+        }
+    }
});
function Counter(props) {
    return (
        <div>
            <p>{props.number}</p>
            <button onClick={() => props.dispatch({ type: "counter/add" })}>+</button>
+            <button onClick={() => props.dispatch({ type: "counter/asyncAdd" })}>异步+</button>
        </div>
    )
}
const ConnectedCounter = connect(
    (state) => state.counter
)(Counter
);
app.router(() => <ConnectedCounter />);
app.start('#root');

4. 支持路由 #

dva 是一个基于 redux 和 redux-saga 的数据流方案,同时它还内置了 react-router 和 fetch,所以你可以认为它是一个针对 react 的轻量级应用框架。

dva 中,router 是用来定义路由的。路由是UI与URL之间的映射关系,用于定义一个应用的导航结构。

4.1 src\index.js #

src\index.js

import React from 'react';
import dva, { connect } from 'dva';
import { HashRouter, Switch, Route } from 'dva/router';
function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(function () {
            resolve();
        }, ms);
    });
}
const app = dva();
app.model({
    namespace: 'counter',
    state: { number: 0 },
    reducers: {
        add(state) {
            return { number: state.number + 1 };
        }
    },
    effects: {
        *asyncAdd(action, { call, put }) {
            yield call(delay, 1000);
            yield put({ type: 'add' });
        }
    }
});
function Counter(props) {
    return (
        <div>
            <p>{props.number}</p>
            <button onClick={() => props.dispatch({ type: "counter/add" })}>+</button>
            <button onClick={() => props.dispatch({ type: "counter/asyncAdd" })}>异步+</button>
        </div>
    )
}
const ConnectedCounter = connect(
    (state) => state.counter
)(Counter
);
const Home = () => <div>Home</div>
app.router(() => (
    <HashRouter>
        <>
            <ul>
                    <li><Link to="/">Home</Link></li>
                    <li><Link to="/counter">Counter</Link></li>
            </ul>
            <Switch>
                <Route path="/" exact={true} component={Home} />
                <Route path="/counter" component={ConnectedCounter} />
            </Switch>
        </>
    </HashRouter>
));
app.start('#root');

5. 跳转路径 #

dva框架中,routerRedux 是一个帮助你进行路由跳转和查询操作的工具。它实际上是对react-router-redux库的一个简化封装,提供了在Redux Saga或Redux中进行路由操作的能力。

其中,routerRedux.push 是该工具的一个方法,允许你进行路由跳转。它的工作方式与react-routerhistory.push相似。

使用方法:

  1. 在model的effects中使用:

当你希望在某个异步操作后进行路由跳转时,可以在effects中使用routerRedux.push

import { routerRedux } from 'dva/router';

effects: {
  *submitLogin({ payload }, { call, put }) {
    const response = yield call(loginService, payload);
    if (response.status === 'ok') {
      yield put(routerRedux.push('/dashboard'));
    } else {
      // 处理错误
    }
  },
},

在上述代码中,假设登录服务成功返回,用户将被重定向到仪表板页面。

  1. 在组件中使用:

在组件中,你通常不直接使用routerRedux.push,但可以通过dispatch来触发一个action,然后在对应的model的effects中进行路由跳转。

function MyComponent({ dispatch }) {
  const goToDashboard = () => {
    dispatch(routerRedux.push('/dashboard'));
  }

  return (
    <button onClick={goToDashboard}>Go to Dashboard</button>
  );
}

参数:

routerRedux.push接受与history.push相同的参数,这意味着你可以传递一个路径字符串或一个location对象。

例如:

总结:

routerRedux.pushdva框架中提供了一种在Redux中进行路由操作的方法,这使得路由跳转可以像其他副作用操作一样在effects中处理,保持了代码的一致性和清晰度。

5.1 src\index.js #

src\index.js

import React from 'react';
import dva, { connect } from 'dva';
import { HashRouter, Link, Switch, Route, routerRedux } from 'dva/router';
function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(function () {
            resolve();
        }, ms);
    });
}
const app = dva();
app.model({
    namespace: 'counter',
    state: { number: 0 },
    reducers: {
        add(state) {
            return { number: state.number + 1 };
        }
    },
    effects: {
        *asyncAdd(action, { call, put }) {
            yield call(delay, 1000);
            yield put({ type: 'add' });
        },
        *goto({ to }, { put }) {
            yield put(routerRedux.push(to));
        }
    }
});
function Counter(props) {
    return (
        <div>
            <p>{props.number}</p>
            <button onClick={() => props.dispatch({ type: "counter/add" })}>+</button>
            <button onClick={() => props.dispatch({ type: "counter/asyncAdd" })}>异步+</button>
            <button onClick={() => props.dispatch({ type: "counter/goto", to: '/' })}>跳转到/</button>
        </div>
    )
}
const ConnectedCounter = connect(
    (state) => state.counter
)(Counter
);
const Home = () => <div>Home</div>
app.router(() => (
    <HashRouter>
        <>
            <ul>
                <li><Link to="/">Home</Link></li>
                <li><Link to="/counter">Counter</Link></li>
            </ul>
            <Switch>
                <Route path="/" exact={true} component={Home} />
                <Route path="/counter" component={ConnectedCounter} />
            </Switch>
        </>
    </HashRouter>
));
app.start('#root');

6. subscriptions #

subscriptionsdva 框架中的一个关键概念。它允许你订阅一个数据源,然后根据条件 dispatch 相应的 action。通常在应用启动时使用,例如动态地从服务器加载数据、添加键盘快捷键、监听路由变化等。

dva 中,subscriptions 是一个对象,其中每一个键都是一个订阅函数。每一个订阅函数接收一个 dispatch 和一个 history(用于路由操作)作为参数。

npm install keymaster --save
import React from 'react';
import dva, { connect } from 'dva';
import { HashRouter, Link, Switch, Route, routerRedux } from 'dva/router';
import key from 'keymaster';
function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(function () {
            resolve();
        }, ms);
    });
}
const app = dva();
app.model({
    namespace: 'counter',
    state: { number: 0 },
    reducers: {
        add(state) {
            return { number: state.number + 1 };
        }
    },
    effects: {
        *asyncAdd(action, { call, put }) {
            yield call(delay, 1000);
            yield put({ type: 'add' });
        },
        *goto({ to }, { put }) {
            yield put(routerRedux.push(to));
        }
    },
    subscriptions: {
        keyboardWatcher({ dispatch }) {
            key('a', () => {
                dispatch({ type: 'counter/add' });
            });
        },
    }
});
function Counter(props) {
    return (
        <div>
            <p>{props.number}</p>
            <button onClick={() => props.dispatch({ type: "counter/add" })}>+</button>
            <button onClick={() => props.dispatch({ type: "counter/asyncAdd" })}>异步+</button>
            <button onClick={() => props.dispatch({ type: "counter/goto", to: '/' })}>跳转到/</button>
        </div>
    )
}
const ConnectedCounter = connect(
    (state) => state.counter
)(Counter
);
const Home = () => <div>Home</div>
app.router(() => (
    <HashRouter>
        <>
            <ul>
                <li><Link to="/">Home</Link></li>
                <li><Link to="/counter">Counter</Link></li>
            </ul>
            <Switch>
                <Route path="/" exact={true} component={Home} />
                <Route path="/counter" component={ConnectedCounter} />
            </Switch>
        </>
    </HashRouter>
));
app.start('#root');