JavaScript 設計模式 —— 代理模式
如果覺得文章不錯,歡迎關注、點贊和分享!
持續分享技術博文,關注微信公眾號 :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
- 使使用者代理在有機會時非同步地向伺服器傳送資料( HTTP POST ),不影響互動效能
- http://developer.mozilla.org...
-
資料聚合上報(未使用代理模式優化版本為,每次 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 的引用計數
小結
代理模式有著許多的小分類,前端開發工作中常用的有虛擬代理、保護代理和快取代理等。其實讀到這裡,大家也能感受到,日常開發工作中常做的一個動作 —— ”封裝“ ,其實就是代理模式的運用 ~
- 即刻報名!SegmentFault AIGC Hackathon 黑客馬拉松全新出發!
- SegmentFault 2022 年社群週報 Vol.9
- 社群精選 | 不容錯過的9個冷門css屬性
- 2022最新版 Redis大廠面試題總結(附答案)
- 手寫一個mini版本的React狀態管理工具
- 【vue3原始碼】十三、認識Block
- 天翼雲全場景業務無縫替換至國產原生作業系統CTyunOS!
- JavaScript 設計模式 —— 代理模式
- MobTech簡訊驗證ApiCloud端SDK
- 以羊了個羊為例,淺談小程式抓包與響應報文修改
- 這幾種常見的 JVM 調優場景,你知道嗎?
- 聊聊如何利用管道模式來進行業務編排(下篇)
- 通用ORM的設計與實現
- 如此狂妄,自稱高效能佇列的Disruptor有啥來頭?
- 為什麼要學習GoF設計模式?
- 827. 最大人工島 : 簡單「並查集 列舉」運用題
- 介紹 Preact Signals
- 手把手教你如何使用 Timestream 實現物聯網時序資料儲存和分析
- 850. 矩形面積 II : 掃描線模板題
- Java 併發程式設計解析 | 基於JDK原始碼解析Java領域中的併發鎖,我們可以從中學習到什麼內容?