Vue項(xiàng)目總結(jié)(3)-前后端分離導(dǎo)致的跨域問(wèn)題分析

前后端分離的一個(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就是一種后端解決方案。corsexpress的中間件(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ù)。

image.png

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

Vue項(xiàng)目總結(jié)(4)-API+token處理流程

Vue項(xiàng)目總結(jié)(5)-表單組件基礎(chǔ)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容