浏览器插件开发(常见功能集)
基于WXT框架实现浏览器插件里会涉及的常用功能,比如修改页面布局,点击插件图标展开侧边栏,侧边栏获取页面内容等等
展示魔改图
最新版本源码框架简介和使用
插件框架都是基于浏览器原生插件API实现,如谷歌浏览器、Firefox浏览器…
插件框架在适配上做了调整,比如 框架.action = 谷歌.action || Firefox.action
相对主流的 plasmo,wxt 的星星少了很多,但是博主就是爱用更新的,哈哈哈哈~
wxt 的文档看起来更易懂一些,程序结构也更清晰,并且调试模式深得我心~
WXT 框架官网plasmo 需要自己将 build 的插件目录添加到浏览器插件里,有点烦~
plasmo 框架官网初始化搭建
pnpm dlx wxt@latest init
# 然后选择项目目录、模板和包管理器
pnpm install
运行模板
个人喜好选择了 vue 和 yarn
yarn dev
运行后自动弹出安装了插件的浏览器(就是这点深得我心~)
简单介绍调试方式
点击插件 > 管理扩展程序,或者
地址栏输入 chrome://chrome-urls/
找到 chrome://extensions/
点击进入
不装逼就地址栏直接输入 chrome://extensions/
① 打开开发者模式,方便调试
② 会有一个或多个视图,此处是除开当前页面在控制台查看以外的 background.js 和 侧边栏 你能查阅到日志和界面元素的地方,默认视图查看 background.js 相关日志
③ 当插件出现异常错误时此处回显时错误按钮,点击进入查看异常信息(非常常用)
另外切记,修改主要配置或者部分文件 background.js 时需要重启项目编译插件文件~
目录和文件讲解
注意查看 重点关注 字样的目录
📂 {rootDir}/
📁 .output/
📁 .wxt/
📁 assets/
📁 components/
📁 composables/
📁 entrypoints/
📁 hooks/
📁 modules/
📁 public/
📁 utils/
📄 .env
📄 .env.publish
📄 app.config.ts
📄 package.json
📄 tsconfig.json
📄 web-ext.config.ts
📄 wxt.config.ts
.output/
:所有构建工件都将放在这里 重点关注 生成的插件就在这里,文件是否编译看这里.wxt/
:由WXT生成,包含TS配置assets/
:包含所有应由 WXT 处理的 CSS、图像和其他资产components/
:默认自动导入,包含UI组件composables/
:默认自动导入,包含项目可组合 Vue 函数的源代码entrypoints/
:包含捆绑到扩展程序中的所有入口点 重点关注 主要代码目录hooks/
:默认自动导入,包含项目用于 React 和 Solid 钩子的源代码modules/
:包含适用于您的项目的本地 WXT 模块public/
:包含您想要按原样复制到输出文件夹的任何文件,无需经过 WXT 处理utils/
:默认自动导入,包含整个项目中使用的通用实用程序.env
:包含环境变量.env.publish
:包含发布的环境变量app.config.ts
:包含运行时配置package.json
:你的包管理器使用的标准文件tsconfig.json
:配置告诉 TypeScript 如何表现web-ext.config.ts
:配置浏览器启动wxt.config.ts
:WXT 项目的主要配置文件 重点关注 主要配置文件
【正题】弹出窗样式调整
这个其实对应的是 entrypoints/popup 目录的内容,可以直接修改 index.html 或者 App.vue
熟悉的配方,不做过多介绍了,想怎么改怎么改~
【正题】界面样式注入
entrypoints/ 目录下新建 reset.css
body{
//部分页面本身css样式具备,需要强制时使用
background: green !important;
}
entrypoints/content.ts 引入 reset.css ,并且修改匹配正则
import './reset.css'
export default defineContentScript({
//匹配所有地址
matches: ['<all_urls>'],
main() {
console.log('Hello content.');
},
});
此时访问百度,发现百度 绿了
正常打印 content 的 console 内容,就会发现 content 针对的就是页面级内容
【正题】JS注入
继续上一个改动…
查看百度页面的元素,可以看到搜索框按钮为
<input type="submit" id="su" value="百度一下" class="bg s_btn">
那根据 id 不就可以,你懂的…
修改 content.ts 代码
//ctx 页面上下文,用于框架方法调用使用
main(ctx:ContentScriptContext) {
console.log('Hello content.');
let su_btn = document.getElementById("su") as HTMLInputElement
su_btn.value = '度娘一下';
}
根据JS可以进行页面元素的访问、修改那么都可以了,悬浮一个按钮不是很轻松?
增加悬浮页面悬浮按钮
悬浮按钮 JS 代码
const button = document.createElement('div');
button.innerHTML = `
<div class="fab-container">
<button class="fab-button">
<svg class="fab-icon" viewBox="0 0 24 24">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</svg>
</button>
<div class="fab-label">Open</div>
</div>
`;
button.onclick = () => {
console.log('click');
}
样式当然可以写入 reset.css
.fab-container {
position: fixed;
bottom: 40px;
right: 20px;
display: flex;
align-items: center;
z-index: 1000;
}
.fab-label {
background: #333;
color: white;
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
opacity: 0;
transform: translateX(10px);
transition: all 0.3s ease;
pointer-events: none;
}
.fab-button {
color: #fff;
width: 40px;
height: 40px;
border-radius: 50%;
background: #e91e63;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
transition: background 0.3s ease;
}
.fab-button:hover {
background: #910334;
}
.fab-button:hover + .fab-label {
opacity: 1;
transform: translateX(0);
}
.fab-icon {
width: 20px;
height: 20px;
fill: white;
}
此时发现问题没,悬浮按钮增加给谁,body元素?或者固定某个标签 其实框架本身提供了注入UI的方式,可以直接选择基于的元素和方式在加载后注入(怎么写就看喜好了)
注:注入方式因为插入在页面元素里,因此会受到页面样式影响
const integrated_Ui = createIntegratedUi(ctx,{
position: 'inline',
anchor: 'body',
onMount:(container) => {
const button = document.createElement('div');
button.innerHTML = `
<div class="fab-container">
<button class="fab-button">
<svg class="fab-icon" viewBox="0 0 24 24">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</svg>
</button>
<div class="fab-label">Open</div>
</div>
`;
button.onclick = () => {
console.log('click');
}
container.insertBefore(button, container.firstChild);
}
});
integrated_Ui.mount();
还有一种注入方式,代码如下,效果类似页面增加 iframe 不受页面本身的样式文件影响
const shadowRoot_Ui = createShadowRootUi(ctx, {
//自定义名称,不能冲突
name: "llzhang-div",
anchor: ".channel-container",
position: "inline",
append: "before",
onMount(container) {
const app = document.createElement("div");
app.innerHTML = '<img src="https://cn.cravatar.com/avatar/3fbc4a959e6b84861a27c2d43c4a550b?s=400&r=G&d=mp&ver=1749039123" alt="头像" style="width:100px">' +
'<p>插件已启动</p>';
container.append(app);
return app;
},
onRemove: (app) => {
if (app) {
app.remove();
}
},
});
shadowRoot_Ui.mount();
综上JS注入和CSS注入,其实就可以实现比如修改右键菜单、改变页面排版、图片大小、获取元素信息并展示等等效果
注:页面增加 a 标签会被网站本身拦截喔,使用window.open也只能 “_blank” 在新标签页打开喔~
【正题】开启侧边栏
侧边栏是什么?
插件提供的扩展小页面,如图,插件图标右键可以选择展开或关闭
此时会发现之前启动的插件,插件图标右键也没有侧边栏,因为还需要前置条件~
前置条件
wxt.config.ts 增加配置
import { defineConfig } from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-vue'],
manifest: {
permissions: [
'sidePanel'
]
},
});
entrypoints/ 目录下增加文件夹 sidepanel
此处因为需要编译后在 .output 里能够查看到 sidepanel 的内容,插件的侧边栏才能正常使用,使用目录是方便管理样式和TS
拷贝 popup 文件夹下的所有内容到 sidepanel 目录下
修改 App.vue
<script lang="ts" setup>
</script>
<template>
<div>
我是侧边栏
</div>
</template>
<style scoped>
</style>
重新运行项目
此时已经可以看见前面侧边栏的图,以及插件图标支持右键里操作侧边栏
【正题】插件图标点击打开侧边栏
前言
这里有个潜在规则,popup 是插件图标点击弹出层,存在 popup 则无法监听插件图标点击事件,因此需要先修改 popup 目录的名称,让项目无法编译出 popup.html
另外此处将使用到 tabs 标签上下文,侧边栏展开需要明确在哪个标签和窗口下,具体看后续代码
wxt.config.ts 增加配置
import {defineConfig} from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-vue'],
manifest: {
permissions: [
'sidePanel',
'tabs',
'activeTab',
],
//必须有action,否则background下无法识别browser.action
action: {
}
},
});
background.ts 文件增加插件图标监听事件
要记得之前说过的background日志要在开发者模式下去查看喔~
// 用于判断当前是否开启,达到切换效果
let isOpen = false;
export default defineBackground(() => {
console.log('Hello background!', { id: browser.runtime.id });
browser.action.onClicked.addListener( (tab) => {
//注意删除popup目录或对应文件,不让编译
console.log('点击图标');
if (!isOpen){
browser.sidePanel.setOptions({
enabled: true,
});
browser.sidePanel.open({tabId: tab.id, windowId: tab.windowId});
isOpen = true;
}else{
browser.sidePanel.setOptions({
enabled: false,
});
isOpen = false;
}
});
});
如上就可以实现点击插件图标切换侧边栏的功能了
【正题】页面悬浮图标点击打开侧边栏
前言
首先得明白基础知识:
侧边栏打开必须由用户行为触发(属于浏览器插件规范,类似微信小程序获取用户信息,需要用户点击某个按钮触发),因此打开浏览器就直接打开侧边栏,但是打开之后切换标签页是不会收缩的喔~
content.ts 里无法获取到 tabs 信息和 sidePanel 信息
background.ts 里无法进行JS、CSS注入
其实也很好理解,background 后台是知道当前的标签页和窗口信息的,而 content 就是页面只能看到当前页面的内容,无法获取外部信息
那么页面悬浮按钮打开侧边栏将受限于无tab信息,这里就会涉及content和background之间的通信
问background拿tabs
browser.runtime.sendMessage({action: "get_tab_info"}, (response) => {
if (browser.runtime.lastError) {
console.error("Error sending message:", browser.runtime.lastError);
return;
}
console.log("Tab info received from background:", response);
});
让background开启/关闭侧边栏
browser.runtime.sendMessage({action: 'open_side_panel', tabId: response.tabId});
background监听消息&处理消息
backgroud.ts 中增加监听,注意放在 defineBackground 里
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log("message", message);
if (message.action === 'open_side_panel') {
if (!isOpen) {
browser.sidePanel.setOptions({
enabled: true,
});
if (message.tabId) {
browser.sidePanel.open({tabId: message.tabId});
} else {
//content里无法触发此内容 popup里可以
const tabs: any = browser.tabs.query({active: true, currentWindow: true})
browser.sidePanel.open({tabId: tabs[0].tabId});
}
isOpen = true;
} else {
console.log(browser.sidePanel.open)
browser.sidePanel.setOptions({
enabled: false,
});
isOpen = false;
}
}
if (message.action === "get_tab_info") {
if (sender.tab) {
sendResponse({
tabId: sender.tab.id,
tabUrl: sender.tab.url,
tabTitle: sender.tab.title
});
} else {
sendResponse({error: "Message not from a tab."});
}
return true;
}
});
问 和 让 两步为什么不合成一步,因为涉及到基础知识第一条,浏览器插件规范了,会被认为不是人为触发的调用,不允许打开,你们可以试试报什么错~ (还记得报错信息在哪里看吧,往顶上翻!)
悬浮按钮的点击事件里增加 问 和 让
content.ts 中增加消息发送,放在 button.onclick = () => {} 里
button.onclick = () => {
console.log('click');
browser.runtime.sendMessage({action: "get_tab_info"}, (response) => {
if (browser.runtime.lastError) {
console.error("Error sending message:", browser.runtime.lastError);
return;
}
console.log("Tab info received from background:", response);
//收到消息拿到tab后调用开启消息
browser.runtime.sendMessage({action: 'open_side_panel', tabId: response.tabId});
});
}
到此也就丝滑实现咯~
教程中,修改主要配置或者部分文件 background.js 时需要重启项目编译插件文件可能没有标注,自己注意重启就好~
后续有什么插件小功能,也在这里扩展,不明白的地方请留言~