二、初识 Dva
2.1 Dva 的特性
dva = React-Router + Redux + Redux-saga
- 仅有 5 个
API
,仅有 5 个主要的api
- 支持
HMR
,支持模块的热更新 - 支持
SSR (ServerSideRender)
,支持服务器端渲染 - 支持
Mobile/ReactNative
,支持移动手机端的代码编写 - 支持
TypeScript
- 支持路由和
Model
的动态加载
2.2 Dva 的五个 API
2.2.1 app = dva(Opts)
app = dva(Opts)
:创建应用,返回 dva
实例。(注:dva 支持多实例)**
在 opts
可以配置所有的 hooks
const app = dva({
history,
initialState,
onError,
onAction,
onStateChange,
onReducer,
onEffect,
onHmr,
extraReducers,
extraEnhancers,
});
hooks 包含如下配置项
1、 onError((err, dispatch) => {})
effect
执行错误或subscription
通过done
主动抛错时触发,可用于管理全局出错状态- 注意:
subscription
并没有加try...catch
,所以有错误时需通过第二个参数done
主动抛错
app.model({
subscriptions: {
setup({ dispatch }, done) {
done(e)
},
},
})
2、 onAction(fn | fn[])
在 action
被 dispatch
时触发,用于注册 redux
中间件。支持函数或函数数组格式
- 例如我们要通过
redux-logger
打印日志
import createLogger from 'redux-logger';
const app = dva({
onAction: createLogger(opts),
})
3、 onStateChange(fn)
state
改变时触发,可用于同步 state 到 localStorage
,服务器端等
4、 onReducer(fn)
封装 reducer
执行,比如借助 redux-undo
实现 redo/undo
import undoable from 'redux-undo';
const app = dva({
onReducer: reducer => {
return (state, action) => {
const undoOpts = {};
const newState = undoable(reducer, undoOpts)(state, action);
// 由于 dva 同步了 routing 数据,所以需要把这部分还原
return { ...newState, routing: newState.present.routing };
},
},
})
5、 onEffect(fn)
封装 effect
执行。比如 dva-loading
基于此实现了自动处理 loading
状态
6、 onHmr(fn)
热替换相关,目前用于 babel-plugin-dva-hmr
7、 extraReducers
指定额外的 reducer
,比如 redux-form
需要指定额外的 form reducer
import { reducer as formReducer } from 'redux-form'
const app = dva({
extraReducers: {
form: formReducer,
},
})
这里比较常用的是, history
的配置,一般默认的是 hashHistory
,如果要配置 history
为 browserHistory
,可以这样
import createHistory from 'history/createBrowserHistory';
const app = dva({
history: createHistory(),
});
initialState
:指定初始数据,优先级高于 model
中的 state
,默认是 {}
,但是基本上都在 modal
里面设置相应的 state
2.2.2 app.use(Hooks)
app.use(Hooks):配置 hooks 或者注册插件
这里最常见的就是 dva-loading
插件的配置
import createLoading from 'dva-loading';
...
app.use(createLoading(opts));
但是一般对于全局的 loading
我们会根据业务的不同来显示相应不同的 loading
图标,我们可以根据自己的需要来选择注册相应的插件
2.2.3 app.model(ModelObject)
app.model(ModelObject)
:这个是你数据逻辑处理,数据流动的地方
2.2.4 app.unmodel(namespace)
取消 model
注册,清理 reducers
, effects
和 subscriptions
。 subscription
如果没有返回 unlisten
函数,使用 app.unmodel
会给予警告
2.2.5 app.router(Function)
注册路由表,这一操作步骤在 dva 中也很重要
// 注册路由
app.router(require('./router'))
// 路由文件
import { Router, Route } from 'dva/router';
import IndexPage from './routes/IndexPage'
import TodoList from './routes/TodoList'
function RouterConfig({ history }) {
return (
<Router history={history}>
<Route path="/" component={IndexPage} />
<Route path='/todoList' components={TodoList}/>
</Router>
)
}
export default RouterConfig
如果我们想解决组件动态加载问题,我们的路由文件也可以按照下面的写法来写
import { Router, Switch, Route } from 'dva/router'
import dynamic from 'dva/dynamic'
function RouterConfig({ history, app }) {
const IndexPage = dynamic({
app,
component: () => import('./routes/IndexPage'),
})
const Users = dynamic({
app,
models: () => [import('./models/users')],
component: () => import('./routes/Users'),
})
return (
<Router history={history}>
<Switch>
<Route exact path="/" component={IndexPage} />
<Route exact path="/users" component={Users} />
</Switch>
</Router>
)
}
export default RouterConfig
其中 dynamic(opts)
中 opt
包含三个配置项:
app
:dva
实例,加载models
时需要models
: 返回Promise
数组的函数,Promise
返回 dva model`component
:返回Promise
的函数,Promise
返回React Component
2.2.6 app.start
启动应用,即将我们的应用跑起来
2.3 Dva 九个概念
2.3.1 State
初始值,我们在 dva()
初始化的时候和在 modal 里面的 state
对其两处进行定义,其中 modal 中的优先级低于传给 dva()
的 opts.initialState
// dva() 初始化
const app = dva({
initialState: { count: 1 },
});
// modal() 定义事件
app.model({
namespace: 'count',
state: 0,
});
2.3.2 Action
表示操作事件,可以是同步,也可以是异步
action
的格式如下,它需要有一个type
,表示这个action
要触发什么操作;payload
则表示这个action
将要传递的数据
{
type: String,
payload: data,
}
我们通过 dispatch 方法来发送一个 action
dispatch({ type: 'todos/add', payload: 'Learn Dva' });
其实我们可以构建一个 Action 创建函数,如下
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
//我们直接 dispatch(addTodo()),就发送了一个 action。
dispatch(addTodo())
2.3.3 Model
model
是 dva
中最重要的概念, Model
非 MVC
中的 M
,而是领域模型,用于把数据相关的逻辑聚合到一起,几乎所有的数据,逻辑都在这边进行处理分发
import queryString from 'query-string'
import * as todoService from '../services/todo'
export default {
namespace: 'todo',
state: {
list: []
},
reducers: {
save(state, { payload: { list } }) {
return { ...state, list }
}
},
effects: {
*addTodo({ payload: value }, { call, put, select }) {
// 模拟网络请求
const data = yield call(todoService.query, value)
console.log(data)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
const tempObj = {}
tempObj.title = value
tempObj.id = list.length
tempObj.finished = false
list.push(tempObj)
yield put({ type: 'save', payload: { list }})
},
*toggle({ payload: index }, { call, put, select }) {
// 模拟网络请求
const data = yield call(todoService.query, index)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
let obj = list[index]
obj.finished = !obj.finished
yield put({ type: 'save', payload: { list } })
},
*delete({ payload: index }, { call, put, select }) {
const data = yield call(todoService.query, index)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
list.splice(index, 1)
yield put({ type: 'save', payload: { list } })
},
*modify({ payload: { value, index } }, { call, put, select }) {
const data = yield call(todoService.query, value)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
let obj = list[index]
obj.title = value
yield put({ type: 'save', payload: { list } })
}
},
subscriptions: {
setup({ dispatch, history }) {
// 监听路由的变化,请求页面数据
return history.listen(({ pathname, search }) => {
const query = queryString.parse(search)
let list = []
if (pathname === 'todoList') {
dispatch({ type: 'save', payload: {list} })
}
})
}
}
}
model
对象中包含 5 个重要的属性
state
这里的 state 跟我们刚刚讲的 state 的概念是一样的,只不过她的优先级比初始化的低,但是基本上项目中的 state 都是在这里定义的
namespace
model
的命名空间,同时也是他在全局 state
上的属性,只能用字符串,我们发送在发送 action
到相应的 reducer
时,就会需要用到 namespace
Reducer
以 key/value
格式定义 reducer
,用于处理同步操作,唯一可以修改 state
的地方。由 action
触发。其实一个纯函数
namespace: 'todo',
state: {
list: []
},
// reducers 写法
reducers: {
save(state, { payload: { list } }) {
return { ...state, list }
}
}
Effect
用于处理异步操作和业务逻辑,不直接修改 state
,简单的来说,就是获取从服务端获取数据,并且发起一个 action
交给 reducer
的地方
其中它用到了 redux-saga
,里面有几个常用的函数。
// effects 写法
effects: {
*addTodo({ payload: value }, { call, put, select }) {
// 模拟网络请求
const data = yield call(todoService.query, value)
console.log(data)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
const tempObj = {}
tempObj.title = value
tempObj.id = list.length
tempObj.finished = false
list.push(tempObj)
yield put({ type: 'save', payload: { list }})
},
*toggle({ payload: index }, { call, put, select }) {
// 模拟网络请求
const data = yield call(todoService.query, index)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
let obj = list[index]
obj.finished = !obj.finished
yield put({ type: 'save', payload: { list } })
},
*delete({ payload: index }, { call, put, select }) {
const data = yield call(todoService.query, index)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
list.splice(index, 1)
yield put({ type: 'save', payload: { list } })
},
*modify({ payload: { value, index } }, { call, put, select }) {
const data = yield call(todoService.query, value)
let tempList = yield select(state => state.todo.list)
let list = []
list = list.concat(tempList)
let obj = list[index]
obj.title = value
yield put({ type: 'save', payload: { list } })
}
}
在项目中最主要的会用到的是 put
与 call
Subscription
- 以
key/value
格式定义subscription
,subscription
是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action subscription
是订阅,用于订阅一个数据源,然后根据需要dispatch
相应的action
。在app.start()
时被执行,数据源可以是当前的时间、当前页面的url
、服务器的websocket
连接、history
路由变化等等。
- 注意 :如果要使用
app.unmodel()
,subscription
必须返回unlisten
方法,用于取消数据订阅
// subscriptions 写法
subscriptions: {
setup({ dispatch, history }) {
// 监听路由的变化,请求页面数据
return history.listen(({ pathname, search }) => {
const query = queryString.parse(search)
let list = []
if (pathname === 'todoList') {
dispatch({ type: 'save', payload: {list} })
}
})
}
}
2.3.4 Router
Router
表示路由配置信息,项目中的 router.js
export default function({ history }){
return(
<Router history={history}>
<Route path="/" component={App} />
</Router>
);
}
RouteComponent
RouteComponent
表示 Router
里匹配路径的 Component
,通常会绑定 model
的数据。如下:
import { connect } from 'dva';
function App() {
return <div>App</div>;
}
function mapStateToProps(state) {
return { todos: state.todos };
}
export default connect(mapStateToProps)(App);
2.4 整体架构
- 首先我们根据
url
访问相关的Route-Component
,在组件中我们通过dispatch
发送action
到model
里面的effect
或者直接Reducer
- 当我们将
action
发送给Effect
,基本上是取服务器上面请求数据的,服务器返回数据之后,effect
会发送相应的action
给reducer
,由唯一能改变state
的reducer
改变state
,然后通过connect
重新渲染组件。 - 当我们将
action
发送给reducer
,那直接由reducer
改变state
,然后通过connect
重新渲染组件
2.5 Dva 图解
图解一:加入 Saga
React
只负责页面渲染,而不负责页面逻辑,页面逻辑可以从中单独抽取出来,变成 store
使用 Middleware
拦截 action
, 这样一来异步的网络操作也就很方便了,做成一个 Middleware
就行了,这里使用 redux-saga
这个类库
- 点击创建
Todo
的按钮,发起一个type == addTodo
的action
saga
拦截这个action
, 发起http
请求,如果请求成功,则继续向reducer
发一个type == addTodoSucc
的action
, 提示创建成功,反之则发送type == addTodoFail
的action
即可
图解二:Dva 表示法
dva 做了 3 件很重要的事情
- 把
store
及saga
统一为一个model
的概念,写在一个 js 文件里面 - 增加了一个
Subscriptions
, 用于收集其他来源的action
, eg: 键盘操作 model
写法很简约,类似于DSL
或者RoR
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论