DvaJS构建配置React项目与使用
DvaJS构建配置React项目与使用
一,介绍与需求分析
1.1,介绍
dva 首先是一个基于redux 和redux-saga的数据流方案,然后为了简化开发体验,dva 还额外内置了react-router 和fetch,所以dva是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装。是由阿里架构师 sorrycc 带领 team 完成的一套前端框架。
1.2,需求
快速搭建基于react的项目(PC端,移动端)。
二,DvaJS构建项目
2.1,初始化项目
第一步:安装node
第二步:安装最新版本dva-cli
$ npm install dva-cli -g
$ dva -v
第三步:dva new 创建新应用
$ dva new myapp
也可以在创建项目目录myapp后,用dva init初始化项目
$ dva init
第四步:运行项目
$ cd myapp
$ npm start
浏览器会自动打开一个窗口

2.2,项目架构介绍
| |-asserts //用于存放静态资源,打包时会经过 webpack 处理
| |-components //组件 存放 React 组件,一般是该项目公用的无状态组件
| |-entries //入口
| |-models //数据模型 存放模型文件
| |-pages //页面视图
| |-routes //路由 存放需要 connect model 的路由组件
| |-services //服务 存放服务文件,一般是网络请求等
| |-test //测试
| |-utils //辅助工具 工具类库
|-package.json //包管理代码
|-webpackrc.js //开发配置
|-tsconfig.json /// ts配置
|-webpack.config.js //webpack配置
三,DvaJS的使用
3.1,DvaJS的五个Api
import dva from 'dva';
import {message} from 'antd';
import './index.css'; // 1. Initialize 创建 dva 应用实例
const app = dva(); // 2. Plugins 装载插件(可选)
app.use({
onError: function (error, action) {
message.error(error.message || '失败', 5);
}
}); // 3. Model 注册model
app.model(require('../models/example').default); // 4. Router 配置路由
app.router(require('../routes/router').default); // 5. Start 启动应用
app.start('#root'); export default app._store; // eslint-disable-line 抛出
1,app = dva(Opts):创建应用,返回 dva 实例。(注:dva 支持多实例)
在opts可以配置所有的hooks
 const app = dva({
      history,
      initialState,
      onError,
      onHmr,
 });
这里比较常用的是,history的配置,一般默认的是hashHistory,如果要配置 history 为 browserHistory,可以这样:
import dva from 'dva';
import createHistory from 'history/createBrowserHistory';
const app = dva({
history: createHistory(),
});
initialState:指定初始数据,优先级高于 model 中的 state,默认是{},但是基本上都在modal里面设置相应的state。
2,app.use(Hooks):配置 hooks 或者注册插件。
 app.use({
   onError: function (error, action) {
     message.error(error.message || '失败', 5);
   }
 });
可以根据自己的需要来选择注册相应的插件
3,app.model(ModelObject):这里是数据逻辑处理,数据流动的地方。
 export default {
   namespace: 'example',//model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,我们发送在发送 action 到相应的 reducer 时,就会需要用到 namespace
   state: {},//表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值)
   subscriptions: {//语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action
     setup({ dispatch, history }) {  // eslint-disable-line
     },
   },
   effects: {//Effect 被称为副作用,最常见的就是异步操作
     *fetch({ payload }, { call, put }) {  // eslint-disable-line
       yield put({ type: 'save' });
     },
   },
   reducers: {//reducers 聚合积累的结果是当前 model 的 state 对象
     save(state, action) {
       return { ...state, ...action.payload };
     },
   },
 };
4,app.router(Function):注册路由表,我们做路由跳转的地方
import React from 'react';
import { routerRedux, Route ,Switch} from 'dva/router';
import { LocaleProvider } from 'antd';
import App from '../components/App/App';
import Flex from '../components/Header/index';
import Login from '../pages/Login/Login';
import Home from '../pages/Home/Home';
import zhCN from 'antd/lib/locale-provider/zh_CN';
const {ConnectedRouter} = routerRedux; function RouterConfig({history}) {
return (
<ConnectedRouter history={history}>
<Switch>
<Route path="/login" component={Login} />
<LocaleProvider locale={zhCN}>
<App>
<Flex>
<Switch>
<Route path="/" exact component={Home} />
</Switch>
</Flex>
</App>
</LocaleProvider>
</Switch>
</ConnectedRouter>
);
} export default RouterConfig;
5,app.start([HTMLElement], opts)
启动我们自己的应用
3.2,DvaJS的十个概念
1,Model
model 是 dva 中最重要的概念,Model 非 MVC 中的 M,而是领域模型,用于把数据相关的逻辑聚合到一起,几乎所有的数据,逻辑都在这边进行处理分发
import Model from 'dva-model';
// import effect from 'dva-model/effect';
import queryString from 'query-string';
import pathToRegexp from 'path-to-regexp';
import {ManagementPage as namespace} from '../../utils/namespace';
import {
getPages,
} from '../../services/page'; export default Model({
namespace,
subscriptions: {
setup({dispatch, history}) { // eslint-disable-line
history.listen(location => {
const {pathname, search} = location;
const query = queryString.parse(search);
const match = pathToRegexp(namespace + '/:action').exec(pathname);
if (match) {
dispatch({
type:'getPages',
payload:{
s:query.s || 10,
p:query.p || 1,
j_code:parseInt(query.j,10) || 1,
}
});
} })
}
},
reducers: {
getPagesSuccess(state, action) {
const {list, total} = action.result;
return {...state, list, loading: false, total};
},
}
}, {
getPages,
})
2,namespace
model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,我们发送在发送 action 到相应的 reducer 时,就会需要用到 namespace
3,State(状态)
初始值,我们在 dva() 初始化的时候和在 modal 里面的 state 对其两处进行定义,其中 modal 中的优先级低于传给 dva() 的 opts.initialState
// dva()初始化
const app = dva({
initialState: { count: 1 },
}); // modal()定义事件
app.model({
namespace: 'count',
state: 0,
});
Model中state的优先级比初始化的低,但是基本上项目中的 state 都是在这里定义的
4,Subscription
Subscriptions 是一种从 源 获取数据的方法,它来自于 elm。语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等
 subscriptions: { //触发器。setup表示初始化即调用。
     setup({dispatch, history}) {
       history.listen(location => {//listen监听路由变化 调用不同的方法
         if (location.pathname === '/login') {
          //清除缓存
         } else {
           dispatch({
             type: 'fetch'
           });
         }
       });
     },
   },
5,Effects
用于处理异步操作和业务逻辑,不直接修改 state,简单的来说,就是获取从服务端获取数据,并且发起一个 action 交给 reducer 的地方。其中它用到了redux-saga里面有几个常用的函数。
- put 用来发起一条action
 - call 以异步的方式调用函数
 - select 从state中获取相关的数据
 - take 获取发送的数据
 
 effects: {
     *login(action, saga){
       const data = yield saga.call(effect(login, 'loginSuccess', authCache), action, saga);//call 用户调用异步逻辑 支持Promise
       if (data && data.token) {
         yield saga.put(routerRedux.replace('/home'));//put 用于触发action 什么是action下面会讲到
       }
     },
     *logout(action, saga){
       const state = yield saga.select(state => state);//select 从state里获取数据
     },
   },
 reducers: {
     add1(state) {
       const newCurrent = state.current + 1;
       return { ...state,
         record: newCurrent > state.record ? newCurrent : state.record,
         current: newCurrent,
       };
     },
     minus(state) {
       return { ...state, current: state.current - 1};
     },
   },
   effects: {
     *add(action, { call, put }) {
       yield put({ type: 'add1' });
       yield call(delayDeal, 1000);
       yield put({ type: 'minus' });
     },
   },
如果effect与reducers中的add方法重合了,这里会陷入一个死循环,因为当组件发送一个dispatch的时候,model会首先去找effect里面的方法,当又找到add的时候,就又会去请求effect里面的方法。
这里的 delayDeal,是我这边写的一个延时的函数,我们在 utils 里面编写一个 utils.js
/**
*超时函数处理
* @param timeout :timeout超时的时间参数
* @returns {*} :返回样式值
*/
export function delayDeal(timeout) {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
}
接着我们在 models/example.js 导入这个 utils.js
 import { delayDeal} from '../utils/utils';
6,Reducer
以key/value 格式定义 reducer,用于处理同步操作,唯一可以修改 state 的地方。由 action 触发。其实一个纯函数。
  reducers: {
     loginSuccess(state, action){
       return {...state, auth: action.result, loading: false};
     },
   }
7,Router
Router 表示路由配置信息,项目中的 router.js
8,RouteComponent
RouteComponent 表示 Router 里匹配路径的 Component,通常会绑定 model 的数据
9,Action:表示操作事件,可以是同步,也可以是异步
action 的格式如下,它需要有一个 type ,表示这个 action 要触发什么操作;payload则表示这个 action 将要传递的数据
 {
      type: namespace + '/login',
      payload: {
           userName: payload.userName,
           password: payload.password
         }
  }
