uniapp 请求封装方案,结合全局和局部 loading 控制、请求拦截、错误处理、超时重试、日志记录、环境切换以及文件上传下载功能。

以下是基于 UniApp 的请求封装方案,结合全局和局部 loading 控制、请求拦截、错误处理、超时重试、日志记录、环境切换以及文件上传下载功能。


1. 项目结构设计

project-root/
├── config/
│   └── api.js                # 环境配置
├── utils/
│   ├── request.js            # 请求封装
│   ├── toast.js              # Toast 工具
│   └── loading.js            # Loading 工具
├── store/
│   └── loadingStore.js       # 全局 Loading 状态管理
├── components/
│   └── GlobalLoading.vue     # 全局 Loading 组件
├── App.vue                   # 全局组件挂载
└── pages/
    └── Home.vue              # 示例页面

2. 环境配置

config/api.js 中定义不同环境下的 API 地址。

config/api.js

const BASE_URL = {
  development: 'http://localhost:996',
  test: 'https://test-api.yourdomain.com',
  production: 'https://api.yourdomain.com'
};

const ENV = process.env.NODE_ENV || 'development'; // 默认为开发环境

export const API_URL = BASE_URL[ENV];

3. 全局 Loading 状态管理

使用 Pinia 或 Vuex 管理全局 loading 状态。

store/loadingStore.js

// 使用 Pinia
import { defineStore } from 'pinia';

export const useLoadingStore = defineStore('loading', {
  state: () => ({
    isLoading: false
  }),
  actions: {
    showLoading() {
      this.isLoading = true;
    },
    hideLoading() {
      this.isLoading = false;
    }
  }
});

4. 全局 Loading 组件

创建一个全局 Loading 组件,用于显示加载动画。

components/GlobalLoading.vue

<template>
  <view v-if="isLoading" class="loading-overlay">
    <view class="loading-spinner"></view>
  </view>
</template>

<script>
import { useLoadingStore } from '@/store/loadingStore';

export default {
  setup() {
    const store = useLoadingStore();
    return {
      isLoading: store.isLoading
    };
  }
};
</script>

<style scoped>
.loading-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 9999;
}

