Firefox 关键词高亮插件的简单实现

目录

[1、配置 manifest.json 文件](#1、配置 manifest.json 文件)

2、编写侧边栏结构

3、查找关键词并高亮的方法

[3-1) 如果直接使用 innerHTML 进行替换](#3-1) 如果直接使用 innerHTML 进行替换)

4、清除关键词高亮

5、页面脚本代码

6、参考


1、配置 manifest.json 文件

javascript 复制代码
{
    "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、编写侧边栏结构

html 复制代码
<!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、查找关键词并高亮的方法

javascript 复制代码
    // 获取当前激活的标签页面 使用 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。

如果有纯文本节点,并且该纯文本节点中的内容包含了关键词,那么构造出一个数组,使用该数组来区分非关键词内容和关键词内容,以及他们之间的位置关系。

javascript 复制代码
let split_arr = content.trim()
    .replaceAll(keyword, '-' + keyword + '-')
    .split('-')
    .filter(e => e);

如关键词为 ++我们++,纯文本节点的内容为:

我们的征途是星辰大海,请和我们一起,永远相信美好的事情即将发生

那么构造的数组为:

[ " ++我们++ ", " 的征途是星辰大海,请和*", "** ++我们++ ", " 一起,永远相信美好的事情即将发生**"**]*

javascript 复制代码
    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 进行替换

如果标签中的属性出现了关键词,则会出现标签结构混乱的问题:

原代码:

html 复制代码
<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、清除关键词高亮

javascript 复制代码
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、页面脚本代码

javascript 复制代码
// 简单封装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](https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/What_are_WebExtensions "扩展是什么? - Mozilla | MDN") \[2\]: [Firefox插件(拓展)开发_火狐浏览器插件开发-CSDN博客](https://blog.csdn.net/hao_13/article/details/130979265 "Firefox插件(拓展)开发_火狐浏览器插件开发-CSDN博客")

相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax