自制浏览器插件:实现网页内容高亮、自动整理收藏夹功能

自制浏览器插件:实现网页内容高亮、自动整理收藏夹功能

以 Chrome 扩展 Manifest V3 为例,构建一个实用型插件:在网页上高亮选中的内容,并自动整理浏览器收藏夹。文章包含架构、权限、核心脚本与选项页示例。

目标与特性

  • 网页内容高亮:选择文本后一键高亮并持久保存,页面再次打开自动恢复。
  • 收藏夹整理:按域名分组、去重、排序,支持定时或手动触发。
  • 配置可选:选项页开关行为,数据存储于 chrome.storage.local
  • 技术栈:Manifest V3content scriptservice workercontextMenusbookmarkshistory API。

架构与文件

  • manifest.json:声明权限、背景脚本、选项页。
  • background.js:服务工作线程,处理菜单、消息、注入脚本与收藏夹整理。
  • content.js:页面脚本,执行文本高亮与应用历史高亮。
  • options.html/options.js:配置界面,管理开关与策略。

权限与 Manifest 配置

json 复制代码
{
  "manifest_version": 3,
  "name": "Web Highlighter & Bookmark Organizer",
  "version": "0.1.0",
  "permissions": ["bookmarks", "storage", "scripting", "activeTab", "contextMenus", "history", "alarms"],
  "host_permissions": ["<all_urls>"],
  "background": { "service_worker": "background.js" },
  "action": { "default_title": "Highlight & Organize" },
  "options_page": "options.html"
}

页面脚本:高亮与恢复(content.js)

js 复制代码
function getXPath(el){
  let path=''
  let node=el
  while(node&&node.nodeType===1){
    const siblings=node.parentNode?Array.from(node.parentNode.children).filter(n=>n.tagName===node.tagName):[]
    const index=siblings.indexOf(node)+1
    path='/' + node.tagName + '[' + index + ']' + path
    node=node.parentElement
  }
  return path.toLowerCase()
}

function ensureStyle(){
  if(document.getElementById('ext-highlight-style'))return
  const style=document.createElement('style')
  style.id='ext-highlight-style'
  style.textContent='.highlight-ext{background:#ffeb3b;padding:0 2px;border-radius:2px}'
  document.documentElement.appendChild(style)
}

function wrapSelection(){
  const sel=window.getSelection()
  if(!sel||sel.isCollapsed)return
  const range=sel.getRangeAt(0)
  const span=document.createElement('span')
  span.className='highlight-ext'
  range.surroundContents(span)
  const payload={url:location.href,text:sel.toString(),xpath:getXPath(span),time:Date.now()}
  chrome.runtime.sendMessage({type:'save-highlight',data:payload})
  sel.removeAllRanges()
}

function wrapTextOccurrences(keyword){
  if(!keyword||keyword.length<2)return
  const walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT,null)
  let node
  let count=0
  const limit=50
  const lower=keyword.toLowerCase()
  while((node=walker.nextNode())){
    const text=node.nodeValue||''
    const idx=text.toLowerCase().indexOf(lower)
    if(idx!==-1){
      const range=document.createRange()
      range.setStart(node,idx)
      range.setEnd(node,idx+keyword.length)
      const span=document.createElement('span')
      span.className='highlight-ext'
      range.surroundContents(span)
      count++
      if(count>=limit)break
    }
  }
}

function applyHighlights(list){
  ensureStyle()
  list.forEach(h=>wrapTextOccurrences(h.text))
}

chrome.runtime.onMessage.addListener((msg)=>{
  if(msg&&msg.type==='do-highlight'){ensureStyle();wrapSelection()}
  if(msg&&msg.type==='apply-highlights'){applyHighlights(msg.data||[])}
})

背景脚本:菜单、注入与收藏夹整理(background.js)

js 复制代码
chrome.runtime.onInstalled.addListener(()=>{
  chrome.contextMenus.create({id:'highlight',title:'高亮选中文本',contexts:['selection']})
  chrome.contextMenus.create({id:'organize',title:'整理收藏夹',contexts:['action']})
})

