前言
不知道下面这个场景大家有没经历过,当某个函数的用法细节一时记不起来,或者需求很赶的时候,我们往往会在网上查询一下相关文章,借助一下先人的智慧;但是看到这种操作我真是麻了
这个是怎么做到的呢,关键代码就一行css
user-select 属性可以控制用户的选中行为,当设置为none的时候用户就无法选中网页内容;
居然妄想通过这一行代码就阻止我,岂可修!
我们要用魔法打败魔法,在开发者工具里加上
* {
user-select: text !important; /* text 表示可复制图文内容 */
}
舒服了
可是,每次搜索完都要手动加入这一行代码,好麻烦啊,怎么办呢?写个插件吧,对目标网页自动注入这一行css;
插件开发
关于插件开发,其实 官网教程 写的也比较简单易懂,但是是全英的,而且需要科学上网..
目前插件配置最新版本是v3版,谷歌网页商城在2022.01.17停止接收v2版的插件,二者配置内容也有些不同,这里我们就与时俱进一下;
下面我们通过这个注入css的插件来快速上手谷歌插件开发,项目地址在这里,下面是目录结构
这里目录内容看起来很多,是因为我写习惯了,用了ts + react + webpack + less,其实这种简单的插件用原生js + html + css 即可
下面是开发插件必须了解的一些内容
manifest.json
每个插件必须提供的配置文件,其中插件名,插件版本号,配置文件版本是必填的,有了这个文件插件就能加载了;
可配置项很多,这里只写了本插件需要用到的
{
// 插件名
"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.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"],
});
}
}
});
这个脚本做了三件事:
-
监听安装事件,插件安装时初始化一个 whiteList 变量储存白名单域名;插件内通过
chrome.storageapi 储存变量,chrome.storage.sync.set会把储存信息同步到每个登录了该账户的谷歌浏览器,如果没联网的话,效果等同于chrome.storage.local.set,即本地储存 -
通过
chrome.webNavigation.onCommitted事件监听域名变动,当域名变动且在白名单外时,注入css;否则移除注入的css/* insert.css */ * { user-select: text !important; }这里我们用的是
chrome.webNavigationapi,这个api可以监听导航变动,它的执行时机如下;onBeforeNavigate -> onCommitted -> [onDOMContentLoaded] -> onCompleted还有一个比较常见的api
chrome.tabs.onUpdated,可以监听 tab 页变动,它也可以拿到当前页面的url,但是 onUpdated 事件在url变更时会多次执行,导致多次注入,因此我们不用;还有当当前tab页内有含有 iframe时会导致
chrome.webNavigation.onCommitted多次触发,也会导致重复注入的问题,因此我们设置了frameId === 0时才注入,因为主页面的frameId 始终等于 0 -
通过
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
用于配置右上角弹框页面
这里我们通过 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"));
需要注意的是,右上角的弹框页面也是一个独立的网页,也有自己的控制台,可以通过右键打开
除了这个页面外,我们还可能用到的有 插件安装后 打开的欢迎页,可以通过 tabs api实现
chrome.tabs.create({
url: "boarding.html",
});
还有插件的配置页面,需要在manifest里配置
// 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)
-
每次修改完代码记得刷新一下
最近刚创的微信公众号:前端一块砖
打包后文件的网盘链接我就直接贴出来了,如果觉得有帮助可以关注支持一下,给我一点更新动力,感谢🙇
再贴一下源码地址👈