1. 跨域到底是什么?为什么uniapp开发总会遇到它?

如果你刚开始用uniapp做项目,尤其是涉及到调用后端接口的时候,大概率会碰到一个让人头疼的报错,浏览器控制台红彤彤地提示你“Access-Control-Allow-Origin”有问题,或者请求直接失败了。这就是我们常说的“跨域”问题。别慌,这几乎是每个前端开发者,特别是做混合应用开发的必经之路。今天我就结合自己这些年踩过的坑,给你把uniapp里的跨域问题掰开揉碎了讲清楚,从本地开发一路讲到项目上线,保证你看完就能上手解决。

咱们先打个比方,帮你理解跨域的本质。你可以把互联网想象成一个由无数个小区(域名)组成的城市。每个小区(比如 www.your-app.com)都有自己的保安(浏览器)。保安有一条非常严格的规定:只允许本小区的居民(同源请求)自由进出,对于其他小区来的访客(跨域请求),必须出示由对方小区物业(服务器)开具的特别通行证(CORS头)。这个“同源”的判断标准很严格,必须协议(http/https)、域名、端口号三者完全一致才行。比如 https://api.example.com:8080https://www.example.com 就是不同的“小区”,哪怕域名主体一样,端口不同也算跨域。

那为什么uniapp开发特别容易遇到跨域呢?这得从它的运行机制说起。uniapp是一个使用Vue.js开发所有前端应用的框架,一套代码可以编译发布到H5、小程序、App等多个平台。在H5平台,你的代码最终是在浏览器里运行的,而浏览器严格遵守上面说的“同源策略”安全规则。当你本地开发时,前端页面通常运行在 http://localhost:8080,而后端API接口可能部署在 https://api.your-service.com 或者另一个端口的本地服务上,这就触发了跨域。而在小程序平台,情况又不一样了。小程序有自己的一套网络请求机制,它的运行环境并非标准浏览器,所以“跨域”这个概念在小程序里是不存在的,小程序只关心配置的合法域名列表。这种多平台的差异,正是导致uniapp跨域处理需要“因地制宜”的根本原因。

所以,解决跨域的核心思路,其实就是如何让我们的请求,在浏览器的安全规则下“合法”地访问另一个“小区”的资源。接下来,我们就从最简单的本地开发调试开始,一步步拆解不同场景下的解决方案。

2. 开发环境:如何优雅地调试,告别红色报错

本地开发时,我们的首要目标是顺畅地调试代码,而不是和浏览器安全策略斗智斗勇。这里有几种经过实战检验的方法,你可以根据项目情况选择。

2.1 方案一:使用HBuilderX的内置浏览器(最省心)

