011 dva 课堂实战

简介: 011 dva 课堂实战

image.png


因为 dva 不再是 Umi 数据流最佳实践方案,因此这篇你可以选择跳过,本文中的 demo 也不会出现在源码归档中。


首先我们来看一下我们需要完成的需求。

写一个列表,包含删除按钮,点删除按钮后延迟 1 秒执行删除。

image.png

这里我们只关注 dva 的相关逻辑,页面样式不一定与上述一致。



新建 models 文件

在 Umi 中,我们约定当 src/models 下存在 dva 的 models 文件时,会被自动加载到项目中。 即约定了存在即生效,因此我们约定仅在这个文件夹下存放 dva 的 models 文件,虽然存放其他的文件会被框架自动过滤忽略,但是在这个目录存放其他文件会增加理解的心智负担,建议不要存放无关的文件。且为了管理方便,一般将文件名和 models 的 namespace 一一对应,不仅可以直观的找到相应的 models ,而且能够保证不会存在 models 冲突的问题,因为文件系统本身就不允许同名文件。


新建文件 src/models/global.js

const GlobalModel = {
  state: {
    name: 'learn umi',
  },
};
export default GlobalModel;
复制代码



在页面绑定 model

这里我们再次复习一下 connect 函数。

import { connect } from 'umi';
connect(映射函数)(组件);
复制代码
// 映射函数
const mapStateToProps = state => state;
复制代码


connect 函数会自动给映射函数传入整个项目的 state,它包含了整个项目的所有的 models。 比如此时我们新建了一个 global 的 models ,那么这时候传入的 state 就是 { global }


映射函数返回的属性,会被自动传入被绑定的组件,比如我们将上面的映射函数修改为:

// 映射函数
const mapStateToProps = state => {
  global: state.global;
};
复制代码


就可以直接从组件的 props 里面取出 global 的属性。这就是 connect 函数做的事情。

将上面的代码整理之后,我们就可以得到一下的页面绑定代码,以下的代码,用到了匿名函数还有一些 es6 的缩写方式,如果你不能直观的理解它们,你可以再看看上面的分析过程。这是一个必须要掌握且很容易被忽略的知识。


请注意下方代码中出现三次的 global,我分别在注释中加了少量的说明,增加理解。不明白的,再看一下上述的分析过程。

import { Link, Helmet, connect } from 'umi';
import { Button } from 'antd-mobile';
// 这里的 global 是 connect 向组件中注入的属性
const IndexPage = ({ global }) => {
  const { name } = global;
  return (
    <div>
      <h1 style={{ color: 'white' }}>{name}</h1>
      <Helmet>
        <title>umi 入门教程</title>
      </Helmet>
      <Button type="primary">Click Me!</Button>
      <Link to="/list">Go to list page</Link>
    </div>
  );
};
// 第一个 global 是指从 state 中取出 global model
// 第二个 global 是指返回一个 global 属性,因为是同名所以此处缩写,其实是 { global:global }
export default connect(({ global }) => ({ global }))(IndexPage);
复制代码



从 model 取数据

我们可以关注到这个地方,我们从组件的 props 中取出了 global 属性,并从中取出了 name。

const { name } = global;
复制代码


这里的数据结构其实是你在 models 中定义的数据结构,即:

state: {
  name: 'learn umi',
},
复制代码


比如这里我们定义了这个 model 的 state 是一个对象,所以绑定到页面中时,global 就是一个对象,你可以根据自己的真实需要定义这里的数据类型。比如:

// 这里定义了是一个数字类型
state: 0;
// 因此在 page 中的 global就是一个数字类型,当你再次取 global.name 的时候就会导致程序出错
复制代码



编写页面事件

你可以简化的理解这一个过程,数据绑定是通过单项传递的方式绑定到页面中的,数据修改和变更是通过抛出事件的方式来修改的。这一整个数据流就是这么简单。(里面还有很多细节,但是我们先不理会。)


所以我们需要修改当前页面的数据,我们就会调用到另一个非常常用的函数,dispatch

