Инкапсуляция сетевых запросов (Axios) и Helper в средних и крупных проектах

Vue.js

предисловие

Я общаюсь с Vue уже несколько лет, и каждый раз, когда я создаю новый проект, у меня появляются новые идеи.Я поделился статьей об использовании перехватчика Axios во Vue (перехват запросов и соответствий), потому что мой долгосрочное обслуживание, поэтому я больше не могу подчиняться. Интерфейсная архитектура создана для небольших проектов, и требования должны быть простыми в обслуживании и хорошо масштабируемыми, но это не значит, что мой первоначальный метод не годится. , он просто не подходит для средних и крупных проектов. Итак, сегодня я делюсь своим подходом к инкапсуляции Axios и Helper в средних и крупных проектах. Потребности у всех разные, и мы вместе делимся своим опытом. Так что не говори глупостей, иди сразу к галантерее.

каталог проекта

Во-первых, это архитектура моего проекта.Поскольку мой проект относительно подробный, я показываю только некоторые модули, относящиеся к этой статье.

|-----src 
|   |-----common 公共文件
|   |    |-----Api 后端接口
|   |    |    |-----modules 接口模块
|   |    |    |    |-----User.js 用户接口模块
|   |    |    |-----index.js 公共接口模块
|   |    |-----Helper 公共Helper
|   |    |    |-----index.js 公共Helper
|   |    |    |-----.....js 其他公共方法抽离
|   |-----sevice 网络请求封装(axios封装)
|   |    |-----api.js 请求方式封装
|   |    |-----request.js axios封装
|   |-----utils 工具类
|   |    |-----vue-install.js vue全局绑定工具类
|   |-----main.js webpack入口文件

Бэкэнд-интерфейс API

Зачем отделять интерфейс от модуля? Это связано с тем, что в средних и крупных проектах серверная часть может использовать архитектуру платформы Saas для разделения различных модулей.Чтобы облегчить обслуживание и быстро найти и найти интерфейс, нам нужно отделить один файл и извлечь один и тот же модуль для легкое обслуживание. Конечно, если бэкенд не использует архитектуру saas-платформы или имеет меньше интерфейсов, можно прописать его напрямую в index.js без использования этого метода, это все изменения под личные нужды без запутанности.

// index.js
import User from './modules/User'

export default {

  User, // 将独立的模块抽离到单独的js文件中可以方便统一维护

  // 将公共的接口抽离出来方便维护 method:请求方式 url:接口地址
  sendSms: { method: 'post', url: '' },

  uploadFile: { method: 'post', url: '' }
}

// User.js

export default {
    getUser: {method: 'get', url: ''}
}

Публичный помощник

Точно так же помощники могут извлекать различные методы для облегчения обслуживания.

// index.js
import deepClone from './deepClone'
import FormatDate from './FormatDate'

const Helper = {

    /**
     * 深度拷贝
     * @param {object} 对象
     * @return {object}
     */
    deepClone,

    /**
     * 日期格式化
     * @param {date} timestamp 日期/时间戳
     * @param {string} format 格式 默认值 Y-m-d
     * @return {string}
     */
    FormatDate,

    /**
     * 获取指定日期之间的所有日期
     * 日期格式 yyyy-MM-dd
     * @param {string} start 开始日期
     * @param {string} start 结束日期
     * @return {array}
     */
    getAllDate,

    /**
     * 随机字符串
     * @param {boolean} lower 小写字母
     * @param {boolean} upper 大写字母
     * @param {boolean} number 数字
     * @param {boolean} symbol 特殊字符
     * @param {boolean} length 长度
    */

    RandomString,

    /**
     * 计算请求分页参数
     * @param {object} route 当前页面路由
     * @return { limit: 10, page: 1, offset: 0 } limit = 长度, page = 页码, offset = 偏移量
     */
    getPerPage (query) {
        let limit = parseInt(query.limit) || 10,
            page = parseInt(query.page) || 1,
            offset = (page - 1) * limit;
        return {
            limit, page, offset
        }
    },

    /**
     * 清理对象
     * 去除属性值为 空(字符串), null, undefined
     * 转换值为数字,true,false的字符串为对应的数据类型
     * @param {object} obj 对象
     * @return {object}
     */
    clearObject (obj) {
        let o = {};
        for (const k in obj) {
            let v = obj[k];
            if (v === null || v === undefined) continue;
            // 非字符串
            if (toString.call(v) !== '[object String]') {
                o[k] = v;
                continue;
            }
            v = obj[k].trim();
            // 过滤空值
            if (v.length === 0) continue;

            // 正数,负数,浮点数
            if (/^(-?\d+)(\.\d+)?$/.test(v)) {
                o[k] = Number(v);
            }
            // 布尔值
            else if (v === 'true' || v === 'false') {
                o[k] = (v === 'true');
            }
            // false
            else {
                o[k] = v;
            }
        }
        return o;
    }

}


