12

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

 3 years ago
source link: https://xie.infoq.cn/article/bdf16fea83df87ca53ae8935e
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

背景

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

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

// 功能模块执行入口文件
importhotFixfrom'hotfix.js'
importobjfrom'./entity.js'

//热修复方法,对obj模块进行热修复(下期介绍:基于双缓存获取热更新代码)
constmoduleName ='obj';
hotFix('moduleName').catch(err=>{
console.warn(`${moduleName}线上代码解析失败`,err)
obj.Init()
})

一、扩展程序通信流程图

eeIja2z.png!mobile

  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

五、总结

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK