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
1 $ npm install dva-cli -g
2 $ dva -v
第三步:dva new 创建新应用
1 $ dva new myapp
也可以在创建项目目录myapp后,用dva init初始化项目
1 $ dva init
第四步:运行项目
1 $ cd myapp
2 $ npm start
浏览器会自动打开一个窗口
2.2,项目架构介绍
|-mock//存放用于 mock 数据的文件
|-node_modules//项目包
|-public//一般用于存放静态文件,打包时会被直接复制到输出目录(./dist)
|-src//项目源代码
||-asserts//用于存放静态资源,打包时会经过 webpack 处理
||-caches//缓存
||-components//组件 存放 React 组件,一般是该项目公用的无状态组件
||-entries//入口
||-models//数据模型 存放模型文件
||-pages//页面视图
||-routes//路由 存放需要 connect model 的路由组件
||-services//服务 存放服务文件,一般是网络请求等
||-test//测试
||-utils//辅助工具 工具类库
|-package.json//包管理代码
|-webpackrc.js//开发配置
|-tsconfig.json/// ts配置
|-webpack.config.js //webpack配置
|-.gitignore //Git忽略文件
在dva项目目录中主要分3层,models,services,components,其中models是最重要概念,这里放的是各种数据,与数据交互的应该都是在这里。services是请求后台接口的方法。components是组件了。
三,DvaJS的使用
3.1,DvaJS的五个Api
复制代码
1 import dva from 'dva';
2 import {message} from 'antd';
3 import './index.css';
4
5 // 1. Initialize 创建 dva 应用实例
6 const app = dva();
7
8 // 2. Plugins 装载插件(可选)
9 app.use({
10onError: function (error, action) {
11message.error(error.message || '失败', 5);
12}
13 });
14
15 // 3. Model 注册model
16app.model(require('../models/example').default);
17
18 // 4. Router 配置路由
19 app.router(require('../routes/router').default);
20
21 // 5. Start 启动应用
22 app.start('#root');
23
24 export default app._store; // eslint-disable-line 抛出
复制代码
1,app = dva(Opts):创建应用,返回 dva 实例。(注:dva 支持多实例)?
在opts可以配置所有的hooks
复制代码
1 const app = dva({
2history,
3initialState,
4onError,
5onHmr,
6 });
复制代码
这里比较常用的是,history的配置,一般默认的是hashHistory,如果要配置 history 为 browserHistory,可以这样:
1 import dva from 'dva';
2 import createHistory from 'history/createBrowserHistory';
3 const app = dva({
4history: createHistory(),
5 });
initialState:指定初始数据,优先级高于 model 中的 state,默认是 {},但是基本上都在modal里面设置相应的state。
2,app.use(Hooks):配置 hooks 或者注册插件。
1 app.use({
2onError: function (error, action) {
3message.error(error.message || '失败', 5);
4}
5 });
可以根据自己的需要来选择注册相应的插件
3,app.model(ModelObject):这里是数据逻辑处理,数据流动的地方。
复制代码
1 export default {
2
3namespace: 'example',//model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,我们发送在发送 action 到相应的 reducer 时,就会需要用到 namespace
4
5state: {},//表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值)
6
7subscriptions: {//语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action
8setup({ dispatch, history }) {// eslint-disable-line
9},
10},
11
12effects: {//Effect 被称为副作用,最常见的就是异步操作
13*fetch({ payload }, { call, put }) {// eslint-disable-line
14yield put({ type: 'save' });
15},
16},
17
18reducers: {//reducers 聚合积累的结果是当前 model 的 state 对象
19save(state, action) {
20return { ...state, ...action.payload };
21},
22},
23
24 };
复制代码
4,app.router(Function):注册路由表,我们做路由跳转的地方
复制代码
1 import React from 'react';
2 import { routerRedux, Route ,Switch} from 'dva/router';
3 import { LocaleProvider } from 'antd';
4 import App from '../components/App/App';
5 import Flex from '../components/Header/index';
6 import Login from '../pages/Login/Login';
7 import Home from '../pages/Home/Home';
8 import zhCN from 'antd/lib/locale-provider/zh_CN';
9 const {ConnectedRouter} = routerRedux;
10
11 function RouterConfig({history}) {
12return (
13
14
15
16
17
18
19
20
21
22

23
24

25
26

27);
28 }
29
30 export default RouterConfig;
复制代码
5,app.start([HTMLElement], opts)
启动我们自己的应用
3.2,DvaJS的十个概念
1,Model
model 是 dva 中最重要的概念,Model 非 MVC 中的 M,而是领域模型,用于把数据相关的逻辑聚合到一起,几乎所有的数据,逻辑都在这边进行处理分发
复制代码
1 import Model from 'dva-model';
2 // import effect from 'dva-model/effect';
3 import queryString from 'query-string';
4 import pathToRegexp from 'path-to-regexp';
5 import {ManagementPage as namespace} from '../../utils/namespace';
6 import {
7getPages,
8 } from '../../services/page';
9
10 export default Model({
11namespace,
12subscriptions: {
13setup({dispatch, history}) {// eslint-disable-line
14history.listen(location => {
15const {pathname, search} = location;
16const query = queryString.parse(search);
17const match = pathToRegexp(namespace + '/:action').exec(pathname);
18if (match) {
19dispatch({
20type:'getPages',
21payload:{
22s:query.s || 10,
23p:query.p || 1,
24j_code:parseInt(query.j,10) || 1,
25}
26});
27}
28
29})
30}
31},
32reducers: {
33getPagesSuccess(state, action) {
34const {list, total} = action.result;
35return {...state, list, loading: false, total};
36},
37}
38 }, {
39getPages,
40 })
复制代码
2,namespace
model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,我们发送在发送 action 到相应的 reducer 时,就会需要用到 namespace
3,State(状态)
初始值,我们在 dva() 初始化的时候和在 modal 里面的 state 对其两处进行定义,其中 modal 中的优先级低于传给 dva() 的 opts.initialState
复制代码
1 // dva()初始化
2 const app = dva({
3initialState: { count: 1 },
4 });
5
6 // modal()定义事件
7 app.model({
8namespace: 'count',
9state: 0,
10 });
复制代码
Model中state的优先级比初始化的低,但是基本上项目中的 state 都是在这里定义的
4,Subscription
Subscriptions 是一种从 源 获取数据的方法,它来自于 elm。语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等
复制代码
1 subscriptions: { //触发器。setup表示初始化即调用。
2setup({dispatch, history}) {
3history.listen(location => {//listen监听路由变化 调用不同的方法
4if (location.pathname === '/login') {
5//清除缓存
6} else {
7dispatch({
8type: 'fetch'
9});
10}
11});
12},
13},
复制代码
5,Effects
用于处理异步操作和业务逻辑,不直接修改 state,简单的来说,就是获取从服务端获取数据,并且发起一个 action 交给 reducer 的地方。其中它用到了redux-saga里面有几个常用的函数。
put用来发起一条action
call 以异步的方式调用函数
select 从state中获取相关的数据
take 获取发送的数据
复制代码
1 effects: {
2*login(action, saga){
3const data = https://www.it610.com/article/yield saga.call(effect(login,'loginSuccess', authCache), action, saga); //call 用户调用异步逻辑 支持Promise
4if (data && data.token) {
5yield saga.put(routerRedux.replace('/home')); //put 用于触发action 什么是action下面会讲到
6}
7},
8*logout(action, saga){
9const state = yield saga.select(state => state); //select 从state里获取数据
10},
11
12},
复制代码
复制代码
1 reducers: {
2add1(state) {
3const newCurrent = state.current + 1;
4return { ...state,
5record: newCurrent > state.record ? newCurrent : state.record,
6current: newCurrent,
7};
8},
9minus(state) {
10return { ...state, current: state.current - 1};
11},
12},
13effects: {
14*add(action, { call, put }) {
15yield put({ type: 'add1' });
16yield call(delayDeal, 1000);
17yield put({ type: 'minus' });
18},
19},
复制代码
如果effect与reducers中的add方法重合了,这里会陷入一个死循环,因为当组件发送一个dispatch的时候,model会首先去找effect里面的方法,当又找到add的时候,就又会去请求effect里面的方法。
这里的 delayDeal,是我这边写的一个延时的函数,我们在 utils 里面编写一个 utils.js
复制代码
1 /**
2*超时函数处理
3* @param timeout:timeout超时的时间参数
4* @returns {*} :返回样式值
5*/
6 export function delayDeal(timeout) {
7return new Promise((resolve) => {
8setTimeout(resolve, timeout);
9});
10 }
复制代码
接着我们在 models/example.js 导入这个 utils.js
1 import { delayDeal} from '../utils/utils';
6,Reducer
以key/value 格式定义 reducer,用于处理同步操作,唯一可以修改 state 的地方。由 action 触发。其实一个纯函数。
1reducers: {
2loginSuccess(state, action){
3return {...state, auth: action.result, loading: false};
4},
5}
7,Router
Router 表示路由配置信息,项目中的 router.js
8,RouteComponent
RouteComponent 表示 Router 里匹配路径的 Component,通常会绑定 model 的数据
9,Action:表示操作事件,可以是同步,也可以是异步
action 的格式如下,它需要有一个 type ,表示这个 action 要触发什么操作;payload则表示这个 action 将要传递的数据
复制代码
1 {
2type: namespace + '/login',
3payload: {
4userName: payload.userName,
5password: payload.password
6}
7}
复制代码
构建一个Action 创建函数,如下:
复制代码
1 function goLogin(payload) {
2 let loginInfo = {
3type: namespace + '/login',
4payload: {
5userName: payload.userName,
6password: payload.password
7}
8}
9return loginInfo
10 }
11
12 //我们直接dispatch(goLogin()),就发送了一个action。
13 dispatch(goLogin())
复制代码
10,dispatch
type dispatch = (a: Action) => Action
dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。
在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,常见的形式如:
1 dispatch({
2type: namespace + '/login', // 如果在 model 外调用,需要添加 namespace,如果在model内调用 无需添加 namespace
3payload: {}, // 需要传递的信息
4 });
reducers 处理数据
effects接收数据
subscriptions 监听数据
3.3,使用antd
先安装 antd 和 babel-plugin-import
1 npm install antd babel-plugin-import --save
2 # 或
3 yarn add antd babel-plugin-import
babel-plugin-import 也可以通过 -D 参数安装到 devDependencies 中,它用于实现按需加载。然后在 .webpackrc 中添加如下配置:
复制代码
1 {
2"extraBabelPlugins": [
3["import", {
4"libraryName": "antd",
5"libraryDirectory": "es",
6"style": true
7}]
8]
9 }
复制代码
现在就可以按需引入 antd 的组件了,如 import { Button } from 'antd',Button 组件的样式文件也会自动帮你引入。
3.4,配置.webpackrc
1,entry是入口文件配置
单页类型:
1 entry: './src/entries/index.js',
多页类型:
1 "entry": "src/entries/*.js"
2,extraBabelPlugins 定义额外的 babel plugin 列表,格式为数组。
3,env针对特定的环境进行配置。dev 的环境变量是?development,build 的环境变量是?production。
复制代码
1 "extraBabelPlugins": ["transform-runtime"],
2 "env": {
3development: {
4extraBabelPlugins: ['dva-hmr'],
5},
6production: {
7define: {
【DvaJS构建配置React项目与使用】 8__CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' }
9}
10 }
复制代码
开发环境下的 extraBabelPlugins 是?["transform-runtime", "dva-hmr"],而生产环境下是?["transform-runtime"]
4,配置 webpack 的?externals?属性
1 // 配置 @antv/data-set和 rollbar 不打入代码
2 "externals": {
3'@antv/data-set': 'DataSet',
4rollbar: 'rollbar',
5 }
5,配置 webpack-dev-server 的 proxy 属性。 如果要代理请求到其他服务器,可以这样配:
复制代码
1proxy: {
2"/api": {
3// "target": "http://127.0.0.1/",
4// "target": "http://127.0.0.1:9090/",
5"target": "http://localhost:8080/",
6"changeOrigin": true,
7"pathRewrite": { "^/api" : "" }
8}
9},
复制代码
6,disableDynamicImport
禁用 import() 按需加载,全部打包在一个文件里,通过 babel-plugin-dynamic-import-node-sync 实现。
7,publicPath
配置 webpack 的 output.publicPath 属性。
8,extraBabelIncludes
定义额外需要做 babel 转换的文件匹配列表,格式为数组
9,outputPath
配置 webpack 的 output.path 属性。
打包输出的文件
1 config["outputPath"] = path.join(process.cwd(), './build/')
10,根据需求完整配置如下:
文件名称是:.webpackrc.js,可根据实际情况添加如下代码:
复制代码
1 const path = require('path');
2
3 const config = {
4entry: './src/entries/index.js',
5extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]],
6env: {
7development: {
8extraBabelPlugins: ['dva-hmr'],
9},
10production: {
11define: {
12__CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' }
13}
14},
15externals: {
16'@antv/data-set': 'DataSet',
17rollbar: 'rollbar',
18},
19lessLoaderOptions: {
20javascriptEnabled: true,
21},
22proxy: {
23"/api": {
24// "target": "http://127.0.0.1/",
25// "target": "http://127.0.0.1:9090/",
26"target": "http://localhost:8080/",
27"changeOrigin": true,
28}
29},
30es5ImcompatibleVersions:true,
31disableDynamicImport: true,
32publicPath: '/',
33hash: false,
34extraBabelIncludes:[
35"node_modules"
36]
37 };
38 if (module.exports.env !== 'development') {
39config["outputPath"] = path.join(process.cwd(), './build/')
40 }
41 export default config
复制代码
更多 .webpackrc 的配置请参考 roadhog 配置。
3.5,使用antd-mobile
先安装 antd-mobile 和 babel-plugin-import
1 npm install antd-mobile babel-plugin-import --save # 或
2 yarn add antd-mobile babel-plugin-import
babel-plugin-import 也可以通过 -D 参数安装到 devDependencies 中,它用于实现按需加载。然后在 .webpackrc 中添加如下配置:
1 {
2"plugins": [
3["import", { libraryName: "antd-mobile", style: "css" }] // `style: true` 会加载 less 文件
4]
5 }
现在就可以按需引入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里的数据:
1 dispatch({type:namespace+'/set',payload:{mdata:[]}});
set是内置的方法
Dva官方文档nginx代理部署Vue与React项目
五,问题记录
5.1,路由相关的问题
1,使用match后的路由跳转问题,版本routerV4
match是一个匹配路径参数的对象,它有一个属性params,里面的内容就是路径参数,除常用的params属性外,它还有url、path、isExact属性。
问题描述:不能跳转新页面或匹配跳转后,刷新时url所传的值会被重置掉
不能跳转的情况
复制代码
1 const {ConnectedRouter} = routerRedux;
2
3 function RouterConfig({history}) {
4 const tests =({match}) =>(
5
6
7
8
9
10);
11return (
12
13
14
15
16
17
18
19
20
21
22
23

24
25

26
27

28);
29 }
复制代码
路由如上写法,使用下面方式不能跳转,但是地址栏路径变了
复制代码
1 import { routerRedux} from 'dva/router';
2 ...
3
4 this.props.dispatch(routerRedux.push({
5pathname: '/test/bindTest',
6search:queryString.stringify({
7// ...query,
8Code: code,
9Name: name
10})
11}));
12
13 ...
复制代码
能跳转,但是刷新所传的参数被重置
复制代码
1 const {ConnectedRouter} = routerRedux;
2
3 function RouterConfig({history}) {
4 const tests =({match}) =>(
5
6
7
8
9
10
11);
12return (
13
14
15
16
17
18
19
20
21
22

23
24

25
26

27);
28 }
复制代码
路由如上写法,使用下面方式可以跳转,但是刷新时所传的参数会被test里所传的参数重置
复制代码
1 ...
2
3 this.props.dispatch(routerRedux.push({
4pathname: '/test/bindTest',
5search:queryString.stringify({
6// ...query,
7Code: code,
8Name: name
9})
10 }));
11
12 ...
复制代码
解决办法如下:地址多加一级,跳出以前的界面
路由配置
复制代码
1 const {ConnectedRouter} = routerRedux;
2
3 function RouterConfig({history}) {
4 const tests =({match}) =>(
5
6
7
8
9
10
11);
12return (
13
14
15
16
17

18);
19 }
复制代码
调用
复制代码
1 ...
2
3 this.props.dispatch(routerRedux.push({
4pathname: '/test/bind/test1',
5search:queryString.stringify({
6// ...query,
7Code: code,
8Name: name
9})
10}));
11
12 ...
东莞网站建设www.zg886.cn

    推荐阅读