export default Helper;

Поскольку некоторые коды относительно длинные, я привожу здесь только некоторые модули.

// 深度复制
const deepClone = (obj) => {
    let o;
    if (typeof obj === 'object') {
        if (obj === null) {
            o = null
        } else {
            // 数组
            if (obj instanceof Array) {
                o = [];
                for (const item of obj) {
                    o.push(deepClone(item))
                }
            }
            // 对象
            else {
                o = {};
                for (const j in obj) {
                    o[j] = deepClone(obj[j])
                }
            }
        }
    }
    else {
        o = obj;
    }
    return o;
}

export default deepClone;

На этом мой общий публичный файл завершен.На самом деле, центральная идея состоит в том, чтобы извлечь предыдущий единый файл из разных модулей, что удобно для последующего расширения. Далее идет инкапсуляция сетевого запроса. Здесь может быть запутанно, но это можно понять после внимательного изучения.

инкапсуляция сервисного сетевого запроса (инкапсуляция axios)

// api.js
import Api from '@/common/Api'
import Helper from '@/common/Helper'
import axios from './request'

const axiosApi = {
  // get请求
  createGet (url) {
    return (args = {}) => {
      return axios({
        method: 'GET',
        url: url,
        ...args
      })
    }
  },

  // post请求
  createPost (url) {
    return (args = {}) => {
      let parmas = ''
      /*
        不挂载到url地址上 为什么我这里要这么写呢,
        因为后端post接收参数有一部分是从地址栏获取,
        虽然我很诧异但是我直接给他两份他随便获取,
        当然可以根据notMountUrl参数选择不绑定到地址栏
        */ 
      if (!args.notMountUrl) {
        parmas = `?${Helper.formatParams(args.data)}`;
      }
      return axios({
        method: 'POST',
        url: `${url}${parmas}`,
        ...args
      })
    }
  },
// .... 当然你也可以写更多请求方式或者根据不同的需求调用不同的Axios封装

  // 创建API请求方法
  crateApi (apiConfig) {
    let methods = {}
    // 获取所有api的Key值进行循环
    Object.keys(apiConfig).forEach(key => {
      let item = apiConfig[key]

      // 子集请求 判断是否是单独模块 如果是就递归子集
      if (!item.method && !item.url) {
        return methods[key] = this.crateApi(item)
      }
      // 接口动态创建
      const method = item.method.toLocaleUpperCase()
      if (method === 'GET') {
        methods[key] = this.createGet(item.url)
      }
      else if (method === 'POST') {
        methods[key] = this.createPost(item.url)
      }
    })
    return methods
  }
}
// 这里一定是抛出创建方法,如果你晕了可以从这一步往回走,慢慢你就懂了
export default axiosApi.crateApi(Api)
// request.js
import axios from 'axios'
// 这里我使用的是vant ui库可根据需求更换不同的ui库
import { Toast } from 'vant';
import store from '@/store'
import router from '@/router'
// statusCode 错误状态码 这里错误码其实就是将网络请求中的所有status进行了键值对的封装方便调用
import statusCode from '@/common/BaseData/status_code.js'


