《uni-app跨平台开发完全指南》- 09 - 网络请求

  • 时间:2025-11-30 21:04 作者: 来源: 阅读:2
  • 扫一扫,手机访问
摘要:引言 网络请求在我们日常的开发过程中极为重要!!!没有它,数据无法流动,应用也就失去了生命力。今天,我们就来深入探讨uni-app中网络请求的方方面面。 一、uni.request 基础使用 1.1 为什么要使用 uni.request? 在我们开始写代码之前,先思考一个问题:为什么uni-app要提供自己的网络请求API,而不是直接使用浏览器的fetch或XMLHttpRequest? 答案

引言

网络请求在我们日常的开发过程中极为重要!!!没有它,数据无法流动,应用也就失去了生命力。今天,我们就来深入探讨uni-app中网络请求的方方面面。

一、uni.request 基础使用

1.1 为什么要使用 uni.request?

在我们开始写代码之前,先思考一个问题:为什么uni-app要提供自己的网络请求API,而不是直接使用浏览器的fetch或XMLHttpRequest?

答案很简单:跨平台兼容性!

想象一下,你在H5环境可以使用fetch,但在微信小程序里呢?在App里呢?每个平台的网络请求API都有差异。uni.request充当翻译官的角色,它把不同平台的网络请求差异都封装起来,给我们提供统一的接口。


// uni.request基础用法
uni.request({
  url: 'https://api.test.com/data', // 请求地址
  method: 'GET',                       // GET请求
  success: (res) => {                  // 成功回调
    console.log('请求成功:', res.data);
  },
  fail: (err) => {                     // 失败回调
    console.log('请求失败:', err);
  },
  complete: () => {                    // 完成回调
    console.log('请求完成');
  }
});

看到这里,你可能觉得这跟其他请求库差不多。但让我们深入看看它背后的工作原理:

1.2 uni.request 的跨平台原理

这就是uni.request的魅力所在:你写一套代码,它在不同平台会自动选择最合适的底层实现。

1.3 请求配置参数

下面我们通过一个实际的例子来了解网络请求的配置选项:


// uni.request配置
uni.request({
  // 配置
  url: 'https://api.test.com/users',
  method: 'POST', // POST
  timeout: 10000, // 超时时间
  
  // 配置请求头
  header: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer your-token-here',
    'X-Custom-Header': 'custom-value'
  },
  
  // 请求数据
  data: {
    name: '张三',
    age: 25,
    email: 'zhangsan@example.com'
  },
  
  // 响应类型
  dataType: 'json', 
  responseType: 'text', 
  
  // 回调
  success: (res) => {
    // res包含以下属性:
    // - res.statusCode: HTTP状态码
    // - res.data: 服务器返回的数据
    // - res.header: 响应头
    // - res.cookies: 小程序端返回的cookies
    
    if (res.statusCode === 200) {
      console.log('请求成功:', res.data);
    } else {
      console.log('服务器返回错误状态码:', res.statusCode);
    }
  },
  
  fail: (err) => {
    // 网络错误、超时
    console.error('请求失败:', err);
    uni.showToast({
      title: '网络请求失败',
      icon: 'none'
    });
  },
  
  complete: () => {
    // 完成回调
    console.log('请求结束');
  }
});

1.4 实际开发场景

场景1:获取用户列表


// 获取用户列表
async function fetchUserList(page = 1, pageSize = 10) {
  return new Promise((resolve, reject) => {
    uni.request({
      url: 'https://api.test.com/users',
      method: 'GET',
      data: {
        page: page,
        page_size: pageSize
      },
      success: (res) => {
        if (res.statusCode === 200) {
          resolve(res.data);
        } else {
          reject(new Error(`服务器错误: ${res.statusCode}`));
        }
      },
      fail: (err) => {
        reject(err);
      }
    });
  });
}

// 使用
try {
  const userList = await fetchUserList(1, 10);
  console.log('用户列表:', userList);
} catch (error) {
  console.error('获取用户列表失败:', error);
}

场景2:提交表单数据


