上篇文章,介绍了扩展程序热更新方案的实现原理以及Content-Scripts的构建部署,其中有段代码如下,这里的hotFix方法,执行的便是获取热更新代码替换执行的逻辑。从接口获取到版本的热更新代码,如何存储和解析才能保证性能和正确呢?
上一篇:Chrome扩展程序热更新方案:1.原理分析及构建部署
// 功能模块执行入口文件
import hotFix from 'hotfix.js'
import obj from './entity.js'
//热修复方法,对obj模块进行热修复(下期介绍:基于双缓存获取热更新代码)
const moduleName = 'obj';
hotFix('moduleName').catch(err=>{
console.warn(`${moduleName}线上代码解析失败`,err)
obj.Init()
})
基于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)
}
})
}
// 其他代码省略
...
...
}
热更新模块代码仅与版本有关,根据版本来建表。 表主键key: 表示模块名 列名value: 表示模块热更新代码
当页面功能模块,首次请求热更新代码,获取成功,则往表添加数据。下次页面请求,则从IndexedDB表获取,以此减少接口的查询次数,以及服务端的IO操作。
创建全局对象缓存模块热更新数据,代替频繁的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
// 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的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
简单例子,完成了模块功能热更新的逻辑设计。
领取专属 10元无门槛券
私享最新 技术干货