本文适合有 HTML/CSS/JS 基础的同学,手把手带你从创建项目到发布,一条龙掌握 Chrome 插件开发。


📌 一、Chrome 插件是什么?

Chrome 插件(也叫扩展程序)是用 HTML、CSS、JavaScript 编写的 Web 小工具,运行在 Chrome 浏览器中,可以读取和修改网页内容、自定义浏览器 UI、与服务器通信等。

常见应用场景:

  • 🖍️ 网页批注/高亮笔记(今天我们要做的)
  • 🌐 广告拦截
  • 📊 价格监控
  • 🔍 SEO 分析工具
  • 📂 标签页管理
  • 🤖 AI 助手侧边栏

📌 二、项目结构

先来看看插件的目录结构,非常清晰:

my-extension/              ← 项目根目录
├── manifest.json           ← 插件配置文件(必填)
├── popup.html             ← 点击图标弹出的页面
├── popup.js               ← popup 逻辑
├── popup.css              ← popup 样式
├── content.js             ← 内容脚本(注入到网页中运行)
├── background.js          ← 后台脚本(独立于网页运行)
├── icons/                 ← 图标资源
│   ├── icon16.png
│   ├── icon48.png
│   └── icon128.png
└── options.html           ← 可选:设置页面

核心原则:content script 运行在网页环境中,能访问 DOM;popup 和 background 运行在 Chrome 扩展环境中,彼此通过消息机制通信。


📌 三、manifest.json —— 插件的"身份证"

这是所有 Chrome 插件的入口文件,Chrome 根据它来了解插件的名称、权限、入口文件等信息。

创建 manifest.json

{
  "manifest_version": 3,
  "name": "网页笔记高亮工具",
  "version": "1.0.0",
  "description": "选中网页文字即可高亮标注,并保存笔记到本地",
  "permissions": [
    "storage",
    "activeTab"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "css": ["content.css"]
    }
  ],
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

manifest_version 说明

版本 说明
Manifest V2 旧版,2023 年前主流,2024 年起 Chrome Web Store 已停止接收新 V2 插件
Manifest V3 当前主流,安全性更高,使用 service_worker 替代后台页面,性能更好

💡 强烈建议使用 Manifest V3,Google 已明确推进 V3 全面替代 V2。


📌 四、内容脚本 content.js —— "入侵"网页

内容脚本运行在网页的上下文中,可以访问和修改网页的 DOM 元素。我们用它来实现:

  1. 监听鼠标选中文本事件
  2. 渲染高亮样式
  3. 弹出笔记输入框

创建 content.js

// content.js

// 监听鼠标选择文字事件
document.addEventListener('mouseup', (e) => {
  const selection = window.getSelection();
  const selectedText = selection.toString().trim();

  // 选中文本超过3个字符才触发
  if (selectedText.length > 3) {
    showNotePopup(selection, selectedText);
  }
});

// 显示笔记弹窗
function showNotePopup(selection, text) {
  // 移除已有的弹窗
  const existing = document.getElementById('highlight-note-popup');
  if (existing) existing.remove();

  // 创建弹窗
  const popup = document.createElement('div');
  popup.id = 'highlight-note-popup';
  popup.innerHTML = `
    <div class="popup-inner">
      <p class="popup-title">📝 为「${text.substring(0, 20)}${text.length > 20 ? '...' : ''}」添加笔记</p>
      <textarea id="note-input" placeholder="写下你的笔记..." rows="3"></textarea>
      <div class="popup-buttons">
        <button id="btn-save">💾 保存</button>
        <button id="btn-cancel">✖ 取消</button>
      </div>
    </div>
  `;
  document.body.appendChild(popup);

  // 定位弹窗(出现在鼠标附近)
  popup.style.top = `${e.clientY + 10}px`;
  popup.style.left = `${e.clientX + 10}px`;

  // 保存按钮
  document.getElementById('btn-save').addEventListener('click', () => {
    const note = document.getElementById('note-input').value;
    saveHighlight(text, note);
    popup.remove();
  });

  // 取消按钮
  document.getElementById('btn-cancel').addEventListener('click', () => {
    popup.remove();
  });

  // 点击其他地方自动关闭
  setTimeout(() => {
    document.addEventListener('click', function closePopup(e) {
      if (!popup.contains(e.target)) {
        popup.remove();
        document.removeEventListener('click', closePopup);
      }
    });
  }, 100);
}

// 保存高亮到 Chrome Storage
function saveHighlight(text, note) {
  const highlight = {
    id: Date.now(),
    text: text,
    note: note,
    url: window.location.href,
    pageTitle: document.title,
    createdAt: new Date().toLocaleString('zh-CN')
  };

  chrome.storage.local.get(['highlights'], (result) => {
    const highlights = result.highlights || [];
    highlights.push(highlight);
    chrome.storage.local.set({ highlights });

    // 同时给选中文本加高亮样式
    const range = window.getSelection().getRangeAt(0);
    const span = document.createElement('span');
    span.className = 'my-highlight';
    span.title = note;
    span.dataset.id = highlight.id;
    range.surroundContents(span);
  });
}

