前后端分离,前端请求后端接口的过程

前端的代码是运行在nodejs环境下,前端代码大概分为service层,model层,和view层(page),service层封装请求接口的函数,model层调用service层定义的函数获取参数并存储起来,然后把存储的数据再page中展示出来。

下面一步步讲解:

由于涉及的前端知识比较多,很多细节并没有讲解得得很充分,只是大概讲一个过程,想学更多得前端知识请看:es6,react,dva,antd等一系列知识。

在service层中需要用到两个封装好的工具函数,在前端项目中可以添加到utils目录中

urlencodeed.js     作用:拼接请求参数

//@ts-check

/**
 * @param {{[x: string]: any}} fields
 */
export function urlencoded(fields) {
  if (!fields)
    return '';

  const result = [];
  Object.keys(fields).forEach(key => {
    const value = fields[key];
    if (typeof value === 'undefined' || (typeof value === 'object' && !value)) // undefined or null
      return;
    if (Array.isArray(value))
      return value.forEach(v => result.push(`${key}=${encodeURIComponent(v)}`));
    return result.push(`${key}=${encodeURIComponent(value)}`);
  });
  return result.join('&');
}

requestWithToken.js  作用:用于发起HTTP请求

//@ts-check
/// 

import _ from 'lodash';
import fetch from 'dva/fetch';
import { notification } from 'antd';
import { saveTokenInfo, getTokenInfo, clearTokenInfo } from './tokenStorage';
import { isDevelopment, unused, devEcho, devWarning } from './debug';
import { urlencoded } from './urlencoded';
import { expandObjectToJSON } from './expandObjectToJSON';

const defaultOptions = { credentials: 'include' };

const X_CSRF = 'X-CSRF-TOKEN';

/**
 * @param  {string} methodUri for example: "GET /api/services/xxx"
 * @param  {{headers?: {[name: string]: string}; body?: any; asJSON?: boolean|"expand";}} [options]
 */
function createHttpFetch(methodUri, options = {}) {
  const [method, uri] = methodUri.split(/\s+/);
  const opts = { ...defaultOptions, ...options, method };
  if (method !== 'GET') {
    if (!(opts.body instanceof FormData)) {
      opts.headers = {
        Accept: 'application/json',
        // 'Content-Type': 'application/json; charset=utf-8',
        'Content-Type': opts.asJSON
          ? 'application/json; charset=utf-8'
          : 'application/x-www-form-urlencoded; charset=utf-8',
        ...opts.headers,
      };
      // opts.body = JSON.stringify(opts.body);
      opts.body = opts.asJSON
        ? JSON.stringify(opts.asJSON === 'expand' ? expandObjectToJSON(opts.body) : opts.body)
        : urlencoded(opts.body);
    } else {
      // newOptions.body is FormData
      opts.headers = {
        Accept: 'application/json',
        // 修复参考: https://github.com/ant-design/ant-design-pro/issues/1217
        // 'Content-Type': 'multipart/form-data',
        ...opts.headers,
      };
    }
  }
  //@ts-ignore
  return fetch(uri, opts);
}

let lastLogoutNotify = 0;
function passiveLogout() {
  clearTokenInfo(); // 清除 localStorage 中的 token 信息

  const now = Date.now();
  if (now > lastLogoutNotify + 1500) {
    lastLogoutNotify = now;
    notification.warning({
      message: '请重新登录',
      description: '由于你的会话已经过期, 请重新登录!',
      placement: 'bottomRight',
    });
  }

  waitingForDispatch().then(dispatch => {
    if(dispatch)
      dispatch({ type: 'login/passiveLogout' });
  });
}

function notifyFatalError(status, reason) {
  notification.warning({
    message: `错误 ${status}`,
    description: `错误原因: ${reason}\n系统内部故障, 请联系开发人员维护.`,
    placement: 'bottomRight',
    duration: 8,
  });
}

export const WITH_CSRF_TOKEN = true;
export const NO_CSRF_TOKEN = false;

export const REQUEST_NORMAL = 0;
export const REQUEST_NO_INTERCEPT = 4;
export const REQUEST_KEEP_RAW = 8;

export async function requestTokenInfo() {
  //eslint-disable-next-line
  const log = isDevelopment() ? (msg => console.log(`${new Date().toJSON()} ${msg}`)) : (() => { });

  const response = await request('GET /api/rest/user/me', {}, NO_CSRF_TOKEN);
  if (!response || response.code !== 200) {
    log(`请求user/me信息出错! response 异常 或 HTTP 响应码不为 200 (实际响应码: ${response && response.code}))`);
    return Promise.resolve(false);
  }
  saveTokenInfo(response.data);
  return Promise.resolve(true);
}

/**
 * @param {string} actionUri for example: "GET /api/services/xxx"
 * @param {{headers?: {[name: string]: string}; body?: any; asJSON?: boolean|"expand"}} [options]
 * @param {boolean} [withCSRF] 默认携带Token Header信息
 * @param {number} [requestType] 允许拦截特定的响应内容, 比如: 401 跳转到登入页面而不返回
 * @returns {Promise<{code: number; data: any}>} 返回响应信息(该Promise一定不会reject), null 则表示该请求有错误, 而且非业务逻辑错误, 可能需要联系 管理员 或 重新登录
 */