// 提交用户注册信息
function submitUserRegistration(userData) {
  uni.request({
    url: 'https://api.test.com/register',
    method: 'POST',
    header: {
      'Content-Type': 'application/json'
    },
    data: userData,
    success: (res) => {
      if (res.statusCode === 201) {
        uni.showToast({
          title: '注册成功!',
          icon: 'success'
        });
        // 跳转到登录页面...
      } else {
        uni.showToast({
          title: res.data.message || '注册失败',
          icon: 'none'
        });
      }
    },
    fail: (err) => {
      uni.showToast({
        title: '网络错误,请重试',
        icon: 'none'
      });
    }
  });
}

二、请求封装与拦截

直接使用uni.request虽然简单,但在实际项目中很快就会遇到问题:

每个请求都要写重复的配置错误处理没有统一登录状态难以管理不方便添加全局loading

这时候,我们就需要进行请求封装了!

2.1 基础封装:创建一个请求工具类

下面我们从最简单的封装开始:


// utils/request.js - 基础封装
class Request {
  constructor(baseURL = '') {
    this.baseURL = baseURL;
    this.interceptors = {
      request: [],
      response: []
    };
  }

  // 添加请求拦截器
  useRequestInterceptor(interceptor) {
    this.interceptors.request.push(interceptor);
  }

  // 添加响应拦截器
  useResponseInterceptor(interceptor) {
    this.interceptors.response.push(interceptor);
  }

  // 执行请求拦截器
  async runRequestInterceptors(config) {
    let currentConfig = { ...config };
    for (const interceptor of this.interceptors.request) {
      currentConfig = await interceptor(currentConfig);
    }
    return currentConfig;
  }

  // 执行响应拦截器
  async runResponseInterceptors(response) {
    let currentResponse = { ...response };
    for (const interceptor of this.interceptors.response) {
      currentResponse = await interceptor(currentResponse);
    }
    return currentResponse;
  }

  // 请求
  async request(config) {
    try {
      // 请求拦截器
      const requestConfig = await this.runRequestInterceptors({
        baseURL: this.baseURL,
        ...config
      });

      // 发送请求
      return new Promise((resolve, reject) => {
        uni.request({
          ...requestConfig,
          success: async (res) => {
            try {
              // 执行响应拦截器
              const processedResponse = await this.runResponseInterceptors(res);
              resolve(processedResponse);
            } catch (error) {
              reject(error);
            }
          },
          fail: (err) => {
            reject(err);
          }
        });
      });
    } catch (error) {
      throw error;
    }
  }

  // GET方法
  get(url, data = {}, config = {}) {
    return this.request({
      url,
      method: 'GET',
      data,
      ...config
    });
  }
  // POST方法
  post(url, data = {}, config = {}) {
    return this.request({
      url,
      method: 'POST',
      data,
      ...config
    });
  }
  // PUT方法
  put(url, data = {}, config = {}) {
    return this.request({
      url,
      method: 'PUT',
      data,
      ...config
    });
  }
  // DELETE方法
  delete(url, data = {}, config = {}) {
    return this.request({
      url,
      method: 'DELETE',
      data,
      ...config
    });
  }
}

export default Request;

2.2 拦截器的工作原理

下面我们看下原理图,来辅助理解:

2.3 封装案例

现在让我们封装一个功能完整的请求:


// utils/http.js
import Request from './request.js';

// 创建请求
const http = new Request(process.env.VUE_APP_BASE_API);

// 请求拦截器
http.useRequestInterceptor(async (config) => {
  // loading
  uni.showLoading({
    title: '加载中...',
    mask: true
  });

  // 添加token到请求头
  const token = uni.getStorageSync('token');
  if (token) {
    config.header = {
      ...config.header,
      'Authorization': `Bearer ${token}`
    };
  }

  // 设置超时时间
  config.timeout = config.timeout || 15000;

  console.log('请求配置:', config);
  return config;
});

