跨域是指違背了瀏覽器同源策略的限制javascript
簡單的說,咱們一般使用的請求工具都是基於XMLHttpRequest對象構建的,它是嚴格遵照同源策略的,因此當須要跨域時,基於它的工具就沒法工做了,咱們須要一些額外的手段來解決這個問題 本文主要對已知的幾種跨域方案進行實踐和總結html
jsonp雖然不是官方的跨域方案,但它是最先出現的跨域方案,另外無論你是怎麼使用jsonp跨域,==服務端支持這種跨域是前提==。vue
因爲jsonp是使用script、img、iframe沒有同源限制的標籤,因此它只支持get請求java
另外jsonp錯誤處理機制不完善node
jsonp的優勢是它能夠兼容低版本的瀏覽器webpack
jsonp是使用script、img、iframe沒有同源限制的標籤,向服務端發送請求,將返回的數據做爲指定的回調函數的參數,script標籤引入的函數是屬於window全局的,因此你只須要在另外一個script中指定回調函數,這樣就能夠獲取到服務端數據了ios
這是個最基礎的jsonp跨域例子nginx
// 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>
複製代碼
控制檯打印以下git
此時在newwork中查看preview,返回的是一個函數,假如回調函數存在的話就執行它 上面只是說說它的原理而已,接下來進入正題,vue是如何使用的vue要使用jsonp,能夠藉助第三方工具包,推薦使用jsonp,假如你喜歡axios風格,用起來確定駕輕就熟。github
cnpm i jsonp --save
複製代碼
新建一個文件,用來放咱們封裝的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);
}
});
});
}
複製代碼
在咱們的接口文件中引入封裝好的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仍然是使用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();
複製代碼
跨域資源共享(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
複製代碼
==注意==簡單請求:
假如咱們的須要使用上面列出的之外的方法或者字段,就是屬於非簡單請求了,對於這種請求,瀏覽器會先調用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);
複製代碼
在vue項目中使用axios,對它進行業務的封裝
cnpm i axios -s
複製代碼
新建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;
複製代碼
在咱們的接口文件中引入封裝好的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>
複製代碼
在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服務,一個模擬客戶端,一個模擬目標服務器
在客戶端,用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數據了
在設置基礎路徑以外,咱們須要指明跨域的目標服務器地址
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接口
這些跨域方案在實際使用中偏冷門,我也沒有實踐過,因此這裏這是簡單的提到
文章若是有錯誤的地方,歡迎指出,我會及時改正