1. 程式人生 > 實用技巧 >第七課之dva以及搭建常用介面

第七課之dva以及搭建常用介面

什麼是redux

Redux 是 JavaScript狀態容器,提供可預測化的狀態管理,可以讓你構建一致化的應用,運行於不同的環境(客戶端、伺服器、原生應用),並且易於測試

為什麼用Redux

因為對於react來說,同級元件之間的通訊尤為麻煩,或者是非常麻煩了,所以我們把所有需要多個元件使用的state拿出來,整合到頂部容器,進行分發

redux實現了什麼

在react的資料互動理念中,只能進行父子元件的通訊,無法想和誰通訊就和誰通訊,redux做到了將資料傳遞新增到了全域性,任何地方都可以進行接收使用。

將需要修改的state都存入到store裡,發起一個action用來描述發生了什麼,用reducers描述action如何改變state樹。建立store的時候需要傳入reducer,真正能改變store中資料的是store.dispatch API

dva介紹

dva 首先是一個基於 redux 和 redux-saga 的資料流方案,然後為了簡化開發體驗,dva 還額外內建了 react-router 和 fetch,所以也可以理解為一個輕量級的應用框架。

資料流向

資料的改變發生通常是通過使用者互動行為或者瀏覽器行為(如路由跳轉等)觸發的,當此類行為會改變資料的時候可以通過 dispatch 發起一個 action,如果是同步行為會直接通過 Reducers 改變 State ,如果是非同步行為(副作用)會先觸發 Effects 然後流向 Reducers 最終改變 State,所以在 dva 中,資料流向非常清晰簡明,並且思路基本跟開源社群保持一致(也是來自於開源社群)

如圖:

Models