其实通过 connect 函数绑定后的函数,不仅会被传入 model 的属性,还会被传入 dispatch 属性,即你可以通过组件的 props 取出 dispatch


const IndexPage = ({ global, dispatch }) => {
  return (
    <button
      onClick={() => {
        dispatch({
          type: 'global/changeName',
          payload: {
            name: 'umi 入门教程',
          },
        });
      }}
    >
      click me!
    </button>
  );
};
复制代码


这里调用 dispatch 通常需要传入两个参数,typepayload。 你可以理解为,这里你发送一个短信,type 就是地址,payload 就是内容。 此处我们用到的 global/changeName 也是一个约定式的写法, / 前面的字符串代表了 models 的 namespace。即我们的 models 的文件名。 / 后面的字符串代表了 models 的 action,也就是定义好的事件,我们需要在 model 中的 effects 里写明。


state: {
    name: 'learn umi',
  },
  effects: {
    *changeName({ payload }, { call, put }) {
      console.log(payload);
      // {name: "umi 入门教程"}
    },
  },
复制代码

到这里,dispatch 的工作就完成了。它只关心从页面上触发了一个什么事件,需要把什么内容发送给谁。 至于后续这个数据会有什么作用,会产生什么副作用,它都不再关心。



副作用与更新数据

这时候数据就有页面交接到了 model,我们在 effects 中处理这些副作用,我这里所谓的副作用包括页面的变化和数据的变化。 比如我们可以在这里请求服务端的接口,也可以直接在此处修改数据。最终我们将处理后的数据,返回到 reducers 事件中。


在 Umi 中,我们建议只写一个 reducers 事件 save, 即

reducers: {
    save(state, action) {
      return {
        ...state,
        ...action.payload,
      };
    },
  },
复制代码


你可以直接将它当作 model 里的 setState 使用。

用法如下:

const GlobalModel = {
  namespace: 'global',
  state: {
    name: 'learn umi',
  },
  effects: {
    *changeName({ payload }, { call, put }) {
      console.log(payload);
      yield put({
        type: 'save',
        payload: { name: payload.name },
      });
    },
  },
  reducers: {
    save(state, action) {
      return {
        ...state,
        ...action.payload,
      };
    },
  },
};
export default GlobalModel;
复制代码


effects 中的 put 函数,作用和 dispatch 类似,不过它是表示向 reducers 抛出一个事件。 这里接受的 type 和 paylaod 与 dispatch 相同,不过在同一个 model 内部,type 可以省略 / 之前的 namespace。


保存代码,查看 http://localhost:8000/#/,这时候当你点击 “Click Me!” 按钮时,页面上的文字,将从 “learn umi” 变为 “umi 入门教程”。


我们回过头来看 effects ,每个 effects 定义时都有一个 * 标志,这表示它是一个 Generator 函数。如果你不太懂得什么是 Generator 函数,那你只需要记住,在编写 effects 时,不要遗漏这个符号即可。


然后它在函数内部的每一个步骤都使用 yield 来定义,你可以使用 async/await 的概念来做类比,帮助理解。但其实此处更加简单,当你调用函数之外的函数时,你就使用 yield 做一个标记,而无需理会这个调用函数的同步还是异步。


effects 函数的第二参数中除了 put ,常用的还有 callselect 即: { call, put, select }

call 就是调用其他函数,这个比较好理解。select 的作用是查找 state,比如如果你想获得最新的 global 的 state。 可以这么写:const globalState = yield select(_=>_.global);


如果你理解了上面的概念,那现在回到我们的问题上:写一个列表,包含删除按钮,点删除按钮后延迟 1 秒执行删除。 对于现在的你是不是有了一点概念了。

  • step1:先在 state 中增加一个数组数据 list:['step1','step2','step3','step4']
  • step2:将数据渲染到页面中
  • step3:点击删除按钮,向 effects 发送一个事件
  • step4:调用睡眠函数,等待一秒钟
  • step5:查询先有 global state 并删除我们点击的对象对应的数据
  • step6:将修改后的数据同步到 gloabl state 中(会自动触发页面数据同步)



demo 演示

