Chrome 扩展程序热更新方案:2. 基于双缓存更新功能模块

微信扫一扫,分享到朋友圈

Chrome 扩展程序热更新方案:2. 基于双缓存更新功能模块

背景

上篇文章,介绍了扩展程序热更新方案的实现原理以及Content-Scripts的构建部署,其中有段代码如下,这里的hotFix方法,执行的便是获取热更新代码替换执行的逻辑。从接口获取到版本的热更新代码,如何存储和解析才能保证性能和正确呢?



上一篇: Chrome扩展程序热更新方案:1.原理分析及构建部署



// 功能模块执行入口文件
importhotFixfrom'hotfix.js'
importobjfrom'./entity.js'
//热修复方法,对obj模块进行热修复(下期介绍:基于双缓存获取热更新代码)
constmoduleName ='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入门教程

附上简易实现:

/**
*@paramdbName 数据库名称
*@paramversion 数据库版本 不传默认为1
*@paramprimary 数据库表主键
*@paramindexList Array 数据库表的字段以及字段的配置,每项为Object,结构为{ name, keyPath, options }
*/
classWebDB{
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) {
throwe
}
}
open (dbName, version) {
constindexedDB =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
constnames =this.db.objectStoreNames
if(names.length) {
for(leti =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'版本参数错误'
}
constv1Arr = v1.split('.')
constv2Arr = v2.split('.')
if(v1 === v2) {
return0
}
if(v1Arr.length === v2Arr.length) {
for(leti =0; i< v1Arr.length; i++) {
if(+v1Arr[i] > +v2Arr[i]) {
return1
}elseif(+v1Arr[i] === +v2Arr[i]) {
continue
}else{
return-1
}
}
}
throw'版本参数错误'
}
/**
* 添加记录
*@paramrecord 结构与indexList 定下的index字段相呼应
*@returnPromise
*/
add (record) {
if(!record.key)throw'需要添加的key为必传字段!'
returnnewPromise((resolve, reject) =>{
letrequest
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: { 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解析热更新代码

  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 解析热更新代码
constdeepsFilterModule = [
  '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

五、总结

简历例子,完成了模块功能热更新的逻辑设计。



北京严打以“结婚”为手段过户京牌:一女子离婚28次/过户23辆被刑拘

上一篇

商品后台系统优化

下一篇

你也可能喜欢

Chrome 扩展程序热更新方案:2. 基于双缓存更新功能模块

长按储存图像,分享给朋友