// 恢复页面上的已有高亮
chrome.storage.local.get(['highlights'], (result) => {
  const highlights = result.highlights || [];
  highlights.forEach(h => {
    if (h.url === window.location.href) {
      highlightExistingText(h.text, h.id, h.note);
    }
  });
});

function highlightExistingText(text, id, note) {
  // 简单实现:在页面中搜索对应文本并高亮
  const walker = document.createTreeWalker(
    document.body,
    NodeFilter.SHOW_TEXT,
    null,
    false
  );
  let node;
  while (node = walker.nextNode()) {
    if (node.textContent.includes(text)) {
      const span = document.createElement('span');
      span.className = 'my-highlight';
      span.title = note;
      span.dataset.id = id;
      span.textContent = text;
      node.parentNode.replaceChild(span, node);
      break;
    }
  }
}

创建 content.css 高亮样式:

/* content.css */

.my-highlight {
  background-color: #ffe066 !important;
  border-radius: 3px;
  padding: 0 2px;
  cursor: pointer;
  position: relative;
}

.my-highlight:hover {
  background-color: #ffd700 !important;
}

#highlight-note-popup {
  position: fixed;
  z-index: 2147483647;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

.popup-inner {
  background: #ffffff;
  border: 1px solid #e0e0e0;
  border-radius: 12px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
  padding: 14px;
  width: 280px;
  animation: fadeInUp 0.2s ease;
}

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(5px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.popup-title {
  margin: 0 0 10px;
  font-size: 13px;
  color: #333;
  word-break: break-all;
}

#note-input {
  width: 100%;
  box-sizing: border-box;
  padding: 8px;
  border: 1px solid #d0d0d0;
  border-radius: 8px;
  font-size: 13px;
  resize: none;
  outline: none;
  font-family: inherit;
}

#note-input:focus {
  border-color: #ffca28;
}

.popup-buttons {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}

.popup-buttons button {
  flex: 1;
  padding: 7px 0;
  border: none;
  border-radius: 8px;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.2s;
}

#btn-save {
  background: #ffca28;
  color: #333;
}

#btn-save:hover {
  background: #ffd700;
}

#btn-cancel {
  background: #f0f0f0;
  color: #666;
}

#btn-cancel:hover {
  background: #e0e0e0;
}

📌 五、popup 弹窗 —— 插件主界面

popup 是点击 Chrome 工具栏插件图标后弹出的页面,通常用于查看和管理插件数据。

创建 popup.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="popup-container">
    <header class="popup-header">
      <h1>📒 我的笔记</h1>
      <button id="btn-clear">🗑️ 清空</button>
    </header>

    <div id="notes-list" class="notes-list">
      <p class="empty-tip">还没有笔记,试试在网页上选中文字添加~</p>
    </div>

    <footer class="popup-footer">
      <span id="count">共 0 条笔记</span>
      <a href="#" id="btn-open" target="_blank">打开笔记页面</a>
    </footer>
  </div>
  <script src="popup.js"></script>
</body>
</html>

创建 popup.css

/* popup.css */

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  width: 360px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: #f8f9fa;
}

.popup-container {
  padding: 16px;
}

.popup-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 14px;
}

.popup-header h1 {
  font-size: 16px;
  color: #1a1a1a;
}

#btn-clear {
  background: none;
  border: none;
  font-size: 12px;
  color: #999;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: 4px;
}

#btn-clear:hover {
  background: #f0f0f0;
  color: #d32f2f;
}

.notes-list {
  max-height: 400px;
  overflow-y: auto;
}

.note-item {
  background: #fff;
  border-radius: 10px;
  padding: 12px;
  margin-bottom: 10px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
  cursor: pointer;
  transition: box-shadow 0.2s;
}

