Bootstrap

axios的使用

在 Vue 项目中,封装 Axios 并实现加密、重复请求优化、请求取消、页面切换时取消未完成的请求、以及区分上传和下载操作是非常常见的需求。下面将逐一讲解这些需求的实现方式。

1. Axios 的基本封装

首先,我们可以将 Axios 封装到一个服务层中,方便统一管理请求和拦截。

// src/utils/request.js
import axios from 'axios';
import { encryptRequestData, decryptResponseData } from './crypto'; // 假设有加密解密工具
import { ElMessage } from 'element-plus'; // UI 组件库的消息提示

// 创建 axios 实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // api base_url
  timeout: 5000, // 请求超时时间
});

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    // 请求加密
    if (config.data) {
      config.data = encryptRequestData(config.data); // 数据加密
    }

    // token 放入 header 以防用户身份验证
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response) => {
    // 响应解密
    if (response.data) {
      response.data = decryptResponseData(response.data); // 数据解密
    }

    return response;
  },
  (error) => {
    ElMessage.error('请求失败');
    return Promise.reject(error);
  }
);

export default service;

2. 前后端加密的使用

前后端的加密一般是基于某种加密算法(例如 AES 或 RSA)。这里假设你有现成的加密和解密工具,可以在请求拦截器中对请求进行加密,在响应拦截器中对响应进行解密。

  • 加密:请求数据通过加密函数处理,传输给后端的就是密文。
  • 解密:响应数据通过解密函数还原为可读内容。
// utils/crypto.js
import CryptoJS from 'crypto-js';

// 加密函数
export function encryptRequestData(data) {
  const key = CryptoJS.enc.Utf8.parse('16characterskey'); // 密钥
  const iv = CryptoJS.enc.Utf8.parse('16charactersiv ');  // 偏移量

  const encrypted = CryptoJS.AES.encrypt(JSON.stringify(data), key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });

  return encrypted.toString();
}

// 解密函数
export function decryptResponseData(ciphertext) {
  const key = CryptoJS.enc.Utf8.parse('16characterskey'); // 密钥
  const iv = CryptoJS.enc.Utf8.parse('16charactersiv ');  // 偏移量

  const bytes = CryptoJS.AES.decrypt(ciphertext, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });

  const decryptedData = bytes.toString(CryptoJS.enc.Utf8);
  return JSON.parse(decryptedData);
}

3. 重复请求的优化

为避免同一请求短时间内被多次发送,可以通过 axios 的请求拦截器实现防重复请求的功能。可以使用一个 Map 来存储每个请求的唯一标识,当同一请求发送时进行检查,防止重复发送。

const pendingRequests = new Map();

