前端必修课:万字长文带你搞定浏览器插件开发

前言:由于私有化的时候需要用到idass(统一登陆),本地ip为:127.0.0.1:8080,统一登陆地址为:11.123.456.10,统一登陆后,idass服务无法跨ip种植cookie,于是乎,想自己写一个浏览器插件自动种植所需页面的cookie和token等凭证(每次调试手动种植cookie太费劲了😭)

什么是浏览器插件

想象一下,你的浏览器是个老实巴交的打工仔,每天只会按部就班地干活。插件就是给它装备的"外挂"

比如:

  • 屏蔽鸡汤文弹窗,装个广告拦截
  • 想在电商网站自动比价,写个比价插件
  • 替换网页所有的图片

从零开始造轮子

核心文件结构

perl 复制代码
my-extension/
├── manifest.json       # 插件配置文件
├── icons/              # 图标(16x16, 48x48, 128x128)用于工具栏小图标,扩展管理页图标,商店展示大图标
├── popup/             # 弹出窗口
├── content-scripts/   # 注入页面的脚本
└── background/           # 后台脚本

1. manifest.json

json 复制代码
{
  // 必须字段:清单版本(Chrome 推荐使用 V3)
  "manifest_version": 3,

  // 插件名称(展示在商店和浏览器工具栏)
  "name": "My Extension",

  // 插件版本(格式为 x.x.x)
  "version": "1.0",

  // 插件描述(简洁说明功能)
  "description": "A demo extension",

  // 图标配置(不同尺寸用于不同场景)
  "icons": {
    "16": "icons/icon16.png",   // 工具栏小图标
    "48": "icons/icon48.png",   // 扩展管理页图标
    "128": "icons/icon128.png"  // 商店展示大图标
  },

  // 权限申请(访问浏览器API的权限列表)
  "permissions": [
    "activeTab",    // 访问当前激活标签页
    "storage",      // 使用本地存储(chrome.storage)
    "scripting"     // 动态执行脚本(Manifest V3 新增)
  ],

  // 浏览器工具栏按钮配置
  "action": {
    "default_icon": "icons/icon16.png",  // 默认图标
    "default_popup": "popup/popup.html", // 点击弹出的页面
    "default_title": "点击我"            // 鼠标悬停提示
  },

  // 后台脚本(Manifest V3 使用 Service Worker)
  "background": {
    "service_worker": "background/sw.js",
    "type": "module"  // 可选:支持ES模块
  },

  // 内容脚本(注入到匹配的网页中)
  "content_scripts": [
    {
      "matches": ["https://*.example.com/*"], // 匹配的URL模式
      "js": ["content-scripts/main.js"],      // 注入的JS文件
      "css": ["content-scripts/style.css"],  // 注入的CSS文件
      "run_at": "document_end"               // 注入时机(document_idle/document_start)
    }
  ],

  // 选项页(用户配置页面)
  "options_page": "options/options.html"
}

2. Content Script

  • 内容脚本用于操作当前网页的dom,比如自动高亮页面的关键词
css 复制代码
// content-scripts/main.js
document.querySelectorAll('p').forEach(p => {
  p.innerHTML = p.innerHTML.replace(/重要/g, '<span style="background:yellow">重要</span>');
});
  • 点击浏览器工具栏图标展示的一个弹窗
xml 复制代码
<!DOCTYPE html>
<html>
  <head>
    <title>Webpage Demo</title>
  </head>
  <body>
    <h3>hello world</h3>
  </body>
</html>

这样写的话点击浏览器插件就会出现hello world

4. Background Script

  • 在浏览器的后台运行
  • 用于监听浏览器事件,跨标签通信等全局事件

比如写一个最简单的通过本地存储去存储comment列表

ini 复制代码
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'getComments') {
    chrome.storage.local.get(['comments'], (result) => {
      const comments = result.comments || []
      sendResponse(comments);
    });
  }

  if (message.action === 'saveComment') {
    chrome.storage.local.get(['comments'], (result) => {
      const comments = result.comments || []
      comments.push(message.comment);
      chrome.storage.local.set({ ['comments']: comments }, () => {
        sendResponse({ status: 'saved' });
      });
    });
  }
});
1. chrome.runtime.onMessage.addListener(...)
  • 给整个插件注册一个"消息监听器"。
  • 任何从 popup.js、content-script 或其他地方用 chrome.runtime.sendMessage 发送到后台(background.js)的消息,都会进入这里
