在我們使用ajax進行前后端數據交互的時候,經常會遇到一個跨域的報錯信息:
Access to XMLHttpRequest at 'http://localhost:3000/cors' from origin 'null' has
been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
為什么產生跨域?
這是因為瀏覽器的同源策略。這是瀏覽器最核心也最基本的安全功能,它阻止一個域上加載的腳本去獲取或操作另一個域上的文檔屬性。(也就是說,受到請求的URL的域必須與當前web頁面的域相同),如果缺少了同源策略,瀏覽器很容易受到XSS、CSFR等攻擊。
哪些情況會產生跨域?
端口不同
協議不同
域名不同
相同的網址和域名對應的IP不同 //localhost和127.0.0.1會生跨域
總結:端口不同,域名不同,協議不同都會出現跨域
同源策略限制以下幾種行為:
Cookie、LocalStorage 和 IndexDB 無法讀取
DOM和JS對象無法獲得
AJAX 請求不能發送
如何解決解決跨域?
在前端開發中,不可避免要出現跨域問題,下面提供四種方法供參考。
注意:一般出現跨域應該是前后端共同配置解決
方法一:服務器代理
基本原理:服務器之間的請求沒有跨域問題。
首先前端請求自己的服務器。 通過第三方中間件cors,來解決前端和自己的服務器之間的跨域問題。
我方服務器發起對目標服務器發送服務器請求。
將網路請求的結果返回給前端
前端代碼:
let url ='http://localhost:3000/cors';//請求自己的服務器
$.get(url,(data)=>{
console.log(data);//后端返回的數據
})
server.js:
const cors = require('cors') const axios =require('axios') const express = require('express') const path = require('path') const app = express() // 通過第三方中間件cors 來實現跨域問題 app.use(cors())//如果訪問的是目標服務器,那么這里就相當於一個服務器代理 app.get('/cors',(req,res)=>{ console.log('請求到了') let url ='http://ustbhuangyi.com/music/api/getDiscList?g_tk=1928093487&inCharset=utf-8&outCharset=utf-8¬ice=0&format=json&platform=yqq&hostUin=0&sin=0&ein=29&sortId=5&needNewCode=0&categoryId=10000000&rnd=0.2209329929181545' // 直接發起一個服務器請求 axios.get(url) .then((data)=>{ // console.log(data.data) res.send(data.data) }) }) app.listen(3000,()=>{ console.log('服務器啟動') })
方法二:JSONP方式(前端常用)
核心:script標簽的src屬性不存在跨域,可以加載任何文件信息。
一般第三方接口可以使用此方法,也是服務器和客戶端之間通信的常用方法。全平台瀏覽器都支持。
比如,需要返回一個淘寶推薦的數據接口
let url = https://suggest.taobao.com
/sug?code=utf-8&q=%E8%A1%A3%E6%9C%8D&_ksTS=1577354356112_350
&callback=taobao&k=1&area=c2c&bucketid=3//前端頁面在調用接口時,需要以callback=回調函數名的形式
可以通過script標簽實現跨域請求,然后再用json數據並執行回調函數
<script>
function taobao(data){
console.log(data);
}
</script>
這個接口打印出來的數據如下圖所示
若項目使用vue寫的,那么也可以用jsonp這個模塊
api.js
import jsonp from 'jsonp'
const getData = () => { let url = 'https://suggest.taobao.com/sug?code=utf-8&q=%E8%A1%A3%E6%9C%8D&_ksTS=1577354356112_350' // param給后端傳遞函數名的字段 由后端確定的 不能隨便寫 return new Promise((resolve, reject) => { jsonp(url,{param:'callback'},(err, data) => { if(err){ reject(err) }else { resolve(data) } }) }) } index.vue import { getData} from "api/api.js"; getData().then((res)=>{ console.log(res) // 若是請求成功,得到數據 })
這個方法很簡單但也有弊端:
jsonp請求方式只能是get。使數據在傳遞的過程中毫無安全性可言,而且所能傳輸的數據長度也相當有限。
接口必須符合jsonp格式,否則采用此方法沒意義。
方法三:cors設置請求的相應頭字段
CORS是一個W3C標准,全稱是"跨域資源共享"(Cross-origin resource sharing)它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
支持所有類型的HTTP請求,但瀏覽器IE10以下不支持),適合做ajax各種跨域請求。
后端代碼:
var express = require('express');
var app = express();
// 中間件 解決跨域問題
function middleware(req, res, next) {
console.log('這里是中間件') // 跨域處理 res.header("Access-Control-Allow-Origin", "*");//設置允許跨域的域名,*代表允許任意域名跨域 res.header("Access-Control-Allow-Headers", "X-Requested-With");//允許的header類型 res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");//跨域允許的請求方式 res.header("X-Powered-By", ' 3.2.1'); res.header("Content-Type", "application/json;charset=utf-8"); next(); // 執行下一個路由 } //all是路由中指代所有的請求方式 app.all(middleware) //使用全局自定義中間件,放到起始位置 app.get('/test', function (req, res) { res.send(req.query); }) app.listen(3000, function () { console.log('服務器啟動'); })
用postman進行測試結果如下圖所示:
方法四:WebSocket(H5新增特性)
WebSocket是HTML5一種新增的通信協議。它實現了瀏覽器與服務器全雙工通信,同時允許跨域通訊,是server push技術的一種很好的實現。
原生websocket有兼容問題,雖然使用起來很簡單,但是低版本瀏覽器使用不了。可以使用第三方庫:socket.io 它很好的解決了兼容問題,而且使用方式也只是稍微復雜了一點。
前端代碼:
<body>
<input type="text" id='msg' ><button onclick="send()">send</button>
<script>
// 與服務器實現連接
var socket = io.connect('http://127.0.0.1:8081');
// 創建一個自定義事件監聽 事件名叫hehe 等待服務端觸發
socket.on('hehe',(data)=>{ console.log('來自服務端的信息',data) }) function send(){ let msg = document.getElementById('msg').value // 客戶端觸發服務端的自定義事件 事件名叫xixi 傳遞的數據是input的value值 socket.emit('xixi',msg) } </script> </body>
server.js:
var express = require('express')
var app = express() // 將socket服務器 和express 綁定到一起 var server = require('http').Server(app); var io = require('socket.io')(server); io.on('connection',(client)=>{ console.log('客戶端連接') // 觸發客戶端的自定義事件 hehe 參數是123 client.emit('hehe',123) // 服務端創建一個叫xixi的自定義事件監聽 等待前端觸發 client.on('xixi',(data)=>{ console.log('來自客戶端的消息',data) }) }) server.listen(8081,()=>{ console.log('服務器啟動') });
方法五: vue-cli的devServer代理配置
如果你的前端應用和后端 API 服務器沒有運行在同一個主機上,你需要在開發環境下將 API 請求代理到 API 服務器。這個問題可以通過 vue.config.js 中的 devServer.proxy 選項來配置。vue cli: devServer.proxy
在項目根目錄下新建vue.config.js文件
在module.exports內設置devServer來處理代理
module.exports={
devServer:{
proxy:{ //配置代理服務器
// 接口小暗號
'/hehe':{ target:'http://www.target.com', //要轉發的目標網址 changeOrigin:true, pathRewrite:{ "^/hehe":'' //將路徑中多余的暗號 刪除 } } } } }
獲取接口的網絡請求
利用我們之前設置修改的接口小暗號,將請求發送至本地服務器。
此時本地服務器接收的地址是:/hehe/music/api/123 所以不會產生跨域
import axios from '../utils/axios'
let url ='/hehe/music/api/123' axios.get(url)
然后再通過本地服務器轉發至目標服務器,在轉發之前,本地服務器會把target目標地址:target:'http://www.target.com'加至接收地址url前:url ='http://www.target.com/hehe/music/api/123'
這時我們發現准備發送的地址和原地址不符,之間多了一個hehe標識符。所以我們需要pathRewrite將/hehe從地址中刪去
使用場景
通過 devServer 設置的代理只適合在本地環境使用,即通過代理把請求發送到目標服務器上,如果發布到線上,則會提示 404 路徑未找到
小結
還有一些解決跨域的方案,比如nginx代理跨域、 postMessage跨域、document.domain + iframe、location.hash + iframe、 window.name + iframe等等。
沒有最好,只有最合適的,結合項目實際場景選擇合適的跨域方案。
