跨域是個老生常談的問題,都談臭了,我在實際工做中,其實遇到很少。如今基本都是先後端分離開發,開發階段(通常是本地起個服務),用 webpack
代理就能夠解決跨域的問題,實在不行,用非安全模式的 chrome
也能夠(我原先就這麼幹過);部署階段,咱們前端須要作的也很少,咱們只需打個包出來就能夠了(頂多就是打包前的配置,路由模式設置等)。可是,面試必問啊,並且一直對 cors
這種方案似懂非懂,因此就用代碼擼一遍咯~javascript
跨域徹底就是瀏覽器搞得鬼,因爲瀏覽器同源策略的限制,協議、域名、端口號只要有一個不一樣,就是不一樣源。html
本文記錄的都是本身敲出來並驗證過的,先後端都是本地起的服務,前端 vue-cli3
搭的 vue
工程,封裝 axios
請求,後端用的 express
+ mysql
。廢話很少說,直接上代碼:前端
// vue.config.js ... devServer: { host: '0.0.0.0', port: 8080, open: true, overlay: { warning: false, errors: true }, proxy: { '/api': { target: 'http://localhost:3001', changeOrigin: true, secure: false, pathRewrite: { '^/api': '' } } } } ...
代碼中 target
就是實際提供接口的地址,本文中的接口完整都是這樣 http://localhost:3001/user/login
, http://localhost:3001/user/get_user_info
。/api
是爲了識別哪些請求須要代理,不然 js
等靜態資源請求也會被代理的。前端發起一個請求時,如登陸 http://localhost:8080/api/user/login
(下文有說明),就會被轉發至 http://localhost:3001/api/user/login
這個接口中,可是咱們的接口是這樣子的 http://localhost:3001/user/login
,沒有 api
, pathRewrite
的做用就是把 api
去掉的。vue
// request.js ... axios.defaults.baseURL = '/api' // 默認爲'/',即 http://localhost:8080/ // 設置 post 請求頭 axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded' axios.defaults.timeout = 5000 ...
上述的代碼中,baseURL
加個 api
,是爲了讓 webpack
可以識別哪些請求須要代理。java
具體的每一個接口node
// api/user.js import { get, post } from '../utils/request' export const login = params => post('/user/login', params) export const getUserInfo = params => post('/user/get_user_info', params) export const getList = params => get('/user/get_list', params) ...
這樣,開發階段就能愉快地開發了mysql
可是面試老師問時,感受仍是沒答到重點,嗯,那就看看 cors
吧webpack
cors
全稱是跨域資源共享,具體能夠看 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS。cors
分爲簡單請求和非簡單請求,具體的區別網上一大堆,本文以簡單請求爲例。仍是以以前登錄的接口爲例,這回就關掉 webpack
來代理了,直接 axios
將服務地址寫死:ios
// request.js ... axios.defaults.baseURL = 'http://localhost:3001' ...
重啓 webpack
,走一波,毫無心外,瀏覽器有錯誤nginx
可是,請求結果結果仍是 200,響應數據也能夠看到
以前說過,這個和後端沒毛線關係,就是瀏覽器搞得鬼,瀏覽器發現響應頭裏面沒有 Access-Control-Allow-Origin
這個字段,就知道這個請求有問題,就拋出個錯誤,這個錯誤被 XMLHttpRequest
的 onerror
回調函數捕獲。那怎麼解決呢,這其實就要用到 cors 了,前端幾乎不須要作什麼,只需後端改改就能夠了。
安裝:npm install --save cors
// app.js ... const cors = require('cors') app.use(cors()) ...
這樣就全部的源就能夠請求了
果真,響應頭裏面就有 Access-Control-Allow-Origin
這個字段了,固然也能夠指定某些域才能請求
// app.js ... const cors = require('cors') app.use(cors({ origin: 'http://localhost:8080' })) ...
瀏覽器就是根據這個字段來判斷是否是存在跨域,這樣跨域就解決了。可是,還有一個須要提一下,嗯,cookie
。前一篇文章中 jwt存在哪 說到,登陸成功後,jwt
保存在 cookie
中了,之後每一個請求都會帶上 cookie
的,可是,用了 cors
以後,login
接口正常了,get_user_info
這個接口報錯了:
403 Forbidden
其實就是這個請求沒有攜帶 cookie
exports.get_user_info = function (req, res, next) { const token = req.cookies.token if (token) { jwt.verify(token, SECRET, async (error, decoded) => { if (error) { // token 過時 return res.status(401).send({ success: false, message: 'token 已過時,請從新登陸' }) } else { const userInfo = await userModel.getUserById(decoded.id) return res.send({ code: 100, message: '返回成功', data: { userInfo: userInfo[0] } }) } }) } else { // 沒有拿到token 返回錯誤 return res.status(403).send({ success: false, message: '沒有找到 token' }) } }
順便說句題外話,以前一直沒有搞明白,瀏覽器接收的狀態碼(200,304, 401,403, 500等)究竟是誰給出來的,我猜應該是 web
容器(nginx
, apache
)或者 nodejs
給的。
因此,cors
解決跨域還得配置請求時能夠發送 cookie
// app.js ... const cors = require('cors') app.use(cors({ origin: 'http://localhost:8080', credentials: true })) ...
此外,前端也要稍微改一下
// request.js ... // axios.defaults.withCredentials = true // 默認爲 false,表示跨域請求時是否須要使用憑證,如 cookie ...
這樣就能夠愉快地請求啦
nginx
反向代理,主要部署時nginx
反向代理主要用於部署時,尤爲是先後端代碼分別部署的時候,本文後端代碼就不涉及到部署,仍是用本地 3001
端口的服務,express
中也把 cors
給去掉了。前端代碼也部署在本地,本地起個 nginx
來跑。
首先打包:執行 npm run build
,靜態資源路徑等所有用的是 vue-cli 3
默認的配置,生成好了 dist
文件。
本地安裝 nginx
,我用的是 mac
,我通常喜歡編譯安裝,網上教程一大推,你們自行安裝。安裝好後,進入 nginx
文件夾,大概是長這樣子的
直接執行 sudo ./sbin/nginx
就能夠啓動 nginx
啦,若是沒有反應,說明啓動成功,在瀏覽器中輸入 http://localhost
,不出意外就會出現如下界面
若是啓動時出現這個報錯:
說明這個地址被佔用了,能夠查看端口占用狀況 lsof -i:80
,以前我就遇到這種狀況,在 mac 啓動過 Apache
(mac 自帶有),停掉它就能夠了 sudo apachectl stop
接下來仍是終端進入 conf
文件中,裏面有個 nginx.conf
文件,這是默認的配置文件,在該目錄下新建一個 vhosts
文件夾(名字隨便取),而後再裏面新建 dev.conf
文件
sudo vim dev.conf
按 i
輸入一下配置內容
server { listen 8001; # dist 包在 8001 端口啓動 server_name localhost; index index.html; root /Users/liuzhiqin/Documents/you/path/to/dist; # 如路由模式是 history,還得配置這個 location / { try_files $uri /index.html; } location /api { # 匹配 url 中帶有 api 的,並轉發到http://localhost:3001/api rewrite ^/api/(.*)$ /$1 break; # 去掉 api 前綴,和前面 webpack 相似 proxy_pass http://localhost:3001; } }
按 wq
保存退出,最後別忘了在 nginx.conf
文件中引入該配置,在最後引入
# 導入配置文件 include /usr/local/nginx/conf/vhosts/*.conf;
修改了配置文件,須要從新啓動 nginx
sudo ./sbin/nginx -s reload
在瀏覽器中跑一下看看 http://localhost:8001
呃,403 forbidden
啦,mac 上很容易 403 forbidden
,這種狀況通常是先查看日誌的,打開 logs
文件中的 error.log
日誌文件
其實就是權限問題,查看一下 nginx
進程,ps aux | grep nginx
發現 nginx
工做用戶是 nobody
,在 nginx.conf
中修改一下便可
去掉 user
前的註釋,將 nobody
改爲當前用戶就能夠,mac
好像要加上 owner
,保存重啓 nginx
,這時在查看一下 nginx
進程
瀏覽器刷新一下,發現能夠正常運行了
發現這種方案最省事了,只需部署時弄弄配置文件就能夠,不須要先後端幹嗎,一勞永逸。
jsonp
實在是不想寫,現今基本已經淘汰,但無賴面試官仍是會問。jsonp
只能用於 get
請求。
先簡單說一下 jsonp
的原理,咱們知道咱們能夠在 img
標籤中引入其餘站點中的圖片(在開發過程當中,須要線上圖片時,我常常打開某寶,從上面找一張圖片),將圖片地址放到 img
的 src
中就能夠了,這是由於具備 src
屬性的標籤不受跨域的影響,如 script
, img
, iframe
等,咱們就能夠用這個特性變通實現,先看個小例子
... <head> <title>demo</title> <script type="text/javascript"> const getData = function(data){ console.log(data); }; </script> <script type="text/javascript" src="http://another.com/dataList.js"></script> </head> ...
其餘域中的 js
文件
// dataList.js // 假設 list 是咱們接口返回的數據 const list = { "code":100, "message":"返回成功", "data":{ "list":[ {"id":1,"title":"我是標題","name":"admin","pageviews":1234,"status":1,"display_time":"2019-10-23 05:57:43"}, {"id":2,"title":"要啥標題","name":"zhiqin","pageviews":562367,"status":2,"display_time":"2019-10-16 21:08:53"} ] } } // 而後執行 getData 這個方法,並將數據傳進去 getData(list)
上述代碼中,咱們本地有個 html
文件,並加載了一個其餘域中的 js
文件,這個 js
能夠執行咱們本地 js
腳本中的方法,這樣就能夠實現將其餘域中的數據傳遞過來,拿到其餘域中的數據啦。這個地址 http://another.com/dataList.js
就至關於咱們的接口地址(固然接口不會是個 js
文件的)。
須要注意的是,接口是提供數據的,供其餘系統來調用的,接口那邊並不知道每一個系統調用時傳過來的方法名是什麼(上述例子咱們在接口中寫死了爲 getData
了),並且你們本地的方法名確定是不同的,那就動態生成好啦。
本地 express
接口地址 http://localhost:3001/user/get_list
,方便起見,我直接在模板文件 index.html
中進行操做,首先看看跨域的狀況
// index.html ... <div id="app"></div> <!-- built files will be auto injected --> <script> fetch('http://localhost:3001/user/get_list') .then(response => response.json()) .then(res => { console.log(res) }) .catch(e => { console.log('err: ', e) }) </script> ...
顯然報錯
接下來直接上代碼了
前端
<div id="app"></div> <!-- built files will be auto injected --> <script> function getData(data) { console.log(data) } let url = 'http://localhost:3001/user/get_list' // 接口地址 url += '?callback=getData' // 將方法傳過去, 接口是 express 寫的,參數名必須是 callback,其餘語言不知道 const script = document.createElement('script') script.setAttribute('src', url) // 把script標籤加入head,此時調用開始 document.getElementsByTagName('head')[0].appendChild(script) </script>
後端
exports.get_list = async function (req, res, next) { const { callback } = req.query console.log(callback) const ret = await userModel.getList() if (ret.length > 0) { return res.jsonp({ // express 直接封裝好了 jsonp code: 100, message: '返回成功', data: { list: ret } }) } }
跑一下,控制檯看到打印出的數據啦
而且 Elements
中也能夠看到動態生成的 script
在 Network
js 請求中也能夠看到這條請求
你們可能會奇怪,接口中也沒有執行 getData
這個方法啊,這實際上是 res.jsonp
幫咱們作好啦(其餘語言你們自行 google
),咱們能夠看一下響應結果
完事啦