您的位置:首页 > Web前端 > React

DvaJS构建配置React项目与使用

2019-07-20 14:33 1971 查看

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,项目架构介绍

|-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

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(),
});
  • [li]
    initialState
    :指定初始数据,优先级高于 model 中的 state,默认是 
    {}
    ,但是基本上都在modal里面设置相应的state。
[/li]

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

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

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

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

export default {

namespace: 'example',//[code]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 }; }, }, };[/code]

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的优先级比初始化的低,但是基本上项目中的 [code]state
 都是在这里定义的[/code]

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里面有几个常用的函数。

  1. put  用来发起一条action
  2. call 以异步的方式调用函数
  3. select 从state中获取相关的数据
  4. 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: {}, // 需要传递的信息
});
  1. reducers 处理数据
  2. effects   接收数据
  3. 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 组件的样式文件也会自动帮你引入。

四,整体架构

  1. 我们根据
    url
    访问相关的
    Route-Component
    ,在组件中我们通过
    dispatch
    发送
    action
    model
    里面的
    effect
    或者直接
    Reducer
  2. 当我们将
    action
    发送给
    Effect
    ,基本上是取服务器上面请求数据的,服务器返回数据之后,
    effect
    会发送相应的
    action
    reducer
    ,由唯一能改变
    state
    reducer
    改变
    state
    ,然后通过
    connect
    重新渲染组件。
  3. 当我们将
    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就继承了定义函数的对象。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: