重构掘金插件:支持多种掘金主题
重构项目
之前写了个插件可以修改掘金主题,供大家选择暗夜模式,最近又发现了个新框架专门为了写插件而生的,他就是 plasmo,官方将其比喻为 浏览器扩展界的 Next.js !
,可见其强大。本次改造即使用plasmo对原来的内容进行重构,为后续支持多种主题打下基础,并且本次主题已经上线chrome商店,欢迎体验。
先来熟悉一下plasmo,其特性如下:
- 对 React + Typescript 的一等支持
- 声明式开发,自动生成 manifest.json (MV3)
- 热重载
- .env* 文件
- 远程代码打包 (例如:使用 gtag4 )
- 自动部署 (通过 BPP)
- 还有更多! 🚀
注意:
- Popup 改动应在 popup.tsx
- Options 页面改动应在 options.tsx
- Content script 改动应在 content.ts
- Background service worker 改动应在 background.ts
对于项目结构来说你可以将文件全放在根目录,也可以放在 src 目录下,对于我们来说当然习惯放在 src 目录下,
需要注意的是我们可以将 manifest 文件的内容写进 package.json 中,替代关系为:
- packageJson.version -> manifest.version
- packageJson.displayName -> manifest.name
- packageJson.description -> manifest.description
- packageJson.author -> manifest.author
- packageJson.homepage -> manifest.homepage_url
下面回到具体项目,一边更新项目一边学习 plasmo 的用法
改造 background
安装好 plasmohq(plasmo 是 plasmohq 的子项目)相关依赖后在 background.ts 中引入
import { Storage } from '@plasmohq/storage';
聪明的你肯定已经猜到了这是用来操作插件缓存的 API,没做,现在我们可以轻松的设置和获取 storage 了。由于本次涉及到的业务改造是支持多个主题,background 实际上是变简单了,原因在于不再需要通过控制 icon 来告诉用户当前设置的主题是什么。所以改造后的代码为:
import { Storage } from '@plasmohq/storage';
class StartServer {
localStorageData: string = 'light';
storage: any = new Storage({ area: 'local' });
constructor() {
this.init();
}
init() {
chrome.runtime.onInstalled.addListener(async () => {
const data = await this.getData('theme');
console.log('插件安装了 获取data', data);
if (data) {
this.localStorageData = data;
} else {
await this.setThemeMode('light');
this.localStorageData = 'light';
}
});
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
console.log('--data- 主题', req, this.localStorageData);
const { theme } = req;
sendResponse({ theme: theme || this.localStorageData });
});
}
async getData(key) {
return await this.storage.get(key);
}
async setThemeMode(mode) {
await this.storage.set('theme', mode);
}
}
new StartServer();
逻辑比较简单,就是初始化时设置主题,或者获取主题后设置
新增popup.tsx
因为我们不再使用监听点击 icon 这种简单的操作模式而是提供了功能更丰富的 popup 页面,所以 popup.tsx 必不可少。 聪明的你看到.tsx 一定想到了支持 react 语法了吧,现在你可以像写 react 一样写插件。 思路是点击插件图标后进一步点击选项以便选择不同的主题样式。那么主要逻辑如下:
import { useState } from "react"
import { Storage, useStorage } from "@plasmohq/storage"
function IndexPopup() {
const [data, setData] = useState('');
const storage = new Storage({ area: 'local' });
async function clickDark(theme) {
console.log('点击了', theme);
await storage.set('theme', theme);
const msg = { theme };
changeTabTheme(msg);
// setData(theme)
}
function changeTabTheme(message) {
chrome.tabs.query({}, (tabs) => {
for (var i = 0; i < tabs.length; i++) {
console.log('获取url', tabs[i].url);
try {
const location = new URL(tabs[i].url);
const host = location.host;
if (host.includes('juejin.cn')) {
chrome.tabs.sendMessage(tabs[i].id, message, (res) => {
console.log('background=>content');
console.log(res);
});
}
} catch (e) {
console.log('报错', e);
}
}
});
}
}
export default IndexPopup;
popup视图:
function IndexPopup() {
···
return (
<div className="w-80 grid grid-cols-2 gap-4 p-5 ">
<div
className="grow cursor-pointer flex items-center justify-center px-2 py-3 border border-2 border-black text-base font-medium rounded-md text-black bg-white-600 hover:bg-white-700 md:py-4 md:text-lg md:px-10"
onClick={() => clickDark("light")}>
默认主题
</div>
<div
className="grow cursor-pointer flex items-center justify-center px-2 py-3 border border-transparent text-base font-medium rounded-md text-white bg-dark-600 hover:bg-dark-700 md:py-4 md:text-lg md:px-10"
onClick={() => clickDark("dark")}>
黑色主题
</div>
<div
className="grow cursor-pointer flex items-center justify-center px-2 py-3 border border-transparent text-base font-medium rounded-md text-napoli-light bg-klein-600 hover:bg-klein-700 md:py-4 md:text-lg md:px-10"
onClick={() => clickDark("klein")}>
克莱因蓝
</div>
<div className="">{data}</div>
</div>
)
}
popup的主要代码逻辑即点击主题后通过 changeTabTheme 向各个浏览器tab窗口发送消息,background接收到消息后设置主题并缓存下来
content大变样
background和popup都完整实现后接下来就是设置content,与background和popup不同,content可谓大变样
import type { PlasmoContentScript } from "plasmo"
export const config: PlasmoContentScript = {
matches: ["https://*.juejin.cn/*"]
}
首先设置matches条件,让插件只在juejin.cn起作用 其次,我将主题样式封装成了配置文件,即config.json,当设置不同的主题时,可以直接读取config.json文件来获取主题样式,这样可以很方便的设置主题 在实现的过程中遇到了很多问题,比如主题之间切换,主题和默认主题之间的切换,dom用MutationObserver监听配置修改,多tab之间主题不同步的问题等,整体逻辑比以前多了一些但是因为兼容了配置文件,所以对以后的新增其他主题的支持也更好了。 值得一提的是本次改造设计到的配置文件,配置文件的数据结构我前后多次揣摩,最终决定将数据结构扁平设计,从而让读取数据结构的逻辑简单化,抽象为如下所示:
{
"dark": [
{
"targetElementClassName": "body", //选定的元素
"className": "blackBackground", //给选定的元素添加的class名
"selector": "querySelector", //选择器 querySelector 或者 querySelectorAll
"type": "background" //类型 background fontcolor 或者 img 是背景图还是文字
},
···
],
···
}
content popup background 通信机制
目前项目涉及到了这三方的通信机制,简单说他们之间可以用消息订阅的方式来通知对方该做什么,这部分的逻辑manifest v2
和manifest v3
并没有什么区别,popup background可以用
chrome.runtime.onMessage
和 chrome.runtime.sendMessage
来通信,与content通信,需要使用chrome.tabs.sendMessage
代替 chrome.runtime.sendMessage
,详细的通信机制如果大家感兴趣欢迎点赞和留言,我会在后续的文章中详细讲解。
好了,本次改造整体代码依然在GitHub dev分支上,欢迎大家star。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。