.note-item:hover {
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.note-text {
  font-size: 13px;
  color: #333;
  margin-bottom: 6px;
  line-height: 1.5;
}

.note-note {
  font-size: 12px;
  color: #666;
  background: #fffbe6;
  border-left: 3px solid #ffca28;
  padding: 6px 8px;
  border-radius: 0 6px 6px 0;
  margin-bottom: 6px;
}

.note-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.note-page {
  font-size: 11px;
  color: #aaa;
  text-decoration: none;
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.note-time {
  font-size: 11px;
  color: #bbb;
}

.empty-tip {
  text-align: center;
  color: #bbb;
  font-size: 13px;
  padding: 30px 0;
}

.popup-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding-top: 10px;
  border-top: 1px solid #eee;
  margin-top: 4px;
}

.popup-footer span {
  font-size: 12px;
  color: #aaa;
}

.popup-footer a {
  font-size: 12px;
  color: #ffca28;
  text-decoration: none;
  font-weight: 500;
}

.popup-footer a:hover {
  text-decoration: underline;
}

创建 popup.js

// popup.js

const notesList = document.getElementById('notes-list');
const countLabel = document.getElementById('count');
const btnClear = document.getElementById('btn-clear');

function renderNotes() {
  chrome.storage.local.get(['highlights'], (result) => {
    const highlights = result.highlights || [];

    countLabel.textContent = `${highlights.length} 条笔记`;

    if (highlights.length === 0) {
      notesList.innerHTML = '<p class="empty-tip">还没有笔记,试试在网页上选中文字添加~</p>';
      return;
    }

    // 按时间倒序
    highlights.slice().reverse().forEach(h => {
      const item = document.createElement('div');
      item.className = 'note-item';

      item.innerHTML = `
        <div class="note-text">📌 ${escapeHtml(h.text)}</div>
        ${h.note ? `<div class="note-note">💬 ${escapeHtml(h.note)}</div>` : ''}
        <div class="note-meta">
          <a href="${escapeHtml(h.url)}" class="note-page" target="_blank" title="${escapeHtml(h.pageTitle)}">🌐 ${escapeHtml(h.pageTitle)}</a>
          <span class="note-time">${h.createdAt}</span>
        </div>
      `;

      item.addEventListener('click', () => {
        // 点击跳转到对应页面
        chrome.tabs.create({ url: h.url });
      });

      notesList.appendChild(item);
    });
  });
}

function escapeHtml(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

// 清空
btnClear.addEventListener('click', () => {
  if (confirm('确定清空所有笔记吗?')) {
    chrome.storage.local.set({ highlights: [] });
    renderNotes();
  }
});

renderNotes();

📌 六、后台脚本 background.js

Manifest V3 中,后台脚本是 Service Worker,关即即停,不需要页面常驻。它主要负责:

  • 监听插件安装/更新事件
  • 跨页面消息通信
  • 定时任务等

创建 background.js

// background.js

// 插件安装时提示
chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'install') {
    console.log('🎉 插件安装成功!');
    // 初始化存储
    chrome.storage.local.set({ highlights: [] });
  }
  if (details.reason === 'update') {
    console.log('🔄 插件已更新到新版本');
  }
});

// 监听来自 content script 的消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'getNotes') {
    chrome.storage.local.get(['highlights'], (result) => {
      sendResponse({ data: result.highlights || [] });
    });
    return true; // 异步响应需要返回 true
  }
});

📌 七、创建图标

图标放在 icons/ 目录下,需要 3 个尺寸:

文件名 尺寸 用途
icon16.png 16×16 地址栏小图标
icon48.png 48×48 扩展管理页面
icon128.png 128×128 Chrome Web Store

💡 如果没有设计工具,可以去 FlaticonIconHub 下载免费图标,或用在线工具生成:https://favicon.io/


📌 八、加载并测试插件

  1. 打开 Chrome,地址栏输入:chrome://extensions/
  2. 右上角打开「开发者模式」
  3. 点击「加载已解压的扩展程序」
  4. 选择你的 my-extension/ 文件夹
  5. 插件图标会出现在 Chrome 工具栏

⚠️ 如果工具栏没有图标,点击拼图 🧩 图标 → 固定你的插件


📌 九、发布到 Chrome 商店(可选)

  1. 打包插件:在 chrome://extensions/ 页面点击「打包扩展程序」
  2. 注册 Chrome Web Store 开发者账号($5 一次性注册费)
  3. 登录 Chrome Web Store Developer Dashboard
  4. 上传 .zip 包,填写插件描述、截图、隐私政策等
  5. 提交审核(通常 1-3 个工作日)

📌 十、完整项目目录一览

my-extension/
├── manifest.json        ✅
├── popup.html           ✅
├── popup.css            ✅
├── popup.js             ✅
├── content.js           ✅
├── content.css          ✅
├── background.js        ✅
└── icons/
    ├── icon16.png
    ├── icon48.png
    └── icon128.png

✅ 总结

通过这个"网页笔记高亮工具",我们学会了:

技术点 说明
manifest.json 插件配置,注册权限、入口文件
Content Script 注入网页,操作 DOM
Popup 页面 工具栏弹窗界面
Background Worker 后台任务处理
Chrome Storage API 数据持久化存储
Service Worker Manifest V3 的后台机制

Chrome 插件本质上是运行在浏览器中的 Web 应用,门槛极低、变现能力强。学会这套技能后,你可以:

  • 🔧 做内部效率工具(团队共享)
  • 💰 开发商业插件变现
  • 📦 打造自己的产品矩阵

快动手做一个属于你的 Chrome 插件吧!有任何问题欢迎在评论区交流 👇


如果你觉得这篇文章有帮助,欢迎点赞、收藏!

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