怎么使用GPT写代码实现高亮页面关键字

其他教程   发布日期:2025年03月06日   浏览次数:145

这篇文章主要介绍“怎么使用GPT写代码实现高亮页面关键字”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“怎么使用GPT写代码实现高亮页面关键字”文章能帮助大家解决问题。

背景

最近用户跟我反馈了一个问题,他们使用的浏览器高亮关键字的插件在我们的网站不生效,我看了一下,因为使用了 webComponent 但插件没有进行兼容。他们说这个功能非常重要,因此,我们就要在系统内自己实现高亮页面关键字。

在做一个新功能的时候,首先要先调研一下方案:

  1. // js部分
  2. var bodyContent = document.body.innerHTMl; // 获取页面内容
  3. var pattern = new RegExp('关键字', 'g'); // 生成正则表达式
  4. // 匹配关键词并替换
  5. document.body.innerHTMl = bodyContent.replace(pattern, '<span class="highlight">$&</span>');

在 innerHTML 匹配关键字,将关键字替换成高亮的 span,然后重新替换 innerHTML 这样实现虽然非常简单,但会导致 shadowRoot 和 iframe 失效。

于是我决定问问 GPT,看看它能不能给我一点惊喜

使用 GPT 编写代码

我:我要写一段 js 代码,传入指定的文本,在整个页面高亮

GPT: 你可以使用 JavaScript 中的 DOM 操作来实现这个功能。以下是一段示例代码:

  1. function highlightText(text) {
  2. const body = document.body;
  3. const regex = new RegExp(text, 'gi');
  4. let match;
  5. while ((match = regex.exec(body.innerText))) {
  6. const span = document.createElement('span');
  7. span.style.backgroundColor = 'yellow';
  8. span.textContent = match[0];
  9. const range = document.createRange();
  10. range.setStart(body, match.index);
  11. range.setEnd(body, regex.lastIndex);
  12. range.deleteContents();
  13. range.insertNode(span);
  14. }
  15. }

这段代码会将页面中所有匹配指定文本的文本节点用黄色高亮显示。你可以将需要高亮的文本作为参数传入

  1. highlightText
函数中,例如:
  1. highlightText('JavaScript');

点评:代码是写出来了,用了一个我还没见过的 API。。。赶紧学习了一下 原理是:用 Range API 去选择对应的文本,然后使用 Range API 删除和新增高亮节点,思路看起来是没问题的,而且跟网上找到的不同

撤销高亮

我: 还要能够撤销高亮

GPT: 好的,为了实现撤销高亮的功能,我们可以在高亮文本时将每个被高亮的文本节点保存到一个数组中,然后在需要撤销高亮时,遍历这个数组,将每个高亮的文本节点恢复到原来的状态。

以下是修改后的代码:

  1. let highlightedNodes = [];
  2. function highlightText(text) {
  3. const body = document.body;
  4. const regex = new RegExp(text, 'gi');
  5. let match;
  6. while ((match = regex.exec(body.innerText))) {
  7. const span = document.createElement('span');
  8. span.style.backgroundColor = 'yellow';
  9. span.textContent = match[0];
  10. const range = document.createRange();
  11. range.setStart(body, match.index);
  12. range.setEnd(body, regex.lastIndex);
  13. range.deleteContents();
  14. range.insertNode(span);
  15. regex.lastIndex -= match[0].length - span.textContent.length;
  16. highlightedNodes.push(span);
  17. }
  18. }
  19. function clearHighlight() {
  20. while (highlightedNodes.length > 0) {
  21. const node = highlightedNodes.pop();
  22. const parent = node.parentNode;
  23. parent.replaceChild(node.firstChild, node);
  24. parent.normalize();
  25. }
  26. }

点评:牛逼,GPT 还是很聪明的,居然还会把 span 存起来,用于撤销高亮,这个思路也是非常的正确的。

Range 范围算不准

但是实际运行后,这段代码是会报错的,我排查了一下,发现计算出来的 Range 的

  1. start
  1. end
