React dva 从入门到精通

引言

如有错误,欢迎大家留言指出来

我的项目地址:https://github.com/jdbi336534/weather

技术栈:react + dva + redux + react-router + redux-sage + antd-mobile

效果图

输入图片说明

1.安装dva-cli

通过 npm 安装 dva-cli 并确保版本是 0.7.0 或以上。

$ npm install dva-cli -g
$ dva -v
0.7.0

2.创建应用

安装完 dva-cli 之后,就可以在命令行里访问到 dva 命令。现在,你可以通过 dva new 创建新应用。

$ dva new dva-quickstart

这会创建 dva-quickstart 目录,包含项目初始化目录和文件,并提供开发服务器、构建脚本、数据 mock 服务、代理服务器等功能。 然后我们 cd 进入 dva-quickstart 目录,并启动开发服务器:

$ cd dva-quickstart
$ npm start

几秒钟后,你会看到以下输出:

Compiled successfully!

The app is running at:

  http://localhost:8000/

Note that the development build is not optimized.
To create a production build, use npm run build.

在浏览器里打开 http://localhost:8989 ,你会看到 dva 的欢迎界面。

3.引入antd-mobile

安装依赖

npm install antd-mobile babel-plugin-import  --save
npm install babel-plugin-import --save

babel-plugin-import 是用来按需加载脚本和样式,编辑 webpack.config.js,使 babel-plugin-import 插件生效。同时使用antd的高清方案。

  // 引入 babel-plugin-import
  webpackConfig.babel.plugins.push(['import', { libraryName: 'antd-mobile', style: 'css' }]);
  // 引入高清方案
  webpackConfig.postcss.push(pxtorem({
    rootValue: 100,
    propWhiteList: [],
  }));

下面是完整的webpack.config.js

const webpack = require('atool-build/lib/webpack');
const pxtorem = require('postcss-pxtorem');
const path = require('path');

module.exports = function(webpackConfig, env) {
  webpackConfig.babel.plugins.push('transform-runtime');

  // Support hmr
  if (env === 'development') {
    webpackConfig.devtool = '#eval';
    webpackConfig.babel.plugins.push('dva-hmr');
  } else {
    webpackConfig.babel.plugins.push('dev-expression');
    webpackConfig.externals = {
      // Use external version of React
      "react": "React",
      "react-dom": "ReactDOM"
    };
  }

  // Don't extract common.js and common.css
  webpackConfig.plugins = webpackConfig.plugins.filter(function(plugin) {
    return !(plugin instanceof webpack.optimize.CommonsChunkPlugin);
  });

  // Support CSS Modules
  // Parse all less files as css module.
  webpackConfig.module.loaders.forEach(function(loader, index) {
    if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.less$') > -1) {
      loader.include = /node_modules/;
      loader.test = /\.less$/;
    }
    if (loader.test.toString() === '/\\.module\\.less$/') {
      loader.exclude = /node_modules/;
      loader.test = /\.less$/;
    }
    if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.css$') > -1) {
      loader.include = /node_modules/;
      loader.test = /\.css$/;
    }
    if (loader.test.toString() === '/\\.module\\.css$/') {
      loader.exclude = /node_modules/;
      loader.test = /\.css$/;
    }
  });

  // 引入 babel-plugin-import
  webpackConfig.babel.plugins.push(['import', { libraryName: 'antd-mobile', style: 'css' }]);
  // 引入高清方案
  webpackConfig.postcss.push(pxtorem({
    rootValue: 100,
    propWhiteList: [],
  }));

  const svgDirs = [
    require.resolve('antd-mobile').replace(/warn\.js$/, ''),  // 1. 属于 antd-mobile 内置 svg 文件
    path.resolve(__dirname, './src/assets/'),  // 2. 自己私人的 svg 存放目录
  ];

  // 因为一个 SVG 文件不能被处理两遍. 在 atool-build 默认为 svg配置的svg-url-loade 里 exclude 掉需要 svg-sprite-loader处理的目录
  // https://github.com/ant-tool/atool-build/blob/master/src/getWebpackCommonConfig.js#L162
  // https://github.com/kisenka/svg-sprite-loader/issues/4
  webpackConfig.module.loaders.forEach(loader => {
    if (loader.test && typeof loader.test.test === 'function' && loader.test.test('.svg')) {
      loader.exclude = svgDirs;
    }
  });
  // 4. 配置 webpack loader
  webpackConfig.module.loaders.unshift({
    test: /\.(svg)$/i,
    loader: 'svg-sprite',
    include: svgDirs, // 把 svgDirs 路径下的所有 svg 文件交给 svg-sprite-loader 插件处理
  });


  return webpackConfig;
};

