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