2. if (message.action === 'getComments')
  • 触发条件:收到一个 action 字段是 'getComments' 的消息。

  • 功能:

    • 从 chrome.storage.local 里读取键名是 'comments' 的数据。
    • 读取成功后,通过 sendResponse 把结果返回给发送消息的一方
3. if (message.action === 'saveComment')
  • 触发条件:收到一个 action 字段是 'saveComment' 的消息。

  • 功能:

    • 先读取现有的 'comments'。
    • 把新的评论 message.comment 加到 comments 里面。
    • 然后重新用 chrome.storage.local.set 把更新后的 comments 保存回去。
    • 保存成功后,调用 sendResponse({ status: 'saved' }) 告知保存成功。

实战demo

接前言来开发,先列下需求:

  1. 首先需要三个输入框,输入目标ip地址,允许的cookie keys,以及localStorage keys
  2. 插件还需要支持保存配置的功能
  3. 支持一键跳转到目标ip页面,并且会携带当前页面允许的cookie keys以及localStorage keys种植到目标页面去

首先需要一个可视化的操作弹窗:

1. popup.html

xml 复制代码
<body>

  <h3>添加新配置</h3>
  <input type="text" id="target-ip" placeholder="目标 IP 地址 (例如: 192.168.1.2)" />
  <textarea id="cookie-keys" placeholder="允许的 Cookie keys(用英文逗号 , 分隔)"></textarea>
  <textarea id="storage-keys" placeholder="允许的 LocalStorage keys(用英文逗号 , 分隔)"></textarea>
  <button id="save-config">保存配置</button>

  <h3>配置列表</h3>
  <div id="config-list"></div>

  <script src="popup.js"></script>
</body>

接下来是处理弹窗逻辑的js文件

2. popup.js

ini 复制代码
// 获取元素
const targetIpInput = document.getElementById('target-ip');
const cookieKeysInput = document.getElementById('cookie-keys');
const storageKeysInput = document.getElementById('storage-keys');
const configListDiv = document.getElementById('config-list');
const saveButton = document.getElementById('save-config');

// 保存新配置
saveButton.addEventListener('click', () => {
  const targetIp = targetIpInput.value.trim();
  const cookieKeys = cookieKeysInput.value.split(',').map(k => k.trim()).filter(k => k);
  const storageKeys = storageKeysInput.value.split(',').map(k => k.trim()).filter(k => k);

  if (!targetIp) {
    alert('请输入目标 IP 地址');
    return;
  }

  const newConfig = { targetIp, cookieKeys, storageKeys };

  chrome.storage.local.get(['configs'], (result) => {
    const configs = result.configs || [];
    configs.push(newConfig);
    chrome.storage.local.set({ configs }, () => {
      renderConfigs();
      targetIpInput.value = '';
      cookieKeysInput.value = '';
      storageKeysInput.value = '';
    });
  });
});

// 渲染已有配置
function renderConfigs() {
  configListDiv.innerHTML = '';
  chrome.storage.local.get(['configs'], (result) => {
    const configs = result.configs || [];
    configs.forEach((config, index) => {
      const div = document.createElement('div');
      div.className = 'config-item';
      div.innerHTML = `
        <div><strong>IP:</strong> ${config.targetIp}</div>
        <div><strong>Cookies:</strong> ${config.cookieKeys.join(', ')}</div>
        <div><strong>LocalStorage:</strong> ${config.storageKeys.join(', ')}</div>
        <button data-index="${index}" class="run-btn">一键跳转</button>
        <button data-index="${index}" class="delete-btn">删除</button>
      `;
      configListDiv.appendChild(div);
    });

    // 绑定按钮
    document.querySelectorAll('.run-btn').forEach(btn => {
      btn.addEventListener('click', (e) => {
        const idx = e.target.dataset.index;
        startCopy(configs[idx]);
      });
    });

    document.querySelectorAll('.delete-btn').forEach(btn => {
      btn.addEventListener('click', (e) => {
        const idx = e.target.dataset.index;
        configs.splice(idx, 1);
        chrome.storage.local.set({ configs }, renderConfigs);
      });
    });
  });
}

