资讯专栏INFORMATION COLUMN

Dva + Ant Design 前后端分离之 React 应用实践

tainzhi / 1160人阅读

摘要:数据缓存对于一个应用来说,缓存是很重要的一步。所以,比较常见的方法就是将数据缓存在中。什么时候做数据缓存例用户信息缓存参见在中配置了检测中的是否存在。

源站链接 https://tkvern.com

继 Rails 从入门到完全放弃 拥抱 Elixir + Phoenix + React + Redux 这篇文章被喷之后,笔者很长一段时候没有上社区逛了。现在 tkvern 又回归了,给大家带来React实践的一些经验,一些踩坑的经验。

Rails嘛,很好用,Laravel也好用。Phoenix也好用。都好,哪个方便用哪个。

还有关于Turbolinks之争,不能单从页面渲染时间去对比,要综合考虑。

Why Dva?

Dva是基于Redux做了一层封装,对于React的state管理,有很多方案,我选择了轻量、简单的Dva。至于Mobx,还没应用到项目中来。先等友军踩踩坑,再往里面跳。

Why dva and what"s dva

支付宝前端应用架构的发展和选择

顺便贴下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-ast:dva-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 (
    
      
      
      
      
      
      
    
  );
}

对于路由的验证配置在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配置允许的请求头

 ["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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81387.html

相关文章

  • 构建前项目

    摘要:解决思路服务器端渲染服务器端和前端公用同一个应用,然后通过构建工具及配置,确定哪些组件需要再服务器端渲染,那些组件需要再客户端渲染。服务器端渲染,由框架与构建工具配合,并依据一定的项目结构和编码方式,共同运行。 分离 为什么需要 前后端分离、web服务器与static服务器分离: 前端与后端耦合 (需求) 自动化、工程化的构建前端的代码 (基础条件) 模块化、组件化,项目共享代码 (...

    mindwind 评论0 收藏0
  • React的移动和PC生态圈的使用汇总

    摘要:调用通过注册表调用到实例,透过的,调用到中的,最后通过,调用,根据参数相应模块执行。京东的,多端解决方案是一套遵循语法规范的多端开发解决方案。 showImg(https://segmentfault.com/img/bVbuMkw?w=1304&h=808); 对于一项技术,我们不能停留在五分钟状态,特别喜欢一句话,用什么方式绘制UI界面一点不重要,重要的是底层的思维,解决问题和优化...

    J4ck_Chan 评论0 收藏0
  • React的移动和PC生态圈的使用汇总

    摘要:调用通过注册表调用到实例,透过的,调用到中的,最后通过,调用,根据参数相应模块执行。京东的,多端解决方案是一套遵循语法规范的多端开发解决方案。 showImg(https://segmentfault.com/img/bVbuMkw?w=1304&h=808); 对于一项技术,我们不能停留在五分钟状态,特别喜欢一句话,用什么方式绘制UI界面一点不重要,重要的是底层的思维,解决问题和优化...

    kun_jian 评论0 收藏0
  • React的移动和PC生态圈的使用汇总

    摘要:调用通过注册表调用到实例,透过的,调用到中的,最后通过,调用,根据参数相应模块执行。京东的,多端解决方案是一套遵循语法规范的多端开发解决方案。 showImg(https://segmentfault.com/img/bVbuMkw?w=1304&h=808); 对于一项技术,我们不能停留在五分钟状态,特别喜欢一句话,用什么方式绘制UI界面一点不重要,重要的是底层的思维,解决问题和优化...

    Travis 评论0 收藏0

发表评论

0条评论

tainzhi

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<