乐于分享
好东西不私藏

源码分享 | 从0到1开发一个浏览器插件

本文最后更新于2026-01-01,某些文章具有时效性,若有错误或已失效,请在下方留言或联系老夜

源码分享 | 从0到1开发一个浏览器插件

0、前言

    本文以文本采集插件为例,基于WXT框架,讲解三大核心组件(后台脚本、内容脚本、弹出页)的分工与通信机制,实现了从项目搭建到功能落地的全过程。

1、相关概念

1.1 什么是浏览器插件

    浏览器插件(或扩展)是小型的软件组件,用于扩展浏览器功能,通过Web技术(HTML, CSS, JS)实现,让浏览体验更个性化和强大。

1.2 浏览器插件可以做什么

    能帮你屏蔽广告、翻译网页、管理密码、美化界面、提高效率(如截图、笔记、抢票)

2、组件说明

2.1 概述

    浏览器扩展的架构独特且多面,它并非集中式结构,而是由分布式元素网络构成。

    包括后台脚本内容脚本弹出和选项页面以及页面。这些元素协同工作,管理跨多个窗口和标签的复杂浏览器页面基础设施。 

    下面简化插件的开发内容,分为background(后台脚本)、content(内容脚本)、popup(弹出

2.1 组件分布

2.2 组件职能

    在Chrome插件开发中,background、content和popup是核心组成部分,各自承担不同职责并协同工作

后台脚是扩展的大脑和指挥中心,权限最高,负责全局监听和协调。

内容脚本是扩展深入网页内部的“手”,能直接修改页面内容。

弹出页是扩展的临时控制面板,为用户提供快速交互的界面。

    从操作对象、生命周期、操作权限来看后台脚本 、内容脚本、弹出页的区别。

    那如果在内容脚本,要操作高权限的api,那就让内容区脚本去找后台脚本,由后台脚本来实现。

    接下来就引出了后面组件的通信。

2.3 组件通信

    先说结论:后台脚本内容脚本能互相通信,3个组件存在6种通信路径,不用记,用的时候查。

https://juejin.cn/post/6844903985711677453#heading-12

3、开发框架

3.1 主流框架对比

    浏览器扩展开发领域正在快速进化。本文将从 GitHub 人气、上手体验、云服务支持、MVVM 框架兼容性、工程化能力和社区生态六大维度,完整呈现三大框架的差异 , 并分析各自更适合的场景。

https://segmentfault.com/a/1190000046364405

3.2 WXT简介

    WXT是一个免费的开源浏览器插件开发框架,它致力于为开发者带来最好的开发体验和最快的开发速度,学习它可以为你的插件搭建一个坚实的基础,并为你节省大量的基础建设时间。

https://wxt.dev/

4、需求

4.1 content上实现的功能

    划线、获取当前url,保存的浏览器内存

4.2 popup实现的功能

    用列表展示记录内容,点击详情,跳转到原页面

5、实现

5.1 初始化项目

npm i -D wxt

5.2 content

export default defineContentScript({  matches: ["<all_urls>"],  main() {    let actionButtonHTMLButtonElement | null = null;    const STORAGE_KEY = "local:saved_texts";    const contentObject = {      text"",      url"",    };    document.addEventListener("mouseup"(eMouseEvent) => {      const selection = window.getSelection();      const _text = selection?.toString().trim() as string;      const _currentURL = window.location.href;      contentObject.text = _text;      contentObject.url = _currentURL;      if (!_text || (actionButton && actionButton.contains(e.target as Node))) {        if (!_text) removeButton();        return;      }      createButton(e.pageX, e.pageY, contentObject);    });    function createButton(xnumberynumbercontentObjectany) {      removeButton();      actionButton = document.createElement("button");      actionButton.innerText = "➕ 收藏文本";      // 精美样式      Object.assign(actionButton.style, {        position"absolute",        left`${x + 10}px`,        top`${y + 10}px`,        zIndex"2147483647",        padding"8px 16px",        backgroundColor"#6366f1",        color"white",        border"none",        borderRadius"8px",        cursor"pointer",        boxShadow"0 4px 12px rgba(99, 102, 241, 0.3)",        fontSize"13px",        fontWeight"bold",        transition"all 0.2s",      });      actionButton.onclick = async (ev) => {        ev.stopPropagation();        // 1. 读取旧数据        const current = (await storage.getItem<string[]>(STORAGE_KEY)) || [];        // 2. 存入新数据        await storage.setItem(STORAGE_KEY, [contentObject, ...current]);        actionButton!.innerText = "✅ 已添加";        actionButton!.style.backgroundColor = "#10b981";        setTimeout(removeButton, 800);      };      document.body.appendChild(actionButton);    }    function removeButton() {      actionButton?.remove();      actionButton = null;    }    document.addEventListener("mousedown"(e) => {      if (actionButton && !actionButton.contains(e.target as Node))        removeButton();    });  },});

5.3 popup

import { useState, useEffect } from "react";import "./App.css";const STORAGE_KEY = "local:saved_texts";function App() {  const [list, setList] = useState<any[]>([]);  // 1. 初始化加载数据  useEffect(() => {    const loadData = async () => {      const data = await storage.getItem<string[]>(STORAGE_KEY);      setList(data || []);    };    loadData();    // 2. 核心:监听存储变化 (当 content script 写入新数据时,这里会自动触发)    const unwatch = storage.watch<string[]>(STORAGE_KEY(newList) => {      setList(newList || []);    });    // 组件卸载时取消监听    return () => unwatch();  }, []);  const handleClear = async () => {    await storage.setItem(STORAGE_KEY, []);  };  // Function to truncate text at 200 characters  const truncateText = (textstringmaxLengthnumber = 200) => {    return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;  };  return (    <divclassName="container">      <headerclassName="header">        <h1>📖 采集列表</h1>        <buttononClick={handleClear}className="clear-btn">          清空        </button>      </header>      <mainclassName="list-container">        {list.length === 0 ? (          <divclassName="empty">暂无数据,请在网页选中文本</div>        ) : (          <ulclassName="item-list">            {list.map((item, index) => (              <li                key={index}                className="list-item"                onClick={() => {                  //@ts-ignore                  chrome.tabs.create({ url: item.url });                }}              >                {truncateText(item.text)}              </li>            ))}          </ul>        )}      </main>    </div>  );}export default App;

5.4 打包

5.5 源码

    代码已共享

https://gitee.com/jlk1912/wxtSample

6、使用

6.1 引入插件

6.2 功能使用

7、总结

    1、在popup和content组件中虽然可以获取浏览器对象,但是操作权限有限

    2、浏览器插件是一个前端组件,插件前端通过网络请求与后端通信,实现更强大的功能。

    3、chrome浏览器与edge浏览器的内核都是chrome内核,所以插件兼容

8、参考

生命周期:https://blog.csdn.net/heeheeai/article/details/142622784

手把手教学开发浏览器插件:https://zhuanlan.zhihu.com/p/16590557449

组件通讯:https://juejin.cn/post/6844903985711677453#heading-12

控制区域:https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 源码分享 | 从0到1开发一个浏览器插件
×
订阅图标按钮