// 是否为生产环境
const isProduction = process.env.NODE_ENV == "production";

// 创建一个axios实例
const service = axios.create({
  baseURL: !isProduction ? '/api' : process.env.VUE_APP_BASE_URL, // 前缀由于我使用的是webpack的全局变量,这里其实也可以写死
  withCredentials: true, // 当跨域请求时发送cookie
  timeout: 20000 // 请求超时时间
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 获取token
    const hasToken = store.getters.Token

    // 设置 token设置token
    if (hasToken) {
      config.headers['token'] = hasToken
    }
    // .... 其他操作根据具体需求增加
    return config
  },
  error => {
    // 处理请求错误
    // console.log(error)
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(

  /**
   * 通过自定义代码确定请求状态
   */
  response => {
    const res = response.data
    // 这里的error是后台返回给我的固定数据格式 可根据后端返回数据自行修改
    const { error } = res;
    // error不为null
    if (error) {

      // 弹出报错信息
      Toast.fail({
        icon: 'failure',
        message: error.msg
      })

      // 后端返回code的报错处理
      switch (error.code) {
        case '1000':
        case '1001':
        case '1002':
        case '1003':
          router.replace({ path: '/error', query: { code: error.code, msg: error.msg } })
          break;
        case '6000':
        case '6100':
          // 清空Token 重新登录
          store.dispatch('user/resetToken')
          return Promise.reject(new Error(error.msg));
        case '6200':
        case '7000':
        case '19000':
        default:
          // 如果状态码不是 则判断为报错信息
          return Promise.reject(new Error(error.msg))
      }
    } else {
      // 正常返回
      return res
    }

  },
  error => {
    // 这里就是status网络请求的报错处理 主要处理300+ 400+ 500+的状态
    console.error('err:' + error)
    // 弹出请求报错信息
    Toast(statusCode[error.statusCode])
    return Promise.reject(error)
  }
)
// 向外抛出
export default service

На этом инкапсуляция сетевого запроса сервиса закончена, здесь главное то, что инкапсуляция api.js относительно сложна, по сути, это метод динамического создания back-end интерфейса.

класс инструментов utils

Последний класс инструментов на самом деле предназначен для привязки всех наших методов к прототипу vue, чтобы сформировать полный замкнутый цикл.Если вы не понимаете механизм привязки плагинов vue, вы можете перейти к официальному vue.js сайте, чтобы найти его самостоятельно, это не так уж сложно. .

// vue-install.js 全局绑定
import Api from '@/service/api'
import Helper from '@/common/Helper'

export default {
  install: (Vue) => {
    // 全局注入 $api 请求 将我们api.js抛出的方法绑定到vue实例上
    Vue.prototype.$api = Api
    // 全局注入 $helper 辅助方法 同理将helper公共方法绑定到vue上
    Vue.prototype.$helper = Helper
  }
}

Связывание класса инструмента завершено, и мы подошли к последнему шагу для завершения нашего замкнутого цикла, то есть ввести его в main.js.

Файл входа веб-пакета main.js

import Vue from 'vue'

import router from './router'
import store from './store'
// .... 其他依赖引入
import vueInstall from './utils/vue-install' // 全局注册


// 全局绑定 直接使用use方法绑定
Vue.use(vueInstall);

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

инструкции

// 某页面
async created () {

    // 请求User模块
    const res = await this.$api.User.getUser({ 
    data: { 
        // 参数.....
    },
    // 选择是否挂载到url上 
    notMountUrl: false
        
    })
    // 深拷贝
    const newResult = this.$helper.deepClone(this.result)
  }

Готово, это текущая инкапсуляция сетевых запросов и вспомогательных общедоступных методов в моем проекте. Я надеюсь быть полезным для вас, и в то же время смиренно принять ваши различные предложения и мнения. Я надеюсь, что вы прокомментируете в разделе комментариев. Я буду продолжать делиться тем, что я узнал. Не забывайте о первоначальном намерении и придерживайтесь открытого исходного кода и открытого обучения.