JavaScript 設計模式 —— 代理模式

語言: CN / TW / HK

如果覺得文章不錯,歡迎關注、點贊和分享!

持續分享技術博文,關注微信公眾號 :point_right|type_1_2: 前端LeBron

好久不見,怎麼這麼久沒更新了呢?

Emm...最近績效評估季,績效總結、360 評估,要寫的東西比較多嚯,耽擱了一段時間

廢話不多說,迎來 JavaScript 設計模式第三篇:代理模式 ~

代理模式概念

代理模式給某一個物件提供一個代理物件或者佔位符,並由代理物件控制原物件的引用,也可以理解為對外暴露的介面並不是原物件。通俗地講,生活中也有比較常見的代理模式:中介、寄賣、經紀人等等。而這種模式存在的意義在於當訪問者與被訪問者不方便直接訪問/接觸的情況下,提供一個替身來處理事務流程,實際訪問的是替身,替身將事務做了一些處理/過濾之後,再轉交給本體物件以減輕本體物件的負擔。

最簡代理模式實現

由簡入繁

上面瞭解了代理模式的相關概念,接下來我們用一個最簡代理模式的例子實現一下代理模式,從程式碼中感受代理模式的流程

Talk is Cheap. Show me the code!

  • client向服務端傳送一個請求
  • proxy代理請求轉發給服務端
  • 服務端處理請求
const Request = function () {};
​
const client = {
  requestTo: (server) => {
    const req = new Request();
    server.receiveRequest(req);
  },
};
​
const server = {
  handleRequest: (request) => {
    console.log('receive request: ', request);
  },
};
​
const proxy = {
  receiveRequest: (request) => {
    console.log('proxy request: ', request);
    server.handleRequest(request);
  },
};
​
client.requestTo(proxy);
/**
 * proxy request:  Request {}
 * receive request:  Request {}
 */

保護代理

保護代理,顧名思義是為了保護本體

基於許可權控制對資源的訪問

下面用一個場景和例子來實際感受一下,基於上面最簡代理模式進行擴充套件,我們可以使用保護代理實現,過濾未通過身份校驗的請求、監聽服務端 ready 才傳送請求等操作,保護實體服務端不被非法請求攻擊和降低服務端負擔。

const proxy = {
  receiveRequest: (request) => {
    // 校驗身份
    const pass = validatePassport(request);
    if (pass) {
      // 監聽服務端 ready 後代理請求
      server.listenReady(() => {
        console.log('proxy request: ', request);
        server.handleRequest(request);
      });
    }
  },
};

虛擬代理

虛擬代理作為建立開銷大的物件的代表,協助控制建立開銷大的資源,直到真正需要一個物件的時候再去建立它,由虛擬代理來扮演物件的替身,物件建立後,再將資源直接委託給實體物件

下面將會實現一個虛擬代理實現圖片預載入的例子,從程式碼和實際場景中感受虛擬代理的作用。

  • 實體圖片物件掛載在body中
  • 由於載入圖片耗時較高,開銷較大,載入圖片資源時

    • 將實體圖片物件設定為loading狀態
    • 使用替身物件執行圖片資源載入
    • 監聽替身物件資源載入完成,將資源替換給實體物件
const img = (() => {
  const imgNode = document.createElement('img');
  document.body.appendChild(img);
  return {
    setSrc: (src) => {
      imgNode.src = src;
    },
    setLoading: () => {
        imgNode.src = 'loading.gif'
    }
  };
})();
​
const proxyImg = (() => {
  // 替身圖片物件
  const tempImg = new Image();
  // 監聽資源載入完成,將資源替換給實體圖片物件
  tempImg.onload = function () {
    img.setSrc(this.src);
  };
  return {
    // 代理開始將實體物件設定為loading狀態,使用替身物件開始載入圖片資源
    setSrc:(src)=>{
        img.setLoading()
        tempImg.src = src;
    }
  }
})();
​
proxyImg.setSrc('file.jpg')

代理模式的應用

看完保護代理和虛擬代理之後,下面來看看代理模式在前端中的一些具體應用

請求優化(埋點、錯誤的資料聚合上報)

前段時間有幸受邀參加了 ByteTech 位元組青訓營的評委,主要參加評審的是前端監控系統主題專案。前端監控就會涉及一些錯誤等資訊的上報,部分專案只實現了最簡的 HTTP 請求上報。

而有部分專案對這塊內容做了以下優化,是一個比較貼切的代理模式實踐場景:

  • Navigator.sendBeacon

  • 資料聚合上報(未使用代理模式優化版本為,每次 report 都使用請求上報)

    • 降低請求次數,聚合多事件/資訊進行上報

      • 定時
      • 定量分組

下面簡單實現一下兩種上報的示意程式碼

  • 定時
const events = [];
const TIMER = 10000;
let timer = null;
​
const init = () => {
  // 初始化時啟動定時器
  timer = setInterval(() => {
    // 定時使用 sendBeacon 上報
    const evts = events.splice(0, events.length);
    navigator.sendBeacon('/path', { events: evts });
  }, TIMER);
};
​
const destroyed = () => {
  // 銷燬時清除定時器
  clearInterval(timer);
};
​
const report = (eventName, data) => {
  // sdk 上報工具函式,聚合事件
  events.push({
    eventName,
    data,
  });
};
  • 定量分組