// 响应拦截器
http.useResponseInterceptor(async (response) => {
  // 隐藏loading
  uni.hideLoading();

  const { statusCode, data } = response;

  console.log('响应数据:', response);

  // HTTP状态码判断
  if (statusCode === 200) {
    // 业务状态码判断
    if (data.code === 0 || data.code === 200) {
      // 成功
      return data;
    } else {
      // 错误
      const errorMessage = data.message || '业务错误';
      uni.showToast({
        title: errorMessage,
        icon: 'none',
        duration: 3000
      });
      throw new Error(errorMessage);
    }
  } else if (statusCode === 401) {
    // 未授权,跳转到登录页
    uni.showModal({
      title: '提示',
      content: '登录已过期,请重新登录',
      showCancel: false,
      success: () => {
        uni.navigateTo({
          url: '/pages/login/login'
        });
      }
    });
    throw new Error('未授权');
  } else if (statusCode >= 500) {
    // 服务器错误
    uni.showToast({
      title: '服务器开小差了,请稍后重试',
      icon: 'none'
    });
    throw new Error('服务器错误');
  } else {
    // 其他错误
    uni.showToast({
      title: `网络错误: ${statusCode}`,
      icon: 'none'
    });
    throw new Error(`HTTP错误: ${statusCode}`);
  }
});

// 错误处理
const handleRequestError = (error) => {
  console.error('请求错误:', error);
  
  if (error.errMsg && error.errMsg.includes('timeout')) {
    uni.showToast({
      title: '请求超时,请检查网络',
      icon: 'none'
    });
  } else if (error.errMsg && error.errMsg.includes('fail')) {
    uni.showToast({
      title: '网络连接失败,请检查网络设置',
      icon: 'none'
    });
  }
  
  throw error;
};

// 封装带错误处理的请求方法
const safeRequest = async (config) => {
  try {
    return await http.request(config);
  } catch (error) {
    return handleRequestError(error);
  }
};

// 导出常用的请求方法
export const get = (url, data, config) => safeRequest({ url, method: 'GET', data, ...config });
export const post = (url, data, config) => safeRequest({ url, method: 'POST', data, ...config });
export const put = (url, data, config) => safeRequest({ url, method: 'PUT', data, ...config });
export const del = (url, data, config) => safeRequest({ url, method: 'DELETE', data, ...config });

export default http;

2.4 在项目中使用封装的请求库


// api/user.js
import { get, post, put, del } from '@/utils/http.js';

export const userApi = {
  // 获取用户列表
  getUsers: (params) => get('/users', params),
  
  // 获取用户详情
  getUserDetail: (id) => get(`/users/${id}`),
  
  // 创建用户
  createUser: (data) => post('/users', data),
  
  // 更新用户
  updateUser: (id, data) => put(`/users/${id}`, data),
  
  // 删除用户
  deleteUser: (id) => del(`/users/${id}`),
  
  // 用户登录
  login: (credentials) => post('/auth/login', credentials),
  
  // 获取用户信息
  getProfile: () => get('/auth/profile')
};

// 在页面中使用
// pages/user/user.vue
export default {
  data() {
    return {
      users: [],
      loading: false
    };
  },
  
  methods: {
    async loadUsers() {
      try {
        this.loading = true;
        const result = await userApi.getUsers({
          page: 1,
          pageSize: 10
        });
        this.users = result.data;
      } catch (error) {
        console.error('加载用户列表失败:', error);
      } finally {
        this.loading = false;
      }
    },
    
    async handleLogin() {
      try {
        const result = await userApi.login({
          username: 'admin',
          password: '123456'
        });
        
        // 保存token
        uni.setStorageSync('token', result.token);
        uni.showToast({
          title: '登录成功',
          icon: 'success'
        });
      } catch (error) {
        console.error('登录失败:', error);
      }
    }
  },
  
  onLoad() {
    this.loadUsers();
  }
};

三、跨域问题解决方案

跨域问题可以说是前端开发中最常见的问题之一。不过别担心,uni-app为我们提供了多种解决方案。

3.1 什么是跨域?

简单来说:浏览器出于安全考虑,限制了从一个源(域名、协议、端口)向另一个源发起的请求。

举个例子

你的网站: https://www.myapp.com接口地址: https://api.server.com这就跨域了!

3.2 uni-app中的跨域解决方案

方案1:H5平台的开发环境代理

vue.config.js中配置代理:


