Dva + Ant Design 前后端星散之 React 运用实践

源站链接 https://tkvern.com

Rails 从入门到完整摒弃 拥抱 Elixir + Phoenix + React + Redux 这篇文章被喷以后,笔者很长一段时刻没有上社区逛了。如今 tkvern 又回归了,给人人带来React实践的一些履历,一些踩坑的履历。

Rails嘛,很好用,Laravel也好用。Phoenix也好用。都好,哪一个轻易用哪一个。

另有关于Turbolinks之争,不能单从页面衬着时候去对照,要综合斟酌。

Why Dva?

Dva是基于Redux做了一层封装,关于React的state治理,有很多计划,我挑选了轻量、简朴的Dva。至于Mobx,还没应用到项目中来。先等友军踩踩坑,再往内里跳。

趁便贴下Dva的特征:

  • 易学易用:唯一 5 个 api,对 redux 用户特别友爱

  • elm 观点:经由过程 reducers, effectssubscriptions 构造 model

  • 支撑 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

    原文作者:tkvern
    原文地址: https://segmentfault.com/a/1190000008250977
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