我們一起聊聊前端接口容災(zāi)
開篇
你說,萬一接口掛了會(huì)怎么樣?
還能咋樣,白屏唄。
有沒有不白屏的方案?
有啊,還挺簡(jiǎn)單的。
容我細(xì)細(xì)細(xì)細(xì)分析。
原因就是接口掛了,拿不到數(shù)據(jù)了。那把數(shù)據(jù)儲(chǔ)存起來就可以解決問題。
思考
存哪里?
第一時(shí)間反應(yīng)瀏覽器本地存儲(chǔ),想起了四兄弟。
選型對(duì)比
特性  | cookie  | localStorage  | sessionStorage  | indexDB  | 
數(shù)據(jù)生命周期  | 服務(wù)器或者客戶端都可以設(shè)置、有過期時(shí)間  | 一直存在  | 關(guān)閉頁面就清空  | 一直存在  | 
數(shù)據(jù)儲(chǔ)存大小  | 4KB  | 5MB  | 5MB  | 動(dòng)態(tài),很大 大于250MB  | 
與服務(wù)器通信  | 每次都帶在header中  | 不帶  | 不帶  | 不帶  | 
兼容性  | 都支持  | 都支持  | 都支持  | IE不支持,其他主流都支持  | 
考慮到需要存儲(chǔ)的數(shù)據(jù)量,5MB 一定不夠的,所以選擇了 IndexDB。
考慮新用戶或者長(zhǎng)時(shí)間未訪問老用戶,會(huì)取不到緩存數(shù)據(jù)與陳舊的數(shù)據(jù)。
因此準(zhǔn)備上云,用阿里云存儲(chǔ),用 CDN 來保障。
總結(jié)下:線上 CDN、線下 IndexDB。
整體方案
整體流程圖
圖片
CDN
先講講線上 CDN。
通常情況下可以讓后端支撐,本質(zhì)就是更新策略問題,這里不細(xì)說。
我們講講另外一種方案,單獨(dú)啟個(gè) Node 服務(wù)更新 CDN 數(shù)據(jù)。
流程圖
圖片
劫持邏輯
劫持所有接口,判斷接口狀態(tài)與緩存標(biāo)識(shí)。從而進(jìn)行更新數(shù)據(jù)、獲取數(shù)據(jù)、緩存策略三種操作
通過配置白名單來控制接口存與取
axios.interceptors.response.use(
      async (resp) => {
        const { config } = resp
        const { url } = config
        // 是否有緩存tag,用于更新CDN數(shù)據(jù)。目前是定時(shí)服務(wù)在跑,訪問頁面帶上tag
        if (this.hasCdnTag() && this.isWhiteApi(url)) {
          this.updateCDN(config, resp)
        }
        return resp;
      },
      async (err) => {
        const { config } = err
        const { url } = config
        // 是否命中緩存策略
        if (this.isWhiteApi(url) && this.useCache()) {
          return this.fetchCDN(config).then(res => {
            pushLog(`cdn緩存數(shù)據(jù)已命中,請(qǐng)?zhí)幚韅, SentryTypeEnum.error)
            return res
          }).catch(()=>{
           pushLog(`cdn緩存數(shù)據(jù)未同步,請(qǐng)?zhí)幚韅, SentryTypeEnum.error)
          })
        }
      }
    );緩存策略
