2017-09-27 10:24:17 qq_28008615 阅读数 1063

之前在工作中使用了了react+redux。近来发现有个叫dva的东西出来。所以用自己能理解的解释下

dva是什么东东

基于 redux、redux-saga 和 react-router 的轻量级前端框架


特性

  • 易学易用:仅有 6 个 api,对 redux 用户尤其友好
  • elm 概念:通过 reducers, effects 和 subscriptions 组织 model
  • 支持 mobile 和 react-native:跨平台 (react-native 例子)
  • 支持 HMR:目前基于 babel-plugin-dva-hmr 支持 components、routes 和 models 的 HMR
  • 动态加载 Model 和路由:按需加载加快访问速度 (例子)
  • 插件机制:比如 dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
  • 完善的语法分析库 dva-ast:dva-cli 基于此实现了智能创建 model, router 等
  • 支持 TypeScript
  • -

数据流向

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State,所以在 dva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。
这里写图片描述

Models

State

type State = any

State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。

在 dva 中你可以通过 dva 的实例属性 _store 看到顶部的 state 数据,但是通常你很少会用到:

const app = dva();
console.log(app._store); // 顶部的 state 数据

Action

type AsyncAction = any

Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。action 必须带有 type 属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 dispatch 函数;需要注意的是 dispatch 是在组件 connect Models以后,通过 props 传入的。

dispatch({
  type: 'add',
});

dispatch 函数

type dispatch = (a: Action) => Action

dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。

在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,常见的形式如:

dispatch({
  type: 'user/add', // 如果在 model 外调用,需要添加 namespace
  payload: {}, // 需要传递的信息
});

Reducer

type Reducer<S, A> = (state: S, action: A) => S

Reducer(也称为 reducing function)函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。

Reducer 的概念来自于是函数式编程,很多语言中都有 reduce API。如在 javascript 中:

[{x:1},{y:2},{z:3}].reduce(function(prev, next){
    return Object.assign(prev, next);
})
//return {x:1, y:2, z:3}

在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。需要注意的是 Reducer 必须是纯函数,所以同样的输入必然得到同样的输出,它们不应该产生任何副作用。并且,每一次的计算都应该使用immutable data,这种特性简单理解就是每次操作都是返回一个全新的数据(独立,纯净),所以热重载和时间旅行这些功能才能够使用。

Effect

Effect 被称为副作用,在我们的应用中,最常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。

dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,由于采用了generator的相关概念,所以将异步转成同步写法,从而将effects转为纯函数。至于为什么我们这么纠结于 纯函数,如果你想了解更多可以阅读Mostly adequate guide to FP,或者它的中文译本JS函数式编程指南。[个人觉得类似于jsonp主要处理数据是异步的]

Subscription

Subscriptions 是一种从 源 获取数据的方法,它来自于 elm。

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

import key from 'keymaster';
...
app.model({
  namespace: 'count',
  subscriptions: {
    keyEvent(dispatch) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
  }
});

Router

这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 History API 可以监听浏览器url的变化,从而控制路由相关操作。

dva 实例提供了 router 方法来控制路由,使用的是react-router。

import { Router, Route } from 'dva/router';
app.router(({history}) =>
  <Router history={history}>
    <Route path="/" component={HomePage} />
  </Router>
);

Route Components

在组件设计方法中,我们提到过 Container Components,在 dva 中我们通常将其约束为 Route Components,因为在 dva 中我们通常以页面维度来设计 Container Components。

所以在 dva 中,通常需要 connect Model的组件都是 Route Components,组织在/routes/目录下,而/components/目录下则是纯组件(Presentational Components)。


一般目录下