// 开始同步并跳转
function startCopy(config) {
  chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
    chrome.runtime.sendMessage({
      action: 'copyData',
      config,
      sourceUrl: tab.url
      tabId: tab.id
    }, (response) => {
      if (!response.success) {
        alert('同步失败!');
      }
    });
  });
}

// 初始化
renderConfigs();

给按钮添加点击事件以及渲染配置都是很基础的,重点说一下startCopy这个方法

chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {...})

  • 通过 chrome.tabs.query 方法,查询当前激活的标签页(active tab)

  • 参数 { active: true, currentWindow: true }:

    • active: true ------ 只找当前窗口中正在激活中的 tab(比如你打开的页面)。
    • currentWindow: true ------ 限制只在当前浏览器窗口里找,不跨窗口
  • 返回的是一个数组,通常数组里只有一个元素,目的是拿到当前页面的url

chrome.runtime.sendMessage({ action: 'copyData', config, sourceUrl: tab.url }, (response) => {...})

  • 拿到当前页面后,就用 chrome.runtime.sendMessage 发送一个消息到后台脚本 background.js
  • 参数会携带配置的url,以及允许的keys
  • 然后就是会处理background回应的错误信息,这里主要是发消息,主要逻辑都在background.js内部

3. background.js

javascript 复制代码
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'copyData') {
    const { config, sourceUrl, tabId } = message;
    const urlObj = new URL(sourceUrl);
    const domain = urlObj.hostname;

    // 读取 cookie
    chrome.cookies.getAll({ domain }, (cookies) => {
      const allowedCookies = cookies.filter(c => config.cookieKeys.includes(c.name));
      
      // 设置到目标 IP
      allowedCookies.forEach(cookie => {
        chrome.cookies.set({
          url: `http://${config.targetIp}`,
          name: cookie.name,
          value: cookie.value,
          path: cookie.path,
          secure: cookie.secure,
          httpOnly: cookie.httpOnly,
          sameSite: cookie.sameSite
        });
      });
      // 存储 localStorage
      chrome.tabs.sendMessage(tabId, {
        action: 'storeData',
        config
      });
      // 发送消息给 content script 处理 localStorage
      chrome.tabs.create({ url: `http://${config.targetIp}` }, (tab) => {
        // 注意:要等新页面加载完成后才能注入 localStorage 操作
        chrome.tabs.onUpdated.addListener(function listener(tab_id, info) {
          if (tab_id === tab.id && info.status === 'complete') {
            chrome.tabs.onUpdated.removeListener(listener); // 移除监听,避免多次触发
            // 给目标tabId发送消息
            chrome.tabs.sendMessage(tab_id, {
              action: 'copyLocalStorage',
              config
            });
          }
        });
      });

      sendResponse({ success: true });
    });

    return true; // 异步
  }
});

整体功能

  • ✅ 从当前页面读取 cookie(指定 keys)
  • ✅ 把这些 cookie 写到目标 IP
  • ✅ 新开一个 tab 跳转到目标 IP
  • ✅ 在目标 IP 页面注入 localStorage 数据

1. 监听消息

javascript 复制代码
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  • chrome.runtime.onMessage.addListener:监听插件内部发送的消息
  • message:收到的消息内容(action, config, sourceUrl)
  • sender:发送消息的 tab 信息(sender.tab)
  • sendResponse:回调函数,异步返回一个响应

2. 取出配置和源地址,并且读取cookie

ini 复制代码
const { config, sourceUrl } = message;
const urlObj = new URL(sourceUrl);
const domain = urlObj.hostname;
chrome.cookies.getAll({ domain }, (cookies) => {})
php 复制代码
allowedCookies.forEach(cookie => {
  chrome.cookies.set({
    url: `http://${config.targetIp}`,
    name: cookie.name,
    value: cookie.value,
    path: cookie.path,
    secure: cookie.secure,
    httpOnly: cookie.httpOnly,
    sameSite: cookie.sameSite
  });
});

4. 打开新标签页跳转到目标 IP

javascript 复制代码
chrome.tabs.create({ url: `http://${config.targetIp}` }, (tab) => {})

5. 等新页面加载完成,再注入 localStorage