如果你是使用DCloud官方推出的HBuilderX进行开发,那么恭喜你,有一个“开箱即用”的免跨域方案。当你点击运行菜单,选择“运行到内置浏览器”时,HBuilderX会启动一个特殊的浏览器环境。这个环境内部已经处理好了网络请求的代理,你直接在前端代码里请求完整的后端API地址(如 https://api.test.com/user),也不会触发浏览器的跨域错误

这个方法特别适合快速起步和验证业务逻辑。我刚开始接触uniapp时,就靠这个功能省去了大量配置时间。它的原理相当于HBuilderX内置了一个代理服务器,帮你把请求转发到了目标地址,对于浏览器来说,请求还是来自“本地”,从而绕过了跨域限制。但要注意,这个便利仅限于在HBuilderX内置浏览器中运行。一旦你需要用Chrome、Edge等外部浏览器进行更深入的调试(比如使用Vue Devtools),或者要打包测试,这个方法就失效了。

2.2 方案二:配置开发服务器代理(Vue2项目)

对于大多数正经开发的项目,配置代理服务器是更通用、更专业的做法。这样你可以在任何浏览器中调试,并且为后续部署铺平道路。如果你的项目是基于Vue2(使用webpack)构建的,需要在项目根目录下创建一个 vue.config.js 文件。

这个文件是uniapp项目(底层基于@vue/cli)的配置文件。我们来写一个完整的配置示例:

// vue.config.js
module.exports = {
  // 开发服务器配置
  devServer: {
    // 允许任何主机访问,解决一些局域网内访问的host检查问题
    disableHostCheck: true,
    // 配置代理规则
    proxy: {
      // 定义一个代理路径前缀,你可以自定义,比如 '/api', '/h5api' 都行
      '/h5api': {
        target: 'https://tiyu.baidu.com', // 这是你真实的后端接口地址
        changeOrigin: true, // 开启代理,将请求头中的host改为target的host
        secure: false, // 如果目标是https,但证书不受信任,可以设置为false(开发环境可用)
        // 路径重写:去掉我们添加的代理前缀,确保后端收到正确的路径
        pathRewrite: {
          '^/h5api': '/' // 将 /h5api 替换为 /
        }
      }
      // 你可以配置多个代理规则,对应不同的后端服务
      // '/user-api': { target: 'http://user-service.com', ... }
    }
  }
}

配置好后,重启你的开发服务器。此时,你在前端代码里发起请求的方式就需要一点小变化。假设你要请求的真实接口是 https://tiyu.baidu.com/posts,你现在应该这样写:

uni.request({
  url: '/h5api/posts', // 注意,这里用的是代理路径前缀 + 接口路径
  method: 'GET',
  success: (res) => {
    console.log('请求成功:', res.data);
  },
  fail: (err) => {
    console.error('请求失败:', err);
  }
});

看到区别了吗?我们不再直接写完整的 https://tiyu.baidu.com/posts,而是写 /h5api/posts。本地开发服务器在收到以 /h5api 开头的请求时,会根据 vue.config.js 的配置,自动将其转发到 https://tiyu.baidu.com,并且将路径中的 /h5api 替换掉。对于浏览器而言,这个请求是发给本地开发服务器的(同源),因此没有跨域问题;对于后端服务器而言,它收到的是一个来自代理服务器的正常请求。

2.3 方案三:配置开发服务器代理(Vue3项目)

如果你的项目使用的是Vite + Vue3(uniapp从某个版本开始支持),配置文件就变成了 vite.config.jsvite.config.ts。配置的语法有所不同,但核心思想一致。

// vite.config.js
import { defineConfig } from 'vite';
import uni from '@dcloudio/vite-plugin-uni';

export default defineConfig({
  plugins: [uni()], // 使用uniapp的vite插件
  server: {
    host: 'localhost', // 开发服务器主机
    port: 8080, // 开发服务器端口,可自定义
    // 代理配置
    proxy: {
      '/h5api': {
        target: 'https://tiyu.baidu.com', // 目标接口地址
        changeOrigin: true, // 修改请求头中的origin为目标地址
        rewrite: (path) => path.replace(/^\/h5api/, ''), // 路径重写,功能同webpack的pathRewrite
        // 如果需要,也可以配置更详细的选项,比如 ws: true 用于代理WebSocket
      }
    }
  }
});

使用方式与Vue2项目完全一样,前端代码中请求 /h5api/posts 即可。Vite的代理配置更简洁一些,rewrite 函数给了你更大的灵活性去处理路径。

这里有个非常重要的点:无论是 vue.config.js 还是 vite.config.js 中的代理配置,都只在开发环境(npm run devnpm run serve)下生效。当你执行 npm run build 构建生产环境的H5包时,这些配置是不会被打包进去的。这意味着,如果你直接打开构建后的 dist 文件夹里的 index.html 文件,代理会失效,跨域问题将再次出现。生产环境的解决方案我们会在第4节详细讲。

3. 多平台适配:一套代码如何兼容H5与小程序

这是uniapp开发跨域问题的精髓所在,也是最能体现“一次开发,多端发布”价值的地方。我们配置好了H5的代理,但小程序端根本不需要这个代理路径,直接请求完整URL就行。如果写死代理路径,小程序会报错;如果写死完整URL,H5开发环境又会跨域。怎么办?答案是:动态判断运行平台,智能拼接请求地址

3.1 封装一个平台自适应的请求函数

最好的实践是封装一个统一的请求工具函数,让它内部去处理这些平台差异。下面我分享一个我在多个项目中使用的、比较健壮的封装方案。

首先,我们创建一个配置文件,用来存放一些环境变量和平台判断逻辑。

// src/config/index.js
// 获取系统信息,里面包含 uniPlatform 字段,用于判断当前平台
export const SYSTEM_INFO = uni.getSystemInfoSync();

// 你的后端API基础地址
export const HOST = 'https://your-real-api.com';

// 根据平台决定API_HOST:H5开发环境用空(走代理),其他平台用真实HOST
export const API_HOST = SYSTEM_INFO.uniPlatform === 'web' ? '' : HOST;

// 根据平台决定代理前缀:H5开发环境用代理前缀,其他平台为空
export const API_PROXY = SYSTEM_INFO.uniPlatform === 'web' ? '/h5api' : '';

接下来,创建一个公共工具函数,用于组装最终的请求URL。

// src/utils/common.js
import { API_HOST, API_PROXY } from '@/config/index.js';

/**
 * 组装完整的接口URL
 * @param {string} url - 接口路径,如 '/api/user/login'
 * @returns {string} - 组装好的完整URL或带代理前缀的路径
 */
export const packApiUrl = (url = '') => {
  // 如果传入的url已经是http或https开头,认为是完整URL,直接返回(可用于请求第三方接口)
  if (url.slice(0, 4) === 'http') {
    return url;
  }
  // 否则,拼接 API_HOST、API_PROXY 和传入的路径
  // H5环境:API_HOST为空,API_PROXY为‘/h5api’,结果是‘/h5api/api/user/login’
  // 小程序/App环境:API_HOST为真实域名,API_PROXY为空,结果是‘https://your-real-api.com/api/user/login’
  return `${API_HOST}${API_PROXY}${url}`;
};

最后,我们来封装核心的 request 函数。

// src/utils/request.js
import { packApiUrl } from './common.js';

// 默认请求配置
const DEFAULT_CONFIG = {
  baseURL: '', // 这里不再需要,因为packApiUrl已经处理
  timeout: 15000, // 15秒超时
  header: {
    'Content-Type': 'application/json',
  },
};

/**
 * 统一的uni.request封装
 * @param {Object} config - 请求配置,支持uni.request所有参数
 * @returns {Promise} - 返回Promise对象
 */
export function request(config = {}) {
  // 合并配置
  const { url, data = {}, method = 'GET', header = {}, ...otherConfig } = config;
  
  // 处理请求URL:根据平台智能拼接
  const finalUrl = packApiUrl(url);
  
  // 合并请求头
  const finalHeader = { ...DEFAULT_CONFIG.header, ...header };
  
  return new Promise((resolve, reject) => {
    uni.request({
      url: finalUrl,
      data,
      method,
      header: finalHeader,
      ...otherConfig,
      success: (res) => {
        // 这里可以根据你的后端统一响应格式进行处理
        // 例如,假设你的后端返回 { code: 0, data: {}, message: 'success' }
        if (res.statusCode === 200) {
          // 业务状态码判断,假设code为0代表成功
          if (res.data.code === 0) {
            resolve(res.data.data); // 只返回业务数据
          } else {
            // 业务错误,可以统一进行Toast提示
            uni.showToast({
              title: res.data.message || '请求失败',
              icon: 'none',
            });
            reject(res.data); // 拒绝Promise,返回整个响应体,方便外部捕获
          }
        } else {
          // HTTP状态码错误
          uni.showToast({
            title: `网络错误: ${res.statusCode}`,
            icon: 'none',
          });
          reject(new Error(`HTTP Error: ${res.statusCode}`));
        }
      },
      fail: (err) => {
        uni.showToast({
          title: '网络请求失败,请检查网络',
          icon: 'none',
        });
        reject(err);
      },
    });
  });
}

// 也可以导出一些快捷方法,方便使用
export const get = (url, data, config) => request({ url, data, method: 'GET', ...config });
export const post = (url, data, config) => request({ url, data, method: 'POST', ...config });
// ... 可以继续添加 put, delete 等

3.2 在业务代码中愉快地调用

封装好之后,在页面或组件中使用就变得非常清晰和一致了。

// src/api/user.js
import { get, post } from '@/utils/request.js';

// 获取用户信息
export function apiGetUserInfo(userId) {
  // 这里只需要写接口路径,不需要关心平台和完整URL
  return get('/api/user/info', { id: userId });
}

// 用户登录
export function apiUserLogin(loginData) {
  return post('/api/auth/login', loginData);
}

在Vue页面中:

<script>
import { apiGetUserInfo } from '@/api/user.js';

export default {
  data() {
    return {
      userInfo: null
    };
  },
  async onLoad() {
    try {
      // 无论在H5、小程序还是App上,这行代码都能正常工作!
      this.userInfo = await apiGetUserInfo(123);
      console.log('获取到的用户信息:', this.userInfo);
    } catch (error) {
      console.error('获取用户信息失败:', error);
    }
  }
};
</script>

通过这样的封装,我们完美解决了多平台适配问题:

  • H5开发环境:请求 /h5api/api/user/info,通过开发服务器代理到真实后端,无跨域。
  • H5生产环境:需要配合Nginx等服务器配置(见第4节),或者后端开启CORS。
  • 小程序环境:请求 https://your-real-api.com/api/user/info,直接访问,需在微信小程序后台配置该域名。
  • App环境:与小程序类似,直接访问真实地址,但要注意Android和iOS的网络权限及HTTPS要求。

4. 生产环境部署:让H5项目在线上也能畅通无阻

开发时配置了代理,但代码构建打包后,代理配置就消失了。当你把H5项目部署到自己的服务器(如Nginx、Apache)时,浏览器会直接向你的服务器请求页面,然后页面中的JavaScript再去请求后端API,此时又会发生跨域。生产环境有几种主流解决方案。

4.1 方案一:后端配置CORS(推荐)

这是最规范、最符合Web标准的解决方案。需要后端同学在服务器的响应头中添加相应的CORS字段。以Node.js的Express框架为例:

// 后端Node.js (Express) 示例
const express = require('express');
const app = express();

// 全局设置CORS中间件
app.use((req, res, next) => {
  // 允许来自指定域的请求,生产环境应替换为你的前端域名,如 'https://www.your-app.com'
  // 切勿在生产环境使用 '*'
  res.header('Access-Control-Allow-Origin', 'https://www.your-app.com');
  // 允许的请求方法
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  // 允许的请求头
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
  // 允许携带Cookie等凭证
  res.header('Access-Control-Allow-Credentials', 'true');
  
  // 处理预检请求(OPTIONS)
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  next();
});

// ... 你的API路由
app.get('/api/data', (req, res) => {
  res.json({ message: '数据获取成功' });
});

app.listen(3000, () => console.log('服务器运行在端口3000'));

如果后端是Nginx,也可以在Nginx配置中添加:

# Nginx 配置示例
server {
    listen 80;
    server_name your-api.com;

    location / {
        # 设置CORS头
        add_header Access-Control-Allow-Origin 'https://www.your-app.com' always;
        add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always;
        add_header Access-Control-Allow-Headers 'Content-Type, Authorization, X-Requested-With' always;
        add_header Access-Control-Allow-Credentials 'true' always;

        # 处理预检请求
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Max-Age 1728000;
            add_header Content-Type 'text/plain; charset=utf-8';
            add_header Content-Length 0;
            return 204;
        }

        # 你的代理转发规则(如果Nginx作为后端网关)
        proxy_pass http://backend-server;
    }
}

