Dva + Ant Design 先後端分離之 React 應用實踐

源站連接 https://tkvern.comjavascript

Rails 從入門到徹底放棄 擁抱 Elixir + Phoenix + React + Redux 這篇文章被噴以後,筆者很長一段時候沒有上社區逛了。如今 tkvern 又迴歸了,給你們帶來React實踐的一些經驗,一些踩坑的經驗。php

Rails嘛,很好用,Laravel也好用。Phoenix也好用。都好,哪一個方便用哪一個。前端

還有關於Turbolinks之爭,不能單從頁面渲染時間去對比,要綜合考慮。java

Why Dva?

Dva是基於Redux作了一層封裝,對於React的state管理,有不少方案,我選擇了輕量、簡單的Dva。至於Mobx,還沒應用到項目中來。先等友軍踩踩坑,再往裏面跳。react

順便貼下Dva的特性:github

  • 易學易用:僅有 5 個 api,對 redux 用戶尤爲友好web

  • elm 概念:經過 reducers, effectssubscriptions 組織 modeltypescript

  • 支持 mobile 和 react-native:跨平臺 (react-native 例子)

  • 支持 HMR:目前基於 babel-plugin-dva-hmr 支持 components 和 routes 的 HMR

  • 動態加載 Model 和路由:按需加載加快訪問速度 (例子)

  • 插件機制:好比 dva-loading 能夠自動處理 loading 狀態,不用一遍遍地寫 showLoading 和 hideLoading

  • 完善的語法分析庫 dva-astdva-cli 基於此實現了智能建立 model, router 等

  • 支持 TypeScript:經過 d.ts (例子)

Why Ant Design?

作爲傳道士,這麼好的UI設計語言,確定不會藏着掖着啦。螞蟻金服的東西,確實不錯,除了Ant Design外,還有Ant Design Mobile、AntV、AntMotion、G2。

Why yarn?

npm install 太慢,試試yarn吧。建議用npm install yarn -g進行安裝。

開發過程當中的先後端分離

項目開始了,前端視圖寫完,要開始數據交互了,後端提供的API還沒好。

那麼問題來了,如何在不依靠後端提供API的狀況下,實現數據交互?

使用Mock.js能夠解決這個問題。先對接好API數據格式,而後使用Mockjs攔截Ajax請求,模擬後端真實數據。

在Mockjs官方提供的API不夠用的狀況下,還可使用正則產生模擬數據。

如何對模擬作數據持久化處理?

這裏給出一個模擬用戶數據並持久化的實例實例:mock/users.js

代碼摘要:

'use strict';

const qs = require('qs');
const mockjs = require('mockjs');

const Random = mockjs.Random;

// 數據持久化
let tableListData = {};

if (!global.tableListData) {
  const data = mockjs.mock({
    'data|100': [{
      'id|+1': 1,
      'name': () => {
        return Random.cname();
      },
      'mobile': /1(3[0-9]|4[57]|5[0-35-9]|7[01678]|8[0-9])\d{8}/,
      'avatar': () => {
        return Random.image('125x125');
      },
      'status|1-2': 1,
      'email': () => {
        return Random.email('visiondk.com');
      },
      'isadmin|0-1': 1,
      'created_at': () => {
        return Random.datetime('yyyy-MM-dd HH:mm:ss');
      },
      'updated_at': () => {
        return Random.datetime('yyyy-MM-dd HH:mm:ss');
      },
    }],
    page: {
      total: 100,
      current: 1,
    },
  });
  tableListData = data;
  global.tableListData = tableListData;
} else {
  tableListData = global.tableListData;
}

模擬API怎麼寫?

完成持久化處理後,就能夠像操做數據庫同樣進行增、刪、改、查

下面是一個刪除用戶的API

參見mock/users.js#L106

'DELETE /api/users' (req, res) {
    setTimeout(() => {
      const deleteItem = qs.parse(req.body);

      tableListData.data = tableListData.data.filter((item) => {
        if (item.id === deleteItem.id) {
          return false;
        }

        return true;
      });

      tableListData.page.total = tableListData.data.length;

      global.tableListData = tableListData;

      res.json({
        success: true,
        data: tableListData.data,
        page: tableListData.page,
      });
    }, 200);
  },

