前后端分離的一個(gè)好處是可以將前端和后端運(yùn)行環(huán)境分開(kāi),各自進(jìn)行管理和優(yōu)化,增加了系統(tǒng)部署的靈活性和彈性。但是,這也帶來(lái)了瀏覽器跨域資源共享(CORS)問(wèn)題,導(dǎo)致瀏覽器無(wú)法訪問(wèn)后端服務(wù)。本文搭建一個(gè)簡(jiǎn)單示例(vue+json-server+nginx)說(shuō)明該問(wèn)題以及解決方法。
什么是CORS
跨域資源共享(CORS) 是一種機(jī)制,它使用額外的HTTP頭來(lái)告訴瀏覽器 讓運(yùn)行在一個(gè) origin (domain) 上的Web應(yīng)用被準(zhǔn)許訪問(wèn)來(lái)自不同源服務(wù)器上的指定的資源。當(dāng)一個(gè)資源從與該資源本身所在的服務(wù)器不同的域、協(xié)議或端口請(qǐng)求一個(gè)資源時(shí),資源會(huì)發(fā)起一個(gè)跨域 HTTP 請(qǐng)求。
比如,站點(diǎn) http://domain-a.com 的某 HTML 頁(yè)面通過(guò) <img> 的 src 請(qǐng)求 http://domain-b.com/image.jpg。網(wǎng)絡(luò)上的許多頁(yè)面都會(huì)加載來(lái)自不同域的CSS樣式表,圖像和腳本等資源。
出于安全原因,瀏覽器限制從腳本內(nèi)發(fā)起的跨源HTTP請(qǐng)求。 例如,XMLHttpRequest和Fetch API遵循同源策略。 這意味著使用這些API的Web應(yīng)用程序只能從加載應(yīng)用程序的同一個(gè)域請(qǐng)求HTTP資源,除非響應(yīng)報(bào)文包含了正確CORS響應(yīng)頭。
參考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
演示項(xiàng)目
創(chuàng)建項(xiàng)目
vue create try-cors
創(chuàng)建測(cè)試服務(wù)
在項(xiàng)目下創(chuàng)建放測(cè)試數(shù)據(jù)的目錄json-server,在目錄下創(chuàng)建文件db.json文件。
{
"hello": {
"id": 1,
"msg": "你好,我是json-server"
}
}
啟動(dòng)模擬API服務(wù),通過(guò)參數(shù)指定測(cè)試數(shù)據(jù)的位置和服務(wù)的端口。
npx json-server json-server/db.json --port 4001
在瀏覽器地址欄中輸入http://localhost:4001/hello,檢驗(yàn)是否啟動(dòng)成功。
引入axios包,修改項(xiàng)目代碼
cnpm i axios -S
修改App.vue文件,引入axios。
<template>
<div id="app">
<button @click="sendRequest">發(fā)送請(qǐng)求</button>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'app',
methods: {
sendRequest() {
let api_url = 'http://localhost:4001/hello'
axios.get(api_url).then(rsp => {
alert(JSON.stringify(rsp.data))
})
}
}
}
</script>
運(yùn)行
在瀏覽器中打開(kāi)頁(yè)面,點(diǎn)擊【發(fā)送請(qǐng)求】按鈕,成功返回結(jié)果。這時(shí)并沒(méi)有因?yàn)榭缬虻膯?wèn)題導(dǎo)致請(qǐng)求失敗,這是因?yàn)閖son-server默認(rèn)是開(kāi)啟支持CORS。
啟動(dòng)json-server時(shí)添加參數(shù)--no-cors或--nc。
npx json-server json-server/db.json --port 4001 --nc
在瀏覽器中打開(kāi)頁(yè)面,打開(kāi)【開(kāi)發(fā)者工具】,點(diǎn)擊【發(fā)送請(qǐng)求】按鈕,查看請(qǐng)求執(zhí)行的結(jié)果。
Access to XMLHttpRequest at 'http://localhost:4001/hello' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
注意:測(cè)試瀏覽器有可能會(huì)緩存結(jié)果,需要關(guān)閉緩存。
解決方案
有兩種解決方式:1、修改業(yè)務(wù)代碼,直接支持CORS;2、通過(guò)nginx進(jìn)行代理,支持CORS。
修改后端代碼
json-server通過(guò)指定參數(shù)開(kāi)關(guān)CORS就是一種后端解決方案。cors是express的中間件(koa用的是@koa/cors),可以設(shè)置各種CORS選項(xiàng)。下面代碼是一種最簡(jiǎn)單的設(shè)置。
cors({
origin: true,
credentials: true
})
json-server支持CORS后,我們?cè)跒g覽器的開(kāi)發(fā)者工具中觀察響應(yīng)頭,會(huì)看到如下內(nèi)容:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:8080
服務(wù)器端添加這兩個(gè)頭就是告訴瀏覽器,“我支持CORS”。
參考:https://www.npmjs.com/package/cors
參考:https://www.npmjs.com/package/@koa/cors
利用nginx代理
關(guān)閉json-server對(duì)CORS的支持,將端口改為4002。
npx json-server --port 4002 --nc json-server/db.json
啟用nginx,開(kāi)啟4001端口,反向代理到4001端口。
server {
listen 4001;
server_name localhost;
location / {
proxy_pass http://localhost:4002;
}
}
在瀏覽器中調(diào)用請(qǐng)求,返回失敗。
修改nginx配置文件,直接加頭。
server {
listen 4001;
server_name localhost;
location / {
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Origin http://localhost:8080;
proxy_pass http://localhost:4002;
}
}
Access-Control-Allow-Credentials 響應(yīng)頭表示是否可以將對(duì)請(qǐng)求的響應(yīng)暴露給頁(yè)面。返回true則可以,其他值均不可以。
參考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
前端代碼與運(yùn)行環(huán)境解耦
弄清楚了CORS,我們研究一下真實(shí)的部署問(wèn)題。
需要和前后端代碼分離一同考慮的問(wèn)題是,后端代碼的微服務(wù)化,也就是一個(gè)前端要訪問(wèn)多個(gè)獨(dú)立部署的后端服務(wù),例如:獨(dú)立的登錄鑒權(quán)服務(wù),獨(dú)立的日志服務(wù)。

