dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架!
介绍 | DvaJS
易学易用,仅有 6 个 api,对 redux 用户尤其友好,配合 umi 使用后更是降低为 0 API
elm 概念,通过 reducers, effects 和 subscriptions 组织 model
插件机制,比如 dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
支持 HMR,基于 babel-plugin-dva-hmr 实现 components、routes 和 models 的 HMR
在create-react-app脚手架的基础上,额外安装的内容:
无需手动进行antd按需导入
无需安装:redux及redux-saga、react-redux、react-router-dom等,dva把这些东西都集成好了,安装一个dva就相当于安装了这些全部东西!!
react-router-dom使用的是v4版本「4.3.1」
redux使用的是 v3.7.2「我们之前使用的都是v4.0」
集成的配套插件版本有点低
在React18的脚手架中使用dva会有警告错误!!
history 是控制路由模式的
其余的按照之前讲的配置方案去配置webpack,包括:less、跨域代理、兼容、响应式布局等
{
"dependencies": {
"antd": "^5.0.0",
"antd-icons": "^0.1.0-alpha.1",
"dva": "^2.4.1",
"http-proxy-middleware": "^2.0.6",
"less": "^4.1.3",
"less-loader": "^8.1.1",
"prop-types": "^15.8.1",
"styled-components": "^5.3.6",
"history": "4.10.1",
......
}
}
项目的结构目录,可以依然沿用之前的命名风格:
api 接口管理和请求封装
assets 静态资源文件
router 路由统一配置
store redux公共状态管理
views 普通业务组件
components 公共业务组件
index.jsx 入口
setupProxy.js 跨域代理
…
但是有很多文件的编写方式和之前是不一样的!!
index.js
入口
import dva from 'dva'; import createHistory from 'history/createHashHistory'; import RouterConfig from './router'; import voteModel from './store/voteModel'; // 初始化配置 const app = dva({ // 设置路由模式{默认HASH路由} history: createHistory() }); // 使用插件 app.use({}); // redux公共状态管理 app.model(voteModel); // 路由配置 app.router(RouterConfig); // 启动dva app.start('#root');
router/index.js
配置页面入口和路由import React from 'react'; import { Router, Route, Switch, Redirect } from 'dva/router'; import Vote from '../views/Vote'; import Demo from '../views/Demo'; /* ANTD */ import { ConfigProvider } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import '../assets/reset.min.css'; function RouterConfig({ history }) { return (
); } export default RouterConfig;
store/voteModel.js
配置每个模块的Model,包含:状态、reducer、异步派发的方法等import _ from '../assets/utils'; const delay = (interval = 1000) => { return new Promise(resolve => { setTimeout(() => { resolve(); }, interval); }); }; export default { namespace: 'vote', state: { supNum: 10, oppNum: 5 }, reducers: { support(state, action) { state = _.clone(true, state); let { payload = 1 } = action; state.supNum += payload; return state; }, oppose(state, action) { state = _.clone(true, state); let { payload = 1 } = action; state.oppNum += payload; return state; } }, effects: { supportAsync: [ function* ({ payload }, { call, put }) { yield call(delay, 2000); yield put({ type: 'support', payload }); }, { type: 'takeLatest' } ], *opposeAsync({ payload }, { call, put }) { yield call(delay, 2000); yield put({ type: 'oppose', payload }); } } };
在组件中如何使用呢?
import React from "react"; import styled from "styled-components"; import { Button } from 'antd'; import { connect } from 'dva'; // 样式处理 const VoteBox = styled.div` ... `; const Vote = function Vote(props) { let { supNum, oppNum, dispatch } = props; return
; }; export default connect(state => state.vote)(Vote); React是很棒的前端框架
{supNum + oppNum}支持人数:{supNum}人
反对人数:{oppNum}人
dva脚手架创建的项目是基于
roadhog /rəʊd hog/
进行webpack的配置!!
roadhog是一个cli工具,提供server、 build和test三个命令,分别用于本地调试和构建,并且提供了特别易用的mock功能。命令行体验和create-react-app一致,配置略有不同,比如默认开启 css modules,然后还提供了JSON格式的配置方式!
$ npm install dva-cli -g
$ dva -v
$ dva new my-project
{
"private": true,
"scripts": {
"start": "cross-env PORT=3000 HOST=127.0.0.1 roadhog server", //开发环境启动
"build": "roadhog build", //生产环境打包
"lint": "eslint --ext .js src test", //单元测试
"precommit": "npm run lint"
},
"dependencies": {
"@babel/polyfill": "^7.12.1",
"antd": "4.24.7", //注意版本用v4「不是最新的v5」
"antd-icons": "^0.1.0-alpha.1",
"babel-plugin-import": "^1.13.5", //antd按需导入
"dva": "^2.4.1",
"history": "4.10.1", //管理路由模式的「用v4不是最新的v5版本」
"lib-flexible": "^0.3.2",
"postcss-pxtorem": "5.1.1",
"prop-types": "^15.8.1",
"qs": "^6.11.0",
"react": "^16.2.0", //react使用的是v16版本
"react-dom": "^16.2.0",
"styled-components": "^5.3.6"
},
"devDependencies": {
"babel-plugin-dva-hmr": "^0.3.2", //热更新
"cross-env": "^7.0.3",
"less": "4.1.3",
"less-loader": "8.1.1",
...
}
}
修改webpack配置项
修改启动的域名和端口号:设置环境变量即可
PORT
HOST
HTTPS 是否开启https,默认关闭
BROWSER 设为none时不自动打开浏览器
CLEAR_CONSOLE 设为none时清屏
“start”: “cross-env PORT=3000 HOST=127.0.0.1 roadhog server”,
把.webpackrc
改为.webpackrc.js
,这样就可以按照JS方式去编写配置项了!!
修改入口、出口、打包配置等
Antd按需导入
配置跨域代理
配置响应式布局方案
配置less
不同环境下的配置
浏览器兼容
……
PC配置:
import px2rem from 'postcss-pxtorem';
export default {
/* 基础配置 */
"entry": "src/index.js", //配置多入口:src/enter/*.js
"outputPath": "./dist",
"publicPath": "/",
"hash": true,
"html": {
"template": "./public/index.ejs"
},
/* 配置LESS */
"disableCSSModules": true,
/* 配置PX转REM */
"extraPostCSSPlugins": [
px2rem({
"rootValue": 75,
"propList": ['*']
})
],
/* 配置BABEL的插件 */
"extraBabelPlugins": [
// antd按需导入
[
"import",
{
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css"
}
],
// 配置PX转REM
[
"styled-components-px2rem",
{
"rootValue": 75
}
]
],
/* 配置跨域代理 */
"proxy": {
"/api": {
"target": "https://news-at.zhihu.com/api/4",
"changeOrigin": true,
"ws": true,
"pathRewrite": {
"/api": ""
}
}
},
/* 不同环境下的不同配置 */
"env": {
"development": {
"extraBabelPlugins": [
"dva-hmr"
]
}
}
};
浏览器兼容:默认情况下,ES6语法和CSS3的兼容已经处理,如果想处理ES6内置API的兼容,则导入@babel/polyfill即可「入口导入」!!
移动配置:
import px2rem from 'postcss-pxtorem';
export default {
// 对于css的处理
disableCSSModules: true,
disableCSSSourceMap: true,
/* 基础配置 */
"entry": "src/index.js", //配置多入口:src/enter/*.js
"outputPath": "./dist",
"publicPath": "/",
"hash": true,
// "html": {
// "template": "./public/index.ejs"
// },
/* 配置LESS */
"disableCSSModules": true,
/* 配置PX转REM */
"extraPostCSSPlugins": [
px2rem({
"rootValue": 75,
"propList": ['*']
})
],
/* 配置BABEL的插件 */
"extraBabelPlugins": [
// antd按需导入
[
"import",
{
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css"
}
],
// 配置PX转REM
[
"styled-components-px2rem",
{
"rootValue": 75
}
]
],
/* 配置跨域代理 */
"proxy": {
"/api": {
"target": "https://localhost:8888",
"changeOrigin": true,
"ws": true,
"pathRewrite": {
"/api": ""
}
}
},
/* 不同环境下的不同配置 */
"env": {
"development": {
"extraBabelPlugins": [
"dva-hmr"
]
}
}
};
import dva from 'dva';
/*
安装history模块「安装v4.10.1版本,不建议安装最新版本」
$ yarn add [email protected]
默认开启的就是HASH路由,如果想使用History路由,则导入createBrowserHistory!!
*/
import createHistory from 'history/createHashHistory';
const app = dva({
// 指定路由模式
history: createHistory()
});
...
app.router(require('./router').default);
app.start('#root');
/*
dva/router中包含了react-router-dom v5版本中所有API,以及react-router-redux中的的API
*/
import React from 'react';
import { Router, Route, Switch, Redirect } from 'dva/router';
import Vote from './routes/Vote';
import Demo from './routes/Demo';
import Personal from './routes/Personal';
/* ANTD */
...
const RouterConfig = function RouterConfig({ history }) {
return
;
}
export default RouterConfig;
路由懒加载主要使用 dva下的dynamic
API | DvaJS
import React from 'react';
import { Router, Route, Switch, Redirect } from 'dva/router';
import Vote from './routes/Vote';
import dynamic from 'dva/dynamic'; //实现动态组件的API
/* ANTD */
...
const RouterConfig = function RouterConfig({ history, app }) {
/* 异步组件 */
const DemoAsync = dynamic({
app,
models: () => [
import(/* webpackChunkName:"demo" */ './models/demoModel')
],
component: () => import(/* webpackChunkName:"demo" */ './routes/Demo')
});
const PersonalAsync = dynamic({
app,
models: () => [
import(/* webpackChunkName:"personal" */ './models/personalModel')
],
component: () => import(/* webpackChunkName:"personal" */ './routes/Personal')
});
return
;
}
export default RouterConfig;
配置路由表和二级路由
routerRoutes.js 路由表
import Vote from './routes/Vote';
import dynamic from 'dva/dynamic';
/* 配置路由懒加载 */
const lazy = function lazy(models, component) {
return dynamic({
app: window.app, //在入口处挂载到window上
models,
component
});
};
const routes = [{
path: '/',
exact: true,
component: Vote,
meta: { title: '首页' }
}, {
path: '/demo',
component: lazy(
() => [import(/* webpackChunkName:"demo" */ './models/demoModel')],
() => import(/* webpackChunkName:"demo" */ './routes/Demo')
),
meta: { title: '测试页' }
}, {
path: '/personal',
component: lazy(
() => [import(/* webpackChunkName:"personal" */ './models/personalModel')],
() => import(/* webpackChunkName:"personal" */ './routes/Personal')
),
meta: { title: '个人中心' },
/* 二级路由 */
children: [{
redirect: true,
exact: true,
from: '/personal',
to: '/personal/order'
}, {
path: '/personal/order',
component: lazy(
() => [],
() => import(/* webpackChunkName:"personal" */ './routes/personal/MyOrder')
),
meta: { title: '个人中心-我的订单' }
}, {
path: '/personal/profile',
component: lazy(
() => [],
() => import(/* webpackChunkName:"personal" */ './routes/personal/MyProfile')
),
meta: { title: '个人中心-我的信息' }
}]
}, {
redirect: true,
to: '/'
}];
export default routes;
import React from 'react';
import { Router, Route, Switch, Redirect } from 'dva/router';
import routes from './routerRoutes';
/* ANTD */
...
/* 动态创建路由 */
const createRoute = function createRoute(routes) {
return
{routes.map((item, index) => {
let { redirect, from, to, exact, path, meta, component: Component } = item,
config = {};
// 重定向
if (redirect) {
config = { to };
if (from) config.from = from;
if (exact) config.exact = exact;
return ;
}
// 正常路由
config = { path };
if (exact) config.exact = exact;
return {
// 修改标题
let { title = '' } = meta;
document.title = `${title}-珠峰培训React`;
return ;
}} />;
})}
;
};
/* 一级路由 */
const RouterConfig = function RouterConfig({ history }) {
return
{createRoute(routes)}
;
};
/* 二级路由 */
export const childrenRouter = function childrenRouter(path) {
let item = routes.find(item => item.path === path),
children;
if (item) children = item.children;
if (!children) return null;
return createRoute(children);
};
export default RouterConfig;
import dva from 'dva';
import createHistory from 'history/createHashHistory';
import voteModel from './models/voteModel';
// 1. Initialize
const app = dva({
history: createHistory()
});
window.app = app;
// 2. Plugins
// app.use({});
// 3. Model
app.model(voteModel);
// 4. Router
app.router(require('./router').default);
// 5. Start
app.start('#root');
import React from "react";
import { NavLink } from 'dva/router';
import styled from "styled-components";
import { childrenRouter } from '../router';
/* 样式处理 */
const PersonalBox = styled.div`
...
`;
const Personal = function Personal() {
return
我的订单
我的信息
{childrenRouter('/personal')}
;
};
export default Personal;
路由跳转及传参
history对象中提供了路由跳转的方法
+ go
+ goBack -> go(-1)
+ goFoward -> go(1)
+ push
+ replace
路径参数:把传递的信息当做路由地址的一部分,但是需要路由地址基于”:?“设置匹配的规则 路由地址:'/personal/profile/:lx?/:name?', history.push(`/personal/profile/0/zhufeng`); 问号传参:传递的信息会存在于地址栏中,即便用户刷新页面,依然可以获取相关传递的信息 history.push({ pathname: '/personal/profile', search: 'lx=0&name=zhufeng' }); 隐式传参:基于state把信息传递给目标组件,但是传递的信息没有在地址中存在「不丑+安全」,这样在目标组件页面刷新,传递的信息就消失了!! history.push({ pathname: '/personal/profile', state: { lx: 0, name: 'zhufeng' } });
方案一:Link 和 NavLink
NavLink可以和路由地址进行匹配,设置选中样式!!
我的订单
我的信息
方案二:编程式导航
routerRedux 是 react-router-redux 中提供的对象,此对象中包含了路由跳转的方法
+ go/goBack/goFoward
+ push/replace
相比较于props.history对象来讲,routerRedux不仅可以在组件中实现路由跳转,而且可以在redux操作中实现路由的跳转!!它本身就是redux和router的结合操作!!
在redux内部
yield put(routerRedux.push(...))
在redux外部「或者组件中」
dispatch(
routerRedux.push(...)
)
一定要基于dispatch进行派发才会跳转;因为执行routerRedux.xxx方法,只会返回一个action对象;
action->{
type:"@@router/CALL_HISTORY_METHOD",
payload:{
method:'push',
args:[...]
}
}
import React from "react";
import { routerRedux } from 'dva/router';
import { connect } from 'dva';
const MyOrder = function MyOrder(props) {
基于路由匹配的组件,其属性中包含:history、location、match!
其中history就是实现路由跳转的
+ push
+ replace
+ go
+ goBack
+ goForward
如果组件不是基于路由匹配的,可以基于 withRouter 高阶函数处理即可!!
let { history, dispatch } = props;
return
我的订单
;
};
export default connect()(MyOrder);
model处理流程
1. 如果有引入多个model ,app.model可以多次执行 这样会降低首屏的加载速度
import voteModel from './models/vote'; app.model(voteModel); import voteModel from './models/vote2'; app.model(voteModel2);
2. 页面需要的时候懒加载,配合路由使用懒加载 dynamic
3. Model的组成
- namespace 命名空间【模块名,后期获取状态和派发的标识】
state 数据 【模块管理的公共状态】
reducers 同步处理的方法 【已一个一个方法的模式,完成reducer中的派发行为标识的判断以及状态的更改+同步修改+外部修改+外部派发dispatch('/demo/xxx')】
effects redux-saga中异步处理方法【实现异步操作,异步派发】
subscriptions 订阅【在这里订阅的方法,会在页面一加载的时候就会被通知执行,所以:我们把页面一加载就要做的事情 (和 redux 相关的)在这里处理,在这里我们可以基于 history.listen做监听,保证进入哪个组件再处理也可以】
4. 在组件中,可以基于 dva中提供的 connect高阶函数,使用公共状态及dispatch方法
import voteModel from './models/voteModel';
...
app.model(voteModel);
...
export default {
// 命名空间「模块名:后期获取状态和派发都需要这个名字」
namespace: 'vote',
// 此模块管理的公共状态
state: {},
// 此模块需要判断的reducer「同步派发直达reducers」
reducers: {},
// 此模块需要异步派发的任务「基于redux-saga语法处理」
effects: {},
// 订阅方法,一开始就自动执行「获取数据,实现派发等」
subscriptions: {}
};
实现计数器累计
Demo.jsx
import React from "react";
import styled from "styled-components";
import { connect } from 'dva'
import { Button } from 'antd';
...
const Demo = function Demo(props) {
let { num, dispatch } = props;
return
{num}
;
};
export default connect(state => state.demo)(Demo);
import _ from '../utils/utils';
const delay = (interval = 1000) => {
...
};
export default {
namespace: 'demo',
state: {
num: 0
},
reducers: {
increment(state, action) {
state = _.clone(true, state);
let { payload = 1 } = action;
state.num += payload;
return state;
}
},
effects: {
*incrementAsync({ payload }, { call, put }) {
yield call(delay, 2000);
yield put({
type: 'increment',
payload
});
}
}
};
effects: {
incrementAsync: [
function* ({ payload }, { call, put, select }) {
try {
// 获取状态
let { num } = yield select(state => state.demo);
// 发送请求
yield call(delay, 2000);
// 派发任务
yield put({
type: 'increment',
payload
});
} catch (err) {
// 异常捕获
console.log(err);
}
},
// 指定监听的类型,默认是takeEvery「还有:takeLatest、throttle等」
{ type: "takeLatest" },
// { type: "throttle", ms: 1000 }
]
}
app.model({
subscriptions: {
setup({ dispatch, history }) {
history.listen(location => {
if (location.pathname === '/demo') {
dispatch({
type: 'demo/increment',
payload: 100
});
}
});
}
}
})
const delay = (interval = 1000) => {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, interval);
});
};
export default {
namespace: 'demo',
state: {
num: 10
},
reducers: {
把原有reducer函数中的每一种switch/case情况都写成一个单独的方法「纯函数」
state:获取“本模块”的公共状态
action:派发时候传递的action对象「包含type和传递的其他值(一般基于payload字段传递)」
我们需要把获取的state克隆一份,然后函数最后返回的值,会替换当前模块的state!!
increment(state, { payload = 1 }) {
/* state = { ...state };
state.num += payload;
return state; */
return {
...state,
num: state.num + payload
};
}
},
effects: {
redux-saga中我们基于take/takLatest/takeEvery等方式创建的监听器,此时写成一个个的“Generator函数”即可!!-> 默认是基于takeEvery的方式创建的监听器
+ 方法名是我们创建的监听器名字
+ 方法就是派发的任务被监听后,执行的working方法
+ 此处的函数名,不要和reducers中的函数名一致,因为:每一次派发,reducers和effects中的方法都会去匹配执行!如果函数名一样,则状态修改两次!!我们一般在effects写的名字,都加Async!!
方法中的参数
+ action:在组件中进行派发时,传递的action对象
+ 第二个参数就是redux-saga中提供的EffectsAPI,但是没有delay/debounce...
+ 基于 yield select() 可以获取所有模块的公共状态
yield select(state=>state.demo) 这样就是获取指定的状态信息
*incrementAsync({ payload }, { call, put }) {
yield call(delay, 2000);
yield put({
type: 'increment',
payload
});
}
如果想设置不同类型的监听器,则这样写
/* incrementAsync: [
// 数组第一项是working函数
function* ({ payload }, { call, put }) {
yield call(delay, 2000);
yield put({
type: 'increment',
payload
});
},
// 数组第二项中指定监听器的类型
{ type: 'takeLatest' }
// { type: 'throttle', ms: 500 }
] */
},
demoModel是被懒加载的,只有访问了/demo这个地址(组件),demoModel才会被注册!!
这里订阅的方法
+ 只有进入到这个组件,Model懒加载完毕,也被注册了,subscriptions中订阅的方法才会被执行
+ 而且只会执行一次,后期路由来回切换的时候,也不再执行了
subscriptions: {
setup() { },
}
};
这个板块的Model是加载页面时就被立即注册的
+ subscriptions中写的方法,在页面一加载的时候,就会把所有设定的方法执行
+ 方法就是普通函数「不能是Generator函数」
+ 传递的实参对象中具备 history/dispatch 两个属性
+ history:包含路由跳转和监听的history对象
+ dispatch:进行派发的方法
+ 如果想页面一加载「或者是指定的某个条件下」,我们就想从服务器异步获取数据,修改此模块的状态值,则可以写在subscriptions中!!
subscriptions: {
// 方法只有页面一加载的时候,订阅执行一次,在后期路由切换中,不再执行
/* async setup({ history, dispatch }) {
console.log('VOTE-SETUP');
await delay(2000);
dispatch({
type: 'support'
});
} */
需求改变了一下:我们想的是,在页面第一次/重新加载的时候,只有进入Vote这个组件,我们在voteModel中写的setup,以及其内部的操作,才让其生效!!
setup({ history, dispatch }) {
// 在Model没有懒加载的情况下,我们可以让setup函数在页面第一次加载的过程中,就订阅到事件池里,并且通知执行!!我们在setup中基于history.listen创建路由跳转监听器:第一次会执行,以后每一次路由切换也会执行!!
let unlisten = history.listen(async (location) => {
let { pathname } = location;
if (pathname === '/') {
await delay(2000);
dispatch({
type: 'support'
});
// 返回的函数就是移除此监听器的操作
unlisten();
}
});
}
}
dva-loading 会监听指定的异步请求方法,方法开始时loading状态值为 true ,异步结束后该值自动置为 false , 可用于骨架屏或某些需要 loading 状态的场景!
$ yarn add dva-loading
使用方式:
- npm or yarn 安装dva-loading ,并在入口Index.js中引入, 示例:import createLoading from 'dva-loading';
- 在入口Index.js中 app.use( createLoading ) ,示例:app.use(createLoading());
- 在组件context高阶函数中 state可以拿到loading ,示例:state => {
return {
...state.demo,
loading: state.loading
};
}- 组件内指定loading对应的 effects 示例:【loading = loading.effects['demo/testAsync'];】
打印loading:
import createLoading from 'dva-loading';
...
app.use(createLoading());
...
const delay = (interval = 1000) => {
...
};
export default {
namespace: 'demo',
state: {
num: 0
},
reducers: {
test(state) {
state = { ...state };
state.num++;
return state;
}
},
effects: {
*testAsync(action, { call, put }) {
yield call(delay, 2000);
yield put({
type: 'test'
});
}
}
};
import { connect } from "dva";
...
const Demo = function Demo({ num, loading, dispatch }) {
loading = loading.effects['demo/testAsync'];
return
{num}
;
};
export default connect(
state => {
return {
...state.demo,
loading: state.loading
};
}
)(Demo);
npm view xxx version 查看历史版本
这里使用 2.10.2 版本
Redux Middleware
import _ from '../utils/utils';
const delay = (interval = 1000) => {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, interval);
});
};
export default {
namespace: 'vote',
state: {
supNum: 10,
oppNum: 5
},
reducers: {
support(state, action) {
state = _.clone(true, state);
let { payload = 1 } = action;
state.supNum += payload;
return state;
},
oppose(state, action) {
state = _.clone(true, state);
let { payload = 1 } = action;
state.oppNum += payload;
return state;
}
},
effects: {
supportAsync: [
function* ({ payload }, { call, put }) {
yield call(delay, 2000);
yield put({
type: 'support',
payload
});
},
{ type: 'takeLatest' }
],
opposeAsync: [
function* opposeAsync({ payload }, { call, put }) {
yield call(delay, 2000);
yield put({
type: 'oppose',
payload
});
},
{ type: 'takeLatest' }
]
}
};
import React from "react";
import styled from "styled-components";
import { Button } from 'antd';
import { connect } from 'dva';
...
const Vote = function Vote(props) {
let { supNum, oppNum, dispatch } = props;
return
React是很棒的前端框架
{supNum + oppNum}
支持人数:{supNum}人
反对人数:{oppNum}人
;
};
export default connect(state => state.vote)(Vote);