Chrome插件踩坑日志(三)热更新解耦 + 本地存储差异化
- 相关链接
上一期讲到了如何手动实现“热更新”,准确来说应该叫hot reload
,这一期就来讲讲如何把之前的热更新插件和实际的业务代码进行解耦,以及多环境本地存储的坑。
老规矩,上代码:
👉 github仓库
插件持续优化
插件解耦
在之前的架构里,插件(下图的第一个红框)的主要功能是:
-
启动
ws
服务用于通信 -
监听动态构建,结束后通知页面更新
第二个红框里的hotReload.ts
是开发环境下引入的热更新监听,和插件建立ws连接
虽然在功能上不影响,但是开发环境脚本和业务代码耦合在一起,难免会有所膈应,并且万一改到代码,也会影响本地开发,所以就打算把这块抽离到插件内做处理。
解耦最简单的做法就是判断下当前的环境是否是开发环境,然后把监听热更新的代码注入到background
中去。
调研发现做这个事情最合适的钩子是transform
:
这个钩子会在每个传入模块进行解析转换前执行,并且第一个入参就是之前的code代码(源码),第二个入参id是每个模块的id,基本上就是文件绝对路径。
有这个钩子就可以在解析到background/index模块前,进行代码注入:
整体的结构就是如下图所示:
本地热更新逻辑完全由插件接收,业务模块不会直接感知到
复用一个ws
在之前的热更新系列中还提到其实会有两个ws存在,一个叫bgWs
,控制background的更新,一个叫contentWs
,控制contentScripts代码的更新。
其实仔细想想没必要用两个ws进行处理,因为都是放在background
里处理的,而且处理ws
的心跳问题,这里代码还得写双份,那就显得很麻瓜。
可以直接修改content
相关的插件,和background
插件建立的ws建立链接,让这个ws做一个中转的角色:
(hotReload/content.ts
)
细节:configResolved配置解析阶段,就可以尝试连接background插件建立的ws
(hotReload/background.ts
)
支持popup
热更新
之前一直没有讲popup
,这次优化把这块的热更新也给支持了
补充下popup
的知识:
平时装了一个插件后,其实浏览器的右上角可能会出现一个小图标,上图就有一排,这个其实就是popup,点击以后就会有不同的功能,比如这个google翻译。
manifest.json配置如下:
这就是一个普通的html文件路径
那么普通的html文件如何实现热更新呢,其实很简单,就两步:
- 和
manifest.json
一样,html文件监听变更后复制
chokidar:一个跨平台的文件监听工具包
- 代码打包构建和background共用一个配置
主要的原因是background环境下如果执行chrome.runtime.reload()
,其实会附带把popup
的代码上下文也更新掉。
本地存储
不同环境的差异
这是一个经典的面试题,浏览器本地存储有哪些?
- cookie
- localStorage
- sessionStorage
- indexDB
这里就不讨论具体的细节了,一般业务中可能会选localStorage一把梭,哈哈哈
但是v3环境下的background里没法用localStorage,会返回undefined
,因为这里的上下文已经改成service worker
了,不支持同步的API,比如localStorage,xhr等,坑还是比较多的。
所以如果想用本地存储还是用indexDB
把。
通用indexDB模块
indexDB其实平时在业务中用的很少,api理解起来也比较费力,所以这里就引用了idb
,并封装了下代码,让使用的体验更加接近于localStorage:
import { IDBPDatabase, openDB } from 'idb'
import { onMessage } from './utils'
/**
* 封装indexDB方便background进行本地缓存
* 暴露三个公共方法(异步调用):
* getValue
* setValue
* deleteValue
*
* 同时注册这三个方法的Message消息,便于contentScript调用
*/
class CrxIndexDB {
private database: string
private tableName: string
private db: any
constructor(database: string, tableName: string) {
this.database = database
this.tableName = tableName
this.createObjectStore()
this.registerMessage()
}
public async getValue(keyName: string): Promise<any> {
await this.dbReady()
const { tableName } = this
const tx = this.db.transaction(tableName, 'readonly')
const store = tx.objectStore(tableName)
const result = await store.get(keyName)
return result.value
}
public async setValue(keyName: string, value: any) {
await this.dbReady()
const { tableName } = this
const tx = this.db.transaction(tableName, 'readwrite')
const store = tx.objectStore(tableName)
const result = await store.put({
keyName,
value
})
return result
}
public async deleteValue(keyName: string) {
await this.dbReady()
const { tableName } = this
const tx = this.db.transaction(tableName, 'readwrite')
const store = tx.objectStore(tableName)
const result = await store.get(keyName)
if (!result) {
return result
}
await store.delete(keyName)
return keyName
}
private sleep = (num): Promise<boolean> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
}, num * 1000)
})
}
private async dbReady() {
if (!this.db) {
await this.sleep(0.5)
return await this.dbReady()
}
return true
}
private registerMessage() {
onMessage('get-value-bg', async (params): Promise<any> => {
try {
const res = await this.getValue(params.keyName)
return {
result: res
}
} catch (e) {
return {
result: null
}
}
})
onMessage('set-value-bg', async ({ data }): Promise<any> => {
try {
const res = await this.setValue(data.keyName, data.value)
return {
result: res
}
} catch (e) {
return {
result: null
}
}
})
onMessage('del-value-bg', async ({ data }): Promise<any> => {
try {
const res = await this.deleteValue(data.keyName)
return {
result: res
}
} catch (e) {
return {
result: null
}
}
})
}
private async createObjectStore() {
const tableName = this.tableName
try {
this.db = await openDB(this.database, 1, {
upgrade(db: IDBPDatabase) {
if (db.objectStoreNames.contains(tableName)) {
return
}
db.createObjectStore(tableName, {
keyPath: 'keyName'
})
}
})
} catch (error) {
return false
}
}
}
const db = new CrxIndexDB('crx_index_db', 'crx_bg_table')
export default db
background中使用案例如下:
注意:这里是异步调用,别忘了async/await
细心的jym可能还发现一个叫registerMessage
的方法,这个方法就是为了给contentScript做通讯用的,注册了三个indexDB封装模块对应的公共方法。
下面是contentScript下的封装:
export const getCache = async (keyName: string): Promise<any> => {
const res = await sendMessage('get-value-bg', {
keyName
})
return res
}
export const setCache = async (keyName: string, value: any) => {
const res = await sendMessage('set-value-bg', {
keyName,
value
})
return res
}
export const delCache = async (keyName: string) => {
const res = await sendMessage('del-value-bg', {
keyName
})
return res
}
onMessage
和sendMessage
就是对原生chrome.runtime下的消息模块的封装,改成了async/await语法,更加贴近实际使用:
export const onMessage = (taskId: string, callback) => {
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
const { params } = request
if (request.taskId === taskId) {
const result = callback(params)
if (result && result.then) {
result.then((info) => {
sendResponse(info)
})
} else {
sendResponse(result)
}
}
return true
})
}
export const sendMessage = (taskId: string, params: any) => {
return new Promise((resolve) => {
chrome.runtime.sendMessage(
{
taskId,
params
},
(result) => {
resolve(result)
}
)
})
}
模板仓库目前已经比较完善,下期的计划会另开仓库来讲讲实际的chrome插件业务开发,请持续关注。
结束
其实这一次的更新优化还有其他几个功能点,比如eslint的配置,build模块的区分,husky的配置等等,但这些基本上都算是前端通用知识,和chrome插件无关,就没有拿来具体讲,感兴趣的jym可以关注下代码的仓库,Readme
里也贴上了讨论交流的方式。
创造不易,希望jym多多 点赞 + 关注 + 收藏 三连,持续更新中!!!
PS: 文中有任何错误,欢迎掘友指正
往期精彩📌
转载自:https://juejin.cn/post/7158607300440096776