正如DvaJS官网所说,命名来自Overwatch(守望先锋)

前言

正如DvaJS官网所说,命名来自Overwatch(守望先锋)

D.Va拥有一部强大的机甲,它具有两台全自动的近距离聚变机炮、可以使机甲飞跃敌人或障碍物的推进器、 还有可以抵御来自正面的远程攻击的防御矩阵。

d788d43f8794a4c28ec5043e06f41bd5ac6e39c5.jpg 守望先锋中的花村,曾经就是D.Va村。这不禁让我想起了,十二个D.Va花村对轰,激情燃烧的岁月。

出于扩展技术栈的求知欲,和对宋哈娜老师的瑞思拜,毕竟之前看过她的一些作品,我不由得对DvaJS产生了一些好奇。当我在网上搜集DvaJS资料的时候,想不到还有意外收获。

0.jpg 有兴趣的同学,可以自己用百度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工作流,如下图所示:

136441247c2559c227ec28b62aa6eb8b.jpeg

store:

推送数据的仓库

reducer:

帮助 store 处理数据的方法(初始化、修改、删除)

actions:

数据更新的指令

react 组件(UI):

订阅 store 中的数据

如果用图书馆的借阅流程来打比方:

  • React组件,好比是借书的人;

  • action数据更新的指令,就是要借什么书;

  • store推送数据的仓库,是图书管理员;

  • reducer帮助 store 处理数据的方法,是图书仓库的记录表,更新图书的借阅状态;

详情请见Redux 中文文档

传送门:

www.redux.org.cn/

React-Redux

Redux 的作者封装了一个 React 专用的库 React-Redux

关键词 

Provider 

connect

7a20f4ff2157262aae1ae3c769f30f25.jpeg 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…

9d04debc79c814ec25abf1626dab6fec.jpeg

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 中文文档

传送门:

redux-saga-in-chinese.js.org/

前面铺垫了这么多前戏,终于要上手搞Dva了,我的大刀早已饥渴难耐。

79ca2bc1d10d5529f16ae5d6bbca6ec5.jpeg

快速上手

DvaJS官网地址

传送门: dvajs.com/guide/getti…

0 (4).jpg

Dva初始化

通过 npm 安装 dva-cli 并确保版本是 0.9.1 或以上

npm install dva-cli -g
dva -v
复制代码

创建新应用

dva new dva-quickstart
复制代码

启动

cd dva-quickstart
npm start
复制代码

项目跑起来,像这样:

57f4e063eff6348315a46f3ed6b7a999.jpeg

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(),
});
复制代码

02bc06c6e0d617964d6ae08400499ce6.jpeg

Dva 概念

数据流向

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State,所以在 dva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。

118f744670c515db07839a690bd8dcf0.jpeg

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);
 })
复制代码

1b632d1515db9ef0b9145ac7d5b512d0.jpeg 以上,就是结合文档和实际操作的DvaJS的简单使用,以及核心功能。

最后,附上我新建的 dva-quickstart 的 gitee地址

gitee.com/OrzR3/dva-q…

0.jpg

如果你觉得有收获,点个赞吧!点个关注!

4f3c996363a06163e09e57b8ab461902.jpeg

猜你喜欢

转载自juejin.im/post/7077863162774552590
今日推荐