从一个想法到上线,一万字记录我开发浏览器插件的全过程

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

什么是浏览器插件

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

比如:

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

从零开始造轮子

核心文件结构

bash 复制代码
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>');
});
  • 点击浏览器工具栏图标展示的一个弹窗
html 复制代码
<!DOCTYPE html>
<html>
  <head>
    <title>Webpage Demo</title>
  </head>
  <body>
    <h3>hello world</h3>
  </body>
</html>

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

4. Background Script

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

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

js 复制代码
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

html 复制代码
<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

js 复制代码
// 获取元素
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

js 复制代码
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. 监听消息

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

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

js 复制代码
const { config, sourceUrl } = message;
const urlObj = new URL(sourceUrl);
const domain = urlObj.hostname;
chrome.cookies.getAll({ domain }, (cookies) => {})
js 复制代码
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

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

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

js 复制代码
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

js 复制代码
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的😄

相关推荐
编程猪猪侠18 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞22 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路44 分钟前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构
风清云淡_A1 小时前
【REACT18.x】CRA+TS+ANTD5.X封装自定义的hooks复用业务功能
前端·react.js
@大迁世界1 小时前
第7章 React性能优化核心
前端·javascript·react.js·性能优化·前端框架