后端配置CORS的好处是前后端完全解耦,前端代码无需任何特殊处理,直接请求完整的后端API地址即可。但前提是你能协调后端同事进行配置。

4.2 方案二:使用Nginx反向代理(前端独立部署时)

如果你的前端H5页面和后端API部署在不同的域名下,且后端不方便配置CORS(比如调用的是第三方服务),那么在前端服务器(如Nginx)上配置反向代理是最佳选择。这其实就是将开发环境的代理思路,搬到生产服务器上。

假设你的前端H5部署在 https://www.your-app.com,而后端API在 https://api.another-service.com

你的Nginx配置可以这样写:

server {
    listen 80;
    listen 443 ssl http2; # 如果启用HTTPS
    server_name www.your-app.com;

    # 前端静态资源目录
    root /path/to/your/uniapp-h5-dist;
    index index.html;

    # 处理前端路由(history模式)
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 关键:配置API请求的代理
    location /api/ {
        # 将 /api/ 开头的请求,代理到真实的后端服务器
        proxy_pass https://api.another-service.com/;
        proxy_set_header Host $proxy_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 可选:修改或添加请求头
        # proxy_set_header Authorization 'Bearer your-token-if-needed';

        # 超时设置
        proxy_connect_timeout 30s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
    }

    # 你也可以代理多个不同的后端服务
    location /user-api/ {
        proxy_pass https://user-service.com/;
        # ... 其他proxy配置
    }
}

