vue跨域方案指北

16,788 阅读12分钟

跨域是指违背了浏览器同源策略的限制

简单的说,我们通常使用的请求工具都是基于XMLHttpRequest对象构建的,它是严格遵守同源策略的,所以当需要跨域时,基于它的工具就无法工作了,我们需要一些额外的手段来解决这个问题 本文主要对已知的几种跨域方案进行实践和总结

一、jsonp

jsonp虽然不是官方的跨域方案,但它是最早出现的跨域方案,另外不管你是怎么使用jsonp跨域,==服务端支持这种跨域是前提==。

缺点

由于jsonp是使用script、img、iframe没有同源限制的标签,所以它只支持get请求

另外jsonp错误处理机制不完善

优点

jsonp的优点是它可以兼容低版本的浏览器

1.原理

jsonp是使用script、img、iframe没有同源限制的标签,向服务端发送请求,将返回的数据作为指定的回调函数的参数,script标签引入的函数是属于window全局的,所以你只需要在另一个script中指定回调函数,这样就可以获取到服务端数据了

这是个最基础的jsonp跨域例子

// test.html
<script>
  function test(data) {
    console.log(data);
    return data;
  }
</script>
<script
  type="text/javascript"
  src="http://127.0.0.1:8090/v1/system/user/getTotal?callback=test"
></script>

控制台打印如下

此时在newwork中查看preview,返回的是一个函数,假如回调函数存在的话就执行它

上面只是说说它的原理而已,接下来进入正题,vue是如何使用的

2.封装

vue要使用jsonp,可以借助第三方工具包,推荐使用jsonp,假如你喜欢axios风格,用起来肯定得心应手。

a.下载
cnpm i jsonp --save
b.引入

新建一个文件,用来放我们封装的jsonp工具

// http.js
import jsonp from "jsonp"

根据jsonp的文档,我们知道jsonp需要三个参数

另外还需要重新定义一个配置对象

var options = {
    parma: "callback",
    timeout: 5000,
    prefix: "",
    name: "callbackFun"
  };
  // jsonp会根据这个配置拼接url
  // 例如上面的配合拼接后是:url+?callback=callbackFun
  // 假如url中已经有参数了:url+&callback=callbackFun

然后返回一个Promise风格的jsonp函数,将url作为形参,options作为实参,另外加一个回调函数

// http.js
export function get(url) {
  var options = {
    parma: "callback",
    timeout: 5000,
    prefix: "",
    name: "callbackFun"
  };
    return new Promise((resolve, reject) => {
    
        jsonp(url, options, (err, res) => {
            if (err) {
            // 可以在这里对错误进行处理,比如引入element-ui等组件库的toast等
            // 将错误提示给用户
            // 也可以根据不同http状态码,导入不同的页面
                reject(err);
              } else {
                resolve(res);
            }
        });
    });
}

有的时候,我们还需要给服务端传参数,这时候就需要将参数对象拼接到URL中去,我们也可以将此封装下

// 将Parma参数拼接到url上
function joinParma(url, parma) {
  let str = "";
  for (const key in parma) {
    str += key + "=" + parma[key] + "&";
  }
  str = str.substring(0, str.length - 1);
  str = url.indexOf("?") == -1 ? "?" + str : "&" + str;
  return url + str;
}

所以完整的代码是这样的

import jsonp from "jsonp"
import router from './router'

// 将Parma参数拼接到url上
function joinParma(url, parma) {
  let str = "";
  for (const key in parma) {
    str += key + "=" + parma[key] + "&";
  }
  str = str.substring(0, str.length - 1);
  str = url.indexOf("?") == -1 ? "?" + str : "&" + str;
  return url + str;
}

export function get(url, parma) {
  var options = {
    parma: "callback",
    timeout: 5000,
    prefix: "",
    name: "callbackFun"
  };
  // 如果有参数就拼接参数,否则不作处理
  if (parma) {
    
    url = joinParma(url, parma);
  }
  return new Promise((resolve, reject) => {

    jsonp(url, options, (err, res) => {
      if (err) {
        // 可以在这里对错误进行处理,比如引入element-ui等组件库的toast等
        // 将错误提示给用户
        // 也可以根据不同http状态码,导入不同的页面
         switch (err.response.status) {
            case 500:
               router.push({
                 path: "/404"
               });
               break;
             case 401:
               router.push({
                 path: "/401"
               });
               break;
        }
        reject(err);
      } else {
        // 你也可以根据与后端约定的消息处理机制,作出提示
        // 你可以引入你使用的UI库的toast模块提示给用户
        if(res.data.code == 200){
            console.log("操作成功")
        }else if(res.data.code == 300){
            console.log("没有这条数据或者查询失败")
        }else{
            console.log("操作失败")
        }
        resolve(res);
      }
    });
  });
}