const getRequestKey = (config) => {
  const { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
};

// 添加请求拦截器
service.interceptors.request.use(
  (config) => {
    const requestKey = getRequestKey(config);

    if (pendingRequests.has(requestKey)) {
      config.cancelToken = new axios.CancelToken((cancel) => {
        cancel(`Duplicate request: ${requestKey}`);
      });
    } else {
      pendingRequests.set(requestKey, config);
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 添加响应拦截器
service.interceptors.response.use(
  (response) => {
    const requestKey = getRequestKey(response.config);
    pendingRequests.delete(requestKey); // 移除完成的请求
    return response;
  },
  (error) => {
    if (axios.isCancel(error)) {
      console.warn(error.message); // 重复请求被取消
    } else {
      // 其他错误处理
    }
    return Promise.reject(error);
  }
);

4. 单个请求的取消控制

对于单个请求,你可以使用 axios.CancelToken 来控制某个请求的取消。

const source = axios.CancelToken.source();

// 发起请求时传入取消 token
service({
  url: '/some-endpoint',
  method: 'get',
  cancelToken: source.token
})
  .then((response) => {
    console.log('请求成功', response);
  })
  .catch((error) => {
    if (axios.isCancel(error)) {
      console.log('请求被取消', error.message);
    }
  });

// 在需要的时候取消请求
source.cancel('取消请求');

5. 页面切换时取消未完成的请求

可以在 Vue 路由的 beforeRouteLeave 钩子中控制页面切换时取消未完成的请求。

export default {
  data() {
    return {
      requestSource: null, // 存储取消 token
    };
  },
  methods: {
    fetchData() {
      this.requestSource = axios.CancelToken.source();
      service.get('/api/data', { cancelToken: this.requestSource.token })
        .then(response => {
          console.log('请求成功', response);
        })
        .catch(error => {
          if (axios.isCancel(error)) {
            console.log('请求被取消');
          } else {
            console.error('请求错误', error);
          }
        });
    }
  },
  beforeRouteLeave(to, from, next) {
    if (this.requestSource) {
      this.requestSource.cancel('页面切换取消请求');
    }
    next();
  }
};

6. 区分上传和下载的使用

  • 上传文件:通常通过 FormData 来上传文件,并在 onUploadProgress 中监控上传进度。
let formData = new FormData();
formData.append('file', file);

service.post('/upload', formData, {
  headers: {
    'Content-Type': 'multipart/form-data'
  },
  onUploadProgress: (progressEvent) => {
    let progress = (progressEvent.loaded / progressEvent.total) * 100;
    console.log(`上传进度: ${progress}%`);
  }
});
  • 下载文件:下载文件时可以指定 responseTypeblob,并处理文件保存。
service.get('/download', {
  responseType: 'blob',
  onDownloadProgress: (progressEvent) => {
    let progress = (progressEvent.loaded / progressEvent.total) * 100;
    console.log(`下载进度: ${progress}%`);
  }
}).then(response => {
  const url = window.URL.createObjectURL(new Blob([response.data]));
  const link = document.createElement('a');
  link.href = url;
  link.setAttribute('download', 'file.pdf'); // 下载文件名
  document.body.appendChild(link);
  link.click();
});

总结

  • Axios 的封装统一管理请求和拦截。
  • 使用加密、解密函数对请求和响应数据进行保护。
  • 防止重复请求,通过 requestKey 唯一标识每个请求。
  • 提供单个请求取消和页面切换时取消未完成请求的机制。
  • 针对上传、下载分别进行处理,监听进度。

根据这些实现,你可以根据需求进一步调整封装的细节和功能。

可以结合 vue-router 的导航守卫使用,尤其是在页面切换时取消未完成的请求。通过在 vue-router 的钩子函数(例如 beforeRouteLeavebeforeEach)中调用 axios 的取消方法,可以确保在用户切换页面时及时取消正在进行的请求,避免无效请求消耗资源。

实现步骤

  1. 创建一个全局的取消请求管理工具:使用一个 Map 来存储每个页面的取消请求函数(CancelToken),当页面切换时从 Map 中找到对应的取消方法并执行。

  2. 在路由守卫中调用取消方法:在 beforeEachbeforeRouteLeave 钩子中调用取消请求的逻辑,确保切换路由时清理未完成的请求。

具体实现方案

1. 取消请求的封装

我们可以在 axios 的封装文件中为每个请求生成一个取消令牌(CancelToken),并将其存储在全局的 cancelMap 中。

// src/utils/request.js
import axios from 'axios';

const cancelMap = new Map(); // 全局的取消请求管理工具

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // API 基础路径
  timeout: 10000, // 超时时间
});

// 请求拦截器
service.interceptors.request.use(
  (config) => {
    const source = axios.CancelToken.source(); // 创建一个取消令牌
    config.cancelToken = source.token; // 将取消令牌附加到请求中

    // 使用唯一的标识(例如:URL+请求方式)来存储取消令牌
    const requestKey = `${config.url}&${config.method}`;
    cancelMap.set(requestKey, source.cancel); // 存储取消请求函数

    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  (response) => {
    // 请求成功后,删除对应的取消函数
    const requestKey = `${response.config.url}&${response.config.method}`;
    cancelMap.delete(requestKey);
    return response;
  },
  (error) => {
    // 如果请求被取消,不做其他处理
    if (axios.isCancel(error)) {
      console.log('请求被取消');
    }
    return Promise.reject(error);
  }
);

// 取消未完成的请求
export function cancelPendingRequests() {
  cancelMap.forEach((cancel) => {
    cancel('路由切换取消请求'); // 执行取消函数
  });
  cancelMap.clear(); // 清空取消函数
}

export default service;
2. vue-router 中使用取消逻辑

接下来我们在 vue-router 中添加导航守卫,确保在每次路由切换时都调用 cancelPendingRequests 来取消未完成的请求。

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { cancelPendingRequests } from '@/utils/request'; // 引入取消请求方法
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    component: About,
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

// 全局路由守卫 - 在每次路由切换前,取消未完成的请求
router.beforeEach((to, from, next) => {
  cancelPendingRequests(); // 取消未完成的请求
  next(); // 继续路由切换
});

export default router;
3. 在组件中发起请求

在组件中正常发起 axios 请求,不需要额外的处理。每次请求都会通过 axios 封装自动附加 CancelToken,并在路由切换时通过 cancelPendingRequests 自动取消。

// src/views/Home.vue
<template>
  <div>
    <h1>Home</h1>
    <p>{{ data }}</p>
  </div>
</template>

<script>
import service from '@/utils/request';

export default {
  data() {
    return {
      data: null,
    };
  },
  mounted() {
    this.fetchData();
  },
  methods: {
    async fetchData() {
      try {
        const response = await service.get('/api/home');
        this.data = response.data;
      } catch (error) {
        console.error(error);
      }
    },
  },
};
</script>

4. 页面级取消请求

有时我们只需要在页面组件内部取消请求,比如当用户离开页面时取消当前页面的请求,这时可以使用组件的 beforeRouteLeave 钩子。

// src/views/About.vue
<template>
  <div>
    <h1>About</h1>
    <p>{{ data }}</p>
  </div>
</template>

<script>
import service from '@/utils/request';
import { ref } from 'vue';

export default {
  setup() {
    const data = ref(null);
    let requestSource = null;

    const fetchData = async () => {
      requestSource = service.CancelToken.source();
      try {
        const response = await service.get('/api/about', {
          cancelToken: requestSource.token,
        });
        data.value = response.data;
      } catch (error) {
        if (axios.isCancel(error)) {
          console.log('请求被取消');
        } else {
          console.error(error);
        }
      }
    };

    return { data, fetchData };
  },
  mounted() {
    this.fetchData();
  },
  beforeRouteLeave(to, from, next) {
    if (this.requestSource) {
      this.requestSource.cancel('页面离开取消请求');
    }
    next();
  },
};
</script>

总结

  • 全局取消:通过在 vue-routerbeforeEach 守卫中调用 cancelPendingRequests,每次路由切换时都会取消未完成的请求,避免页面切换后无用的网络请求消耗。
  • 局部取消:在页面组件内部使用 beforeRouteLeave 钩子取消单个页面的请求,确保当用户离开页面时及时中止未完成的请求。
  • 防止重复请求:通过对请求进行唯一标识,并在发起请求前检查是否有相同请求正在进行,可以防止重复发送相同的请求。

通过结合 vue-routeraxiosCancelToken,可以有效地管理请求的生命周期,确保用户体验的流畅性。

在实际开发中,前端项目可能会需要请求不同的服务地址(例如,不同的 API 网关、微服务等),而 AxiosbaseURL 是请求的默认根路径。如果项目中需要根据具体请求来动态配置不同的 baseURL,有几种方式可以处理:

常见解决方案

1. 在请求时动态指定 baseURL

可以根据不同的业务逻辑或接口,动态为每个请求指定 baseURLAxios 支持在发起请求时覆盖默认的 baseURL,通过为每个请求单独设置 baseURL 来实现多服务地址请求。

import axios from 'axios';

// 默认 axios 实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // 默认的 baseURL
  timeout: 10000, // 超时时间
});

// 示例:根据业务逻辑或特定条件指定不同的 baseURL
export function fetchDataFromServiceA() {
  return service({
    url: '/serviceA/data',
    baseURL: 'https://api.serviceA.com', // 为这个请求动态设置 baseURL
    method: 'get',
  });
}

export function fetchDataFromServiceB() {
  return service({
    url: '/serviceB/data',
    baseURL: 'https://api.serviceB.com', // 另一个服务的 baseURL
    method: 'get',
  });
}
2. 创建多个 Axios 实例

如果不同服务的 baseURL 是固定的,并且它们各自有独立的配置,那么可以为每个服务创建一个独立的 Axios 实例。这样可以对不同服务有不同的 baseURLheaders 或其他配置。

import axios from 'axios';

// 服务A的 axios 实例
const serviceA = axios.create({
  baseURL: 'https://api.serviceA.com',
  timeout: 10000,
});

// 服务B的 axios 实例
const serviceB = axios.create({
  baseURL: 'https://api.serviceB.com',
  timeout: 10000,
});

// 使用 serviceA 发送请求
export function fetchServiceAData() {
  return serviceA.get('/data');
}

// 使用 serviceB 发送请求
export function fetchServiceBData() {
  return serviceB.get('/data');
}

这种方式使得各个服务的配置更加独立清晰,有助于代码的可维护性。

3. 根据环境动态配置 baseURL

对于一些需要根据不同的环境(如开发、生产等)动态切换服务地址的需求,可以在配置文件中根据 NODE_ENV 来设定不同的 baseURL

// .env.development
VUE_APP_BASE_API=https://dev.api.com

// .env.production
VUE_APP_BASE_API=https://prod.api.com

axios 的封装文件中读取环境变量:

import axios from 'axios';

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // 根据环境变量设置 baseURL
  timeout: 10000,
});

