- create-react-app只是创建了一个最基本的reactdemo,需要手动安装 react-router,redux等
- DVA 是基于 redux、redux-saga 和 react-router 的轻量级前端数据流框架
- dva = redux + redux-saga + react-router
- dva没有新概念,只是封装了redux
- react视图层框架 + dva数据流框架 = 复杂的SPA应用架构
- dva特点
- 数据共享 models,数据可以 connect任何组件
- 数据和视图逻辑分离,effects
- 异步请求
https://github.com/xlsdg/dva-antd-starter/blob/master/src/router.jsx
https://github.com/ant-design/create-react-app-antd
dva是一个非常轻量级的封装
react
react-router-dom
connected-react-router
redux
redux-saga
dva-cli创建项目
yarn global add dva-cli
dva -v # 查看版本号
dva-cli version 0.10.1
dva创建项目
dva new mydva cd mydva npm start
3. 出现这个界面,就说明 dva项目创建成功
![dva-demo.png](https://cdn.nlark.com/yuque/0/2020/png/112859/1591320062359-add6647b-35ed-40f1-8f85-ca39f1ce14cc.png#height=605&id=MqqxG&originHeight=605&originWidth=692&originalType=binary&ratio=1&size=90686&status=done&style=none&width=692)
<a name="27JTr"></a>
### dva目录结构
```jsx
.roadhogrc.mock.js Mock配置文件
.webpackrc 自定义的webpack配置文件,JSON格式
如果需要 JS格式,修改为 .webpackrc.js
dva数据流程
- 输入 URL渲染对应组件,用户在 view层组件中,触发 一个action
- 组件 dispatch(action) 触发 model里面的函数
a. 同步,直接进入 reducer里面的方法
b. 异步,进入 effects里面的方法,fetch获取接口数据 - 组件通过 connect连接数据
app.model({
namespace: 'products',
state: {
list: [],
loading: false,
},
subscriptions: [
function(dispatch) {
dispatch({type: 'products/query'});
},
],
// 异步代码,要 dispatch(action)到 reducers修改 state
// 类似 vue的 action,commit() 到 mutation修改 state
effects: {
['products/query']: function*() {
yield call(delay(800));
yield put({
type: 'products/query/success',
payload: ['ant-tool', 'roof'],
});
},
},
reducers: { // 同步代码,类似 vue的 mutation
['products/query'](state) {
return { ...state, loading: true, };
},
['products/query/success'](state, { payload }) {
return { ...state, loading: false, list: payload };
},
},
});
dva概念
- models
- connect
- dispatch
- action
- reducer
models
/src/models/indexPage.js
- namespace 命名空间
- 只能用字符串
- 大型项目有很多 model,通过 namespace区分
- state 初始值
- 表示当前状态,可以设置初始值
- 优先级低于 app.model({ state})
- reducers 同步操作
- (state, action) => newState;reducer修改 state
- 处理同步操作,修改 state
- effects:ajax异步操作,副作用
- ajax请求后台接口,不能修改 state
- dispatch(action) 调用 reducer修改 state
- 类似 vue的 actions - commit(‘actionName’, {}) 到 mutations里面,mutation再修改 store
- IO异步,操作数据库等
- action
- 是 reducers,effects的触发器
import { getUsers } from '../services/user'
export default {
namespace: 'indexPage', // 命名空间,通过 this.props.indexPage获取数据
// 初始化的数据
state: {
data: [ // tbody 表格的数据
{
"key": "10", "name": "张飞宇", "age": 12
}
],
columns: [ // thead 表头
{
title: '姓名',dataIndex: 'name',key: 'name',
},
{
title: '年龄',dataIndex: 'age',key: 'age',
}
]
},
subscriptions: {
setup({ dispatch, history }) { // eslint-disable-line
}
},
effects: { // generator函数
*add({ payload }, { call, put }) {
const users = yield call(getUsers, {}) // services/user,要import导入
yield put({
type: 'addUser', // effects 提交的是 reducers的方法
payload: { data: users.data.data }
})
}
},
reducers: { // store依赖的清单
addUser(state, action) {
const data = [...action.payload.data, ...state.data]
return { ...state, data }
}
}
}
connect
- 装饰器写法,给装饰对象赋予不具备的能力
- connect让组件获取 model中的数据,触发model中的方法
- mapStateToProps,把 dva中的 state通过 props 传递个组件
- mapDispatchToProps,通过 props把 dispatch方法传递给组件
connect()()
conect(mapStateToProps, mapDispatchToProps)(Component)
dispatch
- dispatch是个纯函数,将 action发送给 store
- 组件触发 model的唯一方法
- diapatch接收一个对象作为参数,这个参数称之为 action
- store,全局唯一的数据源;收到 action后会更新数据
- view,react的UI层,从 store获取数据,渲染成 html代码
- 只要 store有变化,view就会自动更新
const action = {
type: 'changeValue',
payload: ev.target.value
}
dispatch(action)
action
- action是描述 组件/UI事件的一个对象
- 必须包含 type字段,payload是参数
- actionTypes.js,保存 action常量;推荐用 constants.js保存常量
- actionReducer.js,抽离所有 action到一个页面
{
type: 'changeValue',
payload: ev.target.value
}
reducer
- 每个 reducer都是一个纯函数,接收 (state, action);没有副作用
- 最多一层嵌套,保持 state 的扁平化
- 深层嵌套会让 reducer 很难写和难以维
app.model({
namespace: 'todos',
state: {},
reducers: {
remove(state, { payload: id }) {
return state.filter(todo => todo.id !== id)
},
// state嵌套太深,不好维护
more(state, { payload: todo }) {
const todos = state.a.b.todos.concat(todo)
const b = { ...state.a.b, todos }
const a = { ...state.a, b }
return { ...state, a }
}
}
}
effects
- put 触发 action
- call 调用异步逻辑,支持 promise
- call & put是 redux-saga的方法
- select 从 state里面获取数据
- 函数必须是 Generator函数, 返回的是迭代器,通过 yield 关键字实现暂停功能
import {AsyncAdd} from '../request'
app.model({
namespace: 'todos',
state: { todo: [], value: 'ok' },
effects: {
*addRemote({ payload: todo }, { put, call, select }) {
const res = yield call(AsyncAdd, todo);
const value = select(state => state.value)
yield put({ type: 'add', payload: {data: res.data, value} });
},
},
reducer: {
add (state, action) {
}
}
})
subscriptions
1. 用于收集其他来源的 action,是个订阅,订阅一个数据源
2. 然后 dispatch(action), app.start() 被执行
3. 数据源可以是:
1. 当前的时间
2. 服务器的 websocket 连接
3. keyboard 输入操作
4. 滚动条
5. geolocation 变化
6. history 路由变化等
4. 如果要使用 app.unmodel(),subscription 必须返回 unlisten 方法,用于取消数据订阅
app.model({
namespace: 'todo',
// app.start() 时被执行
subscriptions: {
// 监听 history变化,当进入 / 触发 load action
setup ({history, dispatch}) {
return history.listen(({pathname}) => {
if (pathname === '/') dispatch({ type: 'load' })
})
},
onClick ({ dispatch }) {
document.addEventListener('click', () => {
dispatch({ type: 'save' })
})
},
backHistory ({ dispatch, history }) {
history.lisen(location => {
console.log('location')
})
}
}
})
app
const app = dva({
history, // 指定给路由用的 history,默认是 hashHistory
initialState, // 指定初始数据,优先级高于 model 中的 state
onError, // effect 执行错误或 subscription 通过 done 主动抛错时触发,可用于管理全局出错状态。
onAction, // 在 action 被 dispatch 时触发
onStateChange, // state 改变时触发,可用于同步 state 到 localStorage,服务器端等
onReducer, // 封装 reducer 执行。比如借助 redux-undo 实现 redo/undo
onEffect, // 封装 effect
onHmr, // 热替换相关
extraReducers, // 指定额外的 reducer,比如 redux-form 需要指定额外的 form reducer
extraEnhancers, // 指定额外的 StoreEnhancer ,比如结合 redux-persist 的使用
});
// hashHistory 转 BrowserHistory
import createHistory from 'history/createBrowserHistory';
const app = dva({
history: createHistory(),
});
// dva-loading
import createLoading from 'dva-loading';
app.use(createLoading(opts));
app.model({
namespace: 'todo',
state: {
value: '',
data: []
},
reducers: {
add (state, { payload: todo }) {
// 保存数据到 state
return [...state, data: todo]
}
},
effects: {
*save (state, action) {
yield call(saveServer, state.todo)
yield put({ type: 'add', payload: state.todo })
}
}
})
父子组件通信
- 父组件管理 state,子组件通过 props获取 state
- 修改 state;子组件调用父组件的方法来 setState
- 方法通过属性方式传递给子组件
dva数据流程
- index.js入口
- 通过 url匹配 routes里面的组件;加载 components里面的公共组件
- 组件 dispatch(action)
- models 通过 action 改变 state;在services里面 ajax请求后台接口
- 通过 connect重新渲染 routes里面的组件
dva-cli项目目录
- 默认安装的npm包: dva & react & react-dom
- 代码规范 eslint & husky
{
"private": true,
"scripts": {
"start": "roadhog server",
"build": "roadhog build",
"lint": "eslint --ext .js src test",
"precommit": "npm run lint"
},
"dependencies": {
"dva": "^2.4.1",
"react": "^16.2.0",
"react-dom": "^16.2.0"
},
"devDependencies": {
"babel-plugin-dva-hmr": "^0.3.2",
"eslint": "^4.14.0",
"eslint-config-umi": "^0.1.1",
"eslint-plugin-flowtype": "^2.34.1",
"eslint-plugin-import": "^2.6.0",
"eslint-plugin-jsx-a11y": "^5.1.1",
"eslint-plugin-react": "^7.1.0",
"husky": "^0.12.0",
"redbox-react": "^1.4.3",
"roadhog": "^2.5.0-beta.4"
}
}
antd安装
- npm 安装 antd, babel-plugin-import 按需加载 antd样式和脚本
- 编辑 .webpackrc,配置 antd按需加载
npm install antd babel-plugin-import
mock数据
- .roadhogrc.mock.js
- mock 数据太多时,可以拆分后放到
./mock
文件夹中- 然后在
.roadhogrc.mock.js
引入
- 然后在
- api参考 express4.*
export default {
// 请求 /api/users 时会返回 JSON 格式的数据
'GET /api/users': { users: [{ username: 'admin' }] },
// 自定义函数
'POST /api/users': (req, res) => { res.end('OK'); },
}
组件动态加载
- 最好封装一下,每个路由组件都这么写一遍很麻烦
import dynamic from 'dva/dynamic';
const UserPageComponent = dynamic({
app,
models: () => [
import('./models/users'),
],
component: () => import('./routes/UserPage'),
})
dva-loading
- 处理 loading 状态的 dva 插件,基于 dva 的管理 effects 执行的 hook 实现
- dva-loading会在 state 中添加一个
loading
字段,该字段可自定义 - 自动处理网络请求的状态,不需再去写
showLoading
和hideLoading
方法 - 在
./src/index.js
中引入使用即可- opts 仅有一个
namespace
字段,默认为loading
- opts 仅有一个
import createLoading from 'dva-loading';
const app = dva();
app.use(createLoading(opts))
models
- models是dva的数据流核心
- 可以理解为 redux、react-redux、redux-saga 的封装
- 通常一个模块对应一个model,基本的格式
dva 6个 api
- namespace 当前model的命名空间
- 全局state上的一个属性,必须是字符串
- 不能点点点,创建多层命名空间;错误的用法
index.page.user
- 组件中触发 action,要带上命名空间,,
{type: 'user/fetch'}
- 当前model中触发 action不需要命名空间,
{type: 'save'}
- state 初始值
- reducer 处理同步
- 纯函数,不可变值;类似 redux的 reducer
- 唯一可以修改 state的,由 action触发;
- 有 state & action2个形参,
save(state, action) {}
- effects 处理异步
- 不能直接修改 state,能触发 action
- 只能是 generator函数
- 有 action & effects2个形参
fetch({ payload }, { call, put }) {}
- effects包含 call, put, select 3个形参
- call 请求ajax接口
- put 触发 action
- select 用于从 state中获取数据
- subscriptions 订阅数据
- 根据情况,dispatch(action)
- 格式:
({ dispatch, history }, done) => {}
model流程演示
- 监听路由变化,当进入
/user
页面时,执行 effects中的 fetch ajax请求,获取后台数据 - 然后 fetch中触发 reducers中的 save方法,将后台数据保存到 state中
import { getUser } from '../services/user.js' // 统一管理 ajax接口
export default {
namespace: 'user',
state: {
usre: {}
},
subscriptions: {
setup({ dispatch, history }) { // eslint-disable-line
return history.listen(({pathname}) => {
if (pathname === '/usre') dispatch({type: 'fetch'})
})
}
},
effects: {
*fetch({ payload }, { call, put }) { // eslint-disable-line
const res = yield put(getUser, payload.data)
yield put({ type: 'save', data: res.data })
},
},
reducers: {
save(state, action) {
return { ...state, ...action.payload }
}
}
}
services/user.js
- 用 axios也行,哪个熟练用哪个
import request from '../utils/request'
export const getUsers = () => request('/api/users')
dva数据流管理
components新增组件
- /src/components
- Header.js 公共导航栏
- Layout.js 布局
// Header.js
import React, { Component } from "react"
import { Menu } from "antd"
import { Link } from "dva/router"
class Header extends Component {
render() {
return (
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={["1"]}
style={{ lineHeight: "64px" }}
>
<Menu.Item key="1">
<Link to="/">首页</Link>
</Menu.Item>
<Menu.Item key="2">
<Link to="/list">ListPage</Link>
</Menu.Item>
<Menu.Item key="3">
<Link to="/about">AboutPage</Link>
</Menu.Item>
</Menu>
)
}
}
export default Header
// Layout.js
import React, { Component } from "react"
import Header from "./Header"
class Layout extends Component {
render() {
const { children } = this.props
return (
<div>
<Header />
<div style={{ background: "#fff", padding: '24px' }}>{children}</div>
</div>
)
}
}
export default Layout
routes新增页面
- /src/routes/
- list/ListPage.js
- about/AboutPage.js ```jsx // list/ListPage.js import React, { Component } from ‘react’ import { connect } from ‘dva’
class ListPage extends Component { render () { return (
list/ListPage.js this.props.children 类似于 vue 的slot
) } } ListPage.propTypes = {} export default connect()(ListPage)// about/AboutPage.js import React, { Component } from ‘react’ import { connect } from ‘dva’
const AboutPage = props => { return
about/AboutPage.js props.children 类似于 vue 的slot
} AboutPage.propTypes = {} export default connect()(AboutPage)
<a name="104144f4"></a>
### /router.js 连接路由
1. /router.js 把新建的页面给 import引入
2. 完成这个操作后,点击路由就会切换不同的页面
```jsx
import React from 'react';
import { Router, Route, Switch } from 'dva/router';
import IndexPage from './routes/IndexPage';
// newPage
import Layout from './components/Layout'
import ListPage from './routes/list/ListPage'
import AboutPage from './routes/about/AboutPage'
function RouterConfig({ history }) {
return (
<Router history={history}>
<Layout>
<Switch>
{/*
路径为 /的时候匹配到IndexPage;exact,精确匹配
如果不加 exact,则/list, /about, /whatever都会匹配到IndexPage
exact精确匹配,只有输入 /的时候才会匹配到 IndexPage
*/}
<Route path="/" exact component={IndexPage} />
<Route path="/list" exact component={ListPage} />
<Route path="/about" exact component={AboutPage} />
</Switch>
</Layout>
</Router>
);
}
export default RouterConfig;
/models创建模型
- /srcmodels/IndexPage.js
- namespace 命名空间
- 组件中,用 this.props.indexPage 获取 state的数据
import { getUsers } from '../services/user'
export default {
namespace: 'indexPage', // 命名空间,通过 this.props.indexPage获取数据
// 初始化的数据
state: {
data: [ // tbody 表格的数据
{
"key": "10", "name": "张飞宇", "age": 12
}
],
columns: [ // thead 表头
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
},
]
},
subscriptions: {
setup({ dispatch, history }) { // eslint-disable-line
},
},
effects: { // generator函数
*fetch({ payload }, { call, put }) { // eslint-disable-line
yield put({ type: 'save' });
},
*add({ payload }, { call, put }) {
const users = yield call(getUsers, {}) // services/user,要import导入
yield put({
type: 'addUser', // effects 提交的是 reducers的方法
payload: { data: users.data.data }
})
}
},
reducers: { // store依赖的清单
save(state, action) {
return { ...state, ...action.payload };
},
addUser(state, action) {
const data = [...action.payload.data, ...state.data]
return { ...state, data }
}
}
}
routes页面绑定models数据
- /src/routes/IndexPage.js
import React from "react"
import { connect } from "dva"
import { Button, Table } from "antd"
// IndexPage用 models/indexPage.js的数据,要用 connect连接
const IndexPage = props => {
const { dispatch, indexPage: { data, columns } } = props
function changeData() {
dispatch({
type: 'indexPage/add', // 调用 models/indexPage.js的 add
payload: {}
})
}
// const { data, columns } = props.indexPage
return (
<div>
<Button type="primary" onClick={changeData}>获取数据</Button>
<Table columns={columns} dataSource={data} />
</div>
);
};
IndexPage.propTypes = {}
// connect(models/数据)(routes/组件)
// models/indexpage.js中的 state数据, 就关联到 routes/IndexPage.js
export default connect(
({ indexPage }) => ({ indexPage }) // 返回一个对象
)(IndexPage)
services ajax请求
- /src/services/user.js
- request请求库,用自己熟练的,比如 axios
- 在 /src/utils/request.js 里面封装 axios请求
import request from '../utils/request'
// import axios from 'axios
export const getUsers = () => request('/api/users')
mock数据
- .roadhogrc.mock.js 设置mock数据,或自己搭建服务器
const users = [
{
"key": "1", "name": "王达美", "age": 30
},
{
"key": "2", "name": "刘高三", "age": 20
}
]
export default {
'GET /api/users': { data: users }
}
组件 dispatch(action)
- dva规范:必须通过触发 actoin来改变 state
- 同步修改:直接调用model中的reducer
- ajax异步:先触发effects,然后调用reducer
- dispatch(action) 到 reducer,在 reducer里面改变 state
// 1 IndexPage.js 组件 dispatch(action)
const IndexPage = props => {
const { dispatch, indexPage: { data, columns } } = props
function changeData() {
dispatch({
type: 'indexPage/add', // 触发 models/indexPage.js的 add方法
payload: {}
})
}
return (
<div>
<Button type="primary" onClick={changeData}>获取数据</Button>
<Table columns={columns} dataSource={data} />
</div>
)
}
/models/indexPage.js
- effects 可以获取全局的 state,也可以触发 action
- yield关键字表示每一操作
- call() 表示调用 异步函数
- put() 表示 dispacth(action)
- 组件内调用 models要加上命名空间
- 当前 model调用不需要加 命名空间
- effects 里面的函数必须是 generator函数 ```jsx import { getUsers } from ‘../services/user’
export default { namespace: ‘indexPage’, // 命名空间,通过 this.props.indexPage获取数据
// 初始化的数据 state: { data: [], // tbody 表格的数据 columns: [ // thead 表头 { title: ‘姓名’,dataIndex: ‘name’,key: ‘name’ }, { title: ‘年龄’,dataIndex: ‘age’,key: ‘age’ }, ] }, subscriptions: { setup({ dispatch, history }) { // eslint-disable-line } },
effects: { // generator函数 *fetch({ payload }, { call, put }) { // eslint-disable-line yield put({ type: ‘save’ }) },
*add({ payload }, { call, put }) {
const res = yield call(getUsers, {}) // services/user,要import导入
yield put({
type: 'ADDUSER', // effects 提交的是 reducers的方法
payload: { data: res.data }
})
}
},
reducers: { // store依赖的清单 save(state, action) { return { …state, …action.payload }; },
[ADDUSER'](state, action) {
const data = [...action.payload.data, ...state.data]
return { ...state, data }
}
} }
到这里,完成了一个基本的 dva数据流程,从新建页面到 connect绑定数据的异步操作。
<a name="index.js"></a>
## app
1. index.js中的 app 就是 dva的实例,实例化 dva的参数有:
1. history
2. initialState 初始数据,优先级高于 model的 state,默认 {}
3. onError
4. onActoin
5. onReducer
6. onEffect
7. onHmr
8. extraReducers
9. extraEnhancers
2. indexjs留出了 model 模型,router路由的位置,开发中
1. 所有的 model模型都要在 index.js注册,才能用 connect关联组件
```jsx
import dva from 'dva';
import './index.css';
// 1. Initialize
const app = dva();
// 2. Plugins
// app.use({});
// 3. Model 所有的model模型都要在这注册,才能用connect跟组件关联起来
app.model(require('./models/indexPage').default);
// 4. Router 引用了router.js
app.router(require('./router').default);
// 5. Start
app.start('#root');
app参数
const app = dva({
history,
initialState,
onError,
onAction,
onStateChange,
onReducer,
onEffect,
onHmr,
extraReducers,
extraEnhancers,
})
app.use
history
- history是给路由用的,默认 hashHistory
- 要用 browserHistory,安装 history,然后在 index.js引入
import dva from 'dva';
import createHistory from 'history/createBrowserHistory';
const app = dva({
history: createHistory()
});
connect-router.js
- 写完 model 和组件后,需要将 model 和组件连接起来
- connect就是 react-redux的 connect
- connect连接组件后,默认参数 dispatch, state,还有 location, history
import React from 'react'
import { connect } from 'dva'
const User = ({ dispatch, user }) => {
return <div className="root"></div>
}
export default connect(
({ user }) => ({ user }) // 返回一个对象
)(User)
错误处理
- dva 里,effects 和 subscriptions 的抛错全部会走 onError hook
- 在 onError 里统一处理错误,全局捕获effects & subscriptions的错误
- 对某些 effects进行错误捕获,需要在 effect 内部
try {} catch() {}
// 全局错误处理
const app = dva({
onError(err, dispatch) {
console.error(err);
}
})
// 函数内部错误处理
app.model({
effects: {
*addRemote() {
try {
// Your Code Here
} catch(e) {
console.log(e.message)
}
}
}
})
ajax异步请求
- dva集成了
isomorphic-fetch
处理异步请求 - 在
./src/utils/request.js
,简单封装了 fetch - https://github.com/matthew-andrews/isomorphic-fetch
- 推荐用 axios,多个项目保持一致性,不推荐用 fetch
- fetch细节
- fetch请求默认不带cookie,需要设置fetch(url,{credentials: ‘include’})
- 服务器返回400、500等错误码不会reject,返回成功态 resolve
- 只有网路错误请求不能完成时才会reject
- 基于 Promise,支持 async/await
npm install --save isomorphic-fetch es6-promise
proxy代理
- create-react-app创建的项目,在
package.json
里设置代理
"proxy": "http://api.xxxx.com" // 单个域名的大力
// 多个子域的代理
"proxy": {
"/api/v2": {
"target": "http://v2.lulongwen.com",
"changeOrigin":true
},
"/api/v1":{
"target":"http://v1.lulongwen.com",
"changeOrigin":true
}
}
- antd 代理,package.json同级目录,创建
.roadhogrc
文件
{
"entry": "src/index.js",
"extraBabelPlugins": [
"transform-runtime",
"transform-decorators-legacy",
"transform-class-properties",
["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
],
"env": {
"development": {
"extraBabelPlugins": [
"dva-hmr"
]
}
},
"externals": {
"g2": "G2",
"g-cloud": "Cloud",
"g2-plugin-slider": "G2.Plugin.slider"
},
"ignoreMomentLocale": true,
"theme": "./src/theme.js",
"proxy": {
"/api": {
"target": "http://api.xxxx.com/",
"changeOrigin": true
}
}
}
roadhog
- 包含 dev, build, test的命令行工具
- 基于 react-dev-utils,和 create-react-app基本一致,可配置版的 create-react-app
npm i roadhog -g
roadhog -v