Skip to content

Latest commit

 

History

History
422 lines (303 loc) · 12.7 KB

230109-2-前端跨域-简单易懂的跨域解决方案.md

File metadata and controls

422 lines (303 loc) · 12.7 KB

前端跨域??简单易懂的跨域解决方案

前端跨域通信:了解前端跨域通信的常用方法,如何使用跨域通信来实现功能。

什么是浏览器跨域?

浏览器跨域是指浏览器不允许脚本从不同的源(协议域名端口)访问资源

跨域问题常见于前后端分离的开发模式,例如使用 React 开发前端,使用 Node.js 开发后端的应用。

在前后端开发过程中,需要解决跨域问题,才能在浏览器中正常访问后端资源。

以下是几个跨域的例子:

  1. https://example1.com 的页面中,通过 <script> 标签加载 https://example2.com/script.js,这是一个跨域的请求。

  2. https://example1.com 的页面中,通过 XMLHttpRequesthttps://example2.com/api 发起一个 HTTP 请求,这是一个跨域的请求。

  3. https://example1.com 的页面中,通过 <iframe> 标签嵌入 https://example2.com/page,这是一个跨域的请求。

  4. https://example1.com 的页面中,通过 WebSocket 建立连接 wss://example2.com,这是一个跨域的请求。

了解前端跨域通信的常用方法

  1. 使用代理服务器:前端向代理服务器发起请求,代理服务器再向目标服务器发起请求,这样就可以实现跨域访问。
  2. 使用服务器端转发:前端向后端服务器发起请求,后端服务器再向目标服务器发起请求,这样就可以实现跨域访问。
  3. 使用 CORS(Cross-Origin Resource Sharing):浏览器会在发送跨域请求时,向服务器发送一个预检请求,服务器返回允许跨域访问的响应头,浏览器再发送正式的请求。

下面两种 (4、5),我没有用过

  1. 使用 JSONP(JSON with Padding):通过动态创建 script 标签的方式,在浏览器端加载远程脚本。
  2. 使用 postMessage:在不同源的窗口、框架、文件之间进行通信,使用 postMessage() 方法发送和接收消息。
  3. 使用 WebSocket:基于 TCP 协议的通信协议,浏览器和服务器可以建立双向的、全双工的连接,实现前后端双向通信。

1 和 2 有什么不一样

使用代理服务器和使用服务器端转发两种方法都是通过在浏览器端和服务器端之间加入一个中间层来实现跨域访问的。

但是,它们有一些区别:

  • 使用代理服务器时,前端向代理服务器发起请求,代理服务器再向目标服务器发起请求。这种方式适用于前端需要访问的服务器支持 CORS 的情况。
  • 使用服务器端转发时,前端向后端服务器发起请求,后端服务器再向目标服务器发起请求。这种方式适用于前端需要访问的服务器不支持 CORS 的情况。

使用代理服务器或服务器端转发时,需要注意安全性和效率问题。

如何使用跨域通信来实现功能

使用代理服务器

vite(vue2,vue3)

https://cn.vitejs.dev/config/server-options.html#server-proxy

vue-cli(webpack)

在 vue.config.js 中配置代理服务器:

module.exports = {
  devServer: {
    // 导出对象的 devServer 属性
    proxy: {
      // devServer 对象的 proxy 属性
      '/api': {
        // 代理规则的路径,所有以 /api 开头的请求将被代理
        target: process.env.VUE_APP_API_URL, // 目标 URL,即代理请求的目标地址,这里的值是环境变量 VUE_APP_API_URL 的值
        changeOrigin: true, // 是否改变代理请求的源地址(origin)为目标 URL
        secure: false, // 是否对目标 URL 的 SSL 证书进行验证
        pathRewrite: {
          // 路径重写规则
          '^/api': '' // 将 /api 前缀去掉
        }
      }
    }
  }
};

create-react-app

在 package.json 中配置代理服务器:

{
  "proxy": "http://localhost:4000",
}

详细参考地址: https://create-react-app.dev/docs/proxying-api-requests-in-development#configuring-the-proxy-manually

nuxt.js

在 nuxt.config.js 中配置代理服务器:

module.exports = {
  serverMiddleware: [
    // 导出对象的 serverMiddleware 属性
    {
      // 中间件数组的第一项
      path: '/api', // 中间件的路径
      handler: '~/api/index.js' // 中间件的处理器,即中间件所执行的文件
    }
  ],
  proxy: {
    // 导出对象的 proxy 属性
    '/api': {
      // 代理规则的路径,所有以 /api 开头的请求将被代理
      target: process.env.NUXT_ENV_API_URL, // 目标 URL,即代理请求的目标地址,这里的值是环境变量 NUXT_ENV_API_URL 的值
      changeOrigin: true, // 是否改变代理请求的源地址(origin)为目标 URL
      secure: false, // 是否对目标 URL 的 SSL 证书进行验证
      pathRewrite: {
        // 路径重写规则
        '^/api': '' // 将 /api 前缀去掉
      }
    }
  }
};

nest.js

page/api/目录下创建[[...proxy]].ts

import {NextApiRequest, NextApiResponse} from 'next';
import httpProxyMiddleware from 'next-http-proxy-middleware';

export const config = {
  api: {
    bodyParser: false
  }
};

const apiproxy = async (req: NextApiRequest, res: NextApiResponse) => {
  return httpProxyMiddleware(req, res, {
    target: process.env.NEXT_PUBLIC_API_URL,
    changeOrigin: true,
    secure: false,
    pathRewrite: [
      {
        patternStr: '^/api/',
        replaceStr: ''
      }
    ],
    onProxyInit: (proxy) => {
      proxy.on('proxyReq', (proxyReq, req, res) => {});
      proxy.on('proxyRes', (proxyRes, req, res) => {});
    }
  });
};

export default apiproxy;

在后端服务器端使用中间件代理:

在后端服务器端使用中间件代理,可以通过使用 http-proxy-middleware 或 http-proxy 库来实现。

例如,使用 http-proxy-middleware 库:

const proxy = require('http-proxy-middleware');

app.use(
  '/api',
  proxy({
    target: process.env.API_URL,
    changeOrigin: true,
    secure: false,
    pathRewrite: {
      '^/api': ''
    }
  })
);

nginx

nginx 是一个开源的、高性能的 HTTP 服务器和反向代理服务器,可以用来代理跨域请求。

要使用 nginx 代理跨域请求,需要在 nginx 配置文件中加入以下内容:

server {
  listen 80;
  server_name example.com;

  location /api {
    proxy_pass http://api.example.com;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
  }
}

这样,所有对 http://example.com/api 的请求都会被代理到 http://api.example.com。

注意:nginx 代理跨域请求时,需要注意安全性,避免被恶意攻击。

使用服务器端转发

在后端服务器端使用中间件实现转发,例如:

Express:

const axios = require('axios');
const express = require('express');
const app = express();

app.use('/api', (req, res) => {
  axios
    .get(process.env.API_URL + req.url)
    .then((response) => res.send(response.data))
    .catch((error) => res.send(error));
});

使用 CORS(Cross-Origin Resource Sharing)

方法 1 使用 cors 模块

下面设置仅仅开发环境使用 cors

const cors = require('cors');

if (process.env.NODE_ENV !== 'production') {
  app.use(cors());
}

方法 2 中间件

使用 CORS,需要在服务器端设置响应头,允许浏览器进行跨域访问。例如:

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  next();
});

使用 JSONP(JSON with Padding)

使用 JSONP 跨域通信的基本原理是,

  • 在前端的 JavaScript 代码中动态创建一个 script 标签,
  • 然后将请求的 URL 作为 script 标签的 src 属性,
  • 最后再将 script 标签插入到文档中。

后端接收到请求后,

  • 会将数据封装成一个 JavaScript 函数,
  • 并将函数名作为参数返回。
  • 前端收到数据后,
  • 会执行这个 JavaScript 函数,
  • 从而实现跨域通信。

下面是一个使用 node.js 和 express 框架实现 JSONP 跨域通信的示例:

const express = require('express');
const app = express();

app.get('/jsonp', (req, res) => {
  // 获取前端请求中的 callback 函数名
  const callbackName = req.query.callback;
  // 要返回的数据
  const data = {name: 'John', age: 30};
  // 返回数据的函数体
  const jsonp = `${callbackName}(${JSON.stringify(data)})`;
  // 设置响应头
  res.setHeader('Content-Type', 'application/javascript');
  // 返回数据
  res.send(jsonp);
});

