目录
1、配置 manifest.json 文件
2、编写侧边栏结构
3、查找关键词并高亮的方法
3-1) 如果直接使用 innerHTML 进行替换
4、清除关键词高亮
5、页面脚本代码
6、参考
1、配置 manifest.json 文件
{"manifest_version": 2,"name": "key_word_plugin","version": "1.0","description": "find_key_word",// 添加权限"permissions":["*://*/*","activeTab"],"icons": {"48": "icons/flower.jpg"},"content_scripts": [{"matches": ["*://*/*"],"js": ["index.js"],"run_at":"document_idle"}],// 侧边栏"sidebar_action": {"default_title": "My tool","default_panel": "./sidebar/sidebar.html","default_icon": "./sidebar/sidebar_icon.png"},// 背景脚本"background": {"scripts": ["bg.js"],"persistent": false,"type": "module"}
}
2、编写侧边栏结构
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>/* 略 */</style><link rel="stylesheet" href="./top_area.css">
</head>
<body><div class="container"><!-- 关键词查找 --><div class="top-area"><section class="inp-area"><input class="inp" type="text" maxlength="10"><button class="find-btn">查找</button></section><section class="result-area"><p>共找到</p><p class="count"><!-- 将查找到的结果条目数量写入此处 --></p><p>处;</p></section><section class="btn-area"><input type="number" step="1" min="1" class="goto-keyword-inp usable"><button class="usable goto-btn">跳转</button></section><section class="btn-area"><button class="usable last-btn">上一个</button><button class="usable next-btn">下一个</button><button class="clear">清除所有标记</button></section></div></div>
</body>
<script src="keyword.js"></script>
</html>
效果图
3、查找关键词并高亮的方法
// 获取当前激活的标签页面 使用 tabs 需要权限 activeTab,在 manifest 中配置browser.tabs.query({active: true, currentWindow: true}).then((logTabs,onError)=>{// 然后往当前页面中注入内容脚本,document将是当前页面的 documentbrowser.tabs.executeScript({code:`
(function action(keyword, nodes){Array.from(nodes).forEach(node =>{let {nodeType, data : content} = node;if(nodeType === 3 && content.includes(keyword)){let parentNode = node.parentNode;let split_arr = node.data.trim().replaceAll(keyword,'-' + keyword + '-').split('-').filter(e => e);for(let item of split_arr){if(item === keyword){let strong = document.createElement('strong')strong.innerText = keyword;strong.classList.add('${KEYWORD_CLASS_NAME}')strong.style = '${__style}'parentNode.insertBefore(strong, node)}else{let text = document.createTextNode(item);parentNode.insertBefore(text, node)}}parentNode.removeChild(node)}else if(nodeType === 1 && node.textContent.includes(keyword)){action(keyword, node.childNodes)}})
})('${keyword}', Array.from(document.body.childNodes).filter((e)=>{return (e.textContent.includes('${keyword}') &&e.tagName !== 'SCRIPT')
}))
document.querySelectorAll('.__keyword_word__').length`}).then((onExecuted, onError)=>{
// onExecuted[0] 的内容就是document.querySelectorAll('.__keyword_word__').length的结果total = onExecuted[0]})})
点击查找关键词后,页面脚本向当前的页面注入一段JavaScript代码。该代码包含一个立即执行的函数 和 一个关键词数量的获取。
该立即执行的函数 action,接收一个 要匹配的关键词 keyword 和 当前搜索节点数组 nodes 作为参数。
遍历每一个节点,取出节点的类型-->nodeType 和节点的文本内容 -->content。
如果是纯文本节点,则该节点的 nodeType 为3,如果是元素节点,则为 1。
如果有纯文本节点,并且该纯文本节点中的内容包含了关键词,那么构造出一个数组,使用该数组来区分非关键词内容和关键词内容,以及他们之间的位置关系。
let split_arr = content.trim().replaceAll(keyword, '-' + keyword + '-').split('-').filter(e => e);
如关键词为 我们 ,纯文本节点的内容为:
我们的征途是星辰大海,请和我们一起,永远相信美好的事情即将发生
那么构造的数组为:
[ "我们", "的征途是星辰大海,请和", "我们", "一起,永远相信美好的事情即将发生" ]
if(nodeType === 3 && content.includes(keyword)){let parentNode = node.parentNode;let split_arr = content.trim().replaceAll(keyword,'-' + keyword + '-').split('- ').filter(e => e);for(let item of split_arr){if(item === keyword){let strong = document.createElement('strong')strong.innerText = keyword;strong.classList.add('${KEYWORD_CLASS_NAME}')strong.style = '${__style}'parentNode.insertBefore(strong, node)}else{let text = document.createTextNode(item);parentNode.insertBefore(text, node)}}parentNode.removeChild(node)}
遍历构造的数组中的内容,如果当前值等于关键词,那么构造一个强调标签 Strong 将关键词作为 innerText,并添加指定的样式和样式类名,然后加入到当前所遍历的节点之前;如果该当前值与关键词不相等,则直接构造一个文本节点,将其添加到当前所遍历的节点之前......
当遍历完构造的数组后,将当前遍历的节点从其父节点中删除。这样就将纯文本节点中的内容全部高亮处理了。
没有包含关键词的纯文本节点直接跳过。
如果该节点不是纯文本结点,那么判断其 textContent 中是否包含关键词,如果是,那么让其所有子节点再参与 action 处理。否则就不用继续递归。
3-1) 如果直接使用 innerHTML 进行替换
如果标签中的属性出现了关键词,则会出现标签结构混乱的问题:
原代码:
<body><div class="my_name"><img src="https://tse1-mm.cn.bing.net/th/id/OIP-C.duz6S7Fvygrqd6Yj_DcXAQHaF7?rs=1&pid=ImgDetMain" alt="我的图片"><p>我的图片</p><div>你的图片<p>我们的图片</p><span>都是</span>图片</div></div><script>document.body.innerHTML = document.body.innerHTML.replaceAll('图片','<strong style="color:red">图片</strong>')</script>
</body>
4、清除关键词高亮
browser.tabs.query({active: true, currentWindow: true}).then(()=>{browser.tabs.executeScript({code:`
(function action(keyword){document.querySelectorAll('.${KEYWORD_CLASS_NAME}').forEach(e =>{let parent = e.parentNode;let textNode = document.createTextNode(keyword);parent.replaceChild(textNode, e)})
})('${keyword}')`})})
获取到所有 strong 强调标签(根据自定义的 class 名称),然后进行遍历,获取到每一个strong 的父元素。使用 createTextNode 创建一个纯文本节点,其内容就是关键词。然后将该文本节点替换掉 strong 标签即可。
5、页面脚本代码
// 简单封装document.querySelector
const getFirstEle = sign => document.querySelector(sign);// 关键词
var KEYWORD = '';// 总共找到多少处
var total = 0;
const count_ele = getFirstEle('.count')
count_ele.innerText = '____'const KEYWORD_CLASS_NAME = '__keyword_word__'
const __style = `color: #b60404; background-color: #f9f906; text-decoration: underline; text-decoration-style: double;`
var INDEX = null; // 当前记录的关键词索引,用于跳转 [1 ~ total]const find_btn = getFirstEle('.find-btn');
const clear_btn = getFirstEle('.clear');
const last_btn = getFirstEle('.last-btn');
const next_btn = getFirstEle('.next-btn');
const goto_keyword_inp = getFirstEle('.goto-keyword-inp')
const goto_btn = getFirstEle('.goto-btn')// 控制关键词跳转是否可用
const usables = document.querySelectorAll('.usable');
const set_usable = (res)=>{ usables.forEach(e => { e.disabled = !res; }) }// 默认不可用
set_usable(false);// 点击查找关键词
find_btn.addEventListener('click', (e)=>{// 获取用户的输入let keyword = document.querySelector('.inp').value.trim()if(!keyword) return;// 获取上次的关键词let last_keyword = sessionStorage.getItem('_keyword_');// 如果上次查找的关键词存在并且与当前的关键词相等if(last_keyword && last_keyword === keyword){ return; }// 如果上次的关键词与当前的关键词不相等,那么页面的高亮没有被清理// 因为上次的关键词session中没有被清除。先清理页面残留else if(last_keyword && last_keyword !== keyword){clear_action(last_keyword, false, false, false)}// 更新关键词sessionStorage.setItem('_keyword_', keyword)KEYWORD = keyword;// 获取当前激活的标签页面 使用 tabs 需要权限 activeTab,在 manifest 中配置browser.tabs.query({active: true, currentWindow: true}).then((logTabs,onError)=>{// 然后往当前页面中注入内容脚本,document将是当前页面的 documentbrowser.tabs.executeScript({code:`
(function action(keyword, nodes){Array.from(nodes).forEach(node =>{let {nodeType, textContent : content} = node;if(nodeType === 3 && content.includes(keyword)){let parentNode = node.parentNode;let split_arr = content.trim().replaceAll(keyword,'-' + keyword + '-').split('-').filter(e => e);for(let item of split_arr){if(item === keyword){let strong = document.createElement('strong')strong.innerText = keyword;strong.classList.add('${KEYWORD_CLASS_NAME}')strong.style = '${__style}'parentNode.insertBefore(strong, node)}else{let text = document.createTextNode(item);parentNode.insertBefore(text, node)}}parentNode.removeChild(node)}else if(nodeType === 1 && content.includes(keyword)){action(keyword, node.childNodes)}})
})('${keyword}', Array.from(document.body.childNodes).filter((e)=>{return (e.textContent.includes('${keyword}') &&e.tagName !== 'SCRIPT')
}))
document.querySelectorAll('.__keyword_word__').length`}).then((onExecuted, onError)=>{total = onExecuted[0]count_ele.innerText = total;// 开启跳转功能if(total > 0) set_usable(true);})})
})// 点击清除按钮 回归页面原始的状态
clear_btn.addEventListener('click', ()=>{let keyword = sessionStorage.getItem('_keyword_');clear_action(keyword)
})// 清除关键词标记
const clear_action = (keyword, clear_inp=true, clear_keyword_session=true, clear_count=true)=>{browser.tabs.query({active: true, currentWindow: true}).then(()=>{browser.tabs.executeScript({code:`
(function action(keyword){document.querySelectorAll('.${KEYWORD_CLASS_NAME}').forEach(e =>{let parent = e.parentNode;let textNode = document.createTextNode(keyword);parent.replaceChild(textNode, e)})
})('${keyword}')`})})if(clear_inp) document.querySelector('.inp').value = '';if(clear_keyword_session) sessionStorage.setItem('_keyword_', '');if(clear_count) count_ele.innerText = '_____';set_usable(false)KEYWORD = ''goto_keyword_inp.value = ''
}// 跳转到上一个关键词位置
last_btn.addEventListener('click', ()=>{if(!INDEX) INDEX = 1;else if(INDEX <= 1 ) INDEX = total;else if(INDEX >= total) INDEX = total - 1;else INDEX --;goto_keyword_site(INDEX - 1, KEYWORD);
})// 跳转到下一个关键词位置
next_btn.addEventListener('click', ()=>{if(!INDEX) INDEX = 1;else if(INDEX <= 1) INDEX = 2;else if(INDEX >= total) INDEX = 1;else INDEX ++;goto_keyword_site(INDEX - 1, KEYWORD);
})// 跳转到指定的位置
goto_btn.addEventListener('click', ()=>{let index = parseInt(goto_keyword_inp.value)if(!index) return;if(index > total) index = total;else if(index < 1) index = 1;goto_keyword_site(index - 1)INDEX = index;
})// 跳转到具体的关键词位置
const goto_keyword_site = (index) =>{goto_keyword_inp.value = index + 1;browser.tabs.query({active: true, currentWindow: true}).then((logTabs,onError)=>{// 然后往当前页面中注入内容脚本,document将是当前页面的 documentbrowser.tabs.executeScript({code:`
document.querySelectorAll(".${KEYWORD_CLASS_NAME}")[${index}].scrollIntoView({behavior:'smooth'
}) `})})
}
6、参考
[1]: 扩展是什么? - Mozilla | MDN
[2]: Firefox插件(拓展)开发_火狐浏览器插件开发-CSDN博客