快速上手v3版谷歌插件开发

635 阅读5分钟

前言

不知道下面这个场景大家有没经历过,当某个函数的用法细节一时记不起来,或者需求很赶的时候,我们往往会在网上查询一下相关文章,借助一下先人的智慧;但是看到这种操作我真是麻了

reason.png

这个是怎么做到的呢,关键代码就一行css

demo.png

user-select 属性可以控制用户的选中行为,当设置为none的时候用户就无法选中网页内容;

居然妄想通过这一行代码就阻止我,岂可修!

你别得意.jpg

我们要用魔法打败魔法,在开发者工具里加上

* {
  user-select: text !important; /* text 表示可复制图文内容 */ 
}

舒服了

result.png

可是,每次搜索完都要手动加入这一行代码,好麻烦啊,怎么办呢?写个插件吧,对目标网页自动注入这一行css;

插件开发

关于插件开发,其实 官网教程 写的也比较简单易懂,但是是全英的,而且需要科学上网..

目前插件配置最新版本是v3版,谷歌网页商城在2022.01.17停止接收v2版的插件,二者配置内容也有些不同,这里我们就与时俱进一下;

下面我们通过这个注入css的插件来快速上手谷歌插件开发,项目地址在这里,下面是目录结构

package.png

这里目录内容看起来很多,是因为我写习惯了,用了ts + react + webpack + less,其实这种简单的插件用原生js + html + css 即可

下面是开发插件必须了解的一些内容

manifest.json

每个插件必须提供的配置文件,其中插件名,插件版本号,配置文件版本是必填的,有了这个文件插件就能加载了;

init.png

可配置项很多,这里只写了本插件需要用到的

{
    // 插件名
    "name": "copier",
    // 插件描述
    "description": "代码本天成,妙手偶得之",
    // 插件版本
    "version": "0.0.1",
    // 配置文件版本
    "manifest_version": 3,
    // 后台脚本
    "background": {
        "service_worker": "js/background.js"
    },
    // 授权插件可访问的网址
    "host_permissions": [
        "http://*/*",
        "https://*/*"
    ],
    // 授权插件可调用的的 chrome API
    "permissions": [
        "storage",
        "tabs",
        "activeTab",
        "scripting",
        "webNavigation"
    ],
    // 右上角的弹窗配置
    "action": {
        "default_popup": "popup.html",
        "default_icon": {}
    },
    // 插件图标配置
    "icons": {}
}

background

类似于一个始终运行在后台的页面,只要插件开启就在后台运行;通过service_worker配置脚本,需要注意的是:

  • 不能通过它访问DOM
  • 在里面脚本里console.log的内容并不会输出在当前访问页面的控制台,它有独立的控制台,需要在扩展工具页面开启

background.png

  • 如果插件有报错会出现在这里

backgroundError.png

// background.js
import { getCurrentTab, getDomain } from "./utils";

/** 安装初始化 */
chrome.runtime.onInstalled.addListener(({ reason }) => {
  if (reason === chrome.runtime.OnInstalledReason.INSTALL) {
    chrome.storage.sync.set({
      whiteList: [],
    });
  }
});

/** 监听域名变动 */
chrome.webNavigation.onCommitted.addListener((details) => {
  const { url, tabId, frameId } = details;
  if (details?.url.startsWith("http")) {
    chrome.storage.sync.get(["whiteList"], (result) => {
      if (result.whiteList.includes(getDomain(url))) {
        // @ts-ignore
        chrome.scripting.removeCSS({
          target: { tabId },
          files: ["inject.css"],
        });
      // 仅主页面注入一次,避免重复注入
      } else if (frameId === 0) {
        chrome.scripting.insertCSS({
          target: { tabId },
          files: ["inject.css"],
        });
      }
    });
  }
});

chrome.storage.onChanged.addListener(async (changes) => {
  const tab = await getCurrentTab();
  if (changes.whiteList?.newValue && tab?.url.startsWith("http")) {
    if (changes.whiteList?.newValue.includes(getDomain(tab?.url))) {
      // @ts-ignore
      chrome.scripting.removeCSS({
        target: { tabId: tab.id },
        files: ["inject.css"],
      });
    } else {
      chrome.scripting.insertCSS({
        target: { tabId: tab.id },
        files: ["inject.css"],
      });
    }
  }
});

