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}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章