基于Axios的网络请求服务封装记录

993 阅读6分钟

概述

作为一名前端开发,在项目开发当中,封装一套能够在业务当中使用的较为完善的网络请求服务是一个必不可少的能力。层次分明的网络请求服务在项目开发当中往往能够让开发者头脑思路更加清晰,同时也使得项目的可维护性大大提高。下边,我们就从头开始封装一套属于我们自己的网络请求服务。基础的组成部分由三部分组成:基础请求工具封装、业务网络请求封装、API的管理。希望通过了解我自己在项目当中封装的网络服务能够对各位开发自己项目,符合自己业务需求的网络服务起到抛砖引玉的作用。

基础请求工具的封装

业务开发当中,对于网络请求服务的实现,我选择的是众所周知的第三方网络请求库axios作为业务网络服务的基础套件。同时,根据项目的开发习惯,一般我都不会直接使用axios进行业务网络请求的使用,而是先进行基础请求工具的封装,然后根据需要调用基础请求工具创造出基础的axios请求实例去进行相应的业务网络请求开发。

src/utils/http.js

import axios from 'axios'

/** 创建网络基础请求工具*/
const createService = () => {
  const service = axios.create()
  return service
}

/** 使用网络基础请求工具生成网络服务*/
const service = createService()

如上,我们实现了一个基础的网络请求工具,再依据工具去创建请求服务实例。当然,目前我们还达不到使用的要求,只是一个基础模型。在实际开发当中,我们不太可能如下使用这个网络请求服务实例,在业务请求中一次次重复调用。

src/service/user.js

service({
  method: 'post',
  url: '/login',
  data: { username: 'cjl', password: '123' }
})

业务接口调用量多,如果一次次的去使用以上的代码调用会显得十分乏力。因此我们需要对这些重复的代码进行进一步简化,考虑到个人实际业务开发的内容复杂度,我使用的是函数柯里化的封装方式,使用创建出来的网络请求服务实例service根据不同的Restful请求方式封装出不同的网络请求基础函数。(以下仅拿get和post方法示例)

src/utils/http.js

import axios from 'axios'

/** 创建网络基础请求工具*/
const createService = () => {
  const service =axios.create()
  return service
}

/** 使用网络基础请求工具生成网络服务*/
const service = createService()

/** 创建get请求方法*/
const $get = (url) => (params) => {
  return service({
    method: 'get',
    url,
    params
  })
}

/** 创建post请求方法 */
const $post = (url) => (data) => {
  return service({
    method: 'post',
    url,
    data
  })
}

/** 导出网络请求方法*/
export default {
  $get,
  $post
}

经过简化,我们使用了函数柯里化的方式,对即将使用到业务网络服务开发中的基础请求服务进行了相应的封装,获得了两个能够直接在业务网络开发当中直接使用到的方法$get$post。于是,在业务的网络请求层中我们就可以按照如下进行调用:

src/services/user.js

import { $post } from 'src/utils/http'

/** 用户登录*/
const login = ({ username, password }) => {
  return $post('/login')({ username, password })
}

业务网络请求封装

在基础网络请求封装的最后,我们将封装化的基础请求方法导入到了业务开发当中的网络服务层进行使用, 而对于前端项目的开发,网络服务层也是必不可少的。 考虑到项目开发的前后端协作,我的项目中在网络服务层对所有业务网络请求函数都进行了统一的格式定义: src/services/user.js

import { $get, $post } from 'src/utils/http'

const fn1 = ({ p1, p2 }) => {
  return $get('/url1')({ p1, p2 })
}

const fn2 = ({ p1, p2 }) => {
  return $post('/url2')({ p1, p2 })
}

const fn3 = ({ p1, p2 }) => {
  p1 = transformP1(p1)
  p2 = transformP2(p2)
  return $post('/url3')({ p1, p2 })
}

const login = ({ username, password }) => {
  return $post('/user/login')({ username, password })
}

