Home
avatar

叁石

浏览器插件开发(常见功能集)

基于WXT框架实现浏览器插件里会涉及的常用功能,比如修改页面布局,点击插件图标展开侧边栏,侧边栏获取页面内容等等

展示魔改图

最新版本源码

框架简介和使用

插件框架都是基于浏览器原生插件API实现,如谷歌浏览器、Firefox浏览器…

插件框架在适配上做了调整,比如 框架.action = 谷歌.action || Firefox.action

谷歌插件API网站

相对主流的 plasmowxt 的星星少了很多,但是博主就是爱用更新的,哈哈哈哈~

wxt 的文档看起来更易懂一些,程序结构也更清晰,并且调试模式深得我心~

WXT 框架官网

plasmo 需要自己将 build 的插件目录添加到浏览器插件里,有点烦~

plasmo 框架官网

初始化搭建

pnpm dlx wxt@latest init
# 然后选择项目目录、模板和包管理器
pnpm install

运行模板

个人喜好选择了 vueyarn

yarn dev

运行后自动弹出安装了插件的浏览器(就是这点深得我心~)

image-20250613165526893

简单介绍调试方式

点击插件 > 管理扩展程序,或者

地址栏输入 chrome://chrome-urls/ 找到 chrome://extensions/ 点击进入

不装逼就地址栏直接输入 chrome://extensions/

① 打开开发者模式,方便调试

② 会有一个或多个视图,此处是除开当前页面在控制台查看以外的 background.js侧边栏 你能查阅到日志和界面元素的地方,默认视图查看 background.js 相关日志

③ 当插件出现异常错误时此处回显时错误按钮,点击进入查看异常信息(非常常用

另外切记,修改主要配置或者部分文件 background.js 时需要重启项目编译插件文件~

image-20250613165800785

目录和文件讲解

注意查看 重点关注 字样的目录

📂 {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 项目的主要配置文件 重点关注 主要配置文件

【正题】弹出窗样式调整

image-20250613170634665

这个其实对应的是 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.');
  },
});

此时访问百度,发现百度 绿了

正常打印 contentconsole 内容,就会发现 content 针对的就是页面级内容

image-20250613173059969

【正题】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 = '度娘一下';
}

image-20250613174036665

根据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();

image-20250613175632848

还有一种注入方式,代码如下,效果类似页面增加 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” 在新标签页打开喔~

【正题】开启侧边栏

侧边栏是什么?

插件提供的扩展小页面,如图,插件图标右键可以选择展开或关闭

image-20250613182222824

此时会发现之前启动的插件,插件图标右键也没有侧边栏,因为还需要前置条件~

前置条件

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

image-20250613185111300

另外此处将使用到 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 里无法进行JSCSS注入

其实也很好理解,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 时需要重启项目编译插件文件可能没有标注,自己注意重启就好~

后续有什么插件小功能,也在这里扩展,不明白的地方请留言~

浏览器插件 插件 WXT框架