这个脚本做了三件事:

  1. 监听安装事件,插件安装时初始化一个 whiteList 变量储存白名单域名;插件内通过chrome.storage api 储存变量,chrome.storage.sync.set 会把储存信息同步到每个登录了该账户的谷歌浏览器,如果没联网的话,效果等同于chrome.storage.local.set,即本地储存

  2. 通过chrome.webNavigation.onCommitted 事件监听域名变动,当域名变动且在白名单外时,注入css;否则移除注入的css

    /* insert.css */
    * {
      user-select: text !important;
    }
    

    这里我们用的是chrome.webNavigation api,这个api可以监听导航变动,它的执行时机如下;

    onBeforeNavigate -> onCommitted -> [onDOMContentLoaded] -> onCompleted
    

    还有一个比较常见的api chrome.tabs.onUpdated,可以监听 tab 页变动,它也可以拿到当前页面的url,但是 onUpdated 事件在url变更时会多次执行,导致多次注入,因此我们不用;

    还有当当前tab页内有含有 iframe时会导致chrome.webNavigation.onCommitted多次触发,也会导致重复注入的问题,因此我们设置了 frameId === 0 时才注入,因为主页面的frameId 始终等于 0

  3. 通过 chrome.storage.onChanged 事件监听白名单值变化,动态注入或移除 css

这里因为我们做了个白名单的功能,所以是通过js动态注入css,其实注入js/css还可以通过配置manifest静态注入

// manifest.json
{
  // ...
  "content_scripts": [
     {
       // 满足matches匹配的域名
       "matches": ["https://xxx"],
       // 注入css
       "css": ["xxx.css"],
       // 注入js
       "js": ["xxx.js"],
       // 注入时机
       "run_at": "document_idle" | "document_start" | "document_end"
     }
  ],
}

action

用于配置右上角弹框页面

popup.png

这里我们通过 chrome.tabs api 封装了一个 getCurrentTab 函数来获取当前tab页信息

/** 获取当前tab信息 */
export async function getCurrentTab() {
  let queryOptions = { active: true, lastFocusedWindow: true };
  let [tab] = await chrome.tabs.query(queryOptions);
  return tab;
}

通过右上角的弹框页面来判断白名单激活与否

import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { getCurrentTab, getDomain, getSyncState } from "../utils";
import "./style.less";

const Popup = () => {
  const [domain, setDomain] = useState("");
  const [isChecked, setIsChecked] = useState(false);

  useEffect(() => {
    getCurrentTab().then((tab) => {
      const curDomain = getDomain(tab.url);
      setDomain(curDomain);
      getSyncState(["whiteList"], ({ whiteList }) => {
        setIsChecked(!whiteList.includes(curDomain));
      });
    });
  }, []);

  const handleChecked = (e) => {
    setIsChecked(e.target.checked);
    getSyncState(["whiteList"], ({ whiteList }) => {
      let temp = [...whiteList];
      if (e.target.checked) {
        temp = whiteList.filter((i) => i !== domain);
      } else {
        temp.push(domain);
      }
      chrome.storage.sync.set({ whiteList: temp });
    });
  };

  return (
    <>
      <header className="card">
        <h1>Copier</h1>
      </header>
      <main>
        <form>
          <div className="card">
            <div className="item">
              <label className="domain">此网站</label>
              <input
                style={{ cursor: "pointer" }}
                type="checkbox"
                onChange={handleChecked}
                checked={isChecked}
              />
            </div>
            <div className="item domain">{domain}</div>
          </div>
        </form>
      </main>
    </>
  );
};

ReactDOM.render(<Popup />, document.getElementById("root"));

需要注意的是,右上角的弹框页面也是一个独立的网页,也有自己的控制台,可以通过右键打开

popup_console.png

除了这个页面外,我们还可能用到的有 插件安装后 打开的欢迎页,可以通过 tabs api实现

chrome.tabs.create({
  url: "boarding.html",
});

还有插件的配置页面,需要在manifest里配置

extension_options.png

// manifest.json
{
    //...
    "options_page": "options.html",
}

以上就是一个插件最基本的页面结构

除此之外需要了解的还有插件间页面间的通信方式,我们这里没有用到就不细讲了

// 最常用的信息收发
chrome.runtime.sendMessage(
  extensionId?: string,
  message: any,
  options?: object,
  callback?: (response: any) => void,
)
chrome.runtime.onMessage.addListener(
  callback: (message: any, sender: MessageSender, sendResponse: function) => boolean | undefined,
)

需要注意的点还有

  • 当我们需要用到DOM时要注意拿到的是哪个页面的DOM(可以通过脚本注入的方式在注入脚本里拿到当前打开页面的DOM)

  • 每次修改完代码记得刷新一下

refresh.png

别走.jpg

最近刚创的微信公众号:前端一块砖

打包后文件的网盘链接我就直接贴出来了,如果觉得有帮助可以关注支持一下,给我一点更新动力,感谢🙇‍

再贴一下源码地址👈