可以看到无论是get方法还是post方法都不需要去考虑axios这个第三方库传递的参数属性是写params还是写data。在设计基础网络请求工具的时候就提前定义好了,利用函数柯里化,我们把接口的请求分成了两部分,第一部分是接口,第二部分是接口携带的参数,一眼望去除了请求的方法不同以外,其他的完全相同,同时看上去更加的清晰明了。 为了更好的实现业务网络请求的封装,我们将业务网络请求函数fn1fn2的入参(外边实际调用fn1fn2的时候传递的参数)和传参(在fn1fn2里头调用$get$post的时候实际传入的参数)都统一起来了。当然实际上开发当中,很可能会进行一些类型转化和入参的加工,那都不要紧,只需要根据个人业务开发需求去定义转换加工的函数。(在我的项目当中,可能会进行实体model加工,或者formData类型转化)

API的管理

业务开发当中,大量接口的情况下,直接将接口API字符串放入业务请求函数当中并非不可。根据个人开发习惯,更倾向于将业务API接口按照模块划分,统一抽出管理。 这个时候,我的做法是将原本的src/services/user.js上升成user目录,然后再在目录当中依次定义src/services/user/index.jssrc/services/user/api.js。其中index.js存放该模块相关的业务网络请求,api.js存放改模块相关的业务API接口。

src/services/user/api.js

/** 用户登录*/
export const USER_LOGIN = '/user/login'

/** 获取用户信息*/
export const USER_INFO = '/user/info'

/** 用户登出*/
export const USER_LOGOUT = '/user/logout'

src/services/user/index.js

import { $get, $post } from 'src/utils/http'
import { USER_LOGIN, USER_INFO, USER_LOGOUT } from './api'
import md5 from 'js-md5'

/** 用户模块网络请求服务*/
const UserService = {
  /**
   * 用户登录
   * @params {string} username 用户名
   * @params {string} password(md5) 用户名
   */
  login({ username, password }) {
    password = md5(password);
    return $post(USER_LOGIN)({ username, password });
  },

  /**
   * 获取用户信息
   */
  async getSuperInfo() {
    const {
      id,
      uname,
      phone,
      real_name,
      utype,
      status,
      power_type,
    } = await $get(USER_INFO)();
    return { id, uname, phone, real_name, utype, status, power_type };
  },

  /**
   * 用户登出
   */
  logout() {
    return $get(USER_LOGOUT)();
  },
};

export default UserService

视图层调用

经过以上封装完成的网络请求服务之后,我们就可以在视图层当中使用UserService用户网络请求服务,获取用户相关网络接口信息了:

src/views/login/index.js

import UserService from 'src/services/user'
export default {
  data() {
    return {
      loginForm: {
        username: '',
        password: ''
      }
    }
  },
  methods: {
    handleLogin() {
      const { username, password } = this.loginForm
      UserService.login({ username, password })
      .then(() => this.$success('登陆成功'))
      .catch(() => this.$error('登录失败'))
    }
  }
  render() {
    return (<el-form model={this.loginForm}>
        <el-form-item>
          <el-input vModel={this.loginForm.username}/>  /** username: 用户名*/
        </el-form-item>
        <el-form-item>
           <el-input vModel={this.loginForm.password}/> /** password: 密码*/
        </el-form-item>
        <el-form-item>
          <el-button onClick={this.handleLogin}>{"登录"}</el-button>
        </el-form-item>
    </el-form>)
  }
}

网络请求拦截器

实际项目开发当中,在获取到我们希望的数据之前都需要进行请求发出时Content-type的转换,数据序列化,服务端的请求接口检测,请求回来时网络响应的统一处理等中间环节。所以在项目当中我们往往还需要利用到axios的拦截器,为此我们还需要对src/utils/http.js网络请求工具进行加强:

src/utils/http.js