import * as addbanner from '../services/addbanner';
export default {
  namespace: 'addbanner',
  state: {},
  subscriptions: {
    setup({ history, dispatch }, onError) {
    }
  },
  effects: {
    //call:執行非同步函式
    //put:發出一個 Action,更新store,類似於 dispatch
    *bannerlist(action, { call, put }) {
      const testRes = yield call(addbanner.bannerlist, action.params);
      yield put({
        type: 'test',
        payload: {
          bannerlistRes: testRes
        },
      });
    },
  reducers: {
    test(state, { payload }) {
      return {
        ...state,
        ...payload,
      };
    }
  },
};

State

State 表示 Model 的狀態資料,通常表現為一個 javascript 物件(當然它可以是任何值);操作的時候每次都要當作不可變資料(immutable data)來對待,保證每次都是全新物件,沒有引用關係,這樣才能保證 State 的獨立性,便於測試和追蹤變化。

Action

Action 是一個普通 javascript 物件,它是改變 State 的唯一途徑。無論是從 UI 事件、網路回撥,還是 WebSocket 等資料來源所獲得的資料,最終都會通過 dispatch 函式呼叫一個 action,從而改變對應的資料。action 必須帶有 type 屬性指明具體的行為,其它欄位可以自定義,如果要發起一個 action 需要使用 dispatch 函式;需要注意的是 dispatch 是在元件 connect Models以後,通過 props 傳入的。

dispatch 函式

dispatching function 是一個用於觸發 action 的函式,action 是改變 State 的唯一途徑,但是它只描述了一個行為,而 dipatch 可以看作是觸發這個行為的方式,而 Reducer 則是描述如何改變資料的。

在 dva 中,connect Model 的元件通過 props 可以訪問到 dispatch,可以呼叫 Model 中的 Reducer 或者 Effects,常見的形式如

dispatch({
  type: 'user/add',
  payload: {}, // 需要傳遞的資訊
});

Reducer

Reducer(也稱為 reducing function)函式接受兩個引數:之前已經累積運算的結果和當前要被累積的值,返回的是一個新的累積結果。該函式把一個集合歸併成一個單值。

Effect

Effect 被稱為副作用,在我們的應用中,最常見的就是非同步操作。它來自於函式程式設計的概念,之所以叫副作用是因為它使得我們的函式變得不純,同樣的輸入不一定獲得同樣的輸出。

Subscription

Subscription 語義是訂閱,用於訂閱一個數據源,然後根據條件 dispatch 需要的 action。資料來源可以是當前的時間、伺服器的 websocket 連線、keyboard 輸入、geolocation 變化、history 路由變化等等。

// 比如:當用戶進入 /users 頁面時,觸發action users/fetch 載入使用者資料。
subscriptions: {
    setup({ dispatch, history }) {
      history.listen(({ pathname }) => {
        debugger
        if (pathname === '/primary') {
          dispatch({
            type: 'primary/getmerProClassList',
          });
        }
      })
    }
}
dva redux
引用dva 要引入多個庫,專案結構複雜
實現一個非同步互動修改的檔案很少 實現一個非同步互動需要修改的檔案太多,容易出錯
使用方式清晰、便捷 使用方式複雜、繁瑣

Router

這裡的路由通常指的是前端路由,由於我們的應用現在通常是單頁應用,所以需要前端程式碼來控制路由邏輯,通過瀏覽器提供的 History API 可以監聽瀏覽器url的變化,從而控制路由相關操作。

import { Router, Route } from 'dva/router';
app.router(({history}) =>
  <Router history={history}>
    <Route path="/" component={HomePage} />
  </Router>
);

安裝

npm install dva-cli -D

使用

  • 1.建立頁面元件,檔案以及元件命名大駝峰
  • 2.新增路由,按專案規則命名(eg:列表頁面:/list,新增列表頁面:/list/listAdd)
  • 3.定義model
  • 4.定義service以及api地址
  • 5.連線model和元件
1.建立頁面元件Primary.js
import React, { Component } from 'react'

export default class Primary extends Component {
   render() {
      return (
         <div>
            Primary
         </div>
      )
   }
}

2.新增路由,並在福祿管家將對應選單新增到自己的商戶應用下,否則系統會進行控制,也是無法開啟
const Primary = dynamic({
  app,
  models: () => [
    import('./models/primary'),
  ],
  component: () => import('./components/Primary')
});
// 使用
<Route exact path="/primary" render={(props) => WraperRouter(props, Primary)} />
3.建立model檔案couponsList.js
import * as primary from '../services/primary';

export default {
  namespace: 'primary',
  state: {},
  effects: {
    *saveOneProClass({ payload, callback }, { call, put }) {
      const testRes = yield call(primary.saveOneProClass, payload);
      yield put({
        type: 'success',
        payload: {
          saveOneProClassResult: testRes
        }
      });
      if (callback instanceof Function) {
        callback(testRes);
      }
      return testRes;
    },
    *deleteProClass({ payload, callback }, { call, put }) {
      const testRes = yield call(primary.deleteProClass, payload);
      yield put({
        type: 'success',
        payload: {
          deleteProClassResult: testRes
        }
      });
      if (callback instanceof Function) {
        callback(testRes);
      }
      return testRes;
    },
    *getmerProClassList({ payload, callback }, { call, put }) {
      const testRes = yield call(primary.getmerProClassList, payload);
      yield put({
        type: 'test',
        payload: {
          getmerProClassListRes: testRes
        },
      });
      if (callback instanceof Function) {
        callback(testRes);
      }
      return testRes;
    },
  },
  reducers: {
    success (state, {payload}) {
      return {
        ...state,
        ...payload,
      };
    }
  }
}
4.建立service檔案couponsList.js以及在api.js新增地址(檢視後端服務地址進行配置)

請求的地址都從service.js進行傳遞

// 引入axios元件
import axios from '../utils/axios';
// 引用專案api地址
import Api from '../configs/api';
// get請求
export function getMerProClassOneList(params) {
  return axios.get(configs.host.test + Api.getMerProClassOneList, { 'params': params });
}

// post請求
export function saveOneProClass(params) {
  return axios.post(configs.host.test + Api.saveOneProClass,params);
}

export function deleteProClass(params) {
  return axios.get(configs.host.test + Api.deleteProClass, { 'params': params });
}

// put請求
// export function modifyMembershipInfo(params) {
//   return axios.put(configs.host.test + // Api.modifyMembershipInfo,params);
// }
// delete請求
// export function guessYouLikeDelete(params) {
//     return axios.delete(configs.host.test + Api.guessYouLike, { // 'params': params })
// }

axios.js

// axios.js用於處理資料請求響應(新增請求header,返回資料異常捕獲等)
import axios from 'axios';
import { message } from 'antd';
import { handleErrCallBack } from 'fl-pro';

// axios.defaults.baseURL = '';  API 域。預設值:當前域
axios.defaults.withCredentials = true;  // 允許跨域且攜帶 Cookie(或自定義頭)。預設值:false
axios.defaults.timeout = 30000; // 設定請求超時時間(ms)不超過半分鐘
axios.defaults.headers.common['Authorization'] = '';  // 攜帶的自定義頭
axios.defaults.headers.post['Content-Type'] = 'application/json';  // 設定請求提內容型別,其他可選值:application/x-www-form-urlencoded

axios.interceptors.request.use(config => {
  // console.log('【request】', config);
  config.headers["Authorization"] = `Bearer ${localStorage.getItem('access_token')}`;
  config.headers["MerchantId"] = localStorage.getItem('MerchantId');
  return config;
}, error => {
  // console.log('【request error】', error);
  return Promise.reject(error);
});

axios.interceptors.response.use(response => {
  return response.data;
},handleErrCallBack
);

export default axios;
5.api.js新增
getmerProClassList: '/api/product/GetMerProClassList',// 獲取商戶商品分類 //商品一級分類
saveOneProClass: '/api/product/SaveOneProClass', //新增編輯一級分類儲存介面
deleteProClass: '/api/product/DeleteProClass', //刪除分類
6.連線model和元件
// 方式一:直接在引用的地方連線
const Primary = dynamic({
  app,
  models: () => [
    import('./models/primary'),
  ],
  component: () => import('./components/Primary')
});



// 方式二:單頁應用入口檔案index.js新增引用

import { default as primary } from './models/primary';
app.model(primary);

// 區別: 在入口檔案註冊的model檔案,可以在專案任何一個路由頁面進行使用,而在單個路由註冊的model檔案只能在該頁面使用,別的頁面無法使用你的model
核心程式碼檢視
  • 查詢
// 查詢公用函式
getData = () => {
   const { postData } = this.state;
   this.props.dispatch({ type: 'primary/getmerProClassList', payload: { ...postData } }).then((res) => {
      const { code, data } = res;
      if (code === '0') {
         return this.setState({
            tableData: data.list,
            total: data.total
         });
      }
      message.error(res.message);
   });
}
// 處理表單values並呼叫,查詢
search = (err, value) => {
   //查詢列表
   const { postData } = this.state;
   postData.pageIndex = 1;
   this.setState({
      postData: { ...postData, ...value }
   }, () => {
      this.getData();
   })
}
  • 新增、編輯
// 子元件彈窗,新增編輯一級分類程式碼
sureAdd = () => {
    this.props.form.validateFields((err, values) => {
        if (!err) {
            const param = values;
            const { iconPath, iconPathEdit, detailInfo } = this.state;
            let nowImgUrl = '';
            if (detailInfo.id) {
                param.id = detailInfo.id;
                // 如果使用者上傳了或者刪除了
                if (iconPathEdit) {
                    nowImgUrl = iconPath ? iconPath.response.data : '';
                }
                else {
                    nowImgUrl = detailInfo.iconPath;
                }
            } else {
                nowImgUrl = iconPath ? (iconPath.flag ? iconPath.fileList[0].url :
                    iconPath.response.data) : '';
            }

            if (!nowImgUrl) {
                return message.error('請選擇一級分類圖!');
            }
            param.iconPath = nowImgUrl;
            this.props.dispatch({
                type: 'primary/saveOneProClass',
                payload: param,
                callback: ({ code, message: info }) => {
                    if (code === '0') {
                        this.props.getData();
                        this.handleClose();
                    } else {
                        message.error(info);
                    }
                }
            });
        }
    });
}

// 圖片上傳的回撥
updateImg = (filed, value, fieldEdit) => {
    this.setState({
        [filed]: value,
        [fieldEdit]: true
    });
}
// 封裝的上傳圖片元件
<UploadImg
    label="一級分類圖"
    field="iconPath"
    updateImg={this.updateImg}
    rowInfo={detailInfo}
    imgSize={10000}
    formItemLayout={{
        labelCol: { span: 8 },
        wrapperCol: { span: 14 },
    }}
/>
  • 刪除
// 批量刪除
batchDeleteInfo = () => {
   let { selectedRowKeys } = this.state;
   let ids = selectedRowKeys.join(',');
   this.deleteInfo(ids);
}
// 刪除公用方法
deleteInfo = (ids) => {
   confirm({
      title: '刪除確認',
      content: '確定要刪除所選資料嗎?',
      okText: '確認',
      centered: true,
      cancelText: '取消',
      onOk: () => {
         this.props.dispatch({
            type: 'primary/deleteProClass', payload: { ids },
            callback: ({ code, data, message: info }) => {
               if (code === '0') {
                  message.success(info);
                  this.setState({
                     selectedRowKeys: []
                  })
                  this.getData();
               } else if (code === '-1') {
                  message.error(info);
               }
            }
         });
      },
      onCancel() {
      },
   });
}
  • 父子元件傳值
{showModal && <ShowModal hideFildModel={this.showModal} getData={this.getData} rowInfo={rowInfo} />}

如何接受dva的props的返回資料

觀察程式碼發現我們使用返回值要麼用的是.then,要麼用的callback回撥函式,如何使用dva資料流處理頁面資料或者做某些事

例如我們在子元件新增、編輯成功後,可以在父元件接受,子元件的成功行為做操作

// 去除子元件的callback獲取返回值的方法
this.props.dispatch({
    type: 'primary/saveOneProClass',
    payload: param,
    // callback: ({ code, message: info }) => {
    //     if (code === '0') {
    //         this.props.getData();
    //         this.handleClose();
    //     } else {
    //         message.error(info);
    //     }
    // }
});

// 父元件通過result接收資料進行比較,做修改
componentWillReceiveProps(nextProps) {
   const { saveOneProClassResult } = nextProps.primary;
   if (saveOneProClassResult !== this.props.primary.saveOneProClassResult) {
      const { code, data } = saveOneProClassResult;
      if (code === '0') {
         this.getData();
         this.showModal('showModal', false);
      } else {
         message.error(saveOneProClassResult.message);
      }
   }
}