const events = [];
const LIMIT = 10;
​
const reportRequest = () => {
  // 定量分組使用 sendBeacon 上報
  const evts = events.splice(0, LIMIT);
  navigator.sendBeacon('/path', { events: evts });
};
​
const report = (eventName, data) => {
  // sdk 上報工具函式,聚合事件
  events.push({
    eventName,
    data,
  });
  if (events.length >= LIMIT) {
    reportRequest();
  }
};

資料快取代理

  • 之前看到過一道面試題:前端怎麼實現快取到期自動刪除快取?

    • 如果正向地去思考,快取到期了,程式都關閉了,怎麼刪?
    • 換個角度:不在過期時立即 set ,get 時才需要判斷快取是否過期

      在 get 時判斷下是否過期,過期了再刪除不就得了 ~

  • 通過 ProxyStorage 代理快取中介軟體,實現支援設定快取過期時間
  • 代理一層,便於切換快取中介軟體,增加可維護性
const Storage = {
  set(key, value, maxAge) {
    localStorage.setItem(
      key,
      JSON.stringify({
        maxAge,
        value,
        time: Date.now(),
      })
    );
  },
  get(key) {
    const v = localStorage.getItem(key);
    if (!v) {
      return null;
    }
    const { maxAge, value, time } = JSON.parse(v);
    const now = Date.now();
    if (now < time + maxAge * 1000) {
      return value;
    } else {
      localStorage.removeItem(key);
      return null;
    }
  },
  has(key) {
    const v = localStorage.getItem(key);
    if (!v) {
      return false;
    }
    const { maxAge, time } = JSON.parse(v);
    const now = Date.now();
    if (now < time + maxAge * 1000) {
      return true;
    } else {
      localStorage.removeItem(key);
      return false;
    }
  },
};

請求函式的封裝

通過代理模式封裝請求函式,可以實現以下功能:

  • 植入通用引數、通用請求頭
  • 全域性請求埋點上報
  • 全域性異常狀態碼處理器
  • 全域性請求錯誤、異常上報和處理

const SUCCESS_STATUS_CODE = 200,
  FAIL_STATUS_CODE = 400;
const isValidHttpStatus = (statusCode) =>
  statusCode >= SUCCESS_STATUS_CODE && statusCode < FAIL_STATUS_CODE;
​
const ErrorCode = {
  NotLogin: 2022,
};
const ErrorHandler = {
  [ErrorCode.NotLogin]: redirectToLoginPage,
};
​
const request = async (reqParams) => {
  const { headers, method, data, params, url } = reqParams;
  // 封裝請求引數,植入通用引數、通用請求頭
  const requestObj = {
    url: url + `?${qs.stringify({ ...commonParams, ...params })}`,
    headers: { ...commonHeaders, ...headers },
    data,
    method,
    start: Date.now(),
  };
  try {
    // 上報請求開始埋點
    reportEvent(AJAX_START, requestObj);
    const res = await ajax(requestObj);
    requestObj.end = Date.now();
    requestObj.response = res;
    // 上報請求結束埋點
    reportEvent(AJAX_END, requestObj);
​
    const { statusCode, data: resData } = res;
    const { errorCode } = resData;
​
    if (!isValidHttpStatus(statusCode)) {
      // 異常狀態碼埋點上報
      reportEvent(AJAX_ERROR, requestObj);
      throw resData;
    } else if (errorCode) {
      // 錯誤碼全域性處理器定義,未定義則把錯誤丟擲給上層業務處理
      reportEvent(AJAX_WARNING, requestObj);
      if (ErrorHandler(errorCode)) {
        ErrorHandler(errorCode)();
      } else {
        throw resData;
      }
    } else {
      // 正常返回請求資料
      return resData;
    }
  } catch (error) {
    // 捕獲錯誤並進行埋點上報,拋給上層業務處理
    requestObj.error = error;
    reportEvent(AJAX_ERROR, requestObj);
    throw error;
  }
};

Vue中的代理模式

將資料、方法、計算屬性等代理到元件例項上

let vm = new Vue({
  data: {
    msg: 'hello',
    vue: 'vue'
  },
  computed:{
    helloVue(){
      return this.msg + ' ' + this.vue
    }
  },
  mounted(){
    console.log(this.helloVue)
  }
})

Koa 中的代理模式

context 上下文代理封裝在 request 和 response 裡的屬性

app.use((context) => {
  console.log(context.request.url)
  console.log(context.url)
  console.log(context.response.body)
  console.log(context.body)
})

其他代理模式

除了本文提到的代理模式應用,還有其他非常多的變體和應用

這裡簡要列舉和介紹一下,就不一一詳細展開說明了

  • 防火牆代理:控制網路資源訪問,保護主體不讓”壞人“接近
  • 遠端代理:為一個物件在不同的地址空間提供區域性代表,比如大家的”科學上網“
  • 保護代理:用於物件應該有不同的訪問許可權的情況
  • 智慧引用代理:取代了簡單的指標,它在訪問物件時執行一些附加的操作,比如計算一個物件被引用的次數(可能用於 GC 的引用計數

小結

代理模式有著許多的小分類,前端開發工作中常用的有虛擬代理、保護代理和快取代理等。其實讀到這裡,大家也能感受到,日常開發工作中常做的一個動作 —— ”封裝“ ,其實就是代理模式的運用 ~