ini 复制代码
chrome.tabs.onUpdated.addListener(function listener(tabId, info) {
  if (tabId === tab.id && info.status === 'complete') {
    chrome.tabs.onUpdated.removeListener(listener);
    chrome.tabs.sendMessage(tabId, {
      action: 'copyLocalStorage',
      config
    });
  }
});

6. return true;

最后返回ture是因为:

  • Chrome 规定:如果 sendResponse 是异步调用,必须 return true,否则页面收不到回调

注意点

下面就是处理注入localStorage的逻辑了,为什么不能在backgroud里直接种植呢?

  • 因为backgroud.js相当于一个大脑,他能调用插件的所有api,但是他不能操作页面的内容
  • 它无法直接读取网页里的 DOM、localStorage、执行页面里的 JS
  • 下面就是content-script.js 了

4. content-script.js

ini 复制代码
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'copyLocalStorage') {
    // 当页面跳转到目标 IP 后,恢复 localStorage
    if (window.location.hostname.match(/^(\d{1,3}.){3}\d{1,3}$/)) {
      console.log('页面跳转到目标 IP');
      chrome.storage.local.get(['tempStorageData'], (result) => {
        console.log(result, 'result');
        const data = result.tempStorageData || {};
        Object.keys(data).forEach(key => {
          localStorage.setItem(key, data[key]);
        });
        chrome.storage.local.remove('tempStorageData');
      });
    }
  }
  if (message.action === 'storeData') {
    const { storageKeys } = message.config;
    const storageData = {};

    storageKeys.forEach(key => {
      console.log(localStorage.getItem(key));
      const value = localStorage.getItem(key);
      if (value !== null) {
        storageData[key] = value;
      }
    });
    console.log(storageData,'storageData');
    chrome.storage.local.set({ tempStorageData: storageData });
  }
});

这里面的逻辑很简单了,利用了一个临时缓存tempStorageData去种植localStorage

5.附上配置文件

json 复制代码
{
  "manifest_version": 3, // 指定使用 Manifest V3,这是 Chrome 插件目前推荐的新版本
  "name": "unlogin-plug-in", // 插件的名称
  "version": "1.0.0", // 插件版本号
  "description": "unlogin-plug-in", // 插件描述

  "permissions": [
    "storage", // 允许使用 chrome.storage API,本地存储数据
    "cookies", // 允许读取和设置 cookie
    "tabs", // 允许访问和操作浏览器标签页信息
    "scripting" // 允许使用 chrome.scripting 注入脚本
  ],

  "host_permissions": [
    "<all_urls>" // 允许访问所有网站的页面,比如读取 cookie、注入脚本等
  ],

  "action": {
    "default_popup": "popup.html" // 点击插件图标时,弹出的界面 HTML 文件
  },

  "background": {
    "service_worker": "background.js" // 后台脚本,使用 service worker 方式运行
  },

  "content_scripts": [
    {
      "matches": ["<all_urls>"], // 指定在哪些页面注入 content-script,这里是所有页面
      "js": ["content-script.js"] // 被注入的脚本文件,处理当前页面的 localStorage、DOM 操作等
    }
  ]
}

小小测试了一下,功能大体是OK的😄

在这里插入图片描述

相关推荐
FliPPeDround2 天前
浏览器扩展 E2E 测试的救星:vitest-environment-web-ext 让你告别繁琐配置
e2e·浏览器·测试
SuperEugene2 天前
浏览器存储:localStorage / sessionStorage / cookie 应该怎么用
前端·javascript·面试·浏览器
宁雨桥2 天前
浏览器渲染原理
前端·浏览器·原理
YZ0994 天前
2026年如何批量保存小红书作者主页的视频、图片和文案?
经验分享·浏览器·插件
程序员ys4 天前
网页白屏的原理与优化
前端·性能优化·浏览器
Wect6 天前
从输入URL到页面显示的完整技术流程
前端·面试·浏览器
NEXT066 天前
从输入 URL 到页面展示的完整链路解析
网络协议·面试·浏览器
CappuccinoRose9 天前
CSS 语法学习文档(十五)
前端·学习·重构·渲染·浏览器
REDcker10 天前
Media Source Extensions (MSE) 详解
前端·网络·chrome·浏览器·web·js
x-cmd11 天前
Browser-Use:用自然语言控制浏览器,告别脆弱的自动化脚本
运维·ai·自动化·agent·浏览器·x-cmd