4.目录结构

输入图片说明

5.初始化dva

import './index.html';
import './index.css';
import dva from 'dva';
import FastClick from 'fastclick'

if ('addEventListener' in document) {
    document.addEventListener('DOMContentLoaded', function() {
        FastClick.attach(document.body);
    }, false);
}

// 1. Initialize
const app = dva();

// 2. Plugins
//app.use({});

// 3. Model
app.model(require('./models/weatherReport'));

// 4. Router
app.router(require('./router'));

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

初始化很简单,接下来详细的说明下

5.1.const app = dva()

app = dva(opts)是用来创建应用,返回dva实例。 opts 包含:

  • history:指定给路由用的 history,默认是 hashHistory
  • initialState:指定初始数据,优先级高于 model 中的 state,默认是 {}

如果要配置 history 为 browserHistory,可以这样:

import { browserHistory } from 'dva/router';
const app = dva({
  history: browserHistory,
});

常见的history有三种形式 另外,出于易用性的考虑,opts 里也可以配所有的 hooks ,下面包含全部的可配属性:

const app = dva({
  history,
  initialState,
  onError,
  onAction,
  onStateChange,
  onReducer,
  onEffect,
  onHmr,
  extraReducers,
  extraEnhancers,
});

5.2.app.use(hooks)

这个是配置 hooks 或者注册插件的,如全局的loading,因为此项目目前还没有用到,所以此句代码被注释

比如注册 dva-loading 插件的例子:

import createLoading from 'dva-loading';
...
app.use(createLoading(opts));

5.3.app.model(model)

modal 是dva中的概念,我的项目所有的数据都存放在此modal中,这样的好处是可以统一管理,它主要是用来接收action的。 简单的说:

state 定义初始化的值

reducers 处理数据(这里可以进行数据的修改)

effects   接收数据(这里可以使用fetch获取数据或者提交数据)

subscriptions 监听数据(监听路由变化等)

如下,一个典型的案例:

app.model({
  namespace: 'todo',
	state: [],
  reducers: {
    add(state, { payload: todo }) {
      // 保存数据到 state
      return [...state, todo];
    },
  },
  effects: {
    *save({ payload: todo }, { put, call }) {
      // 调用 saveTodoToServer,成功后触发 `add` action 保存到 state
      yield call(saveTodoToServer, todo);
      yield put({ type: 'add', payload: todo });
    },
  },
  subscriptions: {
    setup({ history, dispatch }) {
      // 监听 history 变化,当进入 `/` 时触发 `load` action
      return history.listen(({ pathname }) => {
        if (pathname === '/') {
          dispatch({ type: 'load' });
        }
      });
    },
  },
});

官方说明如下:

model 包含 5 个属性:

namespace

model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持通过 . 的方式创建多层命名空间。

state

初始值,优先级低于传给 dva()opts.initialState

比如:

const app = dva({
  initialState: { count: 1 },
});
app.model({
  namespace: 'count',
  state: 0,
});

此时,在 app.start() 后 state.count 为 1 。

reducers

以 key/value 格式定义 reducer。用于处理同步操作,唯一可以修改 state 的地方。由 action 触发。

格式为 (state, action) => newState[(state, action) => newState, enhancer]

详见: https://github.com/dvajs/dva/blob/master/test/reducers-test.js

effects

以 key/value 格式定义 effect。用于处理异步操作和业务逻辑,不直接修改 state。由 action 触发,可以触发 action,可以和服务器交互,可以获取全局 state 的数据等等。

