axios如何利用promise無痛刷新token(二)

16e6265fc4958f20.jpg

前言

前段時間寫了篇文章《axios如何利用promise無痛刷新token》血巍,陸陸續(xù)續(xù)收到一些反饋雏搂。發(fā)現(xiàn)不少同學(xué)會想要從在請求前攔截的思路入手藕施,甚至收到了幾個郵件來詢問博主遇到的問題,所以索性再寫一篇文章來說說另一個思路的實(shí)現(xiàn)和注意的地方凸郑。過程會稍微啰嗦裳食,不想看實(shí)現(xiàn)過程的同學(xué)可以直接拉到最后面看最終代碼。

PS:在本文就略過一些前提條件了芙沥,請新同學(xué)閱讀本文前先看一下前一篇文章《axios如何利用promise無痛刷新token》诲祸。

前提條件

前端登錄后,后端返回token和token有效時間段tokenExprieIn而昨,當(dāng)token過期時間到了救氯,前端需要主動用舊token去獲取一個新的token,做到用戶無感知地去刷新token歌憨。

PS: tokenExprieIn是一個單位為秒的時間段着憨,不建議使用絕對時間,絕對時間可能會由于本地和服務(wù)器時區(qū)不一樣導(dǎo)致出現(xiàn)問題务嫡。

實(shí)現(xiàn)思路

方法一

在請求發(fā)起前攔截每個請求甲抖,判斷token的有效時間是否已經(jīng)過期,若已過期心铃,則將請求掛起准谚,先刷新token后再繼續(xù)請求。

方法二

不在請求前攔截去扣,而是攔截返回后的數(shù)據(jù)柱衔。先發(fā)起請求,接口返回過期后愉棱,先刷新token唆铐,再進(jìn)行一次重試。

前文已經(jīng)實(shí)現(xiàn)了方法二奔滑,本文會從頭實(shí)現(xiàn)一下方法一艾岂。

實(shí)現(xiàn)

基本骨架

在請求前進(jìn)行攔截,我們主要會使用axios.interceptors.request.use()這個方法档押。照例先封裝個request.js的基本骨架:

import axios from 'axios'

// 從localStorage中獲取token澳盐,token存的是object信息祈纯,有tokenExpireTime和token兩個字段
function getToken () {
  let tokenObj = {}
  try {
    tokenObj = storage.get('token')
    tokenObj = tokenObj ? JSON.parse(tokenObj) : {}
  } catch {
    console.error('get token from localStorage error')
  }
  return tokenObj
}

// 給實(shí)例添加一個setToken方法,用于登錄后方便將最新token動態(tài)添加到header叼耙,同時將token保存在localStorage中
instance.setToken = (obj) => {
  instance.defaults.headers['X-Token'] = obj.token
  window.localStorage.setItem('token', JSON.stringify(obj)) // 注意這里需要變成字符串后才能放到localStorage中
}

// 創(chuàng)建一個axios實(shí)例
const instance = axios.create({
  baseURL: '/api',
  timeout: 300000,
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest'
  }
})

// 請求發(fā)起前攔截
instance.interceptors.request.use((config) => {
  const tokenObj = getToken()
  // 為每個請求添加token請求頭
  config.headers['X-Token'] = tokenObj.token
  
  // **接下來主要攔截的實(shí)現(xiàn)就在這里**
  
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

// 請求返回后攔截
instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    // token過期了腕窥,直接跳轉(zhuǎn)到登錄頁 
    window.location.href = '/'
  }
  return response
}, error => {
  console.log('catch', error)
  return Promise.reject(error)
})

export default instance

與前文略微不同的是,由于方法二不需要用到過期時間筛婉,所以前文localStorage中只存了token一個字符串簇爆,而方法一這里需要用到過期時間了,所以得存多一個數(shù)據(jù)爽撒,因此localStorage中存的是Object類型的數(shù)據(jù)入蛆,從localStorage中取值出來需要JSON.parse一下,為了防止發(fā)生錯誤所以盡量使用try...catch硕勿。

axios.interceptors.request.use()實(shí)現(xiàn)

首先不需要想得太復(fù)雜哨毁,先不考慮多個請求同時進(jìn)來的情況,咱從最常見的場景入手:從localStorage拿到上一次存儲的過期時間源武,判斷是否已經(jīng)到了過期時間扼褪,是就立即刷新token然后再發(fā)起請求。

function refreshToken () {
    // instance是當(dāng)前request.js中已創(chuàng)建的axios實(shí)例
    return instance.post('/refreshtoken').then(res => res.data)
}