chrome.contextMenus.onClicked.addListener(async(info,tab)=>{
  if(info.menuItemId==='highlight'&&tab&&tab.id){
    await chrome.scripting.executeScript({target:{tabId:tab.id},files:['content.js']})
    chrome.tabs.sendMessage(tab.id,{type:'do-highlight'})
  }
  if(info.menuItemId==='organize'){
    await organizeBookmarks()
  }
})

chrome.runtime.onMessage.addListener(async(msg,sender)=>{
  if(msg&&msg.type==='save-highlight'){
    const key='highlights:' + msg.data.url
    const prev=await chrome.storage.local.get(key)
    const list=prev[key]||[]
    const exists=list.some(x=>x.text===msg.data.text)
    const next=exists?list:list.concat([msg.data])
    await chrome.storage.local.set({[key]:next})
  }
})

chrome.tabs.onUpdated.addListener(async(tabId,changeInfo,tab)=>{
  if(changeInfo.status==='complete'&&tab&&tab.url){
    const key='highlights:' + tab.url
    const prev=await chrome.storage.local.get(key)
    const list=prev[key]||[]
    await chrome.scripting.executeScript({target:{tabId},files:['content.js']})
    chrome.tabs.sendMessage(tabId,{type:'apply-highlights',data:list})
  }
})

async function organizeBookmarks(){
  const opts=await chrome.storage.local.get(['groupByHostname','dedupeBookmarks','sortByLastVisit'])
  const groupByHostname=opts.groupByHostname!==false
  const dedupe=opts.dedupeBookmarks!==false
  const sortByLastVisit=opts.sortByLastVisit===true
  const tree=await chrome.bookmarks.getTree()
  const list=[]
  function walk(nodes){
    nodes.forEach(n=>{if(n.url)list.push(n);if(n.children)walk(n.children)})
  }
  walk(tree)
  const map=new Map()
  list.forEach(b=>{
    const u=new URL(b.url)
    const host=groupByHostname?u.hostname:'Ungrouped'
    if(!map.has(host))map.set(host,[])
    map.get(host).push(b)
  })
  if(dedupe){
    for(const [host,items] of map.entries()){
      const seen=new Set()
      map.set(host,items.filter(i=>{const k=i.url;if(seen.has(k))return false;seen.add(k);return true}))
    }
  }
  let visits=new Map()
  if(sortByLastVisit){
    const urls=list.map(b=>b.url)
    const hist=await chrome.history.search({text:'',maxResults:10000,startTime:0})
    hist.forEach(h=>visits.set(h.url,h.lastVisitTime||0))
  }
  for(const [host,items] of map.entries()){
    const root=await ensureFolder('By Domain')
    const folder=await ensureSubFolder(root.id,host)
    const sorted=sortByLastVisit?items.sort((a,b)=>((visits.get(b.url)||0)-(visits.get(a.url)||0))):items
    for(const bm of sorted){
      try{await chrome.bookmarks.move(bm.id,{parentId:folder.id})}catch(e){}
    }
  }
}

async function ensureFolder(name){
  const others=await chrome.bookmarks.getTree()
  const root=others[0]
  const target=root.children.find(f=>!f.url&&f.title===name)
  if(target)return target
  const created=await chrome.bookmarks.create({title:name})
  return created
}

async function ensureSubFolder(parentId,name){
  const children=await chrome.bookmarks.getChildren(parentId)
  const target=children.find(f=>!f.url&&f.title===name)
  if(target)return target
  const created=await chrome.bookmarks.create({parentId,title:name})
  return created
}

chrome.alarms.onAlarm.addListener(async a=>{
  if(a.name==='organize-bookmarks')await organizeBookmarks()
})

选项页示例(options.html / options.js)