還有一步

模擬數據和API寫好了,還須要攔截Ajax請求

修改package.json

.
  .
  .
  "scripts": {
    "start": "dora --plugins \"proxy,webpack,webpack-hmr\"",
    "build": "atool-build -o ../../../public",
    "test": "atool-test-mocha ./src/**/*-test.js"
  }
  .
  .
  .

若是與dora有端口衝突可修改dora的端口號

"start": "dora --port 8888 --plugins \"proxy,webpack,webpack-hmr\"",

完成這些基本工做就作好了

友情提示

在模擬數據環境,services下的模塊這麼寫就行了,真實API則替換爲真實API的地址。可將地址前綴寫到統一配置中去。

import request from '../utils/request';
import qs from 'qs';
export async function query(params) {
  return request(`/api/users?${qs.stringify(params)}`);
}

export async function create(params) {
  return request('/api/users', {
    method: 'post',
    body: qs.stringify(params),
  });
}

export async function remove(params) {
  return request('/api/users', {
    method: 'delete',
    body: qs.stringify(params),
  });
}

export async function update(params) {
  return request('/api/users', {
    method: 'put',
    body: qs.stringify(params),
  });
}

真實API參考實例: src/services/users.js

如何保持登陸狀態

在看dva的引導手冊時,並無介紹登陸相關的內容。由於不一樣的項目,對於登陸這塊的實現會有所不一樣,並非惟一的。一般咱們會使用Cookie的方式保持登陸狀態,或者 Auth 2.0的技術。

這裏介紹Cookie的方式。

登陸成功以後服務器會設置一個當前域可使用的Cookie,例如token啥的。而後在每次數據請求的時候在Request Headers中攜帶token,後端會基於這個token進行權限驗證。思路清晰了,來看看具體實現吧。(注:在此次項目中使用了統一登陸模塊,經過Header中的Authorization進行驗證,將只介紹拿到token以後的數據處理)

準備工做

對於操做Cookie的一些操做,建議先封裝到工具類模塊下。同時我把操做LocalStrage的一些操做也寫進來了。

參見src/utils/helper.js

.
.
.
// Operation Cookie
export function getCookie(name) {
  const reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)');
  const arr = document.cookie.match(reg);
  if (arr) {
    return decodeURIComponent(arr[2]);
  } else {
    return null;
  }
}

export function delCookie({ name, domain, path }) {
  if (getCookie(name)) {
    document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=' + 
                      path + '; domain=' + 
                      domain;
  }
}
.
.
.

Header的預處理我放在了src/utils/auth.js#L5,這裏後端返回的數據都是JSON格式,因此在Header裏面須要添加application/json進去,而Authorization是後端用來驗證用戶信息的。變量sso_token爲了方便代碼閱讀就沒有按照規範命名了。

export function getAuthHeader(sso_token) {
  return ({
    headers: {
      'Accept': 'application/json',
      'Authorization': 'Bearer ' + sso_token,
      'Content-Type': 'application/json',
    },
  });
}

修改Request

這裏沒有使用自帶的catch機制來處理請求錯誤,在開發過程當中,最開始打算使用統一錯誤處理,可是發現請求失敗後,不能在models層處理components,因此就換了一種方式處理,後面會講到。

參見src/utils/request.js#L29

export default function request(url, options) {
  const sso_token = getCookie('sso_token');
  const authHeader = getAuthHeader(sso_token);
  return fetch(url, { ...options, ...authHeader })
    .then(checkStatus)
    .then(parseJSON)
    .then((data) => ({ data }));
    // .catch((err) => ({ err }));
}

完成這些配置以後,每次向服務器發送的請求就都攜帶了用戶token了。在token無效時,服務器會拋出401錯誤,這時就須要在中間件中處理401錯誤。

參見src/utils/request.js#L10

redirectLogin是工具類src/utils/auth.js中的重定向登陸方法。