的位置是错误的,于是我跟 GPT 说一下,看看它如何纠正

我:算不准 start 和 end 的位置

GPT: 如果

  1. setStart
  1. setEnd
方法的位置计算不准确,可能是因为在计算位置时没有考虑到文本节点之间的空格和换行符。在这种情况下,你可以使用
  1. createTreeWalker
方法来遍历文档树中的所有文本节点,并计算出每个文本节点的起始和结束位置。

以下是一段示例代码:

  1. function highlightText(text) {
  2. const body = document.body;
  3. const regex = new RegExp(text, 'gi');
  4. let match;
  5. const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null, false);
  6. while (walker.nextNode()) {
  7. const node = walker.currentNode;
  8. while ((match = regex.exec(node.nodeValue))) {
  9. const span = document.createElement('span');
  10. span.style.backgroundColor = 'yellow';
  11. span.textContent = match[0];
  12. const range = document.createRange();
  13. range.setStart(node, match.index);
  14. range.setEnd(node, regex.lastIndex);
  15. range.deleteContents();
  16. range.insertNode(span);
  17. }
  18. }
  19. }

在这个修改后的代码中,我们使用

  1. createTreeWalker
方法来遍历文档树中的所有文本节点。对于每个文本节点,我们使用
  1. exec
方法来查找匹配指定文本的子串,并将每个匹配的子串用黄色高亮显示。在计算位置时,我们使用
  1. setStart
  1. setEnd
方法来设置范围的起始和结束位置,以确保高亮显示的文本范围正确。

点评:有点东西,我其实是不知道为什么算不准

  1. start
  1. end
的位置,但 GPT 自己也能发现问题,并且给出了另一个解决方案,并且非常贴心解析了这段代码

根据 GPT 的提示,是空字符的文本节点导致的问题,跟 GPT 提出的假设类似。

页面卡死

但我把上面代码运行,发现页面卡死了,原因是,在 TreeWalker 遍历过程中,新增了高亮节点,然后又进入高亮节点进行遍历,然后又创建高亮节点。。。

因此应该在 TreeWalker 遍历节点之后再进行高亮节点替换,于是我让 GPT 继续改

我:在 TreeWalker 遍历过程中修改节点,会遍历到新节点,需要延迟修改节点

GPT: 是的,你说的很对。在

  1. TreeWalker
遍历过程中修改节点,会导致遍历到新节点,从而可能会出现意外的结果。为了避免这种情况,我们可以将需要修改的节点保存到一个数组中,在遍历完成后再对这些节点进行修改。

以下是一段示例代码:

  1. function highlightText(text) {
  2. const body = document.body;
  3. const regex = new RegExp(text, 'gi');
  4. let match;
  5. const nodesToHighlight = [];
  6. const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null, false);
  7. while (walker.nextNode()) {
  8. const node = walker.currentNode;
  9. while ((match = regex.exec(node.nodeValue))) {
  10. const span = document.createElement('span');
  11. span.style.backgroundColor = 'yellow';
  12. span.textContent = match[0];
  13. const range = document.createRange();
  14. range.setStart(node, match.index);
  15. range.setEnd(node, regex.lastIndex);
  16. range.deleteContents();
  17. range.insertNode(span);
  18. nodesToHighlight.push(span);
  19. }
  20. }
  21. nodesToHighlight.forEach(node => {
  22. const parent = node.parentNode;
  23. parent.replaceChild(node.firstChild, node);
  24. parent.normalize();
  25. });
  26. }

在这个修改后的代码中,我们新增了一个名为

  1. nodesToHighlight
的数组,用于保存需要高亮的节点。在遍历过程中,每当找到一个需要高亮的节点时,我们将这个节点添加到
  1. nodesToHighlight
数组中。在遍历完成后,我们遍历
  1. nodesToHighlight
数组,对每个节点进行高亮处理。

这样做可以确保在遍历过程中不会修改节点,从而避免出现意外的结果。

点评:改了,但没完全改好,TreeWalker 中仍然有修改 DOM 的操作,于是我换了个说法