// vue.config.js
module.exports = {
  devServer: {
    proxy: {
      // 代理所有以/api开头的请求
      '/api': {
        target: 'https://api.test.com', // 接口域名
        changeOrigin: true, // 改变源
        pathRewrite: {
          '^/api': '' // 去掉/api前缀
        }
      }
    }
  }
};

// 使用时代理请求
// 开发环境:请求 /api/users -> 代理到 https://api.test.com/users
// 生产环境:需要配置完整的URL或者使用环境变量
uni.request({
  url: process.env.NODE_ENV === 'development' ? '/api/users' : 'https://api.test.com/users'
});
方案2:使用条件编译处理多环境

// utils/config.js - 环境配置
const config = {
  // 开发环境
  development: {
    baseURL: '/api' // 使用代理
  },
  // 生产环境
  production: {
    baseURL: 'https://api.release.com' // 生产地址
  },
  // 小程序环境
  mp: {
    baseURL: 'https://api.example.com' // 小程序没有跨域限制
  }
};

// 获取当前环境配置
function getBaseURL() {
  #ifdef H5
    return process.env.NODE_ENV === 'development' ? 
           config.development.baseURL : 
           config.production.baseURL;
  #endif
  
  #ifdef MP-WEIXIN
    return config.mp.baseURL;
  #endif
  
  #ifdef APP-PLUS
    return config.production.baseURL;
  #endif
}

export const BASE_URL = getBaseURL();
方案3:JSONP方案

// utils/jsonp.js
export function jsonp(url, params = {}, timeout = 10000) {
  return new Promise((resolve, reject) => {
    // 生成回调函数名
    const callbackName = `jsonp_${Date.now()}_${Math.random().toString(36).substr(2)}`;
    
    // 创建script标签
    const script = document.createElement('script');
    
    // 构建URL
    const queryParams = new URLSearchParams({
      ...params,
      callback: callbackName
    });
    
    script.src = `${url}?${queryParams.toString()}`;
    
    // 超时处理
    const timeoutId = setTimeout(() => {
      cleanup();
      reject(new Error('JSONP请求超时'));
    }, timeout);
    
    // 清理函数
    const cleanup = () => {
      clearTimeout(timeoutId);
      document.head.removeChild(script);
      delete window[callbackName];
    };
    
    // 定义全局回调函数
    window[callbackName] = (data) => {
      cleanup();
      resolve(data);
    };
    
    // 错误处理
    script.onerror = () => {
      cleanup();
      reject(new Error('JSONP请求失败'));
    };
    
    // 添加到文档
    document.head.appendChild(script);
  });
}

// #ifdef H5
try {
  const data = await jsonp('https://api.test.com/data', {
    page: 1,
    size: 10
  });
  console.log('JSONP数据:', data);
} catch (error) {
  console.error('JSONP请求失败:', error);
}
// #endif

3.4 各平台跨域情况总结

备注

微信小程序需要在后台配置request合法域名App平台通常没有跨域限制,但要注意HTTPS要求H5平台在开发阶段使用代理,生产环境需要后端配置CORS

四、网络状态检测

在网络不稳定的情况下,给用户友好的提示是非常重要的。让我们来看看如何在uni-app中检测和管理网络状态。

4.1 获取网络状态


// utils/network.js - 网络状态检测
class NetworkManager {
  constructor() {
    this.currentState = null;
    this.listeners = new Set();
    this.init();
  }

  // 初始化网络状态监听
  init() {
    // 获取当前网络状态
    this.getCurrentState();
    
    // 监听网络状态变化
    // #ifdef H5 || APP-PLUS
    window.addEventListener('online', () => {
      this.handleNetworkChange('connected');
    });
    
    window.addEventListener('offline', () => {
      this.handleNetworkChange('disconnected');
    });
    // #endif
    
    // #ifdef MP-WEIXIN || APP-PLUS
    uni.onNetworkStatusChange((res) => {
      this.handleNetworkChange(res.isConnected ? 'connected' : 'disconnected');
    });
    // #endif
  }