Server1到Server4是4個(gè)獨(dú)立的服務(wù)環(huán)境,前端代碼部署在Server1,不同的后端服務(wù)分別部署在Server2到Server4,用戶通過(guò)瀏覽器訪問(wèn)Server1獲得前端代碼,瀏覽器中執(zhí)行前端代碼訪問(wèn)Server2-Server4。Server2到Server4是3個(gè)不同地址,前端代碼需要分別指定,這就導(dǎo)致前端代碼依賴運(yùn)行環(huán)境的問(wèn)題。
除了由于前后端代碼單獨(dú)部署導(dǎo)致的代碼依賴運(yùn)行環(huán)境,另一個(gè)問(wèn)題是內(nèi)外網(wǎng)隔離。業(yè)務(wù)服務(wù)器都部署在內(nèi)網(wǎng),只暴露一個(gè)出口(互聯(lián)網(wǎng)ip+端口),這種情況下,如果前端代碼中寫(xiě)的是內(nèi)網(wǎng)服務(wù)的地址肯定訪問(wèn)不了。
綜合前后分離和內(nèi)外隔離兩方面的需求,編寫(xiě)前端代碼時(shí)必須實(shí)現(xiàn)一種機(jī)制,避免在代碼中硬編碼后端服務(wù)地址,應(yīng)支持根據(jù)具體的部署環(huán)境進(jìn)行指定。
用vue開(kāi)發(fā)時(shí),可以通過(guò)指定環(huán)境變量的方式實(shí)現(xiàn)上面的要求。
在項(xiàng)目的根目錄下創(chuàng)建.env,.env.local等文件指定環(huán)境變量,注意環(huán)境變量必須以VUE_APP_開(kāi)頭,例如:
VUE_APP_SERVER2=xxxx
VUE_APP_SERVER3=yyyy
VUE_APP_SERVER4=zzzz
在代碼中通過(guò)process.env訪問(wèn):
console.log("VUE_APP_SERVER2", process.env.VUE_APP_SERVER2)
console.log("VUE_APP_SERVER3", process.env.VUE_APP_SERVER3)
console.log("VUE_APP_SERVER4", process.env.VUE_APP_SERVER4)
這種方式并不是在運(yùn)行時(shí)使用環(huán)境變量,而是在編譯時(shí)用定義的值替換代碼中的環(huán)境變量。所以,針對(duì)不同的部署環(huán)境(可以給不同環(huán)境指定不同名稱的env文件,具體參考官網(wǎng)文檔),指定相應(yīng)的環(huán)境變量后,需要進(jìn)行編譯形成和環(huán)境綁定的發(fā)布版本。這樣就實(shí)現(xiàn)了編碼階段不需要硬編碼服務(wù)地址,編譯時(shí)再根據(jù)需要指定。
參考:https://cli.vuejs.org/zh/guide/mode-and-env.html
總結(jié)
利用nginx可以很好地解決跨域和內(nèi)外網(wǎng)隔離問(wèn)題,所以建議優(yōu)先考慮采用nginx。如果完全是在內(nèi)網(wǎng)環(huán)境,可以在后端服務(wù)上增加CORS支持。
編寫(xiě)前端代碼時(shí),應(yīng)規(guī)劃好需要訪問(wèn)哪些服務(wù),通過(guò)設(shè)置環(huán)境變量,避免對(duì)后端地址硬編碼。
本系列其他文章:
Vue項(xiàng)目總結(jié)(1)-基本概念+Nodejs+VUE+VSCode
Vue項(xiàng)目總結(jié)(2)-前端獨(dú)立測(cè)試+VUE