1. 程式人生 > >axios cookie問題和表單上傳問題探究

axios cookie問題和表單上傳問題探究

自從入了 Vue 之後,一直在用 axios 這個庫來做一些非同步請求。最近在跨域、cookie 以及表單上傳這幾個方面遇到了點小問題,做個簡單探究和總結。本文將涉及使用 axios 在跨域情況下成功得到響應報文中的內容以及讓 cookie 成功設定的解決辦法;使用 axios 上傳表單資料時候遇到的小問題。

首先來看一個同域情況下的cookie例子清楚整個流程以便後面對跨域情況能夠更好地闡述。服務端的程式碼如下,訪問 /get-cookie 介面的時候,通過 session 隨便設定欄位 time,使得響應報文中帶有 set-cookie 欄位,在瀏覽器段設定 cookie,這個 cookie 的內容即相應的 sessionId。然後訪問 /test-cookie 可以判斷瀏覽器端的 cookie 是否設定成功,如果設定成功,那麼瀏覽器傳送的報文會自動帶上 cookie 欄位,伺服器也就可以根據 cookie 找到對應的 session。如果設定失敗,那麼瀏覽器傳送的報文不會帶上 cookie 欄位,伺服器也就無法找到對應的 session,從而返回 error。

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')

前端的程式碼如下,前端只有兩個按鈕,觸發點選事件分別可以呼叫兩個介面。

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 的資訊。

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

為了實現跨域,將伺服器拆分成 2 個伺服器,1個提供靜態資源,1個提供介面。訪問8002埠的伺服器載入前端的介面,然後跨域訪問8001埠的伺服器呼叫 get-cookie 和 test-cookie 介面。為了滿足跨域,8001伺服器的響應報文裡面應該有 Access-Control-Allow-Origin 欄位,欄位值設定為 ‘*’ 表示滿足所有其他域的訪問。

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/')
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 也類似。

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 沒有設定成功。

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

檢視 axios 的文件之後,發現需要配置 withCredentials 屬性,在全域性配置 axios 的 withCredentials 屬性為 true。

axios.defaults.withCredentials = true

配置完成之後再進行測試,得到的結果如下。確實成功設定了 cookie,test-cookie 的請求報文會自動帶上 cookie。但是前端除了成功設定 cookie 之外,還會報錯,而且無法讀取到響應報文的主體部分的內容。

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

根據錯誤提示,應該是說不能直接將 Access-Control-Allow-Origin 欄位的值設定為 ‘*’。因此,我們直接將其值修改為確定的域名試試。對 get-cookie 和 test-cookie 作如下修改。

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。根據提示對服務端程式碼進行修改,如下所示。

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 之外,也可以讀取到響應報文的主體部分的內容,並且沒有任何報錯。

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

因此,可以得到結論,在跨域的情況下使用 axios,首先需要配置 axios 的 withCredentials 屬性為 true。然後伺服器還需要配置響應報文頭部的 Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials 兩個欄位,Access-Control-Allow-Origin 欄位的值需要為確定的域名,而不能直接用 ‘*’ 代替,Access-Control-Allow-Credentials 的值需要設定為 true。前端和服務端兩邊都配置完善之後就可以正常進行跨域訪問以及攜帶 cookie 資訊了。

三、表單上傳問題

之前在使用 axios 上傳表單資料的過程中,我通常像下面會這麼做。

var formData = {
  key1: 'value1',
  key2: 'value2',
  key3: 'value3'
}
axios.post('/upload', formData).then(function (res) {
  if (res.status === 200) {
    // do something
  }
})

上面這種做法在 NodeJS express 服務端使用 body-parser 可以正確解析到請求報文裡面 body 部分的資料,也就是我提交的表單資料,所以這樣玩了很久也沒有出現過什麼問題。直到最近在課程作業中跟1個 Python flask 後臺的同學協作過程中才開始踩到坑。用上面那種方式提交的資料,在他的後臺程式裡面提取不到表單資料。他的後臺通過 req.form 的方式獲取表單資料,跟 express 的 req.body 好像有那麼一點不同。於是我猜想 express 的 body-parser 中介軟體是把報文的主體部分的資料都提取了出來,而他的 req.form 只會提取標準的表單資料。通過修改 axios post 的資料也可以驗證猜想。程式碼修改如下,使用 js 提供的 FormData 來包裝需要提交的表單資料。最後伺服器也成功收到了前端上傳的資料。

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) {
    // do something
  }
})

這時候大概可以簡單理解一下兩者區別,第一種方式是 post 了一個 js 物件,會被變成 json 字串的形式提交。而第二種則是提交了一個 FormData 物件,標準的表單資料物件。因此,如果在 Java、PHP 等其他後臺遇到類似的問題時,也可以考慮嘗試用 FormData 物件來解決。