MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue项目中的跨域问题解决方案

2021-03-214.6k 阅读

一、跨域问题产生的原因

在前端开发中,跨域问题是一个常见且重要的议题。跨域问题的产生源于浏览器的同源策略(Same-Origin Policy)。同源策略是一种约定,它由 Netscape 公司引入浏览器,用于限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这里的 “源” 由协议(protocol)、域名(domain)和端口号(port)三部分组成,当这三部分都相同时,才属于同一个源。例如,http://example.com:8080http://example.com:8081 虽然域名相同,但端口号不同,就属于不同的源;http://example.comhttps://example.com 因为协议不同,也属于不同的源。

当浏览器执行一个跨域的 HTTP 请求时,比如从 http://localhost:8080 发起一个对 http://api.example.com 的请求,就会触发跨域问题。浏览器会阻止该请求,并在控制台抛出类似 “Access to XMLHttpRequest at 'http://api.example.com' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.” 的错误信息。这是因为同源策略的存在,它主要是为了保证用户信息的安全,防止恶意网站窃取数据。如果没有同源策略,一个恶意网站就可以轻易通过 JavaScript 获取用户在其他网站的登录信息、个人资料等敏感数据。

二、Vue 项目中常见的跨域场景

  1. 开发环境跨域 在 Vue 项目开发过程中,前端开发服务器(如使用 vue - cli 搭建的项目默认运行在 http://localhost:8080)与后端 API 服务器(可能运行在 http://localhost:3000 或者其他地址)往往不在同一个源。例如,后端使用 Node.js 和 Express 搭建的 API 服务,前端在开发时需要调用这些 API 接口获取数据,这就会产生跨域问题。在这种场景下,由于是开发阶段,我们需要一种方便快捷的方式来解决跨域问题,以便能够顺利进行前后端联调。
  2. 生产环境跨域 当 Vue 项目部署到生产环境时,也可能面临跨域问题。比如前端项目部署在 https://www.example.com,而后端 API 服务部署在 https://api.example.com,虽然都是 example.com 域名,但由于协议和子域名的不同,仍然属于不同的源,会触发跨域。在生产环境中解决跨域问题需要更加严谨,要考虑到安全性、性能以及部署的便利性等多方面因素。

三、Vue 项目中跨域问题的解决方案

  1. 使用代理服务器(开发环境常用)
    • 原理:在开发环境中,我们可以利用 webpack 的 proxy 功能来设置代理服务器。其原理是通过在前端开发服务器(如 http://localhost:8080)和后端 API 服务器(如 http://localhost:3000)之间设置一个代理,前端请求先发送到代理服务器,代理服务器再将请求转发到真实的后端 API 服务器,由于代理服务器和前端开发服务器属于同一个源,所以不会触发跨域问题。代理服务器接收到后端 API 服务器的响应后,再将响应返回给前端。
    • 配置步骤
      • 首先,确保你的 Vue 项目是使用 vue - cli 创建的。在项目根目录下找到 vue.config.js 文件,如果没有则创建一个。
      • vue.config.js 文件中添加如下配置:
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
};

在上述配置中,/api 是代理的路径前缀。当我们在前端代码中发起形如 axios.get('/api/users') 的请求时,实际请求会被代理到 http://localhost:3000/userstarget 表示目标后端 API 服务器的地址,changeOrigin 设置为 true 表示允许跨域,pathRewrite 用于重写路径,将 /api 前缀去掉,这样后端接收到的请求路径就是正确的。

  • 前端代码示例: 假设我们使用 axios 来发送请求,在 Vue 组件中可以这样写:
<template>
  <div>
    <button @click="fetchData">获取数据</button>
    <ul>
      <li v - for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      users: []
    };
  },
  methods: {
    async fetchData() {
      try {
        const response = await axios.get('/api/users');
        this.users = response.data;
      } catch (error) {
        console.error('获取数据失败', error);
      }
    }
  }
};
</script>
  1. CORS(跨域资源共享,生产环境常用)
    • 原理:CORS 是一种基于 HTTP 头的机制,它允许服务器声明哪些源站有权限访问哪些资源。当浏览器发起一个跨域请求时,首先会发送一个预检请求(OPTIONS 请求),询问服务器是否允许该跨域请求。如果服务器在响应头中包含了正确的 CORS 相关头信息,如 Access - Control - Allow - OriginAccess - Control - Allow - MethodsAccess - Control - Allow - Headers 等,浏览器就会认为该请求是被允许的,从而继续发送实际的请求。
    • 服务器端配置示例(以 Node.js 和 Express 为例)