3.使用

在我们的接口文件中引入封装好的jsonp,就可以根据自己情况使用了

// api/getTotal.js
import { get } from '@/http/jsonp'

export function getTotal(data){
    return get("http://127.0.0.1:8090/v1/system/user/getTotal",data)
}

接着在要使用的组建中引入接口函数

// list.vue
<script>
import { getTotal } from "@/api/getTotal"

export default {
    data(){
        total: 0,
        user:{
            id: 1,
            name: 'zs'
        }
    },
    created(){
        getTotal(this.user)
            .then((res)=>{
                if (res.data.code == 400){
                    this.total = res.data.total
                }
            })
            .catch(err=>{
                console.log(err)
            })
    }
}
</script>

二、CORS(跨域资源共享)

CORS仍然是使用XMLHttpRequest发送请求,只是多了些字段告诉服务器允许跨域,同样,==它也需要服务端的支持==

优点

可以访问多种请求方式

处理机制完善

符合http规范,对于复杂请求,多一次验证,安全性更好

缺点

不支持IE10以下浏览器

为了说明,这里给出一个测试的服务端代码

const http = require("http");

const server = http
  .createServer((req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end("Hello World\n");
  })
  .listen(8090);
console.log("Server running at http://127.0.0.1:8090/");

和一个客户端代码,如果你感兴趣可以将它在纯html页面和vue项目各放一份

let ajax;
  if (XMLHttpRequest) {
    ajax = new XMLHttpRequest();
  } else {
    ajax = new ActiveXObject();
  }
  ajax.onreadystatechange = function(res) {
    if (ajax.readyState == 4 && ajax.status == 200) {
      console.log(ajax.responseText);
    }
  };
  ajax.open("GET", "http://127.0.0.1:8090", true);
  ajax.send();

1.原理

跨域资源共享(CORS)是http官方推荐的跨域方案,它是一种机制,使用额外的HTTP头来告诉浏览器,让运行在一个origin (domain)上的Web应用被准许访问来自不同源服务器上的指定的资源,这是MDN对CORS的标准定义,简单说是在发送请求的时候添加一个名为origin的字段给服务器,服务在判断它是可以接受的范围,并且返回一个Access-Control-Allow-Origin字段

例如,在上面的例子中,不管是vue项目还是单独的html页面,都可以在控制台看到服务端返回的"hello world",

并且字段如下

// request header
// vue项目
"Origin": "http://localhost:8080"
// html页面
"Origin": null


// response header
"Access-Control-Allow-Origin":"*"

当将服务端的字段值改变为http://localhost:8080,发现只有http://localhost:8080的vue项目可以正常跨域,html页面报错

Access to XMLHttpRequest at 'http://127.0.0.1:8090/' from origin 'null' 
has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header 
has a value 'http://localhost:8080' that is not equal to the supplied origin.

==注意,http://localhost:8080和http://127.0.0.1:8080对服务端来说是不一样的==

CORS根据是否使用默认支持的字段和方法将请求分为“简单请求”与“非简单请求”,浏览器会根据不同类型作出不同处理,它们的区别是简单请求不会调用验证机制,而非简单请求则会调用验证机制

对于(CORS默认)简单请求,它的请求方法不超过下面三种,头部信息不超过以下字段

(1) 请求方法:
     HEAD
     GET
     POST
(2)HTTP的头信息:
     Accept
     Accept-Language
     Content-Language
     Last-Event-ID
     Content-Type:
         application/x-www-form-urlencoded、 multipart/form-data、text/plain

==注意==简单请求:

  1. 不能接收或者发送cookie
  2. 不能使用setRequestHeader()设置自定义头部
  3. 调用getAllResponseHeader()得到的事空字符串

假如我们的需要使用上面列出的以外的方法或者字段,就是属于非简单请求了,对于这种请求,浏览器会先调用Preflighted Request透明服务器的验证机制,使用OPTIONS方法和服务器验证,通过验证,浏览器才会正式请求服务器。

前面说了CORS默认(简单请求)是不支持调用setRequestHeader和getAllResponseHeader,在非简单请求里,可以使用OPTIONS方法突破这种默认限制

// 客户端
ajax.open("GET", "http://127.0.0.1:8090", true);
ajax.setRequestHeader("X-PINGOTHER", "pingpong");
ajax.setRequestHeader("Content-Type", "application/xml");
ajax.send();

// 服务端需要设置允许改变的字段
res.setHeader(
      "Access-Control-Allow-Headers",
      "Access-Control-Allow-Headers, Origin,Accept,X-PINGOTHER,X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers"
    );

这时network中会多出一个验证的请求

假如服务端没有设置这个字段权限,那它就会在控制台报错

Request header field x-pingother is not allowed by Access-Control-Allow-Headers in preflight response.

假如你需要发送或者接受cookie,可以将实例的withCredentials属性设为true,同时也需要服务器配合,允许接收带cookie的请求,响应头部信息会返回下面的字段和值

// 客户端
ajax.open("GET", "http://127.0.0.1:8090", true);
ajax.withCredentials = true;
ajax.send();

// 服务端
res.setHeader("Access-Control-Allow-Credentials", true);

// 此时响应报文中多了下面的字段
"Access-Control-Allow-Credentials": true

假如服务端将Credentials设置为false,那么客户端会触发onerror程序,并且status为0,responseText为空字符串

res.setHeader("Access-Control-Allow-Credentials", false);

2.封装

在vue项目中使用axios,对它进行业务的封装

a.下载
cnpm i axios -s
b.引入

新建http/request.js,存放我们封装之后的axios文件

// http/request.js
// 引入
import axios from "axios";

// 创建一个实例
const server = axios.create({
    // 将我们访问的地址设为baseURL
    baseURL: "http://127.0.0.1:8090",
    // 设置超时时间
    timeout: 5000,
    headers:{
        "Content-Type":"text/plain",
        "Access-Control-Allow-Credentials": true
    }
});

在项目里通常需要对请求做统一处理,比如,给每个请求带上cookie,对所以的响应作出反应,根据不同的错误状态导入不同的页面,借用axios的拦截器,可以做到我们想要的效果

// 设置拦截器
// 请求拦截器
server.interceptors.request.use(
  config => {
    // 每个请求都带token与服务器进行身份匹配
    config.headers.token = localStorage.getItem("token");
    return config;
  },
  error => {
    console.log(error);
    Promise.reject(error);
  }
);
// 响应拦截器
server.interceptors.response.use(
  response => {
    // 你也可以根据与后端约定的消息处理机制,作出提示
    // 你可以引入你使用的UI库的toast模块提示给用户
    if(response.data.code == 200){
        console.log("操作成功")
    }else if(response.data.code == 300){
        console.log("没有这条数据或者查询失败")
    }else{
        console.log("操作失败")
    }
    return response;
  },
  error => {
    switch (
      error.response.status
    ) {
    case 500:
       router.push({
         path: "/404"
       });
       break;
     case 401:
       router.push({
         path: "/401"
       });
       break;
    }
  }
);

export default server;

3.使用

在我们的接口文件中引入封装好的axios,就可以使用了

// api/getTotal.js
import axios from '@/http/request'

export function getTotal(){
    return axios({
        url: "/system/user/getTotal",
        method: 'get'
    })
}

接着在要使用的组建中引入接口函数

// list.vue
<script>
import { getTotal } from "@/api/getTotal"

export default {
    data(){
        total: 0,
    },
    created(){
        getTotal()
            .then((res)=>{
                if (res.data.code == 400){
                    this.total = res.data.total
                }
            })
            .catch(err=>{
                console.log(err)
            })
    }
}
</script>

三、proxy(服务端代理)

在vue项目中,webpack选项devServer有一个proxy属性,通常都是使用这个属性将所有带指定字符串的请求,发送到target目标服务器。查看文档,发现devServer调用了http-proxy-middleware插件,将请求转发给目标服务器。

下面是官网给出的示例

var express = require('express');
var proxy = require('http-proxy-middleware');

var app = express();

app.use(
  '/api',
  proxy({ target: 'http://www.example.org', changeOrigin: true })
);
app.listen(3000);