// src/models/global
const GlobalModel = {
  namespace: 'global',
  state: {
    name: 'learn umi',
    list: ['step1', 'step2', 'step3', 'step4'],
  },
  effects: {
    *changeName({ payload }, { call, put }) {
      yield put({
        type: 'save',
        payload: { name: payload.name },
      });
    },
    *deleteItem({ payload }, { call, put, select }) {
      const { list } = yield select(_ => _.global);
      yield call(
        () =>
          new Promise(resolve => {
            setTimeout(resolve, 1000);
          }),
      );
      list.splice(
        list.findIndex(e => e === payload),
        1,
      );
      yield put({
        type: 'save',
        payload: { list },
      });
    },
  },
  reducers: {
    save(state, action) {
      return {
        ...state,
        ...action.payload,
      };
    },
  },
};
export default GlobalModel;
复制代码
// src/pages/index
import { Link, Helmet, connect } from 'umi';
import { Button } from 'antd-mobile';
const IndexPage = ({ global, dispatch }) => {
  const { name, list = [] } = global;
  return (
    <div>
      <h1 style={{ color: 'white' }}>{name}</h1>
      <Helmet>
        <title>umi 入门教程</title>
      </Helmet>
      <Button
        type="primary"
        onClick={() => {
          dispatch({
            type: 'global/changeName',
            payload: {
              name: 'umi 入门教程',
            },
          });
        }}
      >
        Click Me!
      </Button>
      <Link to="/list">Go to list page</Link>
      <ul>
        {list.map(i => (
          <li key={i}>
            <Button
              onClick={() => {
                dispatch({
                  type: 'global/deleteItem',
                  payload: i,
                });
              }}
            >
              删除{i}{' '}
            </Button>
          </li>
        ))}
      </ul>
    </div>
  );
};
export default connect(({ global }) => ({ global }))(IndexPage);
复制代码


按照本教程的预期,你应该能够清晰的理解以上的代码片段,如果你在阅读上存在任何的疑问,你可以反复的阅读,直到你真正掌握为止。



总结

当你理解了 dva 你会觉得在项目中使用它会很清晰和舒服,但是我们现在不推荐在项目中重度的使用它。因为页面的私有数据流如果暴露到全局的数据流当中,反而会增加页面的维护难度。我们会在下一节课《纯 hooks 的数据流》中,介绍如何在页面中维护页面私有的数据。

目录
相关文章
|
5天前
|
数据采集 缓存 编解码
vue项目实战:实战技巧总结(下)
vue项目实战:实战技巧总结
55 0
|
5天前
|
缓存 JavaScript 前端开发
vue项目实战:实战技巧总结(中)
vue项目实战:实战技巧总结
90 1
|
5天前
|
JavaScript 前端开发 区块链
vue项目实战:实战技巧总结(上)
vue项目实战:实战技巧总结
88 0
|
5天前
|
前端开发 JavaScript Java
在线课堂|基于Springboot+Vue实现在线学习平台
在线课堂|基于Springboot+Vue实现在线学习平台
|
5天前
|
移动开发 小程序 前端开发
Uniapp Vue3 基础到实战 教学视频
Uniapp Vue3 基础到实战 教学视频
145 0
|
10月前
|
JavaScript
vue项目创建手把手教会绝不迷路(赞赞赞)
vue项目创建手把手教会绝不迷路(赞赞赞)
35 0
|
11月前
|
JavaScript
vue官网阅读学习笔记整理(第七天)
vue官网阅读学习笔记整理(第七天)
62 0
|
12月前
|
前端开发
前端学习笔记202304学习笔记第十五天-vue3.0-完结
前端学习笔记202304学习笔记第十五天-vue3.0-完结
29 0
|
前端开发 JavaScript 安全
分享一个SpringBoot+Vue豆宝社区手把手免费项目实战视频教程
分享一个SpringBoot+Vue豆宝社区手把手免费项目实战视频教程
188 0
|
设计模式 缓存 JavaScript
Vue 常见面试题一百问(第一期)
优点:用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染; 基于上面一点,SPA 相对对服务器压力小; 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
104 0