admin 管理员组文章数量: 1184232
自制浏览器插件:实现网页内容高亮、自动整理收藏夹功能
以 Chrome 扩展 Manifest V3 为例,构建一个实用型插件:在网页上高亮选中的内容,并自动整理浏览器收藏夹。文章包含架构、权限、核心脚本与选项页示例。
目标与特性
- 网页内容高亮:选择文本后一键高亮并持久保存,页面再次打开自动恢复。
- 收藏夹整理:按域名分组、去重、排序,支持定时或手动触发。
- 配置可选:选项页开关行为,数据存储于
chrome.storage.local。 - 技术栈:
Manifest V3、content script、service worker、contextMenus、bookmarks与historyAPI。
架构与文件
manifest.json:声明权限、背景脚本、选项页。background.js:服务工作线程,处理菜单、消息、注入脚本与收藏夹整理。content.js:页面脚本,执行文本高亮与应用历史高亮。options.html/options.js:配置界面,管理开关与策略。
权限与 Manifest 配置
{
"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)
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)
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)
<!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>
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>并谨慎控制。 - 书签根目录结构因浏览器与用户习惯不同,创建文件夹时需兜底处理。
- 文本高亮避免破坏交互元素节点,建议过滤
SCRIPT、STYLE、AUDIO、VIDEO等标签。
扩展方向
- 多样式高亮与侧边栏列表管理,支持删除与跳转定位。
- 书签分类增强:根据路径与关键词打标签,生成统计图表。
- 同步与备份:导出导入配置与高亮记录,结合云端存储。
总结
- 高亮与收藏夹整理是两个高频痛点,结合 MV3 API 可快速实现。
- 保持脚本职责清晰、数据结构简单,在选项页暴露必要开关,能兼顾易用与稳定。
- 后续可渐进增强精确恢复与智能分类,让插件在个人知识管理中持续发挥价值。
数据模型与存储结构
{
"highlights:URL": [
{ "text": "...", "xpath": "...", "fragment": "...", "time": 1730000000000 }
],
"settings": {
"autoApplyHighlights": true,
"groupByHostname": true,
"dedupeBookmarks": true,
"sortByLastVisit": false
}
}
精确恢复策略
function toFragment(text){ return encodeURIComponent(text.slice(0,64)) }
function jumpWithFragment(text){ location.hash='': location.href=location.origin+location.pathname+'#:~:text='+toFragment(text) }
function reapply(list){ ensureStyle(); list.forEach(h=>wrapTextOccurrences(h.text)) }
删除与管理高亮
function listHighlights(){ return Array.from(document.querySelectorAll('.highlight-ext')) }
function removeHighlight(el){ const parent=el.parentNode; const text=el.textContent; parent.replaceChild(document.createTextNode(text), el) }
chrome.runtime.onMessage.addListener((msg)=>{ if(msg&&msg.type==='remove-highlight'){ const items=listHighlights(); const idx=msg.index|0; if(items[idx]) removeHighlight(items[idx]) } })
DOM 变更监控与重试
function observeAndReapply(data){ const mo=new MutationObserver(()=>{ reapply(data) }); mo.observe(document.body,{subtree:true,childList:true,characterData:true}) }
快捷键与命令
{
"commands": {
"toggle-highlight": { "suggested_key": { "default": "Ctrl+Shift+H" }, "description": "Do highlight" },
"organize-bookmarks": { "suggested_key": { "default": "Ctrl+Shift+O" }, "description": "Organize bookmarks" }
}
}
chrome.commands.onCommand.addListener(async cmd=>{ if(cmd==='toggle-highlight'){ const [tab]=await chrome.tabs.query({active:true,currentWindow:true}); if(tab&&tab.id){ await chrome.scripting.executeScript({target:{tabId:tab.id},files:['content.js']}); chrome.tabs.sendMessage(tab.id,{type:'do-highlight'}) } } if(cmd==='organize-bookmarks'){ await organizeBookmarks() } })
国际化与文案
{
"name": { "message": "网页高亮与收藏夹整理" },
"menu_highlight": { "message": "高亮选中文本" },
"menu_organize": { "message": "整理收藏夹" }
}
{
"default_locale": "zh_CN"
}
function t(k){ return chrome.i18n.getMessage(k) }
侧边栏与列表(可选)
{
"side_panel": { "default_path": "panel.html" }
}
<!doctype html><html><head><meta charset="utf-8"><style>body{font-family:sans-serif}li{margin:6px 0}</style></head><body><ul id="list"></ul><script>chrome.tabs.query({active:true,currentWindow:true}).then(async([tab])=>{const key='highlights:'+tab.url;const v=await chrome.storage.local.get(key);const list=v[key]||[];const ul=document.getElementById('list');list.forEach((h,i)=>{const li=document.createElement('li');li.textContent=h.text;li.dataset.i=i;li.addEventListener('click',()=>chrome.tabs.sendMessage(tab.id,{type:'remove-highlight',index:i}));ul.appendChild(li)});});</script></body></html>
跨浏览器要点
- Chrome/Edge 使用
chrome.*命名空间,Firefox 倾向browser.*与 Promise 风格 bookmarks与storage基本一致,scripting在 Firefox 需改用tabs.executeScript- 采用轻量适配层封装差异,优先最小权限与主流程可用
安装与发布
- 开发模式:Chrome 打开
chrome://extensions,启用开发者模式,加载已解压的扩展 - 生产发布:生成图标与描述,打包
zip并上传到应用商店 - 权限审查与文案国际化,准备隐私政策与最小权限说明
隐私与安全
- 最小权限与按需注入,避免对所有页面长期驻留
- 数据仅存储在本地,提供清空与导出入口
- 不采集隐私数据,不上传浏览历史,开放源代码与使用说明
版权声明:本文标题:自制浏览器插件:实现网页内容高亮、自动整理收藏夹功能 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1765841386a3419379.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论