配置好后,你前端代码中请求 /api/user/info,Nginx会将其代理到 https://api.another-service.com/user/info。对于浏览器来说,请求始终是发往 www.your-app.com 这个同源域名,因此没有跨域问题。

这里有一个至关重要的细节:我们第3节封装的请求工具,在生产环境H5下,API_HOST 被设置为空,API_PROXY 被设置为 /h5api。但我们的Nginx代理规则是 /api/。为了让代码工作,你有两个选择:

  1. 修改前端配置:将生产环境的 API_PROXY 改为 /api,与Nginx配置匹配。
  2. 修改Nginx配置:将 location /h5api/ 的规则代理到后端。

我通常建议采用第一种,保持开发和生产环境代理路径的一致性(比如都叫 /api),可以减少混淆。你可以在构建时通过环境变量来动态设置 API_PROXY

4.3 方案三:JSONP(仅限GET请求,已逐渐淘汰)

JSONP是一种古老的跨域解决方案,只支持GET请求。其原理是利用 <script> 标签没有跨域限制的特性。uniapp的 uni.request 也支持JSONP。

uni.request({
  url: 'https://api.example.com/data',
  method: 'GET', // JSONP必须是GET
  dataType: 'jsonp', // 指定为jsonp
  success: (res) => {
    console.log(res.data);
  }
});