我:不能在遍历过程中调用

  1. deleteContents
  1. insertNode

GPT: 是的,你说的很对。在遍历过程中调用

  1. deleteContents
  1. insertNode
方法,会导致遍历到新节点,从而可能会出现意外的结果。为了避免这种情况,我们可以将需要修改的节点保存到一个数组中,在遍历完成后再对这些节点进行修改。

以下是一段示例代码:

  1. function highlightText(text) {
  2. const body = document.body;
  3. const regex = new RegExp(text, 'gi');
  4. let match;
  5. const nodesToHighlight = [];
  6. const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
  7. acceptNode: function(node) {
  8. if (node.parentNode.nodeName.match(/^(script|style)$/i)) {
  9. return NodeFilter.FILTER_REJECT;
  10. }
  11. return NodeFilter.FILTER_ACCEPT;
  12. }
  13. }, false);
  14. while (walker.nextNode()) {
  15. const node = walker.currentNode;
  16. while ((match = regex.exec(node.nodeValue))) {
  17. const span = document.createElement('span');
  18. span.style.backgroundColor = 'yellow';
  19. span.textContent = match[0];
  20. const range = document.createRange();
  21. range.setStart(node, match.index);
  22. range.setEnd(node, regex.lastIndex);
  23. nodesToHighlight.push({ range, span });
  24. regex.lastIndex -= match[0].length - span.textContent.length;
  25. }
  26. }
  27. setTimeout(() => {
  28. nodesToHighlight.forEach(({ range, span }) => {
  29. range.deleteContents();
  30. range.insertNode(span);
  31. });
  32. }, 0);
  33. }

在这个修改后的代码中,我们新增了一个名为

  1. nodesToHighlight
的数组,用于保存需要高亮的节点和它们对应的范围。在遍历过程中,每当找到一个需要高亮的节点时,我们将这个节点和它对应的范围添加到
  1. nodesToHighlight
数组中。在遍历完成后,我们使用
  1. setTimeout
方法将对节点的修改延迟到下一个事件循环中执行。

这样做可以确保在遍历过程中不会修改节点,从而避免出现意外的结果。同时,使用

  1. setTimeout
方法可以将对节点的修改延迟到下一个事件循环中执行,从而确保在修改节点时不会影响遍历过程。

点评:这段代码终于能运行了,非常的优秀!

支持 shadowRoot

我:但这样还不能遍历 webComponent shadowRoot 里面的元素

GPT:

  1. function highlightText(text) {
  2. const body = document.body;
  3. const regex = new RegExp(text, 'gi');
  4. let match;
  5. const nodesToHighlight = [];
  6. const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
  7. acceptNode: function(node) {
  8. if (node.parentNode.nodeName.match(/^(script|style)$/i)) {
  9. return NodeFilter.FILTER_REJECT;
  10. }
  11. return NodeFilter.FILTER_ACCEPT;
  12. }
  13. }, false);
  14. while (walker.nextNode()) {
  15. const node = walker.currentNode;
  16. while ((match = regex.exec(node.nodeValue))) {
  17. const span = document.createElement('span');
  18. span.style.backgroundColor = 'yellow';
  19. span.textContent = match[0];
  20. const range = document.createRange();
  21. range.setStart(node, match.index);
  22. range.setEnd(node, regex.lastIndex);
  23. nodesToHighlight.push({ range, span });
  24. regex.lastIndex -= match[0].length - span.textContent.length;
  25. }
  26. }
  27. const shadowHosts = document.querySelectorAll('*');
  28. shadowHosts.forEach(shadowHost => {
  29. const shadowRoot = shadowHost.shadowRoot;
  30. if (shadowRoot) {
  31. const walker = document.createTreeWalker(shadowRoot, NodeFilter.SHOW_TEXT, {
  32. acceptNode: function(node) {
  33. if (node.parentNode.nodeName.match(/^(script|style)$/i)) {
  34. return NodeFilter.FILTER_REJECT;
  35. }
  36. return NodeFilter.FILTER_ACCEPT;
  37. }
  38. }, true);
  39. while (walker.nextNode()) {
  40. const node = walker.currentNode;
  41. while ((match = regex.exec(node.nodeValue))) {
  42. const span = document.createElement('span');
  43. span.style.backgroundColor = 'yellow';
  44. span.textContent = match[0];
  45. const range = document.createRange();
  46. range.setStart(node, match.index);
  47. range.setEnd(node, regex.lastIndex);
  48. nodesToHighlight.push({ range, span });
  49. }
  50. }
  51. }
  52. });
  53. setTimeout(() => {
  54. nodesToHighlight.forEach(({ range, span }) => {
  55. range.deleteContents();
  56. range.insertNode(span);
  57. });
  58. }, 0);
  59. }

