解决大中型浏览器 (Chrome) 插件开发痛点:自定义热更新方案——2. 基于双缓存更新功能模块
Chrome扩展程序序列文章:
背景
上篇文章,介绍了扩展程序热更新方案的实现原理以及Content-Scripts的构建部署方案,其中有段代码如下,这里的hotFix方法,执行的便是获取热更新代码替换执行的逻辑。从接口获取到版本的热更新代码,如何存储和解析才能保证性能和正确呢?
// 功能模块执行入口文件import hotFix from 'hotfix.js'import obj from './entity.js'//热修复方法,对obj模块进行热修复(下期介绍:基于双缓存获取热更新代码)const moduleName = 'obj';hotFix('moduleName').catch(err=>{ console.warn(`${moduleName}线上代码解析失败`,err) obj.Init()})
一、扩展程序通信流程图
background.js:背景页面,运行在浏览器后台,单独的进程,浏览器开启到关闭一直都在执行,为扩展程序的"中心",执行应用的主要功能。
content-script(CS):运行在Web页面上下文的JavaScript文件,一个tab产生一个CS环境,它与web页面的上下文环境两者是绝缘的。
基于Chrome通信流程,显然在背景页面中获取热更新代码版本进行统筹管理是最为合理。
二、存储方式的选择
几种常见的存储方式: cookie:会话,每次请求都会发送回服务器,大小不超过4kb。 sessionStorage :会话性能的存储,生命周期为当前窗口或标签页,当窗口或标签页被关闭,存储数据也就清空。
localStorage:记录在内存中,生命周期是永久的,除非用户主动删除数据。
indexedDB:本地事务型的数据库系统,用于在浏览器存较大数据结构,并提供索引功能以实现高性能的查找。
接口缓存:后台接口优先从缓存(内存)中取数据,但时效性相对比较差。
LocalStorage存储数据一般在2.5MB~10MB之间(各家浏览器不同),IndexedDB存储空间更大,一般不少于250M,且IndexedDB具备搜索功能,以及能够建立自定义的索引。考虑到热更新代码模块多,体积大,时效性要求高,且本地需要根据版本来管理热更新代码,因此选择IndexedDB作为存储方案。
IndexedDB学习地址:浏览器数据库IndexedDB入门教程
附上简易实现:
/** * @param dbName 数据库名称 * @param version 数据库版本 不传默认为1 * @param primary 数据库表主键 * @param indexList Array 数据库表的字段以及字段的配置,每项为Object,结构为{ name, keyPath, options } */class WebDB{ constructor({dbName, version, primary, indexList}){ this.db = null this.objectStore = null this.request = null this.primary = primary this.indexList = indexList this.version = version this.intVersion = parseInt(version.replace(/\./g, '')) this.dbName = dbName try { this.open(dbName, this.intVersion) } catch (e) { throw e } } open (dbName, version) { const indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB; if (!indexedDB) { console.error('你的浏览器不支持IndexedDB') } this.request = indexedDB.open(dbName, version) this.request.onsuccess = this.openSuccess.bind(this) this.request.onerror = this.openError.bind(this) this.request.onupgradeneeded = this.onupgradeneeded.bind(this) } onupgradeneeded (event) { console.log('onupgradeneeded success!') this.db = event.target.result const names = this.db.objectStoreNames if (names.length) { for (let i = 0; i< names.length; i++) { if (this.compareVersion(this.version, names[i]) !== 0) { this.db.deleteObjectStore(names[i]) } } } if (!names.contains(this.version)) { // 创建表,配置主键 this.objectStore = this.db.createObjectStore(this.version, { keyPath: this.primary }) this.indexList.forEach(index => { const { name, keyPath, options } = index // 创建列,配置属性 this.objectStore.createIndex(name, keyPath, options) }) } } openSuccess (event) { console.log('openSuccess success!') this.db = event.target.result } openError (event) { console.error('数据库打开报错', event) // 重新链接数据库 if (event.type === 'error' && event.target.error.name === 'VersionError') { indexedDB.deleteDatabase(this.dbName); this.open(this.dbName, this.intVersion) } } compareVersion (v1, v2) { if (!v1 || !v2 || !isString(v1) || !isString(v2)) { throw '版本参数错误' } const v1Arr = v1.split('.') const v2Arr = v2.split('.') if (v1 === v2) { return 0 } if (v1Arr.length === v2Arr.length) { for (let i = 0; i< v1Arr.length; i++) { if (+v1Arr[i] > +v2Arr[i]) { return 1 } else if (+v1Arr[i] === +v2Arr[i]) { continue } else { return -1 } } } throw '版本参数错误' } /** * 添加记录 * @param record 结构与indexList 定下的index字段相呼应 * @return Promise */ add (record) { if (!record.key) throw '需要添加的key为必传字段!' return new Promise((resolve, reject) => { let request try { request = this.db.transaction([this.version], 'readwrite').objectStore(this.version).add(record) request.onsuccess = function (event) { resolve(event) } request.onerror = function (event) { console.error(`${record.key},数据写入失败`) reject(event) } } catch (e) { reject(e) } }) } // 其他代码省略 ... ... }
三、双缓存获取热更新代码
IndexedDB建模存储接口数据
热更新模块代码仅与版本有关,根据版本来建表。 表主键key: 表示模块名 列名value: 表示模块热更新代码
当页面功能模块,首次请求热更新代码,获取成功,则往表添加数据。下次页面请求,则从IndexedDB表获取,以此减少接口的查询次数,以及服务端的IO操作。
背景页全局缓存
创建全局对象缓存模块热更新数据,代替频繁的IndexedDB数据库操作。
附上简易代码:
let DBRequestconst moduleCache = {} // 热更新功能模块缓存const moduleStatus = {} // 存储模块状态// 接口获取热更新代码,更新本地数据库const getLastCode = (moduleName, type) => { const cdnUrl = 'https://***.com' const scriptUrl = addParam(`${cdnUrl}/${version}/${type}/${moduleName}.js`, { _: new Date().getTime() }) return request.get({ url: scriptUrl }).then(res => { updateModuleCode(moduleName, res.trim()) return res.trim() }) }// 更新本地数据库const updateModuleCode = (moduleName, code, dbRequest = DBRequest) => { dbRequest.get(moduleName).then(record => { if (record) { dbRequest.update({key: moduleName,value: code}).then(() => { moduleStatus[moduleName] = 'loaded' }).catch(err => { console.warn(`数据更新${moduleName}失败!`, err) }) } }).catch(() => { dbRequest.add({key: moduleName,value: code}).then(() => { moduleStatus[moduleName] = 'loaded' }).catch(err => { console.warn(`${moduleName} 添加数据库失败!`, err) }) }) moduleCache[moduleName] = code}// 获取模块热更新代码const getHotFixCode = ({moduleName, type}, sendResponse) => { if (!DBRequest) { try { DBRequest = new WebDB({ dbName, version, primary: 'key', indexList: [{ name: 'value', KeyPath: 'value', options: { unique: true } }] }) } catch (e) { console.warn(moduleName, ' :链接数据库失败:', e) return } } // 存在缓存对象 if (moduleCache[moduleName]) { isFunction(sendResponse) && sendResponse({ status: 'success', code: moduleCache[moduleName] }) moduleStatus[moduleName] !== 'loaded' && getLastCode(moduleName, type) } else{ // 不存在缓存对象,则从IndexDB取 setTimeout(()=>{ DBRequest.get(moduleName).then(res => { ... moduleStatus[moduleName] !== 'loaded' && getLastCode(moduleName, type) }).catch(err => { ... moduleStatus[moduleName] !== 'loaded' && getLastCode(moduleName, type) }) },0) }}export default getHotFixCode
四、CS解析热更新代码
背景页注册监听获取热更新代码请求
// HotFix.js背景页封装方法import moduleMap from 'moduleMap' // 上节提到的,所有的功能模块需注册class HotFix { constructor() { // 注册监听请求 chrome.extension.onRequest.addListener(this.requestListener) // 生产环境 & 热修复环境 & 测试环境:浏览器打开默认加载所有配置功能模块的热修复代码 if (__PROD__ || __HOT__ || __TEST__) { try { this.getModuleCode() }catch (e) { console.warn(e) } } } requestListener (request, sender, sendResponse) { switch(request.name) { case 'getHotFixCode': getHotFixCode(request, sendResponse) break } } getModuleCode () { for (let p in moduleMap) { getHotFixCode(...) } }}export default new HotFix() // background.js 注册监听请求import './HotFix'
CS发送请求获取数据,并执行更新
相关简易代码如下:
// CS的hotfix.js 解析热更新代码const deepsFilterModule = [ 'csCommon', 'Popup']const insertScript = (injectUrl, id, reject) => { if (document.getElementById(id)) { return } const temp = document.createElement('script'); temp.setAttribute('type', 'text/javascript'); temp.setAttribute('id', id) temp.src = injectUrl temp.onerror = function() { console.warn(`pageScript ${id},线上代码解析失败`) reject() } document.head.appendChild(temp)}const parseCode = (moduleName, code, reject) => { try { eval(code) window.CRX[moduleName].init() } catch (e) { console.warn(moduleName + ' 解析失败: ', e) reject(e) }}function deepsReady(checkDeeps, execute, time = 100){ let exec = function(){ if(checkDeeps()){ execute(); }else{ setTimeout(exec,time); } } setTimeout(exec,0);}const hotFix = (moduleName, type = 'cs') => { if (!moduleName) { return Promise.reject('参数错误') } return new Promise((resolve, reject) => { // 非生产环境 & 热修复环境 & 测试环境:走本地代码 if (!__PROD__ && !__HOT__ && !__TEST__) { if (deepsFilterModule.indexOf(moduleName) > -1) { reject() } else { deepsReady( () => window.CRX && window.CRX.$COM && Object.keys(window.CRX.$COM).length, reject ) } return } // 向背景页发送取热更新代码的请求 chrome.extension.sendRequest({ name: "getHotFixCode", type: type, moduleName }, function(res) { if (res.status === 'success') { if (type !== 'ps') { // 公共方法、Pop页代码,直接解析代码 // 功能模块代码,需等公共方法解析完成,才可以执行,CS引用公共方法 if (deepsFilterModule.indexOf(moduleName) === -1) { deepsReady(() => window.CRX && window.CRX.$COM && Object.keys(window.CRX.$COM).length, () => parseCode(moduleName, res.code, reject)) } else { parseCode(moduleName, res.code, reject) } } else { insertScript(res.code, moduleName, reject) } } else { if (deepsFilterModule.indexOf(moduleName) === -1) { deepsReady(() => window.CRX && window.CRX.$COM && Object.keys(window.CRX.$COM).length, () => reject('线上代码不存在!')) } else { reject('线上代码不存在!') } } }) })}export default hotFix
五、总结
简单例子,完成了模块功能热更新的逻辑设计。
--END--
作者:梁龙先森 WX:newBlob
原创作品,抄袭必究!
版权声明: 本文为 InfoQ 作者【梁龙先森】的原创文章。
原文链接:【http://xie.infoq.cn/article/bdf16fea83df87ca53ae8935e】。文章转载请联系作者。
梁龙先森
寒江孤影,江湖故人,相逢何必曾相识。 2018.03.17 加入
1月的计划是:重学JS,点个关注,一起学习。
评论 (1 条评论)