0%

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
2
1 $ npm install dva-cli -g
2 $ dva -v

第三步:dva new 创建新应用

1
1 $ dva new myapp

也可以在创建项目目录 myapp 后,用 dva init 初始化项目

1
1 $ dva init

第四步:运行项目

1
2
1 $ cd myapp
2 $ npm start

浏览器会自动打开一个窗口

image

2.2,项目架构介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|-mock             //存放用于 mock 数据的文件
|-node_modules //项目包
|-public //一般用于存放静态文件,打包时会被直接复制到输出目录(./dist)
|-src //项目源代码
| |-assets //用于存放静态资源,打包时会经过 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

1
2
3
4
5
6
const app = dva({
history,
initialState,
onError,
onHmr,
});

这里比较常用的是,history 的配置,一般默认的是 hashHistory,如果要配置 history 为 browserHistory,可以这样:

1
2
3
4
5
1 import dva from 'dva';
2 import createHistory from 'history/createBrowserHistory';
3 const app = dva({
4 history: createHistory(),
5 });
  • initialState:指定初始数据,优先级高于 model 中的 state,默认是 {},但是基本上都在 modal 里面设置相应的 state

2,app.use(Hooks):配置 hooks 或者注册插件。

1
2
3
4
5
1 app.use({
2 onError: function (error, action) {
3 message.error(error.message || '失败', 5);
4 }
5 });

可以根据自己的需要来选择注册相应的插件

3,app.model(ModelObject):这里是数据逻辑处理,数据流动的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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):注册路由表,我们做路由跳转的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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,而是领域模型,用于把数据相关的逻辑聚合到一起,几乎所有的数据,逻辑都在这边进行处理分发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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

1
2
3
4
5
6
7
8
9
10
// 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 路由变化等等

1
2
3
4
5
6
7
8
9
10
11
12
13
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 获取发送的数据
1
2
3
4
5
6
7
8
9
10
11
12
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里获取数据
},

},

如果 effect 与 reducers 中的 add 方法重合了,这里会陷入一个死循环,因为当组件发送一个 dispatch 的时候,model 会首先去找 effect 里面的方法,当又找到 add 的时候,就又会去请求 effect 里面的方法。

这里的 delayDeal,是我这边写的一个延时的函数,我们在 utils 里面编写一个 utils.js

1
2
3
4
5
6
7
8
9
10
/**
*超时函数处理
* @param timeout :timeout超时的时间参数
* @returns {*} :返回样式值
*/
export function delayDeal(timeout) {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
}

接着我们在 models/example.js 导入这个 utils.js

1
1 import { delayDeal} from '../utils/utils';

6,Reducer

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

1
2
3
4
5
1  reducers: {
2 loginSuccess(state, action){
3 return {...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
2
3
4
5
6
7
{
type: namespace + '/login',
payload: {
userName: payload.userName,
password: payload.password
}
}

构建一个 Action 创建函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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,常见的形式如:

1
2
3
4
1 dispatch({
2 type: namespace + '/login', // 如果在 model 外调用,需要添加 namespace,如果在model内调用 无需添加 namespace
3 payload: {}, // 需要传递的信息
4 });

3.3,使用 antd

先安装 antd 和 babel-plugin-import

1
2
3
1 npm install antd babel-plugin-import --save
2 # 或
3 yarn add antd babel-plugin-import

babel-plugin-import 也可以通过 -D 参数安装到 devDependencies 中,它用于实现按需加载。然后在 .webpackrc 中添加如下配置:

1
2
3
4
5
6
7
8
9
{
"extraBabelPlugins": [
["import", {
"libraryName": "antd",
"libraryDirectory": "es",
"style": true
}]
]
}

现在就可以按需引入 antd 的组件了,如 import { Button } from ‘antd’,Button 组件的样式文件也会自动帮你引入。

3.4,配置.webpackrc

1,entry 是入口文件配置

单页类型:

1
1 entry: './src/entries/index.js',

多页类型:

1
1 "entry": "src/entries/*.js"

2,extraBabelPlugins 定义额外的 babel plugin 列表,格式为数组。

3,env 针对特定的环境进行配置。dev 的环境变量是 development,build 的环境变量是 production。

1
2
3
4
5
6
7
8
9
10
"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 属性

1
2
3
4
5
1 // 配置 @antv/data-set和 rollbar 不打入代码
2 "externals": {
3 '@antv/data-set': 'DataSet',
4 rollbar: 'rollbar',
5 }

5,配置 webpack-dev-server 的 proxy 属性。 如果要代理请求到其他服务器,可以这样配:

1
2
3
4
5
6
7
8
9
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 属性。

打包输出的文件

1
1 config["outputPath"] = path.join(process.cwd(), './build/')

10,根据需求完整配置如下:

文件名称是:.webpackrc.js,可根据实际情况添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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

1
2
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
3
4
5
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 重新渲染组件。如下图所示:

image

数据流向

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State

重置 models 里的数据:

1
1 dispatch({type:namespace+'/set',payload:{mdata:[]}});

set 是内置的方法

五,问题记录

5.1,路由相关的问题

1,使用 match 后的路由跳转问题,版本 routerV4

match 是一个匹配路径参数的对象,它有一个属性 params,里面的内容就是路径参数,除常用的 params 属性外,它还有 url、path、isExact 属性。

问题描述:不能跳转新页面或匹配跳转后,刷新时 url 所传的值会被重置掉

不能跳转的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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>
);
}

路由如上写法,使用下面方式不能跳转,但是地址栏路径变了

1
2
3
4
5
6
7
8
9
10
11
12
import { routerRedux} from 'dva/router';
...

this.props.dispatch(routerRedux.push({
pathname: '/test/bindTest',
search:queryString.stringify({
// ...query,
Code: code,
Name: name
})
}));
...

能跳转,但是刷新所传的参数被重置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 里所传的参数重置

1
2
3
4
5
6
7
8
9
10
11
12
...

this.props.dispatch(routerRedux.push({
pathname: '/test/bindTest',
search:queryString.stringify({
// ...query,
Code: code,
Name: name
})
}));

...

解决办法如下:地址多加一级,跳出以前的界面

路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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>
);
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
...

this.props.dispatch(routerRedux.push({
pathname: '/test/bind/test1',
search:queryString.stringify({
// ...query,
Code: code,
Name: name
})
}));

...

5.2,箭头函数 this 指向问题

箭头函数的 this 定义:箭头函数的 this 是在定义函数时绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this 就继承了定义函数的对象