在这个修改后的代码中,我们使用

  1. querySelectorAll
方法来获取所有元素,然后对于每个元素,我们检查它是否有
  1. shadowRoot
。如果有,我们使用
  1. createTreeWalker
方法来遍历它的
  1. shadowRoot
中的所有文本节点。在遍历过程中,我们使用与之前相同的方式来查找需要高亮的节点,并将它们添加到
  1. nodesToHighlight
数组中。

这样做可以确保遍历到

  1. shadowRoot
中的所有文本节点,并将需要高亮的节点保存到
  1. nodesToHighlight
数组中。然后,我们使用
  1. setTimeout
方法将对节点的修改延迟到下一个事件循环中执行,从而确保在修改节点时不会影响遍历过程。

点评:这段代码也是能运行的,很牛逼,不过没考虑代码的复用性,shadowRoot 的处理,可以复用普通的 DOM 的高亮逻辑

撤销操作支持 shadowRoot

我:撤销操作也要作用到 shadowRoot

GPT: 是的,你说的很对。如果需要对

  1. shadowRoot
中的高亮文本进行撤销操作,我们可以使用
  1. Range
对象的
  1. deleteContents()
方法来删除高亮文本所在的节点,并将原始文本重新插入到文档中。

以下是一段示例代码:

  1. function removeHighlight() {
  2. const highlightedSpans = document.querySelectorAll('span[]');
  3. highlightedSpans.forEach(span => {
  4. const range = document.createRange();
  5. range.selectNode(span);
  6. range.deleteContents();
  7. range.insertNode(document.createTextNode(span.textContent));
  8. });
  9. const shadowHosts = document.querySelectorAll('*');
  10. shadowHosts.forEach(shadowHost => {
  11. if (shadowHost.shadowRoot) {
  12. const highlightedSpans = shadowHost.shadowRoot.querySelectorAll('span[]');
  13. highlightedSpans.forEach(span => {
  14. const range = document.createRange();
  15. range.selectNode(span);
  16. range.deleteContents();
  17. range.insertNode(document.createTextNode(span.textContent));
  18. });
  19. }
  20. });
  21. }

点评:代码可以运行,但有以下缺点:

  • 同样是复用性不足,

  • 高亮节点的选择器

    1. span[style="background-color: yellow;"]
    不太优雅,不够健壮 但问题不大,我自己能改hhh

highligh 函数需要支持重复调用

目前的代码会有一个问题,就是重复调用

  1. highlightText
,已高亮的节点,仍然会创建 span,这就会导致 span 重复嵌套。需要先判断是否已经高亮了。

我:

  1. highlightText
如果同一个值多次调用,会给文本节点再嵌套一层高亮节点,要支持重复调用不会重复嵌套节点

GPT: 好的,如果需要支持重复调用

  1. highlightText
函数时不会重复嵌套高亮节点,我们可以在每次调用函数之前先检查文本节点是否已经被高亮过,如果已经被高亮过,则不再重复嵌套高亮节点。

以下是修改后的

  1. highlightText