构建一个Action 创建函数,如下:
 function goLogin(payload) {
 let loginInfo = {
             type: namespace + '/login',
             payload: {
               userName: payload.userName,
               password: payload.password
             }
           }
   return loginInfo
 }
 //我们直接dispatch(goLogin()),就发送了一个action。
 dispatch(goLogin())
10,dispatch
type dispatch = (a: Action) => Action
dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。
在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,常见的形式如:
 dispatch({
    type: namespace + '/login', // 如果在 model 外调用,需要添加 namespace,如果在model内调用 无需添加 namespace
   payload: {}, // 需要传递的信息
 });
- reducers 处理数据
 - effects 接收数据
 - subscriptions 监听数据
 
3.3,使用antd
先安装 antd 和 babel-plugin-import
npm install antd babel-plugin-import --save
# 或
yarn add antd babel-plugin-import
babel-plugin-import 也可以通过 -D 参数安装到 devDependencies 中,它用于实现按需加载。然后在 .webpackrc 中添加如下配置:
 {
   "extraBabelPlugins": [
     ["import", {
       "libraryName": "antd",
       "libraryDirectory": "es",
       "style": true
     }]
   ]
 }
现在就可以按需引入 antd 的组件了,如 import { Button } from 'antd',Button 组件的样式文件也会自动帮你引入。
3.4,配置.webpackrc
1,entry是入口文件配置
单页类型:
entry: './src/entries/index.js',
多页类型:
"entry": "src/entries/*.js"
2,extraBabelPlugins 定义额外的 babel plugin 列表,格式为数组。
3,env针对特定的环境进行配置。dev 的环境变量是?development,build 的环境变量是?production。
"extraBabelPlugins": ["transform-runtime"],
"env": {
development: {
extraBabelPlugins: ['dva-hmr'],
},
production: {
define: {
__CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' }
}
}
开发环境下的 extraBabelPlugins 是?["transform-runtime", "dva-hmr"],而生产环境下是?["transform-runtime"]
4,配置 webpack 的?externals?属性
// 配置 @antv/data-set和 rollbar 不打入代码
"externals": {
'@antv/data-set': 'DataSet',
rollbar: 'rollbar',
}
5,配置 webpack-dev-server 的 proxy 属性。 如果要代理请求到其他服务器,可以这样配:
   proxy: {
     "/api": {
       // "target": "http://127.0.0.1/",
       // "target": "http://127.0.0.1:9090/",
       "target": "http://localhost:8080/",
       "changeOrigin": true,
      "pathRewrite": { "^/api" : "" }
     }
   },
6,disableDynamicImport
禁用 import() 按需加载,全部打包在一个文件里,通过 babel-plugin-dynamic-import-node-sync 实现。
7,publicPath
配置 webpack 的 output.publicPath 属性。
8,extraBabelIncludes
定义额外需要做 babel 转换的文件匹配列表,格式为数组
9,outputPath
配置 webpack 的 output.path 属性。
打包输出的文件
config["outputPath"] = path.join(process.cwd(), './build/')
10,根据需求完整配置如下:
文件名称是:.webpackrc.js,可根据实际情况添加如下代码:
 const path = require('path');
 const config = {
   entry: './src/entries/index.js',
   extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]],
   env: {
     development: {
       extraBabelPlugins: ['dva-hmr'],
     },
     production: {
       define: {
         __CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' }
     }
   },
   externals: {
     '@antv/data-set': 'DataSet',
     rollbar: 'rollbar',
   },
   lessLoaderOptions: {
     javascriptEnabled: true,
   },
   proxy: {
     "/api": {
       // "target": "http://127.0.0.1/",
       // "target": "http://127.0.0.1:9090/",
       "target": "http://localhost:8080/",
       "changeOrigin": true,
     }
   },
   es5ImcompatibleVersions:true,
   disableDynamicImport: true,
   publicPath: '/',
   hash: false,
   extraBabelIncludes:[
     "node_modules"
   ]
 };
 if (module.exports.env !== 'development') {
   config["outputPath"] = path.join(process.cwd(), './build/')
 }
 export default config
更多 .webpackrc 的配置请参考 roadhog 配置。
3.5,使用antd-mobile
先安装 antd-mobile 和 babel-plugin-import
npm install antd-mobile babel-plugin-import --save # 或
yarn add antd-mobile babel-plugin-import
babel-plugin-import 也可以通过 -D 参数安装到 devDependencies 中,它用于实现按需加载。然后在 .webpackrc 中添加如下配置:
 {
   "plugins": [
     ["import", { libraryName: "antd-mobile", style: "css" }] // `style: true` 会加载 less 文件
   ]
 }
