前言
正如DvaJS
官网所说,命名来自Overwatch
(守望先锋)
D.Va拥有一部强大的机甲,它具有两台全自动的近距离聚变机炮、可以使机甲飞跃敌人或障碍物的推进器、 还有可以抵御来自正面的远程攻击的防御矩阵。
守望先锋中的花村,曾经就是D.Va村。这不禁让我想起了,十二个D.Va花村对轰,激情燃烧的岁月。
出于扩展技术栈的求知欲,和对宋哈娜老师的瑞思拜,毕竟之前看过她的一些作品,我不由得对DvaJS产生了一些好奇。当我在网上搜集DvaJS资料的时候,想不到还有意外收获。
有兴趣的同学,可以自己用百度Google一下。
那么,接下来,我们言归正传。
什么是DvaJS
dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
关键词,简短解说
React
React 是一个用于构建用户界面的 JavaScript 库,通过 JSX 生成动态 dom 渲染 UI 界面。
Redux
React只有view层,React做数据管理,需要借助其他工具。Redux就是用来做全局数据状态管理的。Redux之于React,正如同Vuex之于Vue,类推理解。
Redux工作流,如下图所示:
store:
推送数据的仓库
reducer:
帮助 store 处理数据的方法(初始化、修改、删除)
actions:
数据更新的指令
react 组件(UI):
订阅 store 中的数据
如果用图书馆的借阅流程来打比方:
-
React组件,好比是借书的人;
-
action数据更新的指令,就是要借什么书;
-
store推送数据的仓库,是图书管理员;
-
reducer帮助 store 处理数据的方法,是图书仓库的记录表,更新图书的借阅状态;
详情请见Redux 中文文档
传送门:
React-Redux
Redux 的作者封装了一个 React 专用的库 React-Redux
关键词
Provider
connect
React-Redux 提供组件,能够使你的整个app访问到Redux store中的数据:
import React from "react";import ReactDOM from "react-dom";
import { Provider } from "react-redux";import store from "./store";
import App from "./App";
const rootElement = document.getElementById("root");ReactDOM.render( <Provider store={store}> <App /> </Provider>, rootElement);
复制代码
React-Redux 提供一个 connect 方法能够让你把组件和store连接起来。
通常你可以以下面这种方式调用 connect 方法:
import { login, logout } from './actionCreators'
const mapState = (state) => state.user
const mapDispatch = { login, logout }
// first call: returns a hoc that you can use to wrap any component
const connectUser = connect(mapState, mapDispatch)
// second call: returns the wrapper component with mergedProps
// you may use the hoc to enable different components to get the same behavior
const ConnectedUserLogin = connectUser(Login)
const ConnectedUserProfile = connectUser(Profile)
复制代码
React Redux官方文档
传送门:
react-redux.js.org/introductio…
redux-saga
redux-saga 是 redux为了解决异步的方案。
redux-saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试。
有点像 async/await,异步函数名前面还加个星号,这是 Generator 的语法。
Generator 返回的是迭代器,通过 yield 关键字实现暂停功能。
Generator 其实在很大程度上和Promise很像,都是用来解决异步操作相关问题的。
Generator语法,示例
function *Generator(){
var a = yield test('hhhhhhhh');
console.log(a);
}
function test(){
setTimeout(function(){
console.log('halo');
},200)
}
复制代码
使用示例:
class UserComponent extends React.Component {
...
onSomeButtonClicked() {
const { userId, dispatch } = this.props
dispatch({type: 'USER_FETCH_REQUESTED', payload: {userId}})
}
...
}
复制代码
这个组件 dispatch 一个 plain Object 的 action 到 Store。我们将创建一个 Saga 来监听所有的 USER_FETCH_REQUESTED action,并触发一个 API 调用获取用户数据。
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'
// worker Saga : 将在 USER_FETCH_REQUESTED action 被 dispatch 时调用
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}
}
/*
在每个 `USER_FETCH_REQUESTED` action 被 dispatch 时调用 fetchUser
允许并发(译注:即同时处理多个相同的 action)
*/
function* mySaga() {
yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}
/*
也可以使用 takeLatest
不允许并发,dispatch 一个 `USER_FETCH_REQUESTED` action 时,
如果在这之前已经有一个 `USER_FETCH_REQUESTED` action 在处理中,
那么处理中的 action 会被取消,只会执行当前的
*/
function* mySaga() {
yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}
export default mySaga;
复制代码
为了能跑起 Saga,我们需要使用 redux-saga 中间件将 Saga 与 Redux Store 建立连接。
main.js
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import reducer from './reducers'
import mySaga from './sagas'
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
// then run the saga
sagaMiddleware.run(mySaga)
// render the application
复制代码
redux-sage 中文文档
传送门:
前面铺垫了这么多前戏,终于要上手搞Dva了,我的大刀早已饥渴难耐。
快速上手
DvaJS官网地址
Dva初始化
通过 npm 安装 dva-cli 并确保版本是 0.9.1 或以上
npm install dva-cli -g
dva -v
复制代码
创建新应用
dva new dva-quickstart
复制代码
启动
cd dva-quickstart
npm start
复制代码
项目跑起来,像这样:
Dva路由
1.通过Link组件跳转
2.通过点击事件跳转
import React, { Component, Fragment } from 'react';
import { Link } from "dva/router";
import Child from '../components/child.js';
class userPage extends Component {
handleToIndex = () => {
console.log(this.props);
this.props.history.push('/');
}
render() {
return (
<Fragment>
<div>userPage</div>
<Link to="/">首页</Link>
<button onClick={this.handleToIndex}>首页</button>
<Child />
</Fragment>
)
}
}
export default userPage;
复制代码
3.components组件中通过 withRouter 跳转
import React, { Component } from 'react'
import { withRouter } from 'dva/router';
class child extends Component {
handleToIndex() {
console.log(this.props);
this.props.history.push('/');
}
render() {
return (
<div>
<div>child</div>
<button onClick={this.handleToIndex.bind(this)}>首页_child</button>
</div>
)
}
}
export default withRouter(child)
复制代码
4.BrowserHistory的使用
DvaJS默认使用hashHistory,为了去掉url中的出现的 # 号,需要使用 BrowserHistory
安装history依赖
npm install --save history
复制代码
修改入口文件
import createHistory from 'history/createBrowserHistory';
const app = dva({
history: createHistory(),
});
复制代码
报错:
dva Cannot find module 'history/createBrowserHistory'
修改为
// import createHistory from 'history/createBrowserHistory';
// 报错 Error: Cannot find module 'history/createBrowserHistory';
// 改成
import { createBrowserHistory as createHistory} from 'history';
const app = dva({
history: createHistory(),
});
复制代码
Dva 概念
数据流向
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State,所以在 dva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。
models 优化了redux和redux-saga
dva 通过 model 的概念把一个领域的模型管理起来,包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions 。
models_reducers
在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。
操作示例:
/src/models 文件夹下
新建 indexTest.js 文件
export default {
// 命令空间
namespace: "indexTest",
// 状态
state: {
name : 'Mila'
},
reducers:{
setName(state, payLoad){
console.log('run');
console.log(payLoad);
return { ...state, ...payLoad.data}
}
}
}
复制代码
在 /src/index.js 文件中
使用 app.model 引入
import dva from 'dva';
import './index.css';
// import createHistory from 'history/createBrowserHistory';
import { createBrowserHistory as createHistory} from 'history';
// 1. Initialize
const app = dva({
history: createHistory(),
});
// 2. Plugins
// app.use({});
// 3. Model
// app.model(require('./models/example').default);
app.model(require('./models/indexTest').default);
// 4. Router
app.router(require('./router').default);
// 5. Start
app.start('#root');
复制代码
在 routes 文件夹下 IndexPage.js 文件中
import React, { Component } from 'react'
import { connect } from 'dva';
class IndexPage extends Component {
handleSetName = () =>{
this.props.dispatch({
type: 'indexTest/setName',
data:{
name: 'puck'
}
})
}
render() {
console.log(this.props);
return (
<div>
IndexPage
{this.props.msg}
{this.props.name}
<button onClick={this.handleSetName}>setName</button>
</div>
)
}
}
const mapStateToProps = (state, ownProps) => {
console.log(state);
return {
msg: '我是一个msg',
prop: state.prop,
name: state.indexTest.name
}
}
export default connect(mapStateToProps)(IndexPage)
复制代码
正如上方代码所示,首先引入 connect
import { connect } from 'dva';
复制代码
mapStateToProps 来自于redux,是一个函数,用于建立组件跟 store 的 state 的映射关系。
const mapStateToProps = (state, ownProps) => {
console.log(state);
return {
msg: '我是一个msg',
prop: state.prop,
name: state.indexTest.name
}
}
复制代码
导出时,用 connect 进行组件的连接。
export default connect(mapStateToProps)(IndexPage)
复制代码
执行过程:
在 this.props.dispatch() 方法里,派发一个对象,对象里包含一个类型type和要派发的内容data 类型type就是 /src/models/indexTest.js 文件中的 namespace/方法名
譬如:indexTest/setName
this.props.dispatch({
type: 'indexTest/setName',
data:{
name: 'puck'
}
})
复制代码
然后 model 里的reducers去执行操作,改变state的值,正如 /src/models/indexTest.js所示:
export default {
// 命令空间
namespace: "indexTest",
// 状态
state: {
name : 'Mila'
},
reducers:{
setName(state, payLoad){
console.log('run');
console.log(payLoad);
return { ...state, ...payLoad.data}
}
}
}
复制代码
models_effects
Effect 被称为副作用,在我们的应用中,最常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。
models 里的异步,还是基于redux-sage
dva 通过对 model 增加 effects 属性来处理 side effect(异步任务),这是基于 redux-saga 实现的,语法为 Generator语法。
在 model 文件中增加 effects
effects: {
*setNameAsync({ payLoad }, { put, call }) {
yield put({
type: 'setName',
data:{
name: 'malena morgan'
}
})
console.log('run');
}
}
复制代码
修改 models/indexTest.js 文件
export default {
// 命令空间
namespace: "indexTest",
// 状态
state: {
name: 'Mila'
},
reducers: {
setName(state, payLoad) {
console.log('run');
console.log(payLoad);
return { ...state, ...payLoad.data }
}
},
effects: {
*setNameAsync({ payLoad }, { put, call }) {
yield put({
type: 'setName',
data:{
name: 'malena morgan'
}
})
console.log('run');
}
}
}
复制代码
在 routes 文件夹下 IndexPage.js 文件,增加异步操作的方法 handleSetNameAsync,执行成功改变状态。
import React, { Component } from 'react'
import { connect } from 'dva';
class IndexPage extends Component {
handleSetName = () =>{
this.props.dispatch({
type: 'indexTest/setName',
data:{
name: 'puck'
}
})
}
handleSetNameAsync= () =>{
this.props.dispatch({
type: 'indexTest/setNameAsync',
data:{
name: 'puck'
}
})
}
render() {
console.log(this.props);
return (
<div>
IndexPage
{this.props.msg}
{this.props.name}
<button onClick={this.handleSetName}>setName</button>
<button onClick={this.handleSetNameAsync}>setNameAsync</button>
</div>
)
}
}
const mapStateToProps = (state, ownProps) => {
console.log(state);
return {
msg: '我是一个msg',
prop: state.prop,
name: state.indexTest.name
}
}
export default connect(mapStateToProps)(IndexPage)
复制代码
models_Api
配置代理请求
修改根目录下的 .webpackrc 文件
{
"proxy": {
"/apis": {
"target": "https://cnodejs.org",
"changeOrigin": true,
"pathRewrite": {
"^/apis": ""
}
}
}
}
复制代码
修改后保存,项目会重新启动。
修改 /services/example.js 文件
import request from '../utils/request';
const proxy = "/apis/"
export function query() {
return request('/api/users');
}
export function userInfo() {
return request(proxy + '/api/v1/user/alsotang')
}
复制代码
具体请求接口的方法request在 /utils/request.js 文件中,
可以看到,实际上用的是 dva 封装的 fetch
import fetch from 'dva/fetch';
function parseJSON(response) {
return response.json();
}
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
}
/**
* Requests a URL, returning a promise.
*
* @param {string} url The URL we want to request
* @param {object} [options] The options we want to pass to "fetch"
* @return {object} An object containing either "data" or "err"
*/
export default function request(url, options) {
return fetch(url, options)
.then(checkStatus)
.then(parseJSON)
.then(data => ({ data }))
.catch(err => ({ err }));
}
复制代码
接下来,在 routes/IndexPage.js 文件中引入请求接口地方
import * as apis from '../services/example'
复制代码
在 componentDidMount 生命周期中发起请求
componentDidMount() {
apis.userInfo().then((res)=>{
console.log(res);
})
}
复制代码
页面打印出接口返回的数据,请求成功。
然后,在models调用接口
修改 models/indexTest.js 文件
import * as apis from '../services/example'
export default {
// 命令空间
namespace: "indexTest",
// 状态
state: {
name: 'Mila',
userInfo:{}
},
reducers: {
setName(state, payLoad) {
console.log('run');
console.log(payLoad);
return { ...state, ...payLoad.data }
},
setUserInfo(state, payLoad){
console.log(payLoad);
return { ...state, userInfo: payLoad.data }
}
},
effects: {
*setNameAsync({ payLoad }, { put, call }) {
yield put({
type: 'setName',
data:{
name: 'malena morgan'
}
})
console.log('run');
},
*getUserInfo({ payLoad }, { put, call }){
let res = yield call(apis.userInfo)
if(res.data){
console.log(res.data.data);
yield put({
type: 'setUserInfo',
data:res.data.data
})
}
}
}
}
复制代码
新建 getUserInfo 方法,通过call发送异步请求接口,接收到返回数据之后,通过put触发action行为,修改state中的数据。
*getUserInfo({ payLoad }, { put, call }){
let res = yield call(apis.userInfo)
if(res.data){
console.log(res.data.data);
yield put({
type: 'setUserInfo',
data:res.data.data
})
}
}
复制代码
在 routes/IndexPage.js 文件中,新增 getUserInfo 方法,并且在 mapStateToProps 中新增 userInfo 页面就能展示数据了。
import React, { Component } from 'react'
import { connect } from 'dva';
import * as apis from '../services/example'
class IndexPage extends Component {
handleSetName = () =>{
this.props.dispatch({
type: 'indexTest/setName',
data:{
name: 'puck'
}
})
}
handleSetNameAsync= () =>{
this.props.dispatch({
type: 'indexTest/setNameAsync',
data:{
name: 'puck'
}
})
}
getUserInfo = () =>{
this.props.dispatch({
type: 'indexTest/getUserInfo',
})
}
componentDidMount() {
apis.userInfo().then((res)=>{
console.log(res);
})
}
render() {
console.log(this.props);
return (
<div>
IndexPage
{this.props.msg}
{this.props.name}
<button onClick={this.handleSetName}>setName</button>
<button onClick={this.handleSetNameAsync}>setNameAsync</button>
<button onClick={this.getUserInfo}>getUserInfo</button>
</div>
)
}
}
const mapStateToProps = (state, ownProps) => {
console.log(state);
return {
msg: '我是一个msg',
prop: state.prop,
name: state.indexTest.name,
userInfo: state.indexTest.userInfo
}
}
export default connect(mapStateToProps)(IndexPage)
复制代码
models_subscription
Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
以 history 路由变化为例,简单演示一下 subscription 的使用。
在 models/indexTest.js 文件中修改
import * as apis from '../services/example'
export default {
// 命令空间
namespace: "indexTest",
// 状态
state: {
name: 'Mila',
userInfo:{}
},
reducers: {
setName(state, payLoad) {
console.log('run');
console.log(payLoad);
return { ...state, ...payLoad.data }
},
setUserInfo(state, payLoad){
console.log(payLoad);
return { ...state, userInfo: payLoad.data }
},
haloFunction(state, payLoad){
console.log('halo-reducers');
return state;
}
},
effects: {
*setNameAsync({ payLoad }, { put, call }) {
yield put({
type: 'setName',
data:{
name: 'malena morgan'
}
})
console.log('run');
},
*getUserInfo({ payLoad }, { put, call }){
let res = yield call(apis.userInfo)
if(res.data){
console.log(res.data.data);
yield put({
type: 'setUserInfo',
data:res.data.data
})
}
}
},
subscriptions:{
halo({dispatch, history}){
console.log('halo');
console.log(history);
history.listen(({pathname})=>{
if(pathname ==='/user'){
console.log('用户页');
dispatch({
type: 'haloFunction'
})
}
})
}
}
}
复制代码
当页面路径改变时,触发 haloFunction 控制台打印:halo-reducers
dva.mock
第一步,在 mock 文件下新建 testMock.js 文件
{
"proxy": {
"/apis": {
"target": "https://cnodejs.org",
"changeOrigin": true,
"pathRewrite": {
"^/apis": ""
}
}
}
}
复制代码
第二步,修改 .roadhogrc.mock.js 文件
export default {
...require("./mock/testMock")
};
复制代码
第三步,在 services/example.js 中注册
import request from '../utils/request';
const proxy = "/apis/"
export function query() {
return request('/api/users');
}
export function userInfo() {
return request(proxy + '/api/v1/user/alsotang')
}
// 注册mock接口
export function mockData() {
return request('/api/mockData')
}
复制代码
然后在页面 indexPage.js 中调用
apis.mockData().then((res)=>{
console.log(res);
})
复制代码
以上,就是结合文档和实际操作的DvaJS的简单使用,以及核心功能。
最后,附上我新建的 dva-quickstart 的 gitee地址