
以 Chrome 扩展 Manifest V3 为例,构建一个实用型插件:在网页上高亮选中的内容,并自动整理浏览器收藏夹。文章包含架构、权限、核心脚本与选项页示例。
chrome.storage.local。Manifest V3、content script、service worker、contextMenus、bookmarks 与 history API。manifest.json:声明权限、背景脚本、选项页。background.js:服务工作线程,处理菜单、消息、注入脚本与收藏夹整理。content.js:页面脚本,执行文本高亮与应用历史高亮。options.html/options.js:配置界面,管理开关与策略。{
"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"
}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||[])}
})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()
})<!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,隐私模式与部分场景可能不可用。scripting 动态注入页面脚本。host_permissions 中声明,建议使用 <all_urls> 并谨慎控制。SCRIPT、STYLE、AUDIO、VIDEO 等标签。{
"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]) } })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.* 命名空间,Firefox 倾向 browser.* 与 Promise 风格bookmarks 与 storage 基本一致,scripting 在 Firefox 需改用 tabs.executeScriptchrome://extensions,启用开发者模式,加载已解压的扩展zip 并上传到应用商店