但JSONP缺点很明显:只支持GET、安全性较低、错误处理机制不完善。在现代Web开发中,除非对接极其古老且不支持CORS的后端,否则不推荐使用。

5. 小程序与App平台的特别注意事项

虽然小程序和App没有浏览器的同源策略,但它们也有自己的网络请求规则,处理不好同样会请求失败。

5.1 微信小程序域名配置

微信小程序要求所有网络请求的域名都必须事先在小程序管理后台的“开发设置”->“服务器域名”中进行配置。分为以下几类:

  • request合法域名: 普通的HTTPS请求。
  • socket合法域名: WebSocket通信。
  • uploadFile合法域名: 文件上传。
  • downloadFile合法域名: 文件下载。

配置步骤

  1. 登录微信公众平台
  2. 进入你的小程序管理后台。
  3. 左侧菜单找到“开发”->“开发设置”。
  4. 在“服务器域名”区域,按要求填写你的后端API域名(如 https://your-real-api.com)。

重要限制

  • 域名必须备案,且支持HTTPS(TLS 1.2及以上)。
  • 一个月内最多可修改5次。
  • 开发阶段,可以在微信开发者工具中勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”,方便调试。但真机预览和上线前必须配置好。

5.2 App平台的网络安全配置

在uni-app打包成App(Android/iOS)时,网络请求默认是允许的。但需要注意:

  • Android: 如果目标API是HTTP而非HTTPS,在Android 9.0(Pie)及以上,默认禁止明文传输。你需要在 manifest.jsonapp-plus -> distribute -> android 节点下配置 "usesCleartextTraffic": true,或者在项目根目录创建 android 原生配置来允许明文流量(不推荐,应尽量使用HTTPS)。
  • iOS: iOS对网络请求有ATS(App Transport Security)要求,默认也强制要求HTTPS。如果需要允许HTTP,需要在 manifest.jsonapp-plus -> distribute -> ios 节点下配置 "ATS" 相关设置,或直接在Xcode工程中修改Info.plist。同样,生产环境强烈建议使用HTTPS。

5.3 封装代码的最终调整

为了完美适配所有平台,我们第3节封装的 config/index.js 可以进一步优化,加入对生产环境H5的判断:

// src/config/index.js
export const SYSTEM_INFO = uni.getSystemInfoSync();

// 判断是否为生产环境(根据编译模式或特定环境变量)
// 方法1:通过process.env.NODE_ENV判断(需在对应构建命令中设置)
const isProduction = process.env.NODE_ENV === 'production';
// 方法2:自定义环境变量,比如在HBuilderX中配置的运行条件
// const isProduction = process.env.UNI_ENV === 'production';

export const HOST = 'https://your-real-api.com';

// 更精细的平台与环境判断
let API_HOST = '';
let API_PROXY = '';

if (SYSTEM_INFO.uniPlatform === 'web') {
  // H5平台
  if (isProduction) {
    // 生产环境H5:假设我们使用Nginx代理,且代理路径为 /api
    // 如果后端配置了CORS,这里也可以直接写 HOST
    API_HOST = ''; // 或者写成你的前端域名,如果Nginx代理配置正确,这里为空即可
    API_PROXY = '/api'; // 与生产环境Nginx配置的 location /api/ 匹配
  } else {
    // 开发环境H5:走开发服务器代理
    API_HOST = '';
    API_PROXY = '/h5api'; // 与vue.config.js或vite.config.js中的代理前缀匹配
  }
} else {
  // 小程序、App等平台:直接使用真实HOST
  API_HOST = HOST;
  API_PROXY = '';
}

export { API_HOST, API_PROXY };

通过这样层层递进的配置,你的请求封装就能智能地在所有开发和生产场景下,选择正确的请求地址,真正实现“写一次,到处跑”。记住,处理跨域的关键在于理解其原理,然后根据不同的运行环境(开发/生产)和平台(H5/小程序/App)选择最合适的解决方案。多动手配置几次,你就会发现它其实并没有想象中那么复杂。

更多推荐