前端网络请求封装最佳实践

1,197 阅读3分钟

如何写出高质量的前端代码》学习笔记

网络请求存在的问题

    //UserList组件内部
    getUsers(){
        axios.get('/api/v1/users', {
            params: {
                page: 1,
                page_size: 10
            }
        }).then(response =>{
            if(response.status === 200 && response.data.code === 200){
                this.users = response.data.data || []
            }
        }).catch(e =>{
            alert(e.message)
            console.log(e)
        })
    }

在开发中,直接使用像 axios 这样的库进行网络请求可能会导致以下问题:

  1. 硬编码 URL:直接在代码中使用字符串形式的 URL,导致后端接口变更时需要在多个地方修改。
  2. 重复的成功/失败处理逻辑:每个请求都需要重复编写相同的逻辑。
  3. 组件内方法不应直接进行网络请求:这会导致代码难以复用和维护。
  4. 直接依赖第三方库:这会导致项目与库深度耦合,不利于后期更换库。

接口地址的封装

建议将所有接口地址封装为常量,以避免硬编码。可以使用对象、数组或字符串的形式来存储接口地址和请求方法。

对象形式

    export const userApis = {
       getUsers: {
          url: '/api/v1/users',
          method: 'GET'
       },
       addUser: {
          url: '/api/v1/user',
          method: 'POST'
       },
       getUserDetail: {
          url: '/api/v1/user/{id}',
          method: 'GET'
       },
       updateUser: {
          url: '/api/v1/user/{id}',
          method: 'PUT'
       },
       deleteUser: {
          url: '/api/v1/user/{id}',
          method: 'DELETE'
       }
    }

数组形式

    export const userApis1 = {
       getUsers: ['GET', '/api/v1/users'],
       addUser: ['POST', '/api/v1/user'],
       getUserDetail: ['GET','/api/v1/user/{id}'],
       updateUser: ['PUT', '/api/v1/user/{id}'],
       deleteUser: ['DELETE','/api/v1/user/{id}']
    }

字符串形式

    export const userApis = {
       getUsers: 'GET /api/v1/users',
       addUser: 'POST /api/v1/user',
       getUserDetail: 'GET /api/v1/user/{id}',
       updateUser: 'PUT /api/v1/user/{id}',
       deleteUser: 'DELETE /api/v1/user/{id}'
    }

接口服务的封装

将网络请求封装到 service 层,避免在组件中直接调用接口。这样可以减少代码重复,降低对后端接口设计的耦合。

    // /domain/user/service.js
    import userApis from './api.js';
    export function getUsers(page=1, page_size=-1){
       return request(userApis.getUsers, {
          params: {
             page: page,
             page_size: page_size
          }
       })
    }

网络请求方法的封装

封装一个通用的 request 方法,避免直接使用 axios 等库。这样可以在需要时更换底层库而不影响业务代码。

    import axios from "axios";
    import { Notification } from 'element-ui';

    const instance = axios.create();
    const CancelToken = axios.CancelToken;

    //处理url中的参数
    instance.interceptors.request.use(function (config) {
       let url = config.url;
       let params = config.params;
       url = url.replace(/{(\w+)}/g, function (match, $1) {
          if(params[$1]){
             let value = params[$1];
             delete params[$1];
             return encodeURIComponent(value);
          }
       });
       config.url = url;
       return config;
    });

    //提取请求url中的method
    instance.interceptors.request.use(function (config) {
       let urlConfig = config.url.split(/\s+/);
       if(urlConfig.length > 1){
          config.method = urlConfig[0].toLowerCase();
          config.url = urlConfig[1];
       }
       return config;
    });

    //处理获取取消请求的配置
    instance.interceptors.request.use(function (config) {
       if(config.getCancelMethod && typeof config.getCancelMethod === 'function'){
          config.cancelToken = new CancelToken(function executor(c) {
             config.getCancelMethod(c);
          })

          delete config.getCancelMethod;
       }
       return config;
    });

    // 成功、失败处理
    instance.interceptors.response.use(function (response) {
       let {code, data, message} = response.data || {};
       if(code && code === 200){
          return Promise.resolve(data)
       }else{
          Notification({
             message: message || '接口错误',
             type: 'error'
          });
          return Promise.reject(response.data)
       }
    }, function (error) {
       if(error instanceof axios.AxiosError){
          switch (error.response.status){
             case 401:
                location.href = '/login';
                return
             default: {
                Notification({
                   message: error.message || '接口异常',
                   type: 'error'
                });
             }
          }
       }
       return Promise.reject(error);
    });

    export default instance

项目结构和开发流程

建议将项目分为基础层、领域层和应用层:

  • 基础层:如 request.js,封装通用的工具。
  • 领域层:如 api.jsservice.js,封装业务逻辑。
  • 应用层:具体的页面和组件,调用 service 提供的方法。
    ├── base                # 基础层
    │    └── utils          
    │       └── request.js  # 封装的网络请求        
    ├── domain              # 领域层
    │    └── user          
    │       ├── api.js      # 用户接口配置
    │       └── service.js  # 用户相关请求配置
    └── src                 # 应用层
        └── pages           # 页面组件
            └── user-list   # 请求 /domain/user/service 中的方法

开发流程

  1. 阅读 API 文档,创建 api.js 进行接口配置。
  2. 创建 service 服务,封装业务模块的增删改查方法。
  3. 创建业务组件,调用 service 服务。

总结

  • 所有的API不能硬编码,必须封装为常量
  • 所有的增删改查必须封装为service服务,不能在业务模块中单独实现
  • 任何业务代码和service中都不能出现第三方库调用