  // 获取当前网络状态
  getCurrentState() {
    // #ifdef H5
    this.currentState = navigator.onLine ? 'connected' : 'disconnected';
    // #endif
    
    // #ifdef MP-WEIXIN || APP-PLUS
    uni.getNetworkType({
      success: (res) => {
        this.currentState = res.networkType !== 'none' ? 'connected' : 'disconnected';
      }
    });
    // #endif
    
    return this.currentState;
  }

  // 处理网络状态变化
  handleNetworkChange(state) {
    const previousState = this.currentState;
    this.currentState = state;
    
    console.log(`网络状态变化: ${previousState} -> ${state}`);
    
    // 通知所有监听器
    this.listeners.forEach(listener => {
      try {
        listener(state, previousState);
      } catch (error) {
        console.error('网络状态监听器错误:', error);
      }
    });

    // 显示网络状态提示
    this.showNetworkNotice(state, previousState);
  }

  // 显示网络状态提示
  showNetworkNotice(currentState, previousState) {
    if (currentState === 'disconnected' && previousState === 'connected') {
      // 网络断开提示
      uni.showToast({
        title: '网络已断开',
        icon: 'none',
        duration: 3000,
        image: '/static/images/network-offline.png' 
      });
    } else if (currentState === 'connected' && previousState === 'disconnected') {
      // 网络恢复提示
      uni.showToast({
        title: '网络连接已恢复',
        icon: 'success',
        duration: 2000
      });
    }
  }

  // 添加网络状态监听器
  addListener(listener) {
    this.listeners.add(listener);
    // 返回当前状态
    listener(this.currentState, this.currentState);
  }

  // 移除监听器
  removeListener(listener) {
    this.listeners.delete(listener);
  }

  // 检查网络是否连接
  isConnected() {
    return this.currentState === 'connected';
  }

  // 等待网络恢复
  waitForNetwork(timeout = 30000) {
    return new Promise((resolve, reject) => {
      if (this.isConnected()) {
        resolve();
        return;
      }

      const timeoutId = setTimeout(() => {
        this.removeListener(checkNetwork);
        reject(new Error('等待网络超时'));
      }, timeout);

      const checkNetwork = (state) => {
        if (state === 'connected') {
          clearTimeout(timeoutId);
          this.removeListener(checkNetwork);
          resolve();
        }
      };

      this.addListener(checkNetwork);
    });
  }
}

// 创建网络管理器
export const networkManager = new NetworkManager();
export default networkManager;

4.2 在网络请求中使用网络检测

下面我们把网络检测集成到之前的请求封装中:


// utils/http-with-network.js
import Request from './request.js';
import networkManager from './network.js';

const http = new Request(process.env.VUE_APP_BASE_API);

// 添加网络检测的请求拦截器
http.useRequestInterceptor(async (config) => {
  // 检查网络状态
  if (!networkManager.isConnected()) {
    // 显示无网络提示
    uni.showModal({
      title: '网络提示',
      content: '当前网络不可用,请检查网络连接',
      showCancel: false,
      confirmText: '确定'
    });
    throw new Error('网络不可用');
  }

  // 其他拦截逻辑...
  return config;
});

// 响应拦截器中处理网络错误
http.useResponseInterceptor(async (response) => {
  // 处理逻辑...
  return response;
});

// 封装带重试的请求方法
export const requestWithRetry = async (config, maxRetries = 3) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await http.request(config);
    } catch (error) {
      // 如果是网络错误,等待网络恢复后重试
      if (error.message.includes('网络') && attempt < maxRetries) {
        console.log(`网络请求失败,第${attempt}次重试...`);
        try {
          // 等待网络恢复,最多等待5秒
          await networkManager.waitForNetwork(5000);
          continue; // 重试
        } catch (waitError) {
          // 等待网络超时,继续抛出错误
          throw error;
        }
      }
      throw error;
    }
  }
};

4.3 在页面中使用网络状态


<!-- components/NetworkStatus.vue -->
<template>
  <view v-if="!isOnline" class="network-status offline">
    <text>网络连接已断开,请检查网络设置</text>
  </view>
</template>

<script>
import networkManager from '@/utils/network.js';