格式为 *(action, effects) => void[*(action, effects) => void, { type }]

type 类型有:

  • takeEvery
  • takeLatest
  • throttle
  • watcher

详见:https://github.com/dvajs/dva/blob/master/test/effects-test.js

subscriptions

以 key/value 格式定义 subscription。subscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。在 app.start() 时被执行,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。

格式为 ({ dispatch, history }, done) => unlistenFunction

注意:如果要使用 app.unmodel(),subscription 必须返回 unlisten 方法,用于取消数据订阅。

5.3.app.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>
);

当然如果项目非常大页面很多的话,使用上面这种路由的方法,在打包后文件是非常大的,这回导致首次加载页面的时候非常缓慢,大项目可以使用下面这种按需加载的方式:

按需加载,可以以下面的方式进行

const Basicplantform = (location, callback) => {
    require.ensure([], require => {callback(null,
    require('./routes/basicplantform'))}, 'Basicplantform')
}


const Login = (location, callback) => {
    require.ensure([], require => {callback(null,
    require('./routes/login/login'))}, 'Login')
};


export default function({ history }) {

  return (
    <Router history={history}>
        <Route path="/" component={System}>
        <Route path="Login" getComponent={Login} />
        <Route path="Basicplantform" getComponent={Basicplantform} />
        </Route>
    </Router>
  );
}

6.代码/components

在components文件加下面写的组件,主要是ui层面的,事件的处理一般会传递到父组件中统一处理,需要用到的数据也是从父组件中通过props传递过来的。

输入图片说明

其实我这里是使用函数式定义了一个组件,React定义组件的方式有三种

  • 函数式定义的无状态组件(会直接省略生命周期的部分 从而 可以大大的加快加载速度)
  • es5原生方式React.createClass定义的组件
  • es6形式的extends React.Component定义的组件

在大部分React代码中,大多数组件被写成无状态的组件,通过简单组合可以构建成其他的组件等;这种通过多个简单然后合并成一个大应用的设计模式被提倡。

详细区别请点击:React定义组件的三种方式

7.代码/routes

components/Weather/index.js引入到此文件中。

输入图片说明

在定义Model和Component之后,我们需要将它们连接在一起。连接后,Component可以使用Model中的数据,而Model可以接收从Component dispatch的操作。而connect主要作用就是将model中的数据当做props传递组件。 注意: connect 是来自于 react-redux。

通过这样的方式传递给Weather组件,mapStateToProps可以获取到我们想要获取到的值,我这里全部获取到了,传递给组件。

function mapStateToProps({ weather }) {
  return { weather };
}
export default connect(mapStateToProps)(Weather);

8.代码/moudels

import {
  briefforecast3days,
  briefcondition,
  briefaqi
} from '../services/weather';