累計(jì)接口異常發(fā)生 maxCount 次,打開緩存開關(guān),expiresSeconds 秒后關(guān)閉。
緩存開關(guān)用避免網(wǎng)絡(luò)波動(dòng)導(dǎo)致命中緩存,設(shè)置了閥值。
/*
* 緩存策略
*/
useCache = () => {
  if (this.expiresStamp > +new Date()) {
    const d = new Date(this.expiresStamp)
    console.warn(`
    ---------------------------------------
    ---------------------------------------
    啟用緩存中
    關(guān)閉時(shí)間:${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}
    ---------------------------------------
    ---------------------------------------
    `)
    return true
  }
  this.errorCount += 1
  localStorage.setItem(CACHE_ERROR_COUNT_KEY, `${this.errorCount}`)
  if (this.errorCount > this.maxCount) {
    this.expiresStamp = +new Date() + this.expiresSeconds * 1000
    this.errorCount = 0
    localStorage.setItem(CACHE_EXPIRES_KEY, `${this.expiresStamp}`)
    localStorage.removeItem(CACHE_ERROR_COUNT_KEY)
    return true
  }
  return false
}唯一標(biāo)識(shí)
根據(jù) method、url、data 三者來標(biāo)識(shí)接口,保證接口的唯一性
帶動(dòng)態(tài)標(biāo)識(shí),譬如時(shí)間戳等可以手動(dòng)過濾
/**
 * 生成接口唯一鍵值
*/
generateCacheKey = (config) => {
  // 請(qǐng)求方式,參數(shù),請(qǐng)求地址,
  const { method, url, data, params } = config;
  let rawData = ''
  if (method === 'get') {
    rawData = params
  }
  if (method === 'post') {
    rawData = JSON.parse(data)
  }
  // 返回拼接key
  return `${encodeURIComponent([method, url, stringify(rawData)].join('_'))}.json`;
};更新數(shù)據(jù)
/**
 * 更新cdn緩存數(shù)據(jù)
*/
updateCDN = (config, data) => {
  const fileName = this.generateCacheKey(config)
  const cdnUrl = `${this.prefix}/${fileName}`
  axios.post(`${this.nodeDomain}/cdn/update`, {
    cdnUrl,
    data
  })
}Node定時(shí)任務(wù)
構(gòu)建定時(shí)任務(wù),用 puppeteer 去訪問、帶上緩存標(biāo)識(shí),去更新 CDN 數(shù)據(jù)
import schedule from 'node-schedule';
const scheduleJob = {};
export const xxxJob = (ctx) => {
  const { xxx } = ctx.config;
  ctx.logger.info(xxx, 'xxx');
  const { key, url, rule } = xxx;
  if (scheduleJob[key]) {
    scheduleJob[key].cancel();
  }
  scheduleJob[key] = schedule.scheduleJob(rule, async () => {
    ctx.logger.info(url, new Date());
    await browserIndex(ctx, url);
  });
};
export const browserIndex = async (ctx, domain) => {
  ctx.logger.info('browser --start', domain);
  if (!domain) {
    ctx.logger.error('domain為空');
    return false;
  }
  const browser = await puppeteer.launch({
    args: [
      '--use-gl=egl',
      '--disable-gpu',
      '--no-sandbox',
      '--disable-setuid-sandbox',
    ],
    executablePath: process.env.CHROMIUM_PATH,
    headless: true,
    timeout: 0,
  });
  const page = await browser.newPage();
  await page.goto(`${domain}?${URL_CACHE_KEY}`);
  await sleep(10000);
  // 訪問首頁所有查詢接口
  const list = await page.$$('.po-tabs__item');
  if (list?.length) {
    for (let i = 0; i < list.length; i++) {
      await list[i].click();
    }
  }
  await browser.close();
  ctx.logger.info('browser --finish', domain);
  return true;
};效果
手動(dòng) block 整個(gè) domain,整個(gè)頁面正常展示
圖片
IndexDB
線上有 CDN 保證了,線下就輪到 IndexDB 了,基于業(yè)務(wù)簡(jiǎn)單的增刪改查,選用 localForage 三方庫足矣。
axios.interceptors.response.use(
      async (resp) => {
        const { config } = resp
        const { url } = config
        // 是否有緩存tag,用于更新CDN數(shù)據(jù)。目前是定時(shí)服務(wù)在跑,訪問頁面帶上tag
        if (this.hasCdnTag() && this.isWhiteApi(url)) {
          this.updateCDN(config, resp)
        }
        if(this.isIndexDBWhiteApi(url)){
          this.updateIndexDB(config, resp)
        }
        return resp;
      },
      async (err) => {
        const { config } = err
        const { url } = config
        // 是否命中緩存策略
        if (this.isWhiteApi(url) && this.useCache()) {
          return this.fetchCDN(config).then(res => {
            pushLog(`cdn緩存數(shù)據(jù)已命中,請(qǐng)?zhí)幚韅, SentryTypeEnum.error)
            return res
          }).catch(()=>{
           pushLog(`cdn緩存數(shù)據(jù)未同步,請(qǐng)?zhí)幚韅, SentryTypeEnum.error)
           if(this.isIndexDBWhiteApi(url)){
             return this.fetchIndexDB(config).then(res => {
              pushLog(`IndexDB緩存數(shù)據(jù)已命中,請(qǐng)?zhí)幚韅, SentryTypeEnum.error)
              return res
            }).catch(()=>{
             pushLog(`IndexDB緩存數(shù)據(jù)未同步,請(qǐng)?zhí)幚韅, SentryTypeEnum.error)
            })
           }
          })
        }
      }
    );總結(jié)
總結(jié)下,優(yōu)點(diǎn)包括不入侵業(yè)務(wù)代碼,不影響現(xiàn)有業(yè)務(wù),隨上隨用,盡可能避免前端純白屏的場(chǎng)景,成本低。劣勢(shì)包括使用局限,不適合對(duì)數(shù)據(jù)實(shí)效性比較高的業(yè)務(wù)場(chǎng)景,不支持 IE 瀏覽器。
接口容災(zāi)我們也是剛弄不久,有許多細(xì)節(jié)與不足,歡迎溝通交流。
接口容災(zāi)本意是預(yù)防發(fā)生接口服務(wù)掛了的場(chǎng)景,我們不會(huì)很被動(dòng)。原來是P0的故障,能被它降低為 P2、P3,甚至在某些場(chǎng)景下都不會(huì)有用戶反饋。















 
 
 


















 
 
 
 