跨域是个老生常谈的问题,都谈臭了,我在实际工做中,其实遇到很少。如今基本都是先后端分离开发,开发阶段(通常是本地起个服务),用 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
),咱们能够看一下响应结果
完事啦