export default {

  namespace: 'weather',

  state: {
    briefdata: {
      city:{
        name:''
      },
      condition: {
        condition:'',
        updatetime: '',
        humidity:'',
        icon:'0',
        temp:'',
        windDir:'',
        windLevel:''
      }
    },
    brief3day: {
      forecast: [{
          conditionDay: '',
          conditionIdDay: '0',
          conditionIdNight: '0',
          conditionNight: '',
          predictDate: '',
          tempDay: '',
          tempNight: '',
          updatetime: '',
          windDirDay: '',
          windDirNight: '',
          windLevelDay: '',
          windLevelNight: ''
        }, {
          conditionDay: '',
          conditionIdDay: '0',
          conditionIdNight: '0',
          conditionNight: '',
          predictDate: '',
          tempDay: '',
          tempNight: '',
          updatetime: '',
          windDirDay: '',
          windDirNight: '',
          windLevelDay: '',
          windLevelNight: ''
        }, {
          conditionDay: '',
          conditionIdDay: '0',
          conditionIdNight: '0',
          conditionNight: '',
          predictDate: '',
          tempDay: '',
          tempNight: '',
          updatetime: '',
          windDirDay: '',
          windDirNight: '',
          windLevelDay: '',
          windLevelNight: ''
        }]
    },
    Drawerstatus:false
  },

  subscriptions: {
    //   订阅监听
    setup({
      dispatch,
      history
    }) {
      history.listen(location => {
        switch (location.pathname) {
          case '/weather':
            dispatch({
              type: 'fetchbrief',
              payload: {
                lat: "32.812036",
                lon: "106.969741"
              }
            });
            dispatch({
              type: 'fetch3days',
              payload: {
                lat: "32.812036",
                lon: "106.969741"
              }
            });
            break;
        }
      });
    },
  },

  effects: {
    //   请求提交数据
    * fetchbrief({
      payload
    }, {
      call,
      put
    }) {
      const {
        data
      } = yield call(briefcondition, payload);
      console.log('fetchbrief:', data);
      if (data.code === 0) {
        yield put({
          type: 'commonData',
          payload: {
            briefdata: data.data
          }
        })
      }
    },
    * fetch3days({
      payload
    }, {
      call,
      put
    }) {
      const {
        data
      } = yield call(briefforecast3days, payload);
      console.log('brief3day:', data);
      if (data.code === 0) {
        yield put({
          type: 'commonData',
          payload: {
            brief3day: data.data
          }
        })
      }
    }
  },

  reducers: {
    //   修改数据
    commonData(state, action) {
      return { ...state,
        ...action.payload
      };
    },
  },

}

再次强调:

state 定义初始化的值

reducers 处理数据(这里可以进行数据的修改)

effects   接收数据(这里可以使用fetch获取数据或者提交数据)

subscriptions 监听数据(监听路由变化等)

1.首先在subscriptions中监听路由,如果路由为/weather则会去执行effects中的fetchbrieffetch3daysfetchbrieffetch3days主要用来请求数据,请求一天的天气和未来三天的预报。 大家不要奇怪 function *(next){...............}这样的写法,这是就是ES6的新特性Generator Function(生成器函数)。详细请看:生成器函数 生成器函数这样写的好处是,通过yeild关键字,可以用同步的写法去写异步的代码,没有了回调函数。

2.请求到数据后通过reducers中的commonData方法去改变state中定义的briefdatabrief3day,简单的说当我们使用put发送一条action的时候 与之对于的reducers就会接收到这个消息 然后在里面返回state等数据。

流程是这样的一个流程,但是call,put,select又是什么鬼? 一般常用的有put call select take 。

  • put 用来发起一条action
  • call 以异步的方式调用函数
  • select 从state中获取相关的数据
  • take 获取发送的数据

输入图片说明

3.如果需要在生成器函数中做路由的跳转的话可以这样

import { routerRedux } from 'dva/router';

*example({payload},{call,put}){
    yeild put(routerRedux.push('/weather'))
}

9.如何通过dispatch修改state中的值?

输入图片说明

那么看图,图上一共有5步,过程是这样的:

  • 1.子组件将ontap事件传递到父组件。
  • 2.父组件执行dsipatch方法,type是又model的namespace和reducers中的方法名组成,根据你里面设置的type内容 然后转发到指定的model的设置正确后model那边才能接收到你发送的这条action。
  • 3.执行reducer中的方法,改变state中的值。

10.如何通过dispatch跳转路由?

在父组件中的function中,通过如下方式写就能进行跳转:

import { routerRedux } from 'dva/router';
onTap(){
     dispatch(routerRedux.push('/audit'));
}

11.路由的写法(注意:页面较多一定要按需加载)


import React, { PropTypes } from 'react';
import { Router, Route, IndexRoute, Link } from 'dva/router';
import IndexPage from './routes/IndexPage';
import info from './routes/Information';
import Weather from './routes/WeatherRoute';
export default function({ history }) {
  return (
    <Router history={history}>
      <Route path="/" component={info} />
      <Route path="/device" component={IndexPage} />
      <Route path="/weather" component={Weather} />
    </Router>
  );
};

下一篇将持续更新

转载于:https://my.oschina.net/u/1271438/blog/1487937

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值