自从入了 Vue 之后,一直在用 axios 这个库来做一些异步请求。最近在跨域、cookie 以及表单上传这几个方面遇到了点小问题,做个简单探究和总结。本文将涉及使用 axios 在跨域情况下成功得到响应报文中的内容以及让 cookie 成功设置的解决办法;使用 axios 上传表单数据时候遇到的小问题。
一、同域 cookie 问题
首先来看一个同域情况下的cookie
例子清楚整个流程以便后面对跨域情况能够更好地阐述。服务端的代码如下,访问/get-cookie
接口的时候,通过session
随便设置字段time
,使得响应报文中带有set-cookie
字段,在浏览器段设置cookie
,这个cookie
的内容即相应的sessionId
。然后访问/test-cookie
可以判断浏览器端的 cookie
是否设置成功,如果设置成功,那么浏览器发送的报文会自动带上cookie
字段,服务器也就可以根据cookie
找到对应的session
。如果设置失败,那么浏览器发送的报文不会带上cookie
字段,服务器也就无法找到对应的session
,从而返回error
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| const express = require('express') const path = require('path') const cookieParser = require('cookie-parser') const session = require('express-session') const FileStore = require('session-file-store')(session)
let app = express()
app.use('/static', express.static(path.join(__dirname, './static'))) app.use(cookieParser()) app.use(session({ store: new FileStore(), secret: 'hongchh', resave: false, saveUninitialized: false }))
app.use('/get-cookie', (req, res) => { req.session.time = Date.now() res.json({ result: 'ok' }) })
app.use('/test-cookie', (req, res) => { if (req.session.time) { console.log('session.time: ' + req.session.time) console.log(req.cookies) res.json({ result: 'ok' }) } else { res.json({ result: 'error' }) } })
app.listen(8000)
console.log('http://localhost:8000/static/index.html')
|
前端的代码如下,前端只有两个按钮,触发点击事件分别可以调用两个接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| window.onload = function () { 'usr strict'
document.getElementById('get-cookie').addEventListener('click', getCookie, false) document.getElementById('test-cookie').addEventListener('click', testCookie, false)
var getResult = document.getElementById('get-result') var testResult = document.getElementById('test-result')
function getCookie() { axios.get('/get-cookie').then(function (res) { if (res.status === 200) { getResult.textContent = res.data.result } else { getResult.textContent = 'ERROR' } }) }
function testCookie() { axios.get('/test-cookie').then(function (res) { if (res.status === 200) { testResult.textContent = res.data.result } else { testResult.textContent = 'ERROR' } }) } }
|
首先点击get-cookie
按钮,再点击test-cookie
按钮,发送的报文信息如下,可以看到,get-cookie
的响应报文中有set-cookie
字段,而test-cookie
的请求报文中自动带上了cookie
字段。通过 Chrome 的开发者工具也可以看到设置cookie
成功。服务端也输出了相应的session
和cookie
的信息。
get-cookie
test-cookie
cookie
终端输出
二、跨域 cookie 问题
为了实现跨域,将服务器拆分成2
个服务器,1个提供静态资源,1个提供接口。访问8002
端口的服务器加载前端的界面,然后跨域访问8001
端口的服务器调用get-cookie
和test-cookie
接口。为了满足跨域,8001
服务器的响应报文里面应该有Access-Control-Allow-Origin
字段,字段值设置为'*'
表示满足所有其他域的访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| const express = require('express') const cookieParser = require('cookie-parser') const session = require('express-session') const FileStore = require('session-file-store')(session)
let app = express()
app.use(cookieParser()) app.use(session({ store: new FileStore(), secret: 'hongchh', resave: false, saveUninitialized: false }))
app.use('/get-cookie', (req, res) => { req.session.time = Date.now() res.header('Access-Control-Allow-Origin', '*') res.json({ result: 'ok' }) })
app.use('/test-cookie', (req, res) => { res.header('Access-Control-Allow-Origin', '*') if (req.session.time) { console.log('session.time: ' + req.session.time) console.log(req.cookies) res.json({ result: 'ok' }) } else { res.json({ result: 'error' }) } })
app.listen(8001)
console.log('http://localhost:8001/')
|
1 2 3 4 5 6 7 8 9
| const express = require('express') const path = require('path')
let app = express()
app.use('/static', express.static(path.join(__dirname, './static'))) app.listen(8002)
console.log('http://localhost:8002/')
|
前端只需要小做修改,将调用接口的 URL 改为绝对地址。下面是get-cookie
的写法,test-cookie
也类似。
1 2 3 4 5 6 7 8 9
| function getCookie() { axios.get('http://localhost:8001/get-cookie').then(function (res) { if (res.status === 200) { getResult.textContent = res.data.result } else { getResult.textContent = 'ERROR' } }) }
|
同样地,先get-cookie
,然后test-cookie
,得到的结果如下。访问没有出错,但是也没有成功设置cookie
。可以看到get-cookie
的响应报文里面是有set-cookie
字段的,但是test-cookie
的请求报文并没有带上cookie
字段。通过 Chrome 的开发者工具也可以看到cookie
没有设置成功。
get-cookie
test-cookie
cookie
查看axios
的文档之后,发现需要配置withCredentials
属性,在全局配置axios
的withCredentials
属性为true
。
1
| axios.defaults.withCredentials = true
|
配置完成之后再进行测试,得到的结果如下。确实成功设置了cookie
,test-cookie
的请求报文会自动带上cookie
。但是前端除了成功设置cookie
之外,还会报错,而且无法读取到响应报文的主体部分的内容。
get-cookie
test-cookie
报错信息
无法读取响应内容
根据错误提示,应该是说不能直接将Access-Control-Allow-Origin
字段的值设置为'*'
。因此,我们直接将其值修改为确定的域名试试。对get-cookie
和test-cookie
作如下修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| app.use('/get-cookie', (req, res) => { req.session.time = Date.now() res.header('Access-Control-Allow-Origin', 'http://localhost:8002') res.json({ result: 'ok' }) })
app.use('/test-cookie', (req, res) => { res.header('Access-Control-Allow-Origin', 'http://localhost:8002') if (req.session.time) { console.log('session.time: ' + req.session.time) console.log(req.cookies) res.json({ result: 'ok' }) } else { res.json({ result: 'error' }) } })
|
继续进行测试,得到的结果跟之前相似,成功设置了cookie
,但却无法获取响应报文的主体部分的内容,并且继续报错了。其他内容相似这里不重复截图。只有这个报错的信息是不同的,如下图所示。看样子修改为确定的域名成功解决了之前那个问题,接下来需要解决这新的问题。
报错信息
根据报错提示,是说除了Access-Control-Allow-Origin
字段之外,我们还需要设置Access-Control-Allow-Credentials
的值为true
。根据提示对服务端代码进行修改,如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| app.use('/get-cookie', (req, res) => { req.session.time = Date.now() res.header('Access-Control-Allow-Origin', 'http://localhost:8002') res.header('Access-Control-Allow-Credentials', 'true') res.json({ result: 'ok' }) })
app.use('/test-cookie', (req, res) => { res.header('Access-Control-Allow-Origin', 'http://localhost:8002') res.header('Access-Control-Allow-Credentials', 'true') if (req.session.time) { console.log('session.time: ' + req.session.time) console.log(req.cookies) res.json({ result: 'ok' }) } else { res.json({ result: 'error' }) } })
|
继续进行测试,得到结果如下。这次除了可以正确设置cookie
之外,也可以读取到响应报文的主体部分的内容,并且没有任何报错。
get-cookie
test-cookie
终端输出
因此,可以得到结论,在跨域的情况下使用axios
,首先需要配置axios
的withCredentials
属性为true
。然后服务器还需要配置响应报文头部的 Access-Control-Allow-Origin
和Access-Control-Allow-Credentials
两个字段,Access-Control-Allow-Origin
字段的值需要为确定的域名,而不能直接用'*'
代替,Access-Control-Allow-Credentials
的值需要设置为true
。前端和服务端两边都配置完善之后就可以正常进行跨域访问以及携带cookie
信息了。
三、表单上传问题
之前在使用axios
上传表单数据的过程中,我通常像下面会这么做。
1 2 3 4 5 6 7 8 9 10
| var formData = { key1: 'value1', key2: 'value2', key3: 'value3' } axios.post('/upload', formData).then(function (res) { if (res.status === 200) { } })
|
上面这种做法在NodeJS express
服务端使用body-parser
可以正确解析到请求报文里面body
部分的数据,也就是我提交的表单数据,所以这样玩了很久也没有出现过什么问题。直到最近在课程作业中跟1个Python flask
后台的同学协作过程中才开始踩到坑。用上面那种方式提交的数据,在他的后台程序里面提取不到表单数据。他的后台通过 req.form
的方式获取表单数据,跟express
的req.body
好像有那么一点不同。于是我猜想express
的body-parser
中间件是把报文的主体部分的数据都提取了出来,而他的req.form
只会提取标准的表单数据。通过修改axios post
的数据也可以验证猜想。代码修改如下,使用 js 提供的FormData
来包装需要提交的表单数据。最后服务器也成功收到了前端上传的数据。
1 2 3 4 5 6 7 8 9
| var formData = new FormData() formData.append('key1', 'value1') formData.append('key2', 'value2') formData.append('key3', 'value3') axios.post('/upload', formData).then(function (res) { if (res.status === 200) { } })
|
这时候大概可以简单理解一下两者区别,第一种方式是 post 了一个 js 对象,会被变成json 字符串
的形式提交。而第二种则是提交了一个FormData对象
,标准的表单数据对象。因此,如果在 Java、PHP 等其他后台遇到类似的问题时,也可以考虑尝试用FormData对象
来解决。