export default {
  data() {
    return {
      isOnline: true
    };
  },
  
  mounted() {
    // 监听网络状态变化
    networkManager.addListener(this.handleNetworkChange);
  },
  
  beforeDestroy() {
    // 清理监听器
    networkManager.removeListener(this.handleNetworkChange);
  },
  
  methods: {
    handleNetworkChange(state) {
      this.isOnline = state === 'connected';
      
      // 根据网络状态执行其他操作
      if (this.isOnline) {
        this.$emit('network-recovered');
      } else {
        this.$emit('network-lost');
      }
    }
  }
};
</script>

<style scoped>
.network-status {
  padding: 10px;
  text-align: center;
  font-size: 14px;
}

.network-status.offline {
  background-color: #fff2f0;
  color: #ff4d4f;
  border-bottom: 1px solid #ffccc7;
}
</style>

4.4 页面集成案例


<!-- pages/index/index.vue -->
<template>
  <view class="container">
    <!-- 网络状态组件 -->
    <NetworkStatus 
      @network-recovered="onNetworkRecovered"
      @network-lost="onNetworkLost"
    />
    
    <!-- 页面内容 -->
    <view class="content">
      <button @click="loadData" :disabled="loading">
        {{ loading ? '加载中...' : '加载数据' }}
      </button>
      
      <view v-if="error" class="error-message">
        {{ error }}
      </view>
      
      <view v-else-if="data" class="data-container">
        <!-- 显示数据 -->
        <text>{{ data }}</text>
      </view>
    </view>
  </view>
</template>

<script>
import NetworkStatus from '@/components/NetworkStatus.vue';
import { requestWithRetry } from '@/utils/http-with-network.js';
import networkManager from '@/utils/network.js';

export default {
  components: {
    NetworkStatus
  },
  
  data() {
    return {
      loading: false,
      data: null,
      error: null
    };
  },
  
  methods: {
    async loadData() {
      if (this.loading) return;
      
      this.loading = true;
      this.error = null;
      
      try {
        // 使用带重试的请求方法
        const result = await requestWithRetry({
          url: '/api/data',
          method: 'GET'
        }, 3); // 最多重试3次
        
        this.data = result;
      } catch (error) {
        this.error = error.message;
        console.error('数据加载失败:', error);
      } finally {
        this.loading = false;
      }
    },
    
    onNetworkRecovered() {
      console.log('网络恢复,自动重试加载数据...');
      // 网络恢复时自动重新加载数据
      if (this.error && this.error.includes('网络')) {
        this.loadData();
      }
    },
    
    onNetworkLost() {
      console.log('网络断开');
      this.error = '网络连接已断开,请检查网络设置';
    }
  },
  
  onLoad() {
    // 页面加载时尝试获取数据
    this.loadData();
  }
};
</script>

五、总结

至此网络请求就学完了,通过本节的学习,我们掌握了uni-app网络请求以下相关知识点:

uni.request基础使用请求封装与拦截跨域问题解决网络状态检测

优化建议


// 请求缓存
const cache = new Map();

export const getWithCache = async (url, params, config) => {
  const cacheKey = `${url}_${JSON.stringify(params)}`;
  
  // 检查缓存
  if (cache.has(cacheKey)) {
    const { data, timestamp } = cache.get(cacheKey);
    // 缓存5分钟内有效
    if (Date.now() - timestamp < 5 * 60 * 1000) {
      return data;
    }
  }
  
  // 发送请求
  const result = await get(url, params, config);
  
  // 更新缓存
  cache.set(cacheKey, {
    data: result,
    timestamp: Date.now()
  });
  
  return result;
};

5.3 错误监控与上报


// 错误监控
const errorTracker = {
  trackRequestError(error, config) {
    const errorInfo = {
      type: 'request_error',
      message: error.message,
      url: config.url,
      method: config.method,
      timestamp: Date.now(),
      userAgent: navigator.userAgent,
      // ...
    };
    
    // 上报到监控系统
    console.log('上报错误:', errorInfo);
    // uni.request({ url: '/api/error-track', method: 'POST', data: errorInfo });
  }
};

如果觉得这篇文章对你有帮助,请不要忘记一键三连(点赞、关注、收藏)!有任何问题欢迎在评论区留言,我会及时解答!

  • 全部评论(0)
手机二维码手机访问领取大礼包
返回顶部