函数代码:
  1. function highlightText(text) {
  2. const body = document.body;
  3. const regex = new RegExp(text, 'gi');
  4. let match;
  5. const nodesToHighlight = [];
  6. const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
  7. acceptNode: function(node) {
  8. if (node.parentNode.nodeName.match(/^(script|style)$/i)) {
  9. return NodeFilter.FILTER_REJECT;
  10. }
  11. return NodeFilter.FILTER_ACCEPT;
  12. }
  13. }, false);
  14. while (walker.nextNode()) {
  15. const node = walker.currentNode;
  16. if (node.parentNode.classList.contains('highlighted')) {
  17. continue;
  18. }
  19. while ((match = regex.exec(node.nodeValue))) {
  20. const span = document.createElement('span');
  21. span.style.backgroundColor = 'yellow';
  22. span.textContent = match[0];
  23. const range = document.createRange();
  24. range.setStart(node, match.index);
  25. range.setEnd(node, regex.lastIndex);
  26. nodesToHighlight.push({ range, span });
  27. regex.lastIndex -= match[0].length - span.textContent.length;
  28. }
  29. }
  30. const shadowHosts = document.querySelectorAll('*');
  31. shadowHosts.forEach(shadowHost => {
  32. if (shadowHost.shadowRoot) {
  33. const shadowRoot = shadowHost.shadowRoot;
  34. const walker = document.createTreeWalker(shadowRoot, NodeFilter.SHOW_TEXT, {
  35. acceptNode: function(node) {
  36. if (node.parentNode.nodeName.match(/^(script|style)$/i)) {
  37. return NodeFilter.FILTER_REJECT;
  38. }
  39. return NodeFilter.FILTER_ACCEPT;
  40. }
  41. }, true);
  42. while (walker.nextNode()) {
  43. const node = walker.currentNode;
  44. if (node.parentNode.classList.contains('highlighted')) {
  45. continue;
  46. }
  47. while ((match = regex.exec(node.nodeValue))) {
  48. const span = document.createElement('span');
  49. span.style.backgroundColor = 'yellow';
  50. span.textContent = match[0];
  51. const range = document.createRange();
  52. range.setStart(node, match.index);
  53. range.setEnd(node, regex.lastIndex);
  54. nodesToHighlight.push({ range, span });
  55. regex.lastIndex -= match[0].length - span.textContent.length;
  56. }
  57. }
  58. }
  59. });
  60. nodesToHighlight.forEach(nodeToHighlight => {
  61. const range = nodeToHighlight.range;
  62. const span = nodeToHighlight.span;
  63. range.deleteContents();
  64. span.classList.add('highlighted');
  65. range.insertNode(span);
  66. });
  67. }

在这个代码中,我们添加了一个名为

  1. highlighted
的 CSS 类,用于标记已经被高亮过的文本节点。在每次遍历文本节点时,我们检查当前节点的父节点是否已经被标记为
  1. highlighted
,如果是,则跳过当前节点,不再重复嵌套高亮节点。在插入高亮节点时,我们还将其添加到
  1. highlighted
类中,以便下次遍历时可以正确地跳过已经被高亮过的节点。

点评:完美符合要求~我只需要稍微复用以下代码,进行一些小修改即可。

最终还是成为了一名代码美化工程师~~~

使用体验

GPT 确实非常强大,它知道的非常多,有我不知道的知识,如果不是 GPT 我的确不会知道可以使用 Range API 去操作 DOM 实现高亮。

它还能较好的根据提示,纠正自己的错误

我们可以让 GPT 提供思路,写一个简单的 Demo,然后自己进行代码优化。

目前这种使用方式的效率还有待提升。整个过程耗费了 1 个多小时,写了不到 100 行代码。在对话的过程中,还需要将代码复制到 IDE 中,这并没有将 GPT 和 IDE 串联起来。

不过,我们可以尝使用

  1. Cursor
这个 IDE,它可以直接在 IDE 中使用 GPT,这样应该会有更好的编码体验。

以上就是怎么使用GPT写代码实现高亮页面关键字的详细内容,更多关于怎么使用GPT写代码实现高亮页面关键字的资料请关注九品源码其它相关文章!