const express = require('express');
const app = express();

// 允许所有源访问,生产环境应替换为具体的源
app.use((req, res, next) => {
  res.setHeader('Access - Control - Allow - Origin', '*');
  res.setHeader('Access - Control - Allow - Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access - Control - Allow - Headers', 'Content - Type, Authorization');
  next();
});

// 模拟 API 接口
app.get('/users', (req, res) => {
  const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ];
  res.json(users);
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

在上述代码中,Access - Control - Allow - Origin 设置为 * 表示允许所有源访问,这在开发阶段比较方便,但在生产环境中应替换为具体的前端应用源,如 https://www.example.comAccess - Control - Allow - Methods 声明了允许的 HTTP 方法,Access - Control - Allow - Headers 声明了允许的请求头。

  • 前端代码示例(与代理服务器方式类似)
<template>
  <div>
    <button @click="fetchData">获取数据</button>
    <ul>
      <li v - for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      users: []
    };
  },
  methods: {
    async fetchData() {
      try {
        const response = await axios.get('http://localhost:3000/users');
        this.users = response.data;
      } catch (error) {
        console.error('获取数据失败', error);
      }
    }
  }
};
</script>
  1. JSONP(较老的解决方案,部分场景仍适用)
    • 原理:JSONP 利用了 <script> 标签没有跨域限制的特性。它通过动态创建一个 <script> 标签,将请求地址作为 src 属性值,并且在请求地址中添加一个回调函数名参数。服务器接收到请求后,将数据以回调函数调用的形式返回,前端页面接收到响应后,会自动执行这个回调函数,从而达到获取数据的目的。
    • 前端代码示例
<template>
  <div>
    <button @click="fetchData">获取数据</button>
    <ul>
      <li v - for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      users: []
    };
  },
  methods: {
    fetchData() {
      const script = document.createElement('script');
      const callbackName = 'jsonpCallback';
      window[callbackName] = (data) => {
        this.users = data;
        document.body.removeChild(script);
      };
      script.src = `http://localhost:3000/users?callback=${callbackName}`;
      document.body.appendChild(script);
    }
  }
};
</script>
  • 服务器端配置示例(以 Node.js 和 Express 为例)
const express = require('express');
const app = express();

