跨域是个老生常谈的问题,都谈臭了,我在实际工作中,其实遇到不多。现在基本都是前后端分离开发,开发阶段(一般是本地起个服务),用 webpack
代理就可以解决跨域的问题,实在不行,用非安全模式的 chrome
也可以(我原先就这么干过);部署阶段,我们前端需要做的也不多,我们只需打个包出来就可以了(顶多就是打包前的配置,路由模式设置等)。但是,面试必问啊,而且一直对 cors
这种方案似懂非懂,所以就用代码撸一遍咯~
跨域完全就是浏览器搞得鬼,由于浏览器同源策略的限制,协议、域名、端口号只要有一个不同,就是不同源。
本文记录的都是自己敲出来并验证过的,前后端都是本地起的服务,前端 vue-cli3
搭的 vue
工程,封装 axios
请求,后端用的 express
+ mysql
。废话不多说,直接上代码:
1. webpack 代理,主要用于开发阶段
// 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
去掉的。
// 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
能够识别哪些请求需要代理。
具体的每个接口
// 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)
...
这样,开发阶段就能愉快地开发了
但是面试老师问时,感觉还是没答到重点,嗯,那就看看 cors
吧
2. cors
cors
全称是跨域资源共享,具体可以看 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS。cors
分为简单请求和非简单请求,具体的区别网上一大堆,本文以简单请求为例。还是以之前登陆的接口为例,这回就关掉 webpack
来代理了,直接 axios
将服务地址写死:
// request.js
...
axios.defaults.baseURL = 'http://localhost:3001'
...
重启 webpack
,走一波,毫无意外,浏览器有错误
但是,请求结果结果还是 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
...
这样就可以愉快地请求啦
3. 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
进程
浏览器刷新一下,发现可以正常运行了
发现这种方案最省事了,只需部署时弄弄配置文件就可以,不需要前后端干嘛,一劳永逸。
4. 万恶的 JSONP
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
),我们可以看一下响应结果
完事啦
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。