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