function checkStatus(response) {
  if (response && response.status === 401) {
    redirectLogin();
  }
  if (response.status >= 200 && response.status < 500) {
    return response;
  }
  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}

到此爲止,登陸狀態的配置基本完成。

Router

咱們的應用中會有多個頁面,並且有的須要登陸纔可見,那麼如何控制呢?

React的路由控制是比較靈活的,來看看下面這個例子:

src/router.jsx

import React from 'react';
import { Router, Route } from 'dva/router';
import { authenticated } from './utils/auth';
import Dashboard from './routes/Dashboard';
import Users from './routes/Users';
import User from './routes/User';
import Password from './routes/Password';
import Roles from './routes/Roles';
import Permissions from './routes/Permissions';

export default function ({ history }) {
  return (
    <Router history={history}>
      <Route path="/" component={Dashboard} onEnter={authenticated} />
      <Route path="/user" component={User} onEnter={authenticated} />
      <Route path="/password" component={Password} onEnter={authenticated} />
      <Route path="/users" component={Users} onEnter={authenticated} />
      <Route path="/roles" component={Roles} onEnter={authenticated} />
      <Route path="/permissions" component={Permissions} onEnter={authenticated} />
    </Router>
  );
}

對於路由的驗證配置在onEnter屬性中,authenticated方法可統一進行路由驗證,要注意每個Route節點的驗證都須要配置相應的onEnter屬性。若是權限較爲複雜需對每個Route單獨驗證。其實這種基於客戶端渲染的應用,若是頁面限制有遺漏也關係不太,後端提供的API會對數據進行驗證,即便前端訪問到沒有權限的頁面,也一樣不用擔憂,作好客戶端錯誤處理便可。

數據緩存

對於一個React應用來講,緩存是很重要的一步。先後端分離後,頻繁的Ajax請求會消耗大量的服務器資源,若是一些不長變更的持久化數據不作緩存的話,會浪費許多資源。因此,比較常見的方法就是將數據緩存在LocalStorage中。針對一些敏感信息可適當進行加密混淆處理,我這裏就不介紹了。

何時作數據緩存?

例:用戶信息緩存

參見src/models/auth.js#L64

subscriptions中配置了setup檢測LocalStorage中的user是否存在。不存在時會去query用戶信息,而後保存到user中,若是存在就將user中的數據添加到stateuser: {}中。固然在進行請求時,已經在src/utils/auth.js驗證用戶信息是否正確,同時作了相應的限制src/utils/auth.js#L20

import { parse } from 'qs';
import { message } from 'antd';
import { query, update, password } from '../services/auth';
import { getLocalStorage, setLocalStorage } from '../utils/helper';

export default {
  namespace: 'auth',
  state: {
    user: {},
    isLogined: false,
    currentMenu: [],
  },
  reducers: {
    querySuccess(state, action) {
      return { ...state, ...action.payload, isLogined: true };
    },
  },
  effects: {
    *query({ payload }, { call, put }) {
      const { data } = yield call(query, parse(payload));
      if (data && data.err_msg === 'SUCCESS') {
        setLocalStorage('user', data.data);
        yield put({
          type: 'querySuccess',
          payload: {
            user: data.data,
          },
        });
      }
    },
  }
  subscriptions: {
    setup({ dispatch }) {
      const data = getLocalStorage('user');
      if (!data) {
        dispatch({
          type: 'query',
          payload: {},
        });
      } else {
        dispatch({
          type: 'querySuccess',
          payload: {
            user: data,
          },
        });
      }
    },
  },
}

簡單來講,就是沒有緩存的時候緩存。

何時更新數據緩存?

例如,roles添加修改功能都須要用到permissions的數據,哪我怎麼拿到最新的permissions數據呢。首先,我在加載roles列表頁面時就須要將permissions的數據緩存,這樣,在每次點添加修改功能時就不須要再去拉取已緩存的數據了。

參見src/models/roles.js#L166

在監聽路由到roles時查詢permissions是否緩存,將其更新到緩存中去。