instance.interceptors.request.use((config) => {
  const tokenObj = getToken()
  // 為每個請求添加token請求頭
  config.headers['X-Token'] = tokenObj.token
  if (tokenObj.token && tokenObj.tokenExpireTime) {
      const now = Date.now()
      if (now >= tokenObj.tokenExpireTime) {
          // 當(dāng)前時間大于過期時間粱栖,說明已經(jīng)過期了话浇,返回一個Promise,執(zhí)行refreshToken后再return當(dāng)前的config
          return refreshToken().then(res => {
            const { token, tokenExprieIn } = res.data
            const tokenExpireTime = now + tokenExprieIn * 1000
            instance.setToken({ token, tokenExpireTime }) // 存token到localStorage
            console.log('刷新成功, return config即是恢復(fù)當(dāng)前請求')
            config.headers['X-Token'] = token // 將最新的token放到請求頭
            return config
          }).catch(res => {
            console.error('refresh token error: ', res)
          })
      }
  }
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

這里有兩個需要注意的地方:

  1. 之前說到登錄或刷新token的接口返回的是一個單位為秒的時間段tokenExpireIn闹究,而我們存到localStorage中的是已經(jīng)是一個基于當(dāng)前時間有效時間段算出的最終時間tokenExpireTime幔崖,是一個絕對時間,比如當(dāng)前時間是12點(diǎn)渣淤,有效時間是3600秒(1個小時)赏寇,則存到localStorage的過期時間是13點(diǎn)的時間戳,這樣可以少存一個當(dāng)前時間的字段到localStorage中砂代,使用時只需要判斷該絕對時間即可蹋订。
  2. instance.interceptors.request.use中返回一個Promise率挣,就可以使得該請求是先執(zhí)行refreshToken后再return config的刻伊,才能保證先刷新token后再真正發(fā)起請求。

其實(shí)博主直接運(yùn)行上面代碼后發(fā)現(xiàn)了一個嚴(yán)重錯誤椒功,進(jìn)入了一個死循環(huán)捶箱。這是因為博主沒有注意到一個問題:axios.interceptors.request.use()會攔截所有使用該實(shí)例發(fā)起的請求,即執(zhí)行refreshToken()時又一次進(jìn)入了axios.interceptors.request.use()动漾,導(dǎo)致一直在return refreshToken()丁屎。

因此需要將刷新token和登錄這兩種情況排除出去,登錄和刷新token都不需要判斷是否過期的攔截旱眯,我們可以通過config.url來判斷是哪個接口:

instance.interceptors.request.use((config) => {
  const tokenObj = getToken()
  // 為每個請求添加token請求頭
  config.headers['X-Token'] = tokenObj.token
  // 登錄接口和刷新token接口繞過
  if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) {
    return config
  }
  if (tokenObj.token && tokenObj.tokenExpireTime) {
      const now = Date.now()
      if (now >= tokenObj.tokenExpireTime) {
          // 當(dāng)前時間大于過期時間晨川,說明已經(jīng)過期了证九,返回一個Promise,執(zhí)行refreshToken后再return當(dāng)前的config
          return refreshToken().then(res => {
            const { token, tokenExprieIn } = res.data
            const tokenExpireTime = now + tokenExprieIn * 1000
            instance.setToken({ token, tokenExpireTime }) // 存token到localStorage
            console.log('刷新成功, return config即是恢復(fù)當(dāng)前請求')
            config.headers['X-Token'] = token // 將最新的token放到請求頭
            return config
          }).catch(res => {
            console.error('refresh token error: ', res)
          })
      }
  }
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

問題和優(yōu)化

接下來就是要考慮復(fù)雜一點(diǎn)的問題了

防止多次刷新token

當(dāng)幾乎同時進(jìn)來兩個請求共虑,為了避免多次執(zhí)行refreshToken愧怜,需要引入一個isRefreshing的進(jìn)行標(biāo)記:

let isRefreshing = false
instance.interceptors.request.use((config) => {
  const tokenObj = getToken()
  // 為每個請求添加token請求頭
  config.headers['X-Token'] = tokenObj.token
  // 登錄接口和刷新token接口繞過
  if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) {
    return config
  }
  if (tokenObj.token && tokenObj.tokenExpireTime) {
      const now = Date.now()
      if (now >= tokenObj.tokenExpireTime) {
          if (!isRefreshing) {
            isRefreshing = true
            return refreshToken().then(res => {
              const { token, tokenExprieIn } = res.data
              const tokenExpireTime = now + tokenExprieIn * 1000
              instance.setToken({ token, tokenExpireTime }) // 存token到localStorage
              isRefreshing = false //刷新成功,恢復(fù)標(biāo)志位
              config.headers['X-Token'] = token // 將最新的token放到請求頭
              return config
            }).catch(res => {
              console.error('refresh token error: ', res)
            })  
          }
      }
  }
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

多個請求時存到隊列中等刷新token后再發(fā)起

我們已經(jīng)知道了當(dāng)前已經(jīng)過期或者正在刷新token拥坛,此時再有請求發(fā)起,就應(yīng)該讓后面的這些請求等一等猜惋,等到refreshToken結(jié)束后再真正發(fā)起,所以需要用到一個Promise來讓它一直等。而后面的所有請求港粱,我們將它們存放到一個requests的隊列中,等刷新token后再依次resolve羔巢。

instance.interceptors.request.use((config) => {
  const tokenObj = getToken()
  // 添加請求頭
  config.headers['X-Token'] = tokenObj.token
  // 登錄接口和刷新token接口繞過
  if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) {
    return config
  }
  if (tokenObj.token && tokenObj.tokenExpireTime) {
    const now = Date.now()
    if (now >= tokenObj.tokenExpireTime) {
      // 立即刷新token
      if (!isRefreshing) {
        console.log('刷新token ing')
        isRefreshing = true
        refreshToken().then(res => {
          const { token, tokenExprieIn } = res.data
          const tokenExpireTime = now + tokenExprieIn * 1000
          instance.setToken({ token, tokenExpireTime })
          isRefreshing = false
          return token
        }).then((token) => {
          console.log('刷新token成功蕾羊,執(zhí)行隊列')
          requests.forEach(cb => cb(token))
          // 執(zhí)行完成后,清空隊列
          requests = []
        }).catch(res => {
          console.error('refresh token error: ', res)
        })
      }
      const retryOriginalRequest = new Promise((resolve) => {
        requests.push((token) => {
          // 因為config中的token是舊的,所以刷新token后要將新token傳進(jìn)來
          config.headers['X-Token'] = token
          resolve(config)
        })
      })
      return retryOriginalRequest
    }
  }
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

這里做了一點(diǎn)改動,注意到refreshToken()這一句前面去掉了return扒寄,而是改為了在后面return retryOriginalRequest隔披,即當(dāng)發(fā)現(xiàn)有請求是過期的就存進(jìn)requests數(shù)組英上,等refreshToken結(jié)束后再執(zhí)行requests隊列杀糯,這是為了不影響原來的請求執(zhí)行次序。
我們假設(shè)同時有請求1弃衍,請求2呀非,請求3依次同時進(jìn)來,我們希望是請求1發(fā)現(xiàn)過期,refreshToken后再依次執(zhí)行請求1岸裙,請求2猖败,請求3
按之前return refreshToken()的寫法降允,會大概寫成這樣


  if (tokenObj.token && tokenObj.tokenExpireTime) {
    const now = Date.now()
    if (now >= tokenObj.tokenExpireTime) {
      // 立即刷新token
      if (!isRefreshing) {
        console.log('刷新token ing')
        isRefreshing = true
        return refreshToken().then(res => {
          const { token, tokenExprieIn } = res.data
          const tokenExpireTime = now + tokenExprieIn * 1000
          instance.setToken({ token, tokenExpireTime })
          isRefreshing = false
          config.headers['X-Token'] = token
          return config // 請求1
        }).catch(res => {
          console.error('refresh token error: ', res)
        }).finally(() => {
          console.log('執(zhí)行隊列')
          requests.forEach(cb => cb(token))
          // 執(zhí)行完成后恩闻,清空隊列
          requests = []
        })
      } else {
        // 只有請求2和請求3能進(jìn)入隊列
        const retryOriginalRequest = new Promise((resolve) => {
          requests.push((token) => {
            config.headers['X-Token'] = token
            resolve(config)
          })
        })
        return retryOriginalRequest
      }
    }
  }
  return config

隊列里面只有請求2請求3,代碼看起來應(yīng)該是return了請求1后剧董,再在finally執(zhí)行隊列的幢尚,但實(shí)際的執(zhí)行順序會變成請求2請求3翅楼,請求1侠草,即請求1變成了最后一個執(zhí)行的,會改變執(zhí)行順序犁嗅。

所以博主換了個思路边涕,無論是哪個請求進(jìn)入了過期流程,我們都將請求放到隊列中褂微,都return一個未resolve的Promise功蜓,等刷新token結(jié)束后再一一清算,這樣就可以保證請求1宠蚂,請求2式撼,請求3這樣按原來順序執(zhí)行了。

這里多說一句求厕,可能很多剛接觸前端的同學(xué)無法理解requests.forEach(cb => cb(token))是如何執(zhí)行的著隆。

// 我們先看一下,定義fn1
function fn1 () {
    console.log('執(zhí)行fn1')
}

// 執(zhí)行fn1,只需后面加個括號
fn1()

// 回歸到我們request數(shù)組中呀癣,每一項其實(shí)存的就是一個類似fn1的一個函數(shù)
const fn2 = (token) => {
    config.headers['X-Token'] = token
    resolve(config)
}

// 我們要執(zhí)行fn2美浦,也只需在后面加個括號就可以了
fn2()

// 由于requests是一個數(shù)組,所以我們想遍歷執(zhí)行里面的所有的項项栏,所以用上了forEach
requests.forEach(fn => {
  // 執(zhí)行fn
  fn()
})

最后完整代碼

import axios from 'axios'

// 從localStorage中獲取token浦辨,token存的是object信息,有tokenExpireTime和token兩個字段
function getToken () {
  let tokenObj = {}
  try {
    tokenObj = storage.get('token')
    tokenObj = tokenObj ? JSON.parse(tokenObj) : {}
  } catch {
    console.error('get token from localStorage error')
  }
  return tokenObj
}

function refreshToken () {
    // instance是當(dāng)前request.js中已創(chuàng)建的axios實(shí)例
    return instance.post('/refreshtoken').then(res => res.data)
}

// 給實(shí)例添加一個setToken方法沼沈,用于登錄后方便將最新token動態(tài)添加到header流酬,同時將token保存在localStorage中
instance.setToken = (obj) => {
  instance.defaults.headers['X-Token'] = obj.token
  window.localStorage.setItem('token', JSON.stringify(obj)) // 注意這里需要變成字符串后才能放到localStorage中
}

instance.interceptors.request.use((config) => {
  const tokenObj = getToken()
  // 添加請求頭
  config.headers['X-Token'] = tokenObj.token
  // 登錄接口和刷新token接口繞過
  if (config.url.indexOf('/rereshToken') >= 0 || config.url.indexOf('/login') >= 0) {
    return config
  }
  if (tokenObj.token && tokenObj.tokenExpireTime) {
    const now = Date.now()
    if (now >= tokenObj.tokenExpireTime) {
      // 立即刷新token
      if (!isRefreshing) {
        console.log('刷新token ing')
        isRefreshing = true
        refreshToken().then(res => {
          const { token, tokenExprieIn } = res.data
          const tokenExpireTime = now + tokenExprieIn * 1000
          instance.setToken({ token, tokenExpireTime })
          isRefreshing = false
          return token
        }).then((token) => {
          console.log('刷新token成功,執(zhí)行隊列')
          requests.forEach(cb => cb(token))
          // 執(zhí)行完成后列另,清空隊列
          requests = []
        }).catch(res => {
          console.error('refresh token error: ', res)
        })
      }
      const retryOriginalRequest = new Promise((resolve) => {
        requests.push((token) => {
          // 因為config中的token是舊的芽腾,所以刷新token后要將新token傳進(jìn)來
          config.headers['X-Token'] = token
          resolve(config)
        })
      })
      return retryOriginalRequest
    }
  }
  return config
}, (error) => {
  // Do something with request error
  return Promise.reject(error)
})

// 請求返回后攔截
instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    // token過期了,直接跳轉(zhuǎn)到登錄頁 
    window.location.href = '/'
  }
  return response
}, error => {
  console.log('catch', error)
  return Promise.reject(error)
})

export default instance

建議一步步調(diào)試的同學(xué)页衙,可以先去掉window.location.href = '/'這個跳轉(zhuǎn)摊滔,保留log方便調(diào)試。

感謝看到最后,感謝點(diǎn)贊_惭载。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末旱函,一起剝皮案震驚了整個濱河市响巢,隨后出現(xiàn)的幾起案子描滔,更是在濱河造成了極大的恐慌,老刑警劉巖踪古,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件含长,死亡現(xiàn)場離奇詭異,居然都是意外死亡伏穆,警方通過查閱死者的電腦和手機(jī)拘泞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枕扫,“玉大人陪腌,你說我怎么就攤上這事⊙糖疲” “怎么了诗鸭?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長参滴。 經(jīng)常有香客問我强岸,道長,這世上最難降的妖魔是什么砾赔? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任蝌箍,我火速辦了婚禮,結(jié)果婚禮上暴心,老公的妹妹穿的比我還像新娘妓盲。我一直安慰自己,他們只是感情好专普,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布本橙。 她就那樣靜靜地躺著,像睡著了一般脆诉。 火紅的嫁衣襯著肌膚如雪甚亭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天击胜,我揣著相機(jī)與錄音亏狰,去河邊找鬼。 笑死偶摔,一個胖子當(dāng)著我的面吹牛暇唾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼策州,長吁一口氣:“原來是場噩夢啊……” “哼瘸味!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起够挂,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤旁仿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后孽糖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體枯冈,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年办悟,在試婚紗的時候發(fā)現(xiàn)自己被綠了尘奏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡病蛉,死狀恐怖炫加,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情铺然,我是刑警寧澤俗孝,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站探熔,受9級特大地震影響驹针,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜诀艰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一柬甥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧其垄,春花似錦苛蒲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至喇颁,卻和暖如春漏健,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背橘霎。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工蔫浆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人姐叁。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓瓦盛,卻偏偏與公主長得像洗显,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子原环,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容