目录
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将是当前页面的 document
browser.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将是当前页面的 document
browser.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将是当前页面的 document
browser.tabs.executeScript({
code:`
document.querySelectorAll(".${KEYWORD_CLASS_NAME}")[${index}].scrollIntoView({
behavior:'smooth'
})
`
})
})
}
6、参考
[1]: 扩展是什么? - Mozilla | MDN
[2]: Firefox插件(拓展)开发_火狐浏览器插件开发-****博客