首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

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

背景

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

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

代码语言:javascript
复制

// 功能模块执行入口文件
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入门教程

附上简易实现:

代码语言:javascript
复制

/**
 * @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数据库操作。

附上简易代码:

代码语言:javascript
复制


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. 背景页注册监听获取热更新代码请求
代码语言:javascript
复制

// 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发送请求获取数据,并执行更新

相关简易代码如下:

代码语言:javascript
复制

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

五、总结

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

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/bdf16fea83df87ca53ae8935e
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券