现在就可以按需引入antd-mobile 的组件了,如 import { DatePicker} from 'antd-mobile',DatePicker 组件的样式文件也会自动帮你引入。
四,整体架构
- 我们根据 
url访问相关的Route-Component,在组件中我们通过dispatch发送action到model里面的effect或者直接Reducer - 当我们将
action发送给Effect,基本上是取服务器上面请求数据的,服务器返回数据之后,effect会发送相应的action给reducer,由唯一能改变state的reducer改变state,然后通过connect重新渲染组件。 - 当我们将
action发送给reducer,那直接由reducer改变state,然后通过connect重新渲染组件。如下图所示: 

数据流向
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State
重置models里的数据:
 dispatch({type:namespace+'/set',payload:{mdata:[]}});
set是内置的方法
Dva官方文档 nginx代理部署Vue与React项目
五,问题记录
5.1,路由相关的问题
1,使用match后的路由跳转问题,版本routerV4
match是一个匹配路径参数的对象,它有一个属性params,里面的内容就是路径参数,除常用的params属性外,它还有url、path、isExact属性。
问题描述:不能跳转新页面或匹配跳转后,刷新时url所传的值会被重置掉
不能跳转的情况
 const {ConnectedRouter} = routerRedux;
 function RouterConfig({history}) {
 const tests =({match}) =>(
     <div>
       <Route exact path={`${match.url}/:tab`} component={Test}/>
       <Route exact path={match.url} component={Test}/>
     </div>
   );
   return (
     <ConnectedRouter history={history}>
       <Switch>
         <Route path="/login" component={Login}/>
         <LocaleProvider locale={zhCN}>
           <App>
             <Flex>
               <Switch>
                 <Route path="/test" component={tests }/>
                <Route exact path="/test/bindTest" component={BindTest}/>
               </Switch>
             </Flex>
           </App>
         </LocaleProvider>
       </Switch>
     </ConnectedRouter>
   );
 }
路由如上写法,使用下面方式不能跳转,但是地址栏路径变了
 import { routerRedux} from 'dva/router';
 ...
 this.props.dispatch(routerRedux.push({
       pathname: '/test/bindTest',
       search:queryString.stringify({
         // ...query,
         Code: code,
         Name: name
       })
     }));
 ...
能跳转,但是刷新所传的参数被重置
 const {ConnectedRouter} = routerRedux;
 function RouterConfig({history}) {
 const tests =({match}) =>(
     <div>
       <Route exact path={`${match.url}/bindTest`} component={BindTest}/>
       <Route exact path={`${match.url}/:tab`} component={Test}/>
       <Route exact path={match.url} component={Test}/>
     </div>
   );
   return (
     <ConnectedRouter history={history}>
       <Switch>
         <Route path="/login" component={Login}/>
         <LocaleProvider locale={zhCN}>
           <App>
             <Flex>
               <Switch>
                 <Route path="/test" component={tests }/>
               </Switch>
             </Flex>
           </App>
         </LocaleProvider>
       </Switch>
     </ConnectedRouter>
   );
 }
路由如上写法,使用下面方式可以跳转,但是刷新时所传的参数会被test里所传的参数重置
 ...
 this.props.dispatch(routerRedux.push({
         pathname: '/test/bindTest',
         search:queryString.stringify({
           // ...query,
           Code: code,
           Name: name
        })
 }));
 ...
解决办法如下:地址多加一级,跳出以前的界面
路由配置
 const {ConnectedRouter} = routerRedux;
 function RouterConfig({history}) {
 const tests =({match}) =>(
     <div>
       <Route exact path={`${match.url}/bind/test`} component={BindTest}/>
       <Route exact path={`${match.url}/:tab`} component={Test}/>
       <Route exact path={match.url} component={Test}/>
     </div>
   );
   return (
     <ConnectedRouter history={history}>
               <Switch>
                 <Route path="/test" component={tests }/>
               </Switch>
     </ConnectedRouter>
   );
 }
调用
 ...
 this.props.dispatch(routerRedux.push({
       pathname: '/test/bind/test1',
       search:queryString.stringify({
         // ...query,
         Code: code,
         Name: name
       })
     }));
 ...
5.2,箭头函数this指向问题
箭头函数的this定义:箭头函数的this是在定义函数时绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this就继承了定义函数的对象。
DvaJS构建配置React项目与使用的更多相关文章
- 使用 Dawn 构建 React 项目
		
开发一个 React 项目,通常避免不了要去配置 Webpack 和 babel 之类,以支持 commonjs 或 es 模块及各种 es 新语法,及及进行 jsx 语法的转义.当然也可以用 cre ...
 - 🌅 使用 Dawn 快速搭建 React 项目!
		
开发一个 React 项目,通常避免不了要去配置 Webpack 和 babel 之类,以支持 commonjs 或 es 模块及各种 es 新语法,及进行 jsx 语法的转义.当然也可以用 crea ...
 - webpack构建react项目(一)
		
前言 下面是我们使用到技术栈: webpack + react + redux + react-router + react-thunk + ES6 + .... 注意事项: 建议使用npm5.X 或 ...
 - Webpack+React项目入门——入门及配置Webpack
		
一.入门Webpack 参考文章:<入门Webpack,看这篇就够了> 耐心看完这篇非常有帮助 二.React+Webpack环境配置 参考文章:<webpack+react项目初体 ...
 - 【Webpack2.X笔记】 配合react项目进行配置
		
前言: 本文是自己在工作中使用webpack进行react开发项目构建的一些经验总结,做以记录防范后续踩坑. 如果您还没有webpack相关基础,请先移步 入门Webpack,看这篇就够了 进行基础学 ...
 - React环境配置(第一个React项目)
		
使用Webpack构建React项目 1. 使用NPM配置React环境 NPM及React安装自行百度 首先创建一个文件夹,the_first_React 进入到创建好的目录,npm init,然后 ...
 - [react001] 使用webpack自动构建react 项目
		
1.react 简介 React 是一个Facebook出品的前端UI开发框架.react官方的tutorials 为了让人容易上手,并没有给在平常工作使用react的详细配置,随意学习的深入,你为了 ...
 - 简述--构建React项目的几种方式
		
前言: 构建React项目的几种方式: 构建:create-react-app 快速脚手架 构建:generator-react-webpack 构建:webpack一步一步构建 1)构建:creat ...
 - 使用create-react-app+react-router-dom+axios+antd+react-redux构建react项目
		
1.安装.构建 # 全局安装 npm install -g create-react-app # 构建一个my-app的项目 npx create-react-app my-app cd my-app ...
 
随机推荐
- 零元学Expression Blend 4 - Chapter 47 超简单!运用StackPanel配合OpacityMask做出倒影效果
			
原文:零元学Expression Blend 4 - Chapter 47 超简单!运用StackPanel配合OpacityMask做出倒影效果 有网友问我如何在Blend内制作出倒影效果 我提供了 ...
 - Android零基础入门第32节:新推出的GridLayout网格布局
			
原文:Android零基础入门第32节:新推出的GridLayout网格布局 本期主要学习的是网格布局是Android 4.0新增的布局,和前面所学的TableLayout表格布局 有点类似,不过他有 ...
 - Android零基础入门第43节:ListView优化和列表首尾使用
			
原文:Android零基础入门第43节:ListView优化和列表首尾使用 前面连续几期都在学习ListView的各种使用方法,如果细心的同学可能会发现其运行效率是有待提高的,那么本期就来一起学习有哪 ...
 - 毕设(三)NotifyIcon
			
NotifyIcon是一个比较特殊的组件,其特殊之处是既可以把它归类到控件中,也可以把它归类到组件中.这是因为将其拖放到设计窗体后,我们并不能马上看到它的界面(像组件),而是在运行时才能看到它(像控件 ...
 - Delphi For Linux Compiler
			
Embarcadero is about to release a new Delphi compiler for the Linux platform. Here are some of the k ...
 - Delphi指针运用理解
			
现在是面向对象漫天飞的年代了,大家都在在谈面向对象编程.Java对指针“避而不谈”,C#虽然支持指针运用,但是也淡化处理. 然而,指针还是好完全掌握为妙,省得在开发过程碰钉子,至于对指针的运用在于开发 ...
 - 关于Qt 5-MSVC 2015 64位在 win7 64位系统debug程序崩溃的问题
			
关于Qt 5-MSVC 2015 64位在 win7 64位系统debug程序崩溃的问题 在win7 64位系统安装VC2015的编译器,并安装了 Qt 5.6 -5.7 VC2015 64位版本测 ...
 - 【Linux】linux中删除指定日期之前的文件
			
要删除系统中就的备份文件,就需要使用命令了: #find /tmp -mtime +30 -type f -name *.sh[ab] -exec rm -f {} \; 假如在一个目录中保留最近30 ...
 - SYN1618型 高精度天文时间同步系统
			
SYN1618型 高精度天文时间同步系统 产品概述 SYN1618型 高精度天文时间同步系统是由西安同步电子科技有限公司精心设计.自行研发生产的一款高精度的时频频率标准设备,接收GPS.GLON ...
 - C语言实现常用数据结构——图
			
#include<stdio.h> #include<stdlib.h> #define SIZE 20 #define LENGTH(a) (sizeof(a)/sizeof ...