app.get('/users', (req, res) => {
  const callback = req.query.callback;
  const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ];
  res.send(`${callback}(${JSON.stringify(users)})`);
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

JSONP 虽然能解决跨域问题,但它只支持 GET 请求,并且存在一定的安全风险,如容易受到 XSS 攻击。所以在现代开发中,除非是一些特殊场景,如与不支持 CORS 的第三方 API 交互,一般较少使用。 4. 使用 Nginx 反向代理(生产环境常用)

  • 原理:Nginx 是一款高性能的 HTTP 和反向代理服务器。通过配置 Nginx 的反向代理功能,可以将前端应用的跨域请求转发到后端 API 服务器。当用户请求前端应用时,Nginx 接收到请求,如果是跨域请求,Nginx 会将其转发到后端 API 服务器,后端 API 服务器返回响应后,Nginx 再将响应返回给用户。由于用户请求和响应都是通过 Nginx 进行的,对于前端应用来说,不存在跨域问题。
  • Nginx 配置示例: 假设前端应用部署在 /var/www/html,后端 API 服务器运行在 http://backend.example.com:8080。在 Nginx 的配置文件(如 /etc/nginx/sites - available/default)中添加如下配置:
server {
    listen 80;
    server_name www.example.com;

    location / {
        root /var/www/html;
        index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }

    location /api {
        proxy_pass http://backend.example.com:8080;
        proxy_set_header Host $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;
    }
}

在上述配置中,/api 路径的请求会被代理到 http://backend.example.com:8080proxy_set_header 用于设置一些请求头信息,保证后端服务器能够正确识别请求来源等信息。配置完成后,使用 sudo ln -s /etc/nginx/sites - available/default /etc/nginx/sites - enabled/ 启用配置,并通过 sudo systemctl restart nginx 重启 Nginx 服务。 5. 使用 iframe 跨域(特定场景)

  • 原理:通过在页面中嵌入一个 <iframe> 标签,该 <iframe> 的源与当前页面不同。然后利用 postMessage 方法在父窗口和 <iframe> 子窗口之间进行跨域通信。postMessage 方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现安全的跨域数据传输。
  • 前端代码示例: 父页面(parent.html):
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF - 8">
    <meta name="viewport" content="width=device - width, initial - scale=1.0">
    <title>Parent Page</title>
</head>

<body>
    <iframe id="childFrame" src="child.html"></iframe>
    <script>
        const childFrame = document.getElementById('childFrame');
        childFrame.contentWindow.postMessage('Hello from parent', 'http://child - domain.com');
        window.addEventListener('message', (event) => {
            if (event.origin === 'http://child - domain.com') {
                console.log('Received from child:', event.data);
            }
        });
    </script>
</body>

</html>

子页面(child.html):

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF - 8">
    <meta name="viewport" content="width=device - width, initial - scale=1.0">
    <title>Child Page</title>
</head>

<body>
    <script>
        window.addEventListener('message', (event) => {
            if (event.origin === 'http://parent - domain.com') {
                console.log('Received from parent:', event.data);
                event.source.postMessage('Hello from child', event.origin);
            }
        });
    </script>
</body>

</html>

在 Vue 项目中使用 iframe 跨域时,可以将相关的逻辑封装在 Vue 组件中。但需要注意的是,iframe 跨域方式会增加页面的复杂性,并且可能存在性能问题,一般适用于特定的跨域需求,如嵌入第三方页面并与之交互等场景。

四、不同解决方案的优缺点及适用场景

  1. 代理服务器(开发环境常用)
    • 优点
      • 配置简单,在开发环境中通过修改 vue.config.js 文件即可完成配置,不需要对后端代码进行大量修改。
      • 方便开发和调试,能够快速搭建起前后端联调的环境。
    • 缺点
      • 只适用于开发环境,生产环境中不能使用这种方式,因为代理服务器是基于开发服务器的,生产环境的部署架构与开发环境不同。
    • 适用场景:Vue 项目开发阶段,前端与后端进行联调时,后端 API 服务器与前端开发服务器不在同一个源的情况。
  2. CORS(跨域资源共享,生产环境常用)
    • 优点
      • 是一种标准的跨域解决方案,被现代浏览器广泛支持。
      • 安全性较高,通过设置 Access - Control - Allow - Origin 等头信息,可以精确控制哪些源可以访问资源。
      • 支持多种 HTTP 方法和请求头,适用范围广。
    • 缺点
      • 需要后端服务器进行配合配置,如果后端是第三方 API 且不支持 CORS,就无法使用。
      • 配置相对复杂,需要正确设置各种 CORS 相关的头信息,特别是在处理预检请求等情况时。
    • 适用场景:生产环境中,当前后端都由自己团队开发维护,且后端能够进行 CORS 配置的情况。适用于大多数前后端分离项目的生产部署场景。
  3. JSONP(较老的解决方案,部分场景仍适用)
    • 优点
      • 兼容性好,在一些较老的浏览器中也能使用。
      • 不需要后端进行复杂的 CORS 配置,只需要按照 JSONP 的格式返回数据即可。
    • 缺点
      • 只支持 GET 请求,对于需要使用 POST、PUT 等其他 HTTP 方法的场景不适用。
      • 存在安全风险,容易受到 XSS 攻击,因为它是通过动态创建 <script> 标签来实现的,恶意脚本可能会被注入。
    • 适用场景:与一些不支持 CORS 的第三方 API 进行交互,且只需要进行 GET 请求获取数据的场景。例如,某些第三方统计服务的 API 调用。
  4. 使用 Nginx 反向代理(生产环境常用)
    • 优点
      • 性能高,Nginx 本身就是一款高性能的服务器,能够高效地处理请求转发。
      • 安全性好,可以通过 Nginx 的配置进行一些安全设置,如限制请求频率、设置防火墙规则等。
      • 适用于生产环境的复杂部署架构,可以与负载均衡等功能结合使用。
    • 缺点
      • 配置相对复杂,需要对 Nginx 的配置语法有一定的了解。
      • 部署和维护成本相对较高,需要专门的服务器来运行 Nginx 并进行管理。
    • 适用场景:生产环境中,当前端应用和后端 API 服务器部署在不同的服务器或域上,且需要高性能、高安全性的跨域解决方案时。适用于大型企业级项目的生产部署。
  5. 使用 iframe 跨域(特定场景)
    • 优点
      • 可以在特定场景下实现跨域通信,如嵌入第三方页面并与之进行有限的交互。
      • 对于一些已经存在的、难以修改的第三方页面,通过 iframe 嵌入并跨域通信是一种可行的方式。
    • 缺点
      • 增加页面复杂性,由于引入了 <iframe>,页面结构和脚本逻辑会变得更加复杂。
      • 性能问题,<iframe> 会增加页面的加载时间和资源消耗,并且与父页面之间的通信存在一定的性能开销。
    • 适用场景:特定的跨域需求,如嵌入第三方广告页面、地图等,并需要与之进行简单交互的场景。但要谨慎使用,避免过度影响页面性能和用户体验。

五、在 Vue 项目中选择合适解决方案的建议

  1. 开发阶段 在 Vue 项目的开发阶段,优先选择使用代理服务器的方式来解决跨域问题。因为这种方式配置简单,能够快速搭建起前后端联调的环境,方便开发人员进行开发和调试。通过在 vue.config.js 中简单配置 proxy 选项,就可以轻松实现请求的代理转发,无需后端进行额外的配置。这样可以让前端开发人员专注于业务逻辑的实现,提高开发效率。
  2. 生产阶段
    • 如果前后端都由自己团队开发维护,且后端能够进行 CORS 配置,那么 CORS 是生产环境中首选的跨域解决方案。它是一种标准的、安全的跨域方式,能够精确控制哪些源可以访问后端资源。通过在后端服务器的响应头中正确设置 CORS 相关的头信息,如 Access - Control - Allow - OriginAccess - Control - Allow - Methods 等,可以有效地解决跨域问题,同时保证系统的安全性。
    • 当后端 API 是第三方提供且不支持 CORS 时,如果只需要进行 GET 请求获取数据,可以考虑使用 JSONP 方式。但要注意其安全性问题,在使用过程中做好防范措施,如对回调函数名进行严格的校验,避免 XSS 攻击。
    • 对于大型企业级项目,生产环境部署架构复杂,需要高性能、高安全性的跨域解决方案时,使用 Nginx 反向代理是一个不错的选择。通过合理配置 Nginx,可以实现请求的高效转发,同时结合 Nginx 的安全功能,如设置防火墙规则、限制请求频率等,保障系统的安全运行。但这种方式需要有专业的运维人员进行部署和维护。
  3. 特殊场景 在一些特殊场景下,如需要嵌入第三方页面并与之进行交互,可以考虑使用 iframe 跨域方式。但要充分评估其对页面性能和复杂性的影响,尽量优化代码,减少性能开销。例如,在嵌入第三方地图或广告页面时,通过 postMessage 方法进行安全的跨域通信,实现与第三方页面的交互功能。

在 Vue 项目中解决跨域问题,需要根据项目的具体情况,包括开发阶段、生产环境的部署架构、后端 API 的情况以及业务需求等,综合选择合适的解决方案。同时,要注重安全性和性能,确保项目能够稳定、高效地运行。