// http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar

为了说明原理,我自己创建了两个node服务,一个模拟客户端,一个模拟目标服务器

1.原理

在客户端,用http创建一个服务,并返回展示的页面

const http = require("http");
const fs = require("fs");

const server = http.createServer();
server.listen(8090);
server.on("request", (req, res) => {
  res.writeHead(200, { "Content-Type": "text/html" });
  if (req.url == "/") {
    fs.readFile("./index.html", (err, data) => {
      if (err) {
        console.log(err);
      } else {
        res.end(data);
      }
    });
  } 
});
console.log("Server running at http://127.0.0.1:8090/");

另外在页面中,可以自己定义或者引用已经写好的ajax,向客户端服务器发送请求,客户端在接受到这个这个请求时,转发给目标服务器,实现如下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>hello world</title>
  </head>
  <body>
    <button onclick="openUrl()">测试连接</button>
    <script>
      let ajax;
      if (XMLHttpRequest) {
        ajax = new XMLHttpRequest();
      } else {
        ajax = new ActiveXObject();
      }
      function openUrl() {
        ajax.open("get", "/api/index", false);
        ajax.send();
      }
    </script>
  </body>
</html>

// 客户端服务器
 if (req.url == "/") {
    fs.readFile("./index.html", (err, data) => {
      if (err) {
        console.log(err);
      } else {
        res.end(data);
      }
    });
  } else if (req.url.search("/api") != -1) {
    http.get("http://127.0.0.1:8091" + req.url, response => {
      let data = "";
      response.on("data", chunk => {
        data += chunk;
      });
      response.on("end", () => {
        res.end(data);
      });
    });
    console.log(req.url, "进来了");
  }

目标服务器负责接收不同的url,并分别作出响应

// 目标服务器
const http = require("http");

const server = http.createServer();
server.listen(8091);
server.on("request", (req, res) => {
  res.writeHead(200, { "Content-Type": "application/json" });
  if (req.url == "/api/index") {
    // 这里给客户端服务器发送一个json对象
    let data = {
      id: 1,
      name: "cjr"
    };
    res.end(JSON.stringify(data));
  }
});
console.log("Server running at http://127.0.0.1:8091/");

这时浏览器控制台可以接收到目标服务器发送的json数据了

2.封装和使用

在设置基础路径之外,我们需要指明跨域的目标服务器地址

devServer:{
    '/api':{
        target: "http://127.0.0.1:8091", // 这里一定要记得添加http
        changeOrigin: true  // 这里设置允许跨域
    }
}

同样是使用axios,封装和使用同cros,只是baseURL不一样,这里只给出完整的代码

// 引入
import axios from "axios";

// 创建一个实例
const server = axios.create({
    // 将我们访问的地址设为baseURL
    baseURL: "/v1",
    // 设置超时时间
    timeout: 5000
});

// 设置拦截器
// 请求拦截器
server.interceptors.request.use(
  config => {
    // 每个请求都带token与服务器进行身份匹配
    config.headers.token = localStorage.getItem("token");
    return config;
  },
  error => {
    console.log(error);
    Promise.reject(error);
  }
);
// 响应拦截器
server.interceptors.response.use(
  response => {
    // 你也可以根据与后端约定的消息处理机制,作出提示
    // 你可以引入你使用的UI库的toast模块提示给用户
    if(response.data.code == 200){
        console.log("操作成功")
    }else if(response.data.code == 300){
        console.log("没有这条数据或者查询失败")
    }else{
        console.log("操作失败")
    }
    return response;
  },
  error => {
    switch (
      error.response.status
    ) {
    case 500:
       router.push({
         path: "/404"
       });
       break;
     case 401:
       router.push({
         path: "/401"
       });
       break;
    }
  }
);

export default server;

四、更多方案

除了前面提到的跨域方案,你还可以使用nginx反向代理,它和proxy十分类似,都是访问中间层,由中间层去访问目标服务器

另外还有很多讨巧的方法进行跨域,比如你在页面中使用iframe标签,使用哈希location.hash的方式,进行跨域并且传递数据;也可以使用window.name或者H5的postMessage接口

这些跨域方案在实际使用中偏冷门,我也没有实践过,所以这里这是简单的提到

文章如果有错误的地方,欢迎指出,我会及时改正