重构掘金插件:支持多种掘金主题

801 阅读4分钟

重构掘金插件:支持多种掘金主题

重构项目

之前写了个插件可以修改掘金主题,供大家选择暗夜模式,最近又发现了个新框架专门为了写插件而生的,他就是 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 v2manifest v3并没有什么区别,popup background可以用 chrome.runtime.onMessagechrome.runtime.sendMessage 来通信,与content通信,需要使用chrome.tabs.sendMessage代替 chrome.runtime.sendMessage,详细的通信机制如果大家感兴趣欢迎点赞和留言,我会在后续的文章中详细讲解。

好了,本次改造整体代码依然在GitHub dev分支上,欢迎大家star。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