跨域是指违背了浏览器同源策略的限制
简单的说,我们通常使用的请求工具都是基于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
==注意==简单请求:
- 不能接收或者发送cookie
- 不能使用setRequestHeader()设置自定义头部
- 调用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接口
这些跨域方案在实际使用中偏冷门,我也没有实践过,所以这里这是简单的提到
文章如果有错误的地方,欢迎指出,我会及时改正