app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

前端 JavaScript 代码如下:

function getData(data) {
  console.log(data); // {name: "John", age: 30}
}

const script = document.createElement('script');
script.src = 'http://localhost:3000/jsonp?callback=getData';
document.body.appendChild(script);

在这个例子中,

  • 前端通过动态创建 script 标签并设置 src 属性,发出了一个 JSONP 请求。
  • 后端接收到请求后,使用前端请求中的 callback 函数名封装了数据,
  • 并将数据以 JavaScript 函数的形式返回给前端。
  • 前端收到数据后,会执行 callback 函数,从而实现跨域通信。

JSONP 在现代 Web 开发中已经不常使用,因为现在的浏览器都支持 CORS(Cross-Origin Resource Sharing)跨域通信。

但是,由于 JSONP 只需要在服务器端做很少的配置,而且兼容性比较好(只要浏览器支持 JavaScript 就可以使用),所以仍然有人使用 JSONP 跨域通信。

下面是一些使用 JSONP 跨域通信的注意事项:

  • JSONP 只能用于 GET 请求,不支持其他类型的 HTTP 请求。
  • JSONP 只能用于跨域请求,不能在同源的情况下使用。
  • JSONP 只能接收 JavaScript 代码,不能接收其他类型的数据。
  • JSONP 只能获取到预先约定好的数据,无法获取动态生成的数据。
  • JSONP 安全性较差,不支持身份认证和授权,不建议在敏感信息的跨域通信中使用。

最后,我想再提醒你一次,在现代 Web 开发中,最好使用 CORS 跨域通信,而不是 JSONP。

使用 postMessage

使用 postMessage,需要在发送消息的窗口、框架、文件中调用 postMessage() 方法,并在接收消息的窗口、框架、文件中监听 message 事件。例如:

发送消息的窗口:

window.parent.postMessage('Hello, Parent!', '*');

接收消息的窗口:

window.addEventListener('message', (event) => {
  console.log(event.data);
});

使用 WebSocket

在现实中,WebSocket 常见于聊天应用、实时数据应用、游戏应用等。

WebSocket 是基于 TCP 协议的通信协议,浏览器和服务器可以建立双向的、全双工的连接。通过 WebSocket,可以实现前后端双向通信,支持跨域通信。

与 HTTP 不同的是,WebSocket 通信是以帧(frame)的形式传输数据的,每一帧都有自己的标识符和数据。浏览器和服务器可以通过 WebSocket API 发送和接收帧。

要使用 WebSocket,需要在前端和后端都安装 WebSocket 库,例如 ws 或 socket.io。

在前端,可以使用 WebSocket 对象连接服务器:

const socket = new WebSocket('ws://example.com');

socket.onopen = () => {
  console.log('WebSocket opened');
};

socket.onmessage = (event) => {
  console.log(event.data);
};

socket.onclose = () => {
  console.log('WebSocket closed');
};

在后端,可以使用 WebSocket 库创建 WebSocket 服务器:

const WebSocket = require('ws');

const wss = new WebSocket.Server({port: 8080});

wss.on('connection', (ws) => {
  ws.on('message', (message) => {
    console.log(message);
  });

  ws.send('Hello from the server');
});

WebSocket 服务器可以接收来自客户端的连接请求,并与客户端建立连接。在连接建立后,服务器可以接收客户端发送的帧,并向客户端发送帧。

通过 WebSocket,前后端可以实现双向通信,解决跨域问题。

遇到的问题

老项目运行起来很慢?

改用 vite

注意:有的配置 vite 和 webpack,要统一修改,如果是商业化的项目,不建议修改

为什么不建议老项目升级到 vite 工具?

成本,改完内容之后,出现问题谁负责?

改完内容,要求测试介入

如何让每一次的代理请求,都在控制台显示

logLevel: 'debug'

 proxy: {
      // 配置跨域
      '/api': {
          target: 'https://example.com',
          ws: true,
          changeOrigin: true,
          logLevel: 'debug'
          // ,
          // pathRewrite: {
          //     '^/api': ''
          // }
      }
  }