export async function request(actionUri, options = {}, withCSRF = true, requestType = REQUEST_NORMAL) {
  const promiseNull = Promise.resolve(null);

  if (withCSRF) {
    const csrf = _.get(getTokenInfo(), 'csrf', '');
    if (!options) options = { headers: { [X_CSRF]: csrf } };
    else if (!options.headers) options.headers = { [X_CSRF]: csrf };
    else options.headers[X_CSRF] = csrf;
  }

  let status = 0;
  return createHttpFetch(actionUri, options)
    .then(response => {
      status = response.status;
      if (!(requestType & REQUEST_NO_INTERCEPT)) {
        if (status === 401) {
          //@ts-ignore
          passiveLogout();
          return promiseNull;
        }
      }
      return new Promise(resolve => {
        ((requestType & REQUEST_KEEP_RAW) ? response.text() : response.json())
          .then(object => resolve(object))
          .catch(ex => {
            //eslint-disable-next-line
            console.error(ex);
            notifyFatalError('JSON异常', `${actionUri} 返回的不是有效JSON数据.`);
            resolve({});
          })
      });
    })
    .then(data => {
      if (!data)
        if (!(requestType & REQUEST_KEEP_RAW))
          return promiseNull;
      return { code: status, data };
    });
}

function waitingForDispatch() {
  return new Promise(resolve => {
    let timeout = 50;
    const asker = setInterval(() => {
      //@ts-ignore
      const app = window._i_am_an_app;
      if (app && app._store && app._store.dispatch) {
        clearInterval(asker);
        return resolve(app._store.dispatch);
      }
      timeout--;
      if (timeout < 0) {
        devWarning('!!! 超时后仍未获得: window._i_am_an_app._store.dispatch 函数');
        clearInterval(asker);
        return resolve(null);
      }
    }, 50);
  });
}

例如需要对下面这个接口发起请求:

前后端分离,前端请求后端接口的过程_第1张图片

 根据接口文档提供的接口,可以在services层这样写,使用urlencoded和request工具函数拼接请求参数:

  /**
 * 
 * @param {any} filter
 * @param {number} page
 * @param {number} size
 */
export async function queryDeviceFaultRepair(filter = {}, page = 0, size = 30) {
  const qs = urlencoded({ ...filter, page, size, trace: true });
  return request(`GET /api/rest/device/fault/repair/?${qs}`);
}

在modal层调用这个函数:代码并不完成,只提供参考

export default {

namespace: 'deviceFaultRepair',

state: {

faultRepairList: {},

},

effects: {

*fetchDeviceFaultRepair({ payload }, { put }) {
//这个是调试函数
devMustIncludeFields(payload, 'filter', 'page', 'size', 'onError');

const response = yield services.queryDeviceFaultRepair(payload.filter, payload.page, payload.size);
/获取到response数据
if (!response) return;

if (response.code !== 200) return payload.onError(response);
//把数据推到reduces的gotfaultRepairList, payload是携带数据的一个对象
yield put({ type: 'gotfaultRepairList', payload: { data: response.data } });

},

}

reducers: {

gotfaultRepairList(state, { payload }) {
//获取到数据,并把数据存储到state的faultRepairList中

return { ...state, faultRepairList: payload.data };

},

}

链接modal层和page层:

在某个页面中写下面代码

deviceFaultRepair是model的命名空间的名字。


@connect(({ deviceFaultRepair, loading }) => ({
  deviceFaultRepair,
  loadingList: loading.effects['deviceFaultRepair/fetchDeviceFaultRepair'],
}))

组件的生命周期函数中调用model层effects 中的函数,来dispatch一个action

//表示组件在挂载得时候发起这个action
  componentDidMount() {
    this.query();
  }

 query(filter = {}, page = 0, size = defaultPageSize) {
     向modal层发起一个Action,easyDispatch是封装好的一个dispatch函数
    easyDispatch(this, 'deviceFaultRepair/fetchDeviceFaultRepair', {
      filter, page, size, onError: this.onError.bind(this),
    });
  }

在浏览器的控制台network中可以看见已经获取到服务器的数据:

前后端分离,前端请求后端接口的过程_第2张图片

以及请求头的内容:  对了这里需要用到proxy代理,挂载自己服务器。

?以的就是urlencoded.js工具函数拼接而成

前后端分离,前端请求后端接口的过程_第3张图片

到此已经是向接口发起请求并获取到数据的全过程,在modal层获取到的数据并保存在deviceFaultRepair中,在modal和怕个connect以后可以通过:

const { deviceFaultRepair } = this.props;
//获取到deviceFaultRepair

然后可以在组件中使用deviceFaultRepair里面的数据了,比如做个列表,刚好deviceFaultRepair里面的json数据格式和Colum对应,就可以这样传入:

代码不全,仅供参考

//CRUDTableCard是自己封装的列表组件

最终在浏览器上的效果是:

 

你可能感兴趣的:(tool-funtion,前端,react)