.loading-spinner {
  width: 50px;
  height: 50px;
  border: 5px solid rgba(255, 255, 255, 0.5);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
</style>

5. 请求封装

封装请求方法,支持拦截器、超时重试、日志记录等功能。

utils/request.js

import { API_URL } from '@/config/api';
import { useLoadingStore } from '@/store/loadingStore';
import { showToast } from '@/utils/toast';

const loadingStore = useLoadingStore();

// 默认请求配置
const defaultOptions = {
  timeout: 15000, // 请求超时时间
  dataType: 'json',
  header: {
    'Content-Type': 'application/json'
  },
  retry: 3, // 默认重试次数
  showErrToast: true // 是否显示错误提示
};

// 请求方法封装
const request = async (options) => {
  const { url, method, data, loading = true, retry = defaultOptions.retry, showErrToast } = options;
  const fullUrl = url.startsWith('http') ? url : `${API_URL}${url}`;

  // 设置 Token(如果有)
  const token = uni.getStorageSync('token');
  if (token) {
    options.header = { ...options.header, Authorization: `Bearer ${token}` };
  }

  // 显示加载动画
  if (loading) {
    loadingStore.showLoading();
  }

  try {
    const res = await uni.request({
      ...defaultOptions,
      ...options,
      url: fullUrl,
      method,
      data
    });

    loadingStore.hideLoading(); // 隐藏加载动画

    if (res.statusCode === 200) {
      return res.data;
    } else {
      throw new Error(`Request failed with status code ${res.statusCode}`);
    }
  } catch (err) {
    loadingStore.hideLoading(); // 隐藏加载动画

    if (retry > 0) {
      console.warn(`Retrying request... Retries left: ${retry}`);
      return request({ ...options, retry: retry - 1 });
    } else {
      if (showErrToast) {
        showToast(`请求失败:${err.message}`);
      }
      throw err;
    }
  }
};

// 请求拦截器
const requestInterceptor = (options) => {
  console.log(`[Request] ${options.method} ${options.url}`, options.data); // 打印请求日志
  return options;
};

// 响应拦截器
const responseInterceptor = (res) => {
  console.log(`[Response] ${res.statusCode}`, res); // 打印响应日志
  return res;
};

// 封装请求方法
const http = (method, url, data = {}, options = {}) => {
  const finalOptions = requestInterceptor({
    url,
    method,
    data,
    ...options
  });

  return request(finalOptions).then((res) => responseInterceptor(res));
};

export const get = (url, data = {}, options = {}) => http('GET', url, data, options);
export const post = (url, data = {}, options = {}) => http('POST', url, data, options);
export const put = (url, data = {}, options = {}) => http('PUT', url, data, options);
export const del = (url, data = {}, options = {}) => http('DELETE', url, data, options);

// 文件上传
export const uploadFile = (url, filePath, options = {}) => {
  const fullUrl = url.startsWith('http') ? url : `${API_URL}${url}`;
  const token = uni.getStorageSync('token');
  const header = token ? { Authorization: `Bearer ${token}` } : {};

  return new Promise((resolve, reject) => {
    uni.uploadFile({
      url: fullUrl,
      filePath,
      name: 'file',
      header,
      ...options,
      success: (res) => {
        if (res.statusCode === 200) {
          resolve(res.data);
        } else {
          reject(res);
        }
      },
      fail: (err) => {
        reject(err);
      }
    });
  });
};

// 文件下载
export const downloadFile = (url, options = {}) => {
  const fullUrl = url.startsWith('http') ? url : `${API_URL}${url}`;
  return uni.downloadFile({
    url: fullUrl,
    ...options
  });
};

6. Toast 工具

封装一个简单的 Toast 工具用于显示提示信息。

utils/toast.js

export const showToast = (message, icon = 'none') => {
  uni.showToast({
    title: message,
    icon,
    duration: 2000
  });
};

7. 全局 Loading 组件的使用

App.vue 中引入全局 Loading 组件。

App.vue

<template>
  <view class="app-container">
    <GlobalLoading />
    <router-view />
  </view>
</template>

<script>
import GlobalLoading from '@/components/GlobalLoading.vue';

export default {
  components: {
    GlobalLoading
  }
};
</script>

<style>
.app-container {
  position: relative;
  width: 100%;
  height: 100%;
}
</style>

8. 使用封装的请求方法

在页面或组件中使用封装的请求方法时,loading 状态会自动显示和隐藏。

示例页面:Home.vue

<template>
  <view>
    <button @click="fetchData">获取数据</button>
    <button @click="submitData">提交数据</button>
    <button @click="uploadFile">上传文件</button>
    <button @click="downloadFile">下载文件</button>
  </view>
</template>

<script>
import { get, post, uploadFile, downloadFile } from '@/utils/request';

export default {
  methods: {
    async fetchData() {
      try {
        const res = await get('/api/user', { userId: 123 });
        console.log(res);
      } catch (err) {
        console.error(err);
      }
    },
    async submitData() {
      try {
        const res = await post('/api/user', { name: 'Kimi', age: 25 });
        console.log(res);
      } catch (err) {
        console.error(err);
      }
    },
    async uploadFile() {
      try {
        const filePath = '/path/to/file.jpg';
        const res = await uploadFile('/api/upload', filePath);
        console.log(res);
      } catch (err) {
        console.error(err);
      }
    },
    async downloadFile() {
      try {
        const url = '/api/download';
        const res = await downloadFile(url);
        console.log(res);
      } catch (err) {
        console.error(err);
      }
    }
  }
};
</script>

总结

通过上述封装,我们实现了以下功能:

  1. 全局和局部 loading 控制:支持灵活的加载状态管理。

  2. 请求和响应拦截器:支持拦截器,便于统一处理请求头、Token、错误状态码等。

  3. 超时重试机制:支持请求超时自动重试。

  4. 错误统一处理:支持自定义错误处理逻辑。

  5. 日志记录:记录请求和响应的详细信息,方便调试。

  6. 环境切换:支持开发、测试、生产环境的无缝切换。

  7. 支持文件上传和下载:扩展请求功能,支持文件上传和下载。

  8. 支持自定义配置:允许用户根据需求自定义请求配置,如超时时间、请求头等。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值