.
.
.
  subscriptions: {
    setup({ dispatch, history }) {
      history.listen((location) => {
        const match = pathToRegexp('/roles').exec(location.pathname);
        if (match) {
          const data = getLocalStorage('permissions');
          if (!data) {
            dispatch({
              type: 'permissions/updateCache',
            });
          }
          dispatch({
            type: 'query',
            payload: location.query,
          });
        }
      });
    },
  },
.
.
.

何時刪除數據緩存?

刪除緩存的配置是比較靈活的,這裏的業務場景並不複雜因此,我用了比較簡單的處理方式。

參見src/models/permissions.js#L112

在執行新增或更新操做成功後,將本地原有的緩存刪除。加上數據聯動的特性,當再次回到roles操做時,緩存已經更新了。

.
.
.
    *update({ payload }, { select, call, put }) {
      yield put({ type: 'hideModal' });
      yield put({ type: 'showLoading' });
      const id = yield select(({ permissions }) => permissions.currentItem.id);
      const newRole = { ...payload, id };
      const { data } = yield call(update, newRole);
      if (data && data.err_msg === 'SUCCESS') {
        yield put({
          type: 'updateSuccess',
          payload: newRole,
        });
        localStorage.removeItem('permissions');
        message.success('更新成功!');
      }
    },
.
.
.

State的臨時緩存

state的中的數據是變化的,刷新頁面以後會重置掉,也能夠將部分models中的state存到Localstorage中,讓state的數據從Localstorage讀取,但不是必要的。而list數據的更新,是直接操做state中的數據的。

以下(這樣就不用更新整個list的數據了)。

.
.
.
    grantSuccess(state, action) {
      const grantUser = action.payload;
      const newList = state.list.map((user) => {
        if (user.id === grantUser.id) {
          user.roles = grantUser.roles;
          return { ...user };
        }
        return user;
      });
      return { ...state, ...newList, loading: false };
    },
.
.
.

視圖組件運用

Ant 提供的組件很是多,但用起來仍是須要一些學習成本的,同時多個組件組合使用時也須要有不少地方注意的。

Modal注意事項

在使用Modal組件時,不免會出現一個頁面多個Modal的狀況,首先要注意的就是Modal的命名,在多Modal狀況下,命名不注意很容易出現分不清用的是哪一個Modal。建議命名時能望名知意。而後就是Modal須要用到別的Models的數據時,若是在彈窗時經過Ajax獲取須要的數據再顯示Modal,這樣就會出現Modal延遲,並且Modal的動畫也沒法加載出來。因此,個人處理方式是,在進入這一級Route的時候就將須要的數據預緩存,這樣調用時就可隨用隨取,不會出現延遲了。

參見src/components/user/UserModalGrant.jsx#L33

Form注意

Ant的form組件很完善,須要注意的就是表單的多條件查詢。若是單單是一個條件查詢的處理比較簡單,將查詢關鍵詞設成string類型存到相應的Models中的state便可,多條件的話,稍微麻煩一點,需存成Hash對象。靈活處理便可。

其餘

官方文檔的描述很清楚,我就不充大頭了。注意寫法規範便可,直接複製粘貼官方例子代碼會很難看。

跨域問題

終於說到點子上了,先後端分離遇到跨域問題很正常,而這種基於RESTful API的先後端分離就更好弄了。我這以Fetch + PHP + Laravel爲例,這種並非最有解決方案!僅供參考!

header中進行以下配置

Access-Control-Allow-Origin配置容許的域

Access-Control-Allow-Methods配置容許的請求方式

Access-Control-Allow-Headers配置容許的請求頭

<?php

use Illuminate\Http\Request;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/

Route::group(['middleware'=> ['auth:api']], function() {
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Methods: GET, HEAD, POST, PUT, PATCH, DELETE");
    header("Access-Control-Allow-Headers: Access-Control-Allow-Headers, Origin, Accept, Authorization, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers");
    require base_path('routes/common.php');
});

基於其餘編程語言的處理相似。

結語

瞭解前端、熟悉前端、精通前端、熟悉前端、不懂前端

瞭解 X X 、熟悉 X X 、精通 X X 、熟悉 X X 、不懂 X X

相關文章
相關標籤/搜索