export default service;
4. 通过请求拦截器动态修改 baseURL

如果需要根据请求的特定条件(例如请求的路径、参数等)来动态设置不同的 baseURL,可以在 axios 的请求拦截器中根据条件修改 config.baseURL

import axios from 'axios';

// 默认 axios 实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // 默认的 baseURL
  timeout: 10000,
});

// 请求拦截器 - 根据特定条件动态设置 baseURL
service.interceptors.request.use(
  (config) => {
    if (config.url.includes('/serviceA')) {
      config.baseURL = 'https://api.serviceA.com'; // 动态切换到 serviceA 的 baseURL
    } else if (config.url.includes('/serviceB')) {
      config.baseURL = 'https://api.serviceB.com'; // 动态切换到 serviceB 的 baseURL
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default service;
5. 在组件中指定 baseURL

在组件中直接为特定的请求指定 baseURL,这也是一种灵活的方式,适用于只在特定组件内进行请求的情况。

<template>
  <div>
    <button @click="fetchData">获取数据</button>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  methods: {
    fetchData() {
      axios({
        url: '/data',
        baseURL: 'https://api.serviceA.com', // 在组件中指定 baseURL
        method: 'get',
      })
        .then((response) => {
          console.log('数据:', response.data);
        })
        .catch((error) => {
          console.error('请求错误:', error);
        });
    },
  },
};
</script>

6. 根据 API 名称或者模块切换 baseURL

可以将 API 分模块管理,通过模块名自动选择对应的 baseURL。例如,可以根据模块名动态选择不同的 baseURL

const serviceMap = {
  serviceA: 'https://api.serviceA.com',
  serviceB: 'https://api.serviceB.com',
};

export function request({ module, url, ...options }) {
  const service = axios.create({
    baseURL: serviceMap[module], // 动态选择 baseURL
    timeout: 10000,
  });

  return service({ url, ...options });
}

// 使用
request({
  module: 'serviceA',
  url: '/data',
  method: 'get',
}).then(response => {
  console.log(response.data);
});

总结

  1. 动态设置 baseURL:直接在请求中指定 baseURL,适用于小范围的不同服务地址请求。
  2. 多个 Axios 实例:为每个服务创建独立的 Axios 实例,适用于多个不同服务地址的项目,清晰明确。
  3. 环境变量切换:根据开发、生产环境自动配置不同的 baseURL
  4. 拦截器动态修改:通过请求拦截器动态修改 baseURL,适合复杂场景的需求。
  5. 模块化管理:根据模块名或者业务需求选择不同的 baseURL,可读性更高。

根据你的项目规模和需求,可以选择最适合的方式处理多 baseURL 问题。

加密解密方案

在前后端通信中,使用加密方案来保护数据的安全性是非常重要的。常见的加密方式通常有对称加密、非对称加密和哈希算法。以下是一些在 Axios 中常用的加密方案,结合前后端的使用场景以及如何在 Axios 请求中应用这些加密方式。

常见加密方式

1. 对称加密(Symmetric Encryption)

对称加密是指加密和解密使用相同的密钥。常见的对称加密算法有 AES(Advanced Encryption Standard)。在前后端通信中,可以使用对称加密对敏感信息进行加密,后端使用相同的密钥解密。

使用场景:

对称加密通常用于加密敏感数据(如用户密码、银行信息等)在传输中的保护。

在 Axios 中使用 AES 加密:
npm install crypto-js
import axios from 'axios';
import CryptoJS from 'crypto-js';

const secretKey = 'your-secret-key'; // 需要和后端保持一致的密钥

// 加密函数
function encryptData(data) {
  const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(data), secretKey).toString();
  return ciphertext;
}

// 解密函数(在后端实现,前端不需要)
function decryptData(ciphertext) {
  const bytes = CryptoJS.AES.decrypt(ciphertext, secretKey);
  const decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
  return decryptedData;
}

// 使用加密后的数据发送请求
axios.post('/api/encrypt', {
  data: encryptData({
    username: 'user1',
    password: 'mypassword',
  }),
}).then(response => {
  console.log('Response:', response.data);
}).catch(error => {
  console.error('Error:', error);
});

在这种情况下,前端对敏感数据进行 AES 加密,然后通过 Axios 发送给服务器,服务器在收到数据后用相同的密钥进行解密。

2. 非对称加密(Asymmetric Encryption)

非对称加密使用一对密钥:公钥和私钥。常见的非对称加密算法是 RSA。通常,前端使用公钥加密数据,后端使用私钥解密数据。非对称加密适用于加密少量数据,因为它的加密速度较慢。

使用场景:

非对称加密常用于需要高度安全性的数据传输,如敏感的身份验证信息、密钥交换等。

在 Axios 中使用 RSA 加密:
npm install node-forge
import axios from 'axios';
import forge from 'node-forge';

// 从后端获取公钥
axios.get('/api/public-key').then(response => {
  const publicKey = response.data.publicKey;

  // 使用 RSA 公钥加密
  const encryptWithRSA = (data, publicKey) => {
    const rsa = forge.pki.publicKeyFromPem(publicKey);
    const encrypted = rsa.encrypt(forge.util.encodeUtf8(data));
    return forge.util.encode64(encrypted);
  };

  // 加密数据
  const encryptedData = encryptWithRSA(JSON.stringify({
    username: 'user1',
    password: 'mypassword',
  }), publicKey);

  // 发送加密后的数据
  axios.post('/api/encrypt', {
    data: encryptedData,
  }).then(response => {
    console.log('Response:', response.data);
  });
});

在这种场景下,前端获取后端的公钥后,用 RSA 加密敏感数据,发送给服务器。后端用私钥解密数据。

3. 哈希算法(Hashing)

哈希算法用于将任意长度的数据映射为固定长度的散列值。常见的哈希算法包括 MD5、SHA-256 等。哈希算法通常用于验证数据的完整性,例如校验文件是否被篡改或传输过程中是否损坏。

使用场景:

哈希函数常用于密码的加盐哈希存储,或通过签名验证请求的完整性(如 HMAC)。

在 Axios 中使用 SHA-256 进行数据签名:
npm install crypto-js
import axios from 'axios';
import CryptoJS from 'crypto-js';

// 加盐哈希
function hashData(data, secret) {
  const hash = CryptoJS.HmacSHA256(data, secret);
  return hash.toString(CryptoJS.enc.Hex);
}

// 创建要发送的数据
const data = JSON.stringify({
  username: 'user1',
  password: 'mypassword',
});

// 生成签名
const secret = 'your-secret-key';
const signature = hashData(data, secret);

// 发送带有签名的请求
axios.post('/api/verify', {
  data,
  signature, // 传递签名以验证完整性
}).then(response => {
  console.log('Response:', response.data);
});

这种方式确保请求在传输过程中没有被篡改,后端可以使用相同的密钥对接收到的数据进行哈希计算,并验证签名是否匹配。

4. 混合加密

混合加密是对称加密和非对称加密的结合,通常用于提高安全性和效率。在实际应用中,通常会使用 RSA 非对称加密来加密 AES 对称加密的密钥,而数据则使用 AES 加密,这样可以兼顾效率和安全性。

使用场景:

混合加密常用于需要传输大量数据但仍需要高度安全性的场景,特别是前端生成随机的对称密钥,用 RSA 加密这个密钥后传递给后端。

实现混合加密:
import axios from 'axios';
import CryptoJS from 'crypto-js';
import forge from 'node-forge';

// 使用 AES 生成随机密钥
function generateAESKey() {
  return CryptoJS.lib.WordArray.random(16).toString();
}

// AES 加密数据
function encryptData(data, aesKey) {
  return CryptoJS.AES.encrypt(data, aesKey).toString();
}

// RSA 公钥加密 AES 密钥
function encryptAESKeyWithRSA(aesKey, publicKey) {
  const rsa = forge.pki.publicKeyFromPem(publicKey);
  const encrypted = rsa.encrypt(forge.util.encodeUtf8(aesKey));
  return forge.util.encode64(encrypted);
}

// 混合加密过程
axios.get('/api/public-key').then(response => {
  const publicKey = response.data.publicKey;

  const aesKey = generateAESKey();
  const encryptedData = encryptData(JSON.stringify({
    username: 'user1',
    password: 'mypassword',
  }), aesKey);

  const encryptedAESKey = encryptAESKeyWithRSA(aesKey, publicKey);

  // 发送加密的数据和加密的 AES 密钥
  axios.post('/api/encrypt', {
    encryptedData,
    encryptedAESKey,
  }).then(response => {
    console.log('Response:', response.data);
  });
});

在这种情况下,前端使用 AES 加密实际数据,并使用 RSA 加密 AES 密钥。后端接收到加密的密钥和数据后,先用 RSA 私钥解密 AES 密钥,再用 AES 密钥解密数据。

重复请求的优化和请求取消

  1. 防止重复请求
    通过使用请求的唯一标识(例如 url + method)来管理请求,防止用户短时间内重复发送相同请求。可以在发送请求前检查是否有相同的请求正在进行,如果有则不发送新的请求。

  2. 取消请求
    可以结合 axiosCancelToken,在用户离开页面或切换操作时取消未完成的请求,避免浪费资源。

总结

  • 对称加密:如 AES,适用于加密传输的敏感数据。
  • 非对称加密:如 RSA,适用于密钥交换和少量敏感数据的加密。
  • 哈希算法:如 SHA-256,常用于数据完整性验证。
  • 混合加密:结合 AES 和 RSA,兼顾性能和安全性,常用于大数据加密。

通过这些加密方案,能够有效地保护前后端通信中的数据安全。

;