Chrome擴展程序熱更新方案:2.基於雙緩存更新功能模塊
{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上篇文章,介紹了擴展程序熱更新方案的實現原理以及Content-Scripts的構建部署,其中有段代碼如下,這裏的hotFix方法,執行的便是獲取熱更新代碼替換執行的邏輯。從接口獲取到版本的熱更新代碼,如何存儲和解析才能保證性能和正確呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上一篇:"},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/bb39fa0b70cd3b6b6da33ab8d","title":""},"content":[{"type":"text","text":"Chrome擴展程序熱更新方案:1.原理分析及構建部署"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// 功能模塊執行入口文件\nimport hotFix from 'hotfix.js'\nimport obj from './entity.js'\n\n//熱修復方法,對obj模塊進行熱修復(下期介紹:基於雙緩存獲取熱更新代碼)\nconst moduleName = 'obj';\nhotFix('moduleName').catch(err=>{\n console.warn(`${moduleName}線上代碼解析失敗`,err)\n obj.Init()\n})"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"一、擴展程序通信流程圖"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5c/5cbe5857947b401fa35452485aa58cc7.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"background.js:背景頁面,運行在瀏覽器後臺,單獨的進程,瀏覽器開啓到關閉一直都在執行,爲擴展程序的\"中心\",執行應用的主要功能。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"content-script(CS):運行在Web頁面上下文的JavaScript文件,一個tab產生一個CS環境,它與web頁面的上下文環境兩者是絕緣的。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於Chrome通信流程,顯然在背景頁面中獲取熱更新代碼版本進行統籌管理是最爲合理。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"二、存儲方式的選擇"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"幾種常見的存儲方式:\n"},{"type":"text","marks":[{"type":"strong"}],"text":"cookie"},{"type":"text","text":": 會話,每次請求都會發送回服務器,大小不超過4kb。\n"},{"type":"text","marks":[{"type":"strong"}],"text":"sessionStorage"},{"type":"text","text":": 會話性能的存儲,生命週期爲當前窗口或標籤頁,當窗口或標籤頁被關閉,存儲數據也就清空。\n"},{"type":"text","marks":[{"type":"strong"}],"text":"localStorage"},{"type":"text","text":": 記錄在內存中,生命週期是永久的,除非用戶主動刪除數據。\n"},{"type":"text","marks":[{"type":"strong"}],"text":"indexedDB"},{"type":"text","text":":本地事務型的數據庫系統,用於在瀏覽器存較大數據結構,並提供索引功能以實現高性能的查找。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LocalStorage存儲數據一般在2.5MB~10MB之間(各家瀏覽器不同),IndexedDB存儲空間更大,一般不少於250M,且IndexedDB具備搜索功能,以及能夠建立自定義的索引。考慮到熱更新代碼模塊多,體積大,且本地需要根據版本來管理熱更新代碼,因此選擇IndexedDB作爲存儲方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"IndexedDB學習地址:"},{"type":"link","attrs":{"href":"http://www.ruanyifeng.com/blog/2018/07/indexeddb.html","title":""},"content":[{"type":"text","text":"瀏覽器數據庫IndexedDB入門教程"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"附上簡易實現:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"/**\n * @param dbName 數據庫名稱\n * @param version 數據庫版本 不傳默認爲1\n * @param primary 數據庫表主鍵\n * @param indexList Array 數據庫表的字段以及字段的配置,每項爲Object,結構爲{ name, keyPath, options }\n */\nclass WebDB{\n constructor({dbName, version, primary, indexList}){\n this.db = null\n this.objectStore = null\n this.request = null\n this.primary = primary\n this.indexList = indexList\n this.version = version\n this.intVersion = parseInt(version.replace(/\\./g, ''))\n this.dbName = dbName\n try {\n this.open(dbName, this.intVersion)\n } catch (e) {\n throw e\n }\n }\n\n open (dbName, version) {\n const indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;\n if (!indexedDB) {\n console.error('你的瀏覽器不支持IndexedDB')\n }\n this.request = indexedDB.open(dbName, version)\n this.request.onsuccess = this.openSuccess.bind(this)\n this.request.onerror = this.openError.bind(this)\n this.request.onupgradeneeded = this.onupgradeneeded.bind(this)\n }\n\n onupgradeneeded (event) {\n console.log('onupgradeneeded success!')\n this.db = event.target.result\n const names = this.db.objectStoreNames\n if (names.length) {\n for (let i = 0; i< names.length; i++) {\n if (this.compareVersion(this.version, names[i]) !== 0) {\n this.db.deleteObjectStore(names[i])\n }\n }\n }\n if (!names.contains(this.version)) {\n // 創建表,配置主鍵\n this.objectStore = this.db.createObjectStore(this.version, { keyPath: this.primary })\n this.indexList.forEach(index => {\n const { name, keyPath, options } = index\n // 創建列,配置屬性\n this.objectStore.createIndex(name, keyPath, options)\n })\n }\n }\n\n openSuccess (event) {\n console.log('openSuccess success!')\n this.db = event.target.result\n }\n\n openError (event) {\n console.error('數據庫打開報錯', event)\n // 重新鏈接數據庫\n if (event.type === 'error' && event.target.error.name === 'VersionError') {\n indexedDB.deleteDatabase(this.dbName);\n this.open(this.dbName, this.intVersion)\n }\n }\n\n compareVersion (v1, v2) {\n if (!v1 || !v2 || !isString(v1) || !isString(v2)) {\n throw '版本參數錯誤'\n }\n const v1Arr = v1.split('.')\n const v2Arr = v2.split('.')\n if (v1 === v2) {\n return 0\n }\n if (v1Arr.length === v2Arr.length) {\n for (let i = 0; i< v1Arr.length; i++) {\n if (+v1Arr[i] > +v2Arr[i]) {\n return 1\n } else if (+v1Arr[i] === +v2Arr[i]) {\n continue\n } else {\n return -1\n }\n }\n }\n throw '版本參數錯誤'\n }\n\n /**\n * 添加記錄\n * @param record 結構與indexList 定下的index字段相呼應\n * @return Promise\n */\n add (record) {\n if (!record.key) throw '需要添加的key爲必傳字段!'\n return new Promise((resolve, reject) => {\n let request\n try {\n request = this.db.transaction([this.version], 'readwrite').objectStore(this.version).add(record)\n request.onsuccess = function (event) {\n resolve(event)\n }\n\n request.onerror = function (event) {\n console.error(`${record.key},數據寫入失敗`)\n reject(event)\n }\n } catch (e) {\n reject(e)\n }\n })\n }\n \n // 其他代碼省略\n ...\n ... \n}"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"三、雙緩存獲取熱更新代碼"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":" IndexedDB建模存儲接口數據"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 熱更新模塊代碼僅與版本有關,根據版本來建表。 表主鍵key: 表示模塊名 列名value: 表示模塊熱更新代碼 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"當頁面功能模塊,首次請求熱更新代碼,獲取成功,則往表添加數據。下次頁面請求,則從IndexedDB表獲取,以此減少接口的查詢次數,以及服務端的IO操作。"}]},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"背景頁全局緩存"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"創建全局對象緩存模塊熱更新數據,代替頻繁的IndexedDB數據庫操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"附上簡易代碼:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\nlet DBRequest\nconst moduleCache = {} // 熱更新功能模塊緩存\nconst moduleStatus = {} // 存儲模塊狀態\n\n// 接口獲取熱更新代碼,更新本地數據庫\nconst getLastCode = (moduleName, type) => {\n const cdnUrl = 'https://***.com'\n const scriptUrl = addParam(`${cdnUrl}/${version}/${type}/${moduleName}.js`, {\n _: new Date().getTime()\n })\n return request.get({\n url: scriptUrl\n }).then(res => {\n updateModuleCode(moduleName, res.trim())\n return res.trim()\n }) \n}\n\n// 更新本地數據庫\nconst updateModuleCode = (moduleName, code, dbRequest = DBRequest) => {\n dbRequest.get(moduleName).then(record => {\n if (record) {\n dbRequest.update({key: moduleName,value: code}).then(() => {\n moduleStatus[moduleName] = 'loaded'\n }).catch(err => {\n console.warn(`數據更新${moduleName}失敗!`, err)\n })\n }\n }).catch(() => {\n dbRequest.add({key: moduleName,value: code}).then(() => {\n moduleStatus[moduleName] = 'loaded'\n }).catch(err => {\n console.warn(`${moduleName} 添加數據庫失敗!`, err)\n })\n })\n moduleCache[moduleName] = code\n}\n\n// 獲取模塊熱更新代碼\nconst getHotFixCode = ({moduleName, type}, sendResponse) => {\n if (!DBRequest) {\n try {\n DBRequest = new WebDB({\n dbName,\n version,\n primary: 'key',\n indexList: [{ name: 'value', KeyPath: 'value', options: { unique: true } }]\n })\n } catch (e) {\n console.warn(moduleName, ' :鏈接數據庫失敗:', e)\n return\n }\n } \n\n // 存在緩存對象\n if (moduleCache[moduleName]) {\n isFunction(sendResponse) && sendResponse({\n status: 'success',\n code: moduleCache[moduleName]\n })\n moduleStatus[moduleName] !== 'loaded' && getLastCode(moduleName, type)\n }\n else{ // 不存在緩存對象,則從IndexDB取\n setTimeout(()=>{\n DBRequest.get(moduleName).then(res => {\n ...\n moduleStatus[moduleName] !== 'loaded' && getLastCode(moduleName, type)\n }).catch(err => {\n ...\n moduleStatus[moduleName] !== 'loaded' && getLastCode(moduleName, type)\n })\n },0)\n }\n}\n\nexport default getHotFixCode\n"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}}],"text":"四、CS解析熱更新代碼"}]},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}}],"text":"背景頁註冊監聽獲取熱更新代碼請求"}]}]}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// HotFix.js背景頁封裝方法\nimport moduleMap from 'moduleMap' // 上節提到的,所有的功能模塊需註冊\n\nclass HotFix {\n constructor() {\n // 註冊監聽請求 \n chrome.extension.onRequest.addListener(this.requestListener)\n // 生產環境 & 熱修復環境 & 測試環境:瀏覽器打開默認加載所有配置功能模塊的熱修復代碼\n if (__PROD__ || __HOT__ || __TEST__) {\n try {\n this.getModuleCode()\n }catch (e) {\n console.warn(e)\n }\n }\n }\n\n requestListener (request, sender, sendResponse) {\n switch(request.name) {\n case 'getHotFixCode':\n getHotFixCode(request, sendResponse)\n break\n }\n }\n\n getModuleCode () {\n for (let p in moduleMap) {\n getHotFixCode(...)\n }\n }\n}\n\nexport default new HotFix()\n \n// background.js 註冊監聽請求\nimport './HotFix'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}}],"text":"CS發送請求獲取數據,並執行更新"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"相關簡易代碼如下:"}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// CS的hotfix.js 解析熱更新代碼\nconst deepsFilterModule = [\n 'csCommon',\n 'Popup'\n]\n\nconst insertScript = (injectUrl, id, reject) => {\n if (document.getElementById(id)) {\n return\n }\n\n const temp = document.createElement('script');\n temp.setAttribute('type', 'text/javascript');\n temp.setAttribute('id', id)\n temp.src = injectUrl\n temp.onerror = function() {\n console.warn(`pageScript ${id},線上代碼解析失敗`)\n reject()\n }\n document.head.appendChild(temp)\n}\n\nconst parseCode = (moduleName, code, reject) => {\n try {\n eval(code)\n window.CRX[moduleName].init()\n } catch (e) {\n console.warn(moduleName + ' 解析失敗: ', e)\n reject(e)\n }\n}\n\nfunction deepsReady(checkDeeps, execute, time = 100){\n let exec = function(){\n if(checkDeeps()){\n execute();\n }else{\n setTimeout(exec,time);\n }\n }\n setTimeout(exec,0);\n}\n\nconst hotFix = (moduleName, type = 'cs') => {\n if (!moduleName) {\n return Promise.reject('參數錯誤')\n }\n\n return new Promise((resolve, reject) => {\n // 非生產環境 & 熱修復環境 & 測試環境:走本地代碼\n if (!__PROD__ && !__HOT__ && !__TEST__) {\n if (deepsFilterModule.indexOf(moduleName) > -1) {\n reject()\n } else {\n deepsReady(\n () => window.CRX && window.CRX.$COM && Object.keys(window.CRX.$COM).length,\n reject\n )\n }\n return\n }\n\n // 向背景頁發送取熱更新代碼的請求\n chrome.extension.sendRequest({\n name: \"getHotFixCode\",\n type: type,\n moduleName\n }, function(res) {\n if (res.status === 'success') {\n if (type !== 'ps') {\n // 公共方法、Pop頁代碼,直接解析代碼\n // 功能模塊代碼,需等公共方法解析完成,纔可以執行,CS引用公共方法 \n if (deepsFilterModule.indexOf(moduleName) === -1) {\n deepsReady(() => window.CRX && window.CRX.$COM && Object.keys(window.CRX.$COM).length, () => parseCode(moduleName, res.code, reject))\n } else {\n parseCode(moduleName, res.code, reject)\n }\n } else {\n insertScript(res.code, moduleName, reject)\n }\n } else {\n if (deepsFilterModule.indexOf(moduleName) === -1) {\n deepsReady(() => window.CRX && window.CRX.$COM && Object.keys(window.CRX.$COM).length, () => reject('線上代碼不存在!'))\n } else {\n reject('線上代碼不存在!')\n }\n }\n })\n })\n}\n\nexport default hotFix"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"五、總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"black"}}],"text":"簡歷例子,完成了模塊功能熱更新的邏輯設計。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.