写点什么

解决大中型浏览器 (Chrome) 插件开发痛点:自定义热更新方案——2. 基于双缓存更新功能模块

用户头像
梁龙先森
关注
发布于: 2020 年 11 月 09 日
解决大中型浏览器(Chrome)插件开发痛点:自定义热更新方案——2.基于双缓存更新功能模块

Chrome扩展程序序列文章:

  1. 浏览器插件:那些你需会的操作

  2. 热更新方案设计:1.原理分析及构建部署实现

  3. 热更新方案设计:2.基于双缓存更新功能模块

背景

上篇文章,介绍了扩展程序热更新方案的实现原理以及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()
})

一、扩展程序通信流程图

  1. background.js:背景页面,运行在浏览器后台,单独的进程,浏览器开启到关闭一直都在执行,为扩展程序的"中心",执行应用的主要功能。

  2. 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)
}
})
}
// 其他代码省略
...
...
}

三、双缓存获取热更新代码

  1.  IndexedDB建模存储接口数据

热更新模块代码仅与版本有关,根据版本来建表。 表主键key: 表示模块名   列名value: 表示模块热更新代码  

当页面功能模块,首次请求热更新代码,获取成功,则往表添加数据。下次页面请求,则从IndexedDB表获取,以此减少接口的查询次数,以及服务端的IO操作。

  1. 背景页全局缓存

创建全局对象缓存模块热更新数据,代替频繁的IndexedDB数据库操作。

附上简易代码:

let DBRequest
const 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: { uniquetrue } }]
      })
    } 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解析热更新代码

  1. 背景页注册监听获取热更新代码请求

// 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'



  1. 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



原创作品,抄袭必究!



发布于: 2020 年 11 月 09 日阅读数: 1063
用户头像

梁龙先森

关注

寒江孤影,江湖故人,相逢何必曾相识。 2018.03.17 加入

1月的计划是:重学JS,点个关注,一起学习。

评论 (1 条评论)

发布
用户头像
您好,我是开源中国的⼯作⼈员-李岚,我们社区最近启动了⼀项「源创计划」,可以⼀站式同步您公众号的博客⽂章到OSCHINA账号上。
不知道您有没有兴趣参与「源创计划」,让更多开发者看到您的⽂章呢?
2020 年 11 月 14 日 15:24
回复
没有更多了
解决大中型浏览器(Chrome)插件开发痛点:自定义热更新方案——2.基于双缓存更新功能模块