“`bash
├── /dist/ # 项目输出目录
├── /src/ # 项目源码目录
│ ├── /components/ # UI组件及UI相关方法
│ │ ├── skin.less # 全局样式
│ │ └── vars.less # 全局样式变量
│ ├── /routes/ # 路由组件
│ │ └── app.js # 路由入口
│ ├── /models/ # 数据模型
│ ├── /services/ # 数据接口
│ ├── /themes/ # 项目样式
│ ├── /mock/ # 数据mock
│ ├── /utils/ # 工具函数
│ │ ├── config.js # 项目常规配置
│ │ ├── menu.js # 菜单及面包屑配置
│ │ ├── config.js # 项目常规配置
│ │ ├── request.js # 异步请求函数
│ │ └── theme.js # 项目需要在js中使用到样式变量
│ ├── route.js # 路由配置
│ ├── index.js # 入口文件
│ └── index.html
├── package.json # 项目信息
├── .eslintrc # Eslint配置
└── .roadhogrc.js # roadhog配置

2019-11-30 13:25:51 naisi2422553065 阅读数 111

前言:

用react 来做了很多小项目 一直都没结合router来使用更别说redux ,今天就尝试了下使用react的全家桶,试着装环境下包什么的太麻烦了 ,然后就了解了 umi 这个框架, 只能说太nb了 然后就跟着文档撸了起来 . 约定大于配置这个是真的好 ,以前写后台的时候就是遵循的这个. 然后又发现 umi 又是集成了 dav+ant +react全家桶的 ,但是dav是个什么鬼,不知道啊,然后又去看了下文档 哦原来dav和umi都是阿里的大神开发的 umi就是比 dav更优雅 更多功能的框架,特别是对react-redux的封装,那是一个爽啊.然后就尝试做了个小项目.(注:之前完全没有了用过router所以就遇到了那些问题)

 

1.umi官网:https://umijs.org/zh/

搭建项目什么的就看官网就行了很好懂.

 

2.我要做项目的亚子

 

404

 

3.项目结构 用umi脚手架搭的 里面的文件都是自动生成的

404

 

里面对应的东西其实umi官网都有讲,我简单说下

layouts 写的东西就是整个网站公用的 

pages 就是你写的没个文件 只要你不在 配置文件里做router配置 都会自动帮你配置路由  里面的index.js文件 自动配置的路由是"/"

例如 你在里面建 user/index.js 就是自动帮你生成user的路由 你在浏览器 输入ip+端口+/user{http://localhost:8000/user} 就可以访问到这个页面的内容

其他的文件先不管 想了解更多的 详细看文档 一天就能搞定全部

4.问题

重点来了, 导航栏我用的是ant的组件 ,umi已经是内置的了 开箱即用,

接下来 修改layouts/index.js

import React from 'react';
import { Carousel, Menu, Icon } from 'antd';
import 'antd/dist/antd.css';
import styles from './index.css';
import Link from 'umi/link';

// ---redux使用--
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import appStore from '../reducers/index';
const store = createStore(appStore);
// ----

console.log(window.g_app._store.getState().products);
const { SubMenu } = Menu;

class BasicLayout extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      current: '/',
    };
    this.props.history.listen(route => {
      console.log(route.pathname);
      this.setState({
        current: route.pathname,
      });
    });
  }

  handleClick = e => {
    // console.log('click ', e);
    // console.log(e.key);
    // console.log(window.location.pathname);
    this.setState({
      current: window.location.pathname,
    });
  };

  render() {
    return (
      <div className={styles.normal}>
        {/* <p>{this.state.current}</p>
        <p>{window.g_app._store.getState().products.current}</p> */}
        <Provider store={store}>
          <Carousel autoplay>
            <div>
              <h3 className={styles.aa}>1</h3>
            </div>
            <div>
              <h3>2</h3>
            </div>
            <div>
              <h3 className={styles.aa}>3</h3>
            </div>
            <div>
              <h3>4</h3>
            </div>
          </Carousel>

          <Menu
            onClick={this.handleClick}
            // selectedKeys={[window.g_app._store.getState().products.current]}
            selectedKeys={[this.state.current]}
            mode="horizontal"
          >
            <Menu.Item key="/">
              <Link to="/">
                <Icon type="home" />
                首页
              </Link>
            </Menu.Item>
            <Menu.Item key="/read">
              <Link to="/read">
                <Icon type="read" />
                课程安排
              </Link>
            </Menu.Item>
            <Menu.Item key="/feedback">
              <Link to="/feedback">
                <Icon type="bug" />
                反馈
              </Link>
            </Menu.Item>

            <Menu.Item key="/ellipsis">
              <Link to="/ellipsis">
                <Icon type="ellipsis"></Icon>
                其他
              </Link>
            </Menu.Item>
          </Menu>

          {this.props.children}
        </Provider>
      </div>
    );
  }
}

export default BasicLayout;

然后效果就出来了 然后分别在 pages 新建对应的路由

404

根据约定 只要往里写 就会自动帮我配置路由

然后运行项目 发现网页后退操作 导航的状态并不能也跟着回退状态. 

然后分析 一个导航在的组件是共有组件 生命周期没有变化 也不是路由要变化的组件 ,所以并不能改变,

然后我就想着 把那个状态 提取出来 放到外面 然后里面的路由组件用到react的生命周期函数 当组件销毁一次就 修改状态.把状态派发到store里 更新全局的状态 为此我硬是学会了 redux .但是尝试了一番 发现 更新的历史并不能是最新加载的 拿到的总是上一次的数据 到这里 就比较麻烦了  .

然后 想想 让里面的状态跟着 路由走不就好啦  为此我用到了window.location.pathname, 来获取路由路径 ,我让导航的状态 跟着它走就ok了 

但是问题又来了 回退的时候 怎么监听路由的变化呢

然后百度一下 发现可以监听路由的变化

this.props.history.listen(route => { // 监听
      console.log(route.pathname);
      this.setState({
        current: route.pathname,// 更新state
      });
    });

然后就可以啦

其实整片文章的重点只有一个就是 怎么监听路由的变化 ,不需要写那么长, 写这么长是记录一下 我思考的一个流程,和解决问题的的一个思路,现在肯定是还有很多问题的(比较蠢) 等过段时间再回头看看 有没有成长 哈哈..

2019-03-06 23:13:42 root5 阅读数 559

效果展示

本项目搭建过程参考 Dva官方快速上手教程
项目环境:(在该环境下测试通过,默认使用cnpm)
dva 0.9.2
node 8.10.0

安装 dva-cli

确保版本在 0.9.1 及以上

$ npm install dva-cli -g
$ dva -v
dva-cli version 0.9.2

创建新应用

$ dva new dva-demo

完成之后进入生成的项目文件夹并启动项目

$ cd dva-demo
$ npm start

在浏览器里打开 http://localhost:8000
项目文件组织结构

使用 antd

antd 是淘宝前端团队开源的一个UI库

安装 antd 和 babel-plugin-import(babel-plugin-import 用来按需加载 antd 的脚本和样式)

$ cnpm install antd babel-plugin-import --save

编辑 .webpackrc,使 babel-plugin-import 插件生效(代码每行首个字符为 + 表示该行为新增内容,下同)

{
+  "extraBabelPlugins": [
+    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
+  ]
}

定义路由

创建一个数据列表页面,实现增删改查

新建路由组件routes/Products.js

import React from 'react';

const Products = (props) => (
  <h2>List of Products</h2>
);

export default Products;

添加路由信息到路由表 router.js

+ import Products from './routes/Products';
...
+ <Route path="/products" exact component={Products} />

然后在浏览器里打开 http://localhost:8000/#/products

编写 UI 组件

新建 components/ProductList.js 文件

import React from 'react';
import PropTypes from 'prop-types';
import { Table, Popconfirm, Button } from 'antd';

const ProductList = ({ products }) => {
  const columns = [{
    title: '名称',
    dataIndex: 'name',
  }, {
    title: '描述',
    dataIndex: 'desc'
  }];
  return (
    <Table
      dataSource={products}
      columns={columns}
    />
  );
};

ProductList.propTypes = {
  products: PropTypes.array.isRequired,
};

export default ProductList;

定义 Model

现在开始处理数据和逻辑:dva 通过 model 的概念把一个领域的模型管理起来,包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions 。

  • namespace 表示在全局 state 上的 key
  • state 是初始值,在这里是空数组
  • reducers 等同于 redux 里的 reducer,接收action,同步更新 state
    新建 model models/products.js
export default {
  namespace: 'products',
  state: [],
  reducers: {},
};

index.js 里载入model

// 3. Model
+ app.model(require('./models/products').default);

connect 起来

dva 提供了 connect 方法将 model 和 component 串联起来
编辑 routes/Products.js

import React from 'react';
import { connect } from 'dva';
import ProductList from '../components/ProductList';

const Products = ({ dispatch, products }) => {
  return (
    <div>
      <h2>List of Products</h2>
      <ProductList products={products} />
    </div>
  );
};

// export default Products;
export default connect(({ products }) => ({
  products,
}))(Products);

在 model 里面添加初始数据 models/products.js

export default {
  namespace: 'products',
  state: [
    {
      id: 1,
      name: 'dva',
      desc: 'dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。'
    },
    {
      id: 2,
      name: 'antd',
      desc: '服务于企业级产品的设计体系,基于确定和自然的设计价值观上的模块化解决方案,让设计者和开发者专注于更好的用户体验。'
    },
  ],
  reducers: {},
};

刷新浏览器看到以下效果
数据列表

添加删除等功能

编辑 components/ProductList.js

+ import { Table, Popconfirm, Button } from 'antd';

+ const ProductList = ({ onDelete, products }) => {
...
+   }, {
+     title: 'Actions',
+     render: (text, record) => {
+       return (
+         <Popconfirm title="Delete?" onConfirm={() => onDelete(record.id)}>
+            <Button>Delete</Button>
+         </Popconfirm>
+       );
+     },
+   }];
... 
ProductList.propTypes = {
+   onDelete: PropTypes.func.isRequired,
  products: PropTypes.array.isRequired,
};
...

编辑 models/products.js

reducers: {
+    'delete'(state, { payload: id }) {
+       return state.filter(item => item.id !== id);
+     },
  },

编辑 routes/Products.js

+ const Products = ({ dispatch, products }) => {
+   function handleDelete(id) {
+     dispatch({
+       type: 'products/delete',
+       payload: id,
+     });
+   }
  return (
    <div>
      <h2>List of Products</h2>
+       <ProductList onDelete={handleDelete} products={products} />
    </div>
  );
};

刷新浏览器看到以下效果
数据列表删除功能
项目文件组织结构(可 查看,删除 数据列表页面)

使用 Class 语法创建拥有 增/删/改/查 功能的数据列表页面

定义路由

新建路由组件routes/Users.js

import React, { Component } from 'react';
import { connect } from 'dva';
import styles from './Users.less';
import { LocaleProvider } from 'antd';
import zhCN from 'antd/lib/locale-provider/zh_CN';

import { Table, Popconfirm, Button } from 'antd';
import * as lodash from 'lodash';
import EditModal from '../components/EditModal';


class Users extends Component {
  constructor(props) {
    super(props);
    this.state = {
      editModalVisible: false, // 修改弹框显示状态
      selectedRecord: null, // 当前选中的记录
    };
    this.columns = [{
      title: 'ID',
      dataIndex: 'id',
      }, {
        title: '名称',
        dataIndex: 'name',
      }, {
        title: '性别',
        dataIndex: 'sex',
        render: (text) => {
          return (
            text === 'F' ? '女' : '男'
          );
        },
      }, {
        title: '年龄',
        dataIndex: 'age'
      }, {
        title: '操作',
        render: (text, record) => {
          return (
            <div>
              <Button onClick={()=> this.onShowEidtModal(record)}>Edit</Button>
              <Popconfirm title="Delete?" onConfirm={() => this.onDelete(record.id)}>
                <Button>Delete</Button>
              </Popconfirm>
            </div>
          );
        },
      }
    ];
  }


  componentWillMount() {
    this.props.dispatch({ type: 'users/init'});
  }

  componentDidMount() {
  }

  componentWillReceiveProps(nextProps) {

  }

  handleAdd = () => {
    this.setState({
      editModalVisible: true,
    })
  }

  onDelete = (id) => {
    let { dispatch, userList } = this.props;
    let newUserList =  userList.filter(user => user.id != id);
    dispatch({
      type: 'users/saveUserList',
      payload: newUserList
    })
  }

  onShowEidtModal = (record) => {
    console.log('onShowEditModal(), record:', record);
    this.setState({
      editModalVisible: true,
      selectedRecord: record,
    });
  }

  handleOk = (newUser, type) => {
    console.log('handleOk(), newUser:', newUser);
    const { dispatch, userList } = this.props;
    let newUserList = userList || [];
    if (type == 0) {
      newUserList.push(newUser);
    } else if (type == 1 && !!newUserList) {
      for(let i = 0; i < newUserList.length; i ++) {
        if (newUserList[i].id == newUser.id) {
          newUserList[i] = newUser;
        }
      }
    }

    dispatch({
      type: 'users/saveUserList',
      payload: newUserList,
    })
    this.setState({
      editModalVisible: false,
      selectedRecord: null,
    })
  }

  handleCancel = (e) => {
    console.log('handleCancel:', e);
    this.setState({
      editModalVisible: false,
      selectedRecord: null,
    })
  }

  render() {
    const { userList } = this.props;
    console.log('users => render(), userList:', userList);
    const {
      editModalVisible, selectedRecord } = this.state;

    console.log('render(), editModalVisible:', editModalVisible, 'selectedRecord:', selectedRecord);
    return (
      <LocaleProvider locale={zhCN}>
        <div className={styles.container}>
          <Button onClick={() => this.handleAdd()}>添加</Button>
          <Table
            dataSource={userList}
            columns={this.columns}
          />
          {
            editModalVisible &&
            <EditModal
              type={lodash.isEmpty(selectedRecord) ? 0 : 1}
              data={selectedRecord}
              handleOk={this.handleOk}
              handleCancel={this.handleCancel}
            />
          }
        </div>
      </LocaleProvider>
    );
  }
}

function mapStateToProps(state) {
  const { userList } = state.users;
  return {
    userList,
  };
}

export default connect(mapStateToProps)(Users);

新建 users/Users.less

.container {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: 0;
  background-color: antiquewhite;
}

添加路由信息到路由表 router.js

+ import Users from './routes/Users';
...
+ <Route path="/users" exact component={Users} />

然后在浏览器里打开 http://localhost:8000/#/users

编写 UI 组件

新建 components/EditModal.js 文件

import React, { Component } from 'react';
import styles from './EditModal.less';
import {
  Modal,
  Form,
  Input,
  DatePicker,
  TimePicker,
  Select,
  Cascader,
  InputNumber,
}
from 'antd';
const uuidv1 = require('uuid/v1');

const { Option } = Select;

class EditModal extends Component {
  constructor(props) {
    super(props);
    this.state = {
      id: !!props.data && props.data.id || null,
      name: !!props.data && props.data.name || null, // 当前选中的记录
      sex: !!props.data && props.data.sex || null, // 当前选中的记录
      age: !!props.data && props.data.age || null, // 当前选中的记录
    };
  }

  handleOk = (e) => {
    console.log(e);
    const { id, name, age, sex } = this.state;
    let user = {
      id,
      name,
      age,
      sex,
    };
    if (!id || id === 'undefined' || id === '') {
      user.id = uuidv1();
      this.props.handleOk(user, 0);
    } else {
      this.props.handleOk(user, 1);
    }
  }

  handleCancel = (e) => {
    console.log(e);
    this.props.handleCancel(e);
  }

  handleChangeName = (e) => {
    this.setState({
      name: e.target.value,
    })
  }

  handleChangeSex = (sex) => {
    console.log('handleChangeSex(), sex:', sex);
    this.setState({
      sex,
    })
  }

  handleChangeAge = (e) => {
    this.setState({
      age: e.target.value,
    })
  }

  getFormDom() {
    const { name, age, sex } = this.state;
    return (
      <div className={styles.form}>
        <div>
          <lable>姓名</lable><Input value={name} onChange={this.handleChangeName} />
        </div>
        <div>
          <lable>性别</lable>
          <Select defaultValue={sex || "F"} style={{width: '100%'}} onChange={this.handleChangeSex} >
            <Option value="M">男</Option>
            <Option value="F">女</Option>
          </Select>
        </div>
        <div>
          <lable>年龄</lable><Input value={age} onChange={this.handleChangeAge} />
        </div>
      </div>
    )
  }

  render() {
    return (
      <div>
        <Modal
          title={this.props.type === 0 ? '添加' : '修改'}
          onOk={this.handleOk}
          onCancel={this.handleCancel}
          visible={true}
        >
          {this.getFormDom()}
        </Modal>
      </div>
    );
  }
}

export default EditModal;

新建 components/EditModal.less 文件

.form {
  > div {
    display: flex;
    margin: 10px 0;
    text-align: center;
    align-items: center;
    justify-content: flex-start;

    > lable {
      width: 80px;
    }
  }
}

定义 Model

新建 model models/users.js

export default {
  namespace: 'users',
  state: {
    userList: [],
  },
  reducers: {
    saveUserList(state, { payload }) {
      console.log('model users => reducers/saveUserList(), payload:', payload);
      return { ...state, userList: Object.assign([], payload || []) };
    }
  },
  effects: {
    *init(_, { call, put, select }) {
      //获取后台数据
      const userList = [{
        id: 1,
        name: '张三',
        age: 13,
        sex: 'F'
      }, {
        id: 2,
        name: '张四',
        age: 14,
        sex: 'M'
      }, {
        id: 3,
        name: '张五',
        age: 15,
        sex: 'F'
      }, {
        id: 4,
        name: '张六',
        age: 16,
        sex: 'M'
      }, {
        id: 5,
        name: '张七',
        age: 16,
        sex: 'F'
      }];
      yield put({
        type: 'saveUserList',
        payload: userList
      });
    }
  },
  subscriptions: {
    setup({ dispatch, history }) {
      return history.listen(({ pathname, query }) => {
      });
    }
  },
};

index.js 里载入model

// 3. Model
+ app.model(require('./models/users').default);

然后在浏览器里打开 http://localhost:8000/#/users
看到如下效果
用户列表增删改查
用户添加/修改数据
用户增删改查功能
到此完成基本功能,详见源码 github
文中若有错误或者需要改进的地方,欢迎大家留言指正,或到github提交issues

2017-06-27 21:38:12 jiangcs520 阅读数 884

这里写图片描述
不管我们项目中是否是用的redux思想的最佳实践,但是他的基本思想基本如上图,是不是十分简单明了。有人说,redux这类不能称为框架,只能说是一种思想,不过我们在此就不讨论这么学术的问题了,为了描述统一,以下我们姑且称之为框架。redux这个框架思想很简单,不过,有时候最简单的就是最复杂的,简单的,灵活,也意味着我们需要做的更多,这时候,最佳实践方案就比较不好说了,这里我也只能是就着自己的一点点项目经验来稍稍讨论一下,肯定谈不上最佳实践,但是作为抛砖引玉还是有一定价值的。接下来直接进入正题。
首先简单介绍一下上图,redux基本原则是,state中的数据由store进行统一管理,不可对state中的数据进行直接操作,也就是说,store是作为数据仓库的存在,当然,store并不是state,他只是个仓库,当我们变更store中的数据后,可以根据store.getState()这个方法获取数据,然后取出需要的数据,进行setState操作更新视图。这就很容易明白store跟state具体是什么关系了,store是将数据逻辑层与视图层更好地隔离开来。当然,这还得有其他两个小兄弟的帮忙,那就是action和reducer,严格的说,应该是actions和reducers。顾名思义很好理解,action就是定义了一系列动作,reducer就是进行数据的裁剪,清洗,再输出,reducer应该是无副作用的纯函数。我们再来看上图,这看着其实是不是有点眼熟,dispatch/subscribe的方式,好像有点像监听者模式,没错了,不过,这是个更加粗犷的监听者。为什么说粗犷,后面再说。先说说这个监听者模式,这是redux的关键所在。代码中,我们首先会引入redux库文件,其中我们最常用的两个函数是createStore和applyMiddleware,伪代码如下:

let store = createStore(reducer,{},applyMiddleware(thunk));

reducer作为参数,创建了一个store,reducer就是store的监听器,这个监听器是在store创建之初就已经定下来的,可以称为内置监听器,不再需要我们手动去绑定。这里我们就有一个疑惑了,那难不成这个监听器是每次都会调用的?没错,就是这样的,这也是一开始让我不太适应的地方。暂且先往下看,接下来看看store的几个重要方法,首先是store.subscribe(action,()=>{}),然后是dispatch(action),当我们派发一个action的时候,首先触发的是内置监听器reducer,生成可用于state来更新视图的数据,执行完毕之后,触发subscribe监听的函数,可进行store.getState()操作获取数据以及setState()更新视图。以上还有两个东西没有细讲,action和applyMiddleware是什么呢?action好理解,就是动作定义,它是个对象,也可以是个函数,但最终它还得是个对象。说起来有点绕,解释一下,定义一个动作,其实就是定义一个对象,类似这样:{type:"testAction",text:"行为描述",data:{}},但它也不一定是个对象,比如,我们有个数据需要从服务端获取,然后获取到数据后才能进行派发,这时候,我们有两种方式,一、是直接进行数据请求,请求成功后,将获取的数据填充入action,并派发;二、派发一个函数action,也叫异步action,由该函数进行数据请求,请求完成后自动派发。我们来对比一下两种action:

actionNotify = {type:"通知",text:"我准备请求数据了"};
actionAsync1 = (opts)=>{
    return (dispatch)=>{
        dispatch(actionNotify);//发个通知,我要开始请求数据了
        //发起网络请求
        return fetch(...).then((resData)=>{
            dispatch({type:"数据到了",text:"数据请求成功了,请显示",data:resData});
        });
    }
}

看以上代码,是两个不同类型的action,异步actionAsync1返回的函数,可以直接dispatch一个普通actionNotify,也可以网络请求数据完成后,再dispatch新的action,但最终被dispatch的还是一个普通的action,所以说action是个对象,也可以是个函数,但最终它还得是个对象。其实我们可以简单理解一下,action之所以会有异步函数的形式,只是为了数据层和UI层更好的分离,网络请求必不可少,如果不在action,那必然在UI层或者reducer层,显然都不太合适,所以异步action就是为了解决这个问题而存在的。但是我们知道,store.dispatch本身并不支持action是个函数,redux-thunk帮我们完美解决了这个问题。上面我们略过的redux中的applyMiddleware方法,就是用于引入thunk这个中间件,对普通的store进行dispatch异步action的赋能。如此一来,store.dispatch(异步action)就解决了。至此,我们就大致讲清楚了store,action,reducer三者的关系了,也就基本搞清楚redux究竟是干什么的问题。
本篇就谈到这里,下一篇我们继续。如有问题,随喷。


个人简介:
前端小司机,倒腾前端一切,砸尽一切非前端的饭碗。
有兴趣请加前端交流群
这里写图片描述

dva的用法

阅读数 365

没有更多推荐了,返回首页