const createService = () => {
  const service = axios.create()

/** 网络请求服务的默认配置*/
+ service.defaults.timeout = 10000 /** 发出的请求默认10秒超时*/
+ service.defaults.baseURL = process.env.VUE_APP_HTTP_BASEURL /** 请求的基础路径 - VUE_APP_HTTP_BASEURL放在.env.xx中 */
+ service.defaults.transformRequest = [function(data, headers) { /** 正式发出请求之前对数据格式进行转换*/
+   if (data === null || data === undefined) return
+   if (data.constructor.name.toLowerCase() === 'formdata') return data /** 对于formData的数据不需要转换,直接发出*/
+   return qs.stringify(data) /** 普通对象就序列化后再发出*/
+ }]

/** 网络请求服务的请求拦截*/
+ service.interceptors.request.use(
+   config => {
+     config.headers.Authorization = getToken() /** 往往项目都需要接口校验,所以统一带上token*/
+     config.headers.source = window.env.SOURCE /** 这个是我的项目是有两个子系统,需要辨别不同的系统业务要求携带的*/
+     return config
+   },
+   err => Promise.reject(err)
+ )

/** 网络请求服务的响应拦截*/
+ service.interceptors.response.use(
+   ({ data: { data, code, msg }}) => {
+     if (code === 0) return data
+     if (code === 40001) {
+       window.dispatchEvent(LogoutEventObj) /** 业务监听40001是账号失效等账号异常情况, window.dispatchEvent(LogoutEventObj)是自己实现的业务事件派发*/
+       return Promise.reject(data)
+     }
+     return Promise.reject({ code, msg, data })
+   },
+   err => {
+     if (!navigator.onLine) err = { msg: '网络异常,请检查后重试' } /** 业务场景需要进行断网判读*/
+     return Promise.reject(err)
+   }
+ )
  return service
}

/** 创建get请求方法*/
const $get = (url) => (params) => {
  return service({
    method: 'get',
    url,
    params
  })
}

/** 创建post请求方法 */
const $post = (url) => (data) => {
  return service({
    method: 'post',
    url,
    data
  })
}

/** 导出网络请求方法*/
export default {
  $get,
  $post
}

文件上传服务

实际上,在业务开发当中,特别是后台管理系统的开发,会涉及到文件上传等服务开发,需要专门对数据进行formData的转化。在我自己的开发当中,是将上传服务用到的网络请求基础工具单独创建一个请求服务实例。

src/utils/http.js

... 
const UploadService = createService()

/** 将对象转换成formData*/
const transform2FormData = (params) => {
  const formData = new FormData()
  Object.entries(params).forEach(([key, val]) => {
    formData.append(key, val)
  })
  return formData
}

/** 专门用于网络请求的方法*/
const $upload = (url) => (data, config) => {
  data = transform2FormData(data)
  return UploadService(Object.assign({
    method: 'post',
    url,
    data
  }, config))
}

export default {
  $get,
  $post,
  $upload
}

src/services/app/index.js

import { CHUNK_UPLOAD } from './api' 
/**
   * 上传App分片
   * @param {number} identifier 应用唯一标识
   * @param {number} file 应用的分片
   * @param {number} file_name 应用名
   * @param {number} chunk_number 分片的序号
   * @param {number} total_chunks 分片总数
   * @param {number} total_size 应用大小
   *
   */
  uploadAppChunk({
    identifier,
    file,
    file_name,
    chunk_number,
    total_chunks,
    total_size
  }, config = {}) {
    return $upload(CHUNK_UPLOAD)({
      identifier,
      file,
      file_name,
      chunk_number,
      total_chunks,
      total_size
    }, config)
  },

src/views/app/util.js

/** 生成分片请求列表*/
export const createRequestList = function({ fileChunkList, progress }) {
  let totalSize = 0
  return fileChunkList.map(
    (chunk) => {
      const { fileChunk, fileName, fileSize, fileIndex, identifier } = chunk
      let i = 0
      return AppService.uploadAppChunk({
        file: fileChunk,
        identifier,
        total_chunks: fileChunkList.length,
        file_name: fileName,
        total_size: fileSize,
        chunk_number: fileIndex
      }, {
        timeout: 1000 * 60 * 5, // 5分钟上传超时
        onUploadProgress(event) {
          const { loaded } = event
          totalSize += (loaded - i)
          i = loaded
          progress.totalSize = totalSize
        }
      })
    }
  )
}

总结

至此,一套能够应用到业务当中的网络请求服务就这样封装好了。当然以上请求服务的封装是基于个人业务开发场景去考虑设计的,在我的请求服务当中,可以看到实际上对于响应我最后还是reject出去了,并没有进行统一的比如说弹窗的处理,是因为我的业务场景中对于响应的处理每个模块都不太相同,没法做到统一处理,因此我并没有对响应进行统一UI处理。 以上便是我在业务开发当中摸索的较为符合自己业务场景的网络服务,希望因此对大家封装自己业务的网络请求服务有所启发。