html 复制代码
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Extension Options</title>
    <style>
      body{font-family:sans-serif;padding:16px}
      label{display:flex;align-items:center;gap:8px;margin:8px 0}
      button{margin-top:12px}
    </style>
  </head>
  <body>
    <label><input id="auto" type="checkbox" /> 页面加载自动应用高亮</label>
    <label><input id="group" type="checkbox" checked /> 收藏夹按域名分组</label>
    <label><input id="dedupe" type="checkbox" checked /> 收藏夹去重</label>
    <label><input id="sort" type="checkbox" /> 按最近访问排序</label>
    <button id="save">保存</button>
    <script src="options.js"></script>
  </body>
</html>
js 复制代码
const auto=document.getElementById('auto')
const group=document.getElementById('group')
const dedupe=document.getElementById('dedupe')
const sort=document.getElementById('sort')
const save=document.getElementById('save')

chrome.storage.local.get(['autoApplyHighlights','groupByHostname','dedupeBookmarks','sortByLastVisit']).then(v=>{
  auto.checked=v.autoApplyHighlights===true
  group.checked=v.groupByHostname!==false
  dedupe.checked=v.dedupeBookmarks!==false
  sort.checked=v.sortByLastVisit===true
})

save.addEventListener('click',async()=>{
  await chrome.storage.local.set({
    autoApplyHighlights:auto.checked,
    groupByHostname:group.checked,
    dedupeBookmarks:dedupe.checked,
    sortByLastVisit:sort.checked
  })
})

交互与使用

  • 选中文本后右键选择"高亮选中文本",内容将被包裹并保存;再次打开页面自动恢复。
  • 点击扩展图标菜单"整理收藏夹",按照域名分组并去重,选项页可开启排序与自动执行(可用 chrome.alarms)。
  • 选项页可修改策略;数据均存储在 chrome.storage.local

性能与局限

  • 高亮恢复使用文本检索,若页面结构变化较大可能偏移;可结合 xpath 与文本片段增强。
  • 大量书签操作建议分批执行并设置简单的速率限制。
  • 历史访问时间依赖 chrome.history,隐私模式与部分场景可能不可用。

常见坑与规约

  • MV3 背景脚本为 Service Worker,需通过 scripting 动态注入页面脚本。
  • 跨域注入需在 host_permissions 中声明,建议使用 <all_urls> 并谨慎控制。
  • 书签根目录结构因浏览器与用户习惯不同,创建文件夹时需兜底处理。
  • 文本高亮避免破坏交互元素节点,建议过滤 SCRIPTSTYLEAUDIOVIDEO 等标签。

扩展方向

  • 多样式高亮与侧边栏列表管理,支持删除与跳转定位。
  • 书签分类增强:根据路径与关键词打标签,生成统计图表。
  • 同步与备份:导出导入配置与高亮记录,结合云端存储。

总结

  • 高亮与收藏夹整理是两个高频痛点,结合 MV3 API 可快速实现。
  • 保持脚本职责清晰、数据结构简单,在选项页暴露必要开关,能兼顾易用与稳定。
  • 后续可渐进增强精确恢复与智能分类,让插件在个人知识管理中持续发挥价值。
相关推荐
云帆小二2 小时前
从开发语言出发如何选择学习考试系统
开发语言·学习
少卿2 小时前
React Compiler 完全指南:自动化性能优化的未来
前端·javascript
广州华水科技2 小时前
水库变形监测推荐:2025年单北斗GNSS变形监测系统TOP5,助力基础设施安全
前端
广州华水科技2 小时前
北斗GNSS变形监测一体机在基础设施安全中的应用与优势
前端
七淮2 小时前
umi4暗黑模式设置
前端
8***B2 小时前
前端路由权限控制,动态路由生成
前端
爱隐身的官人2 小时前
beef-xss hook.js访问失败500错误
javascript·xss
光泽雨2 小时前
python学习基础
开发语言·数据库·python
军军3603 小时前
从图片到点阵:用JavaScript重现复古数码点阵艺术图
前端·javascript
znhy@1233 小时前
Vue基础知识(一)
前端·javascript·vue.js