使用GPT寫代碼實(shí)現(xiàn)高亮頁面關(guān)鍵字
背景
最近用戶跟我反饋了一個問題,他們使用的瀏覽器高亮關(guān)鍵字的插件在我們的網(wǎng)站不生效,我看了一下,因?yàn)槭褂昧?webComponent 但插件沒有進(jìn)行兼容。他們說這個功能非常重要,因此,我們就要在系統(tǒng)內(nèi)自己實(shí)現(xiàn)高亮頁面關(guān)鍵字。
在做一個新功能的時候,首先要先調(diào)研一下方案。
但在網(wǎng)上找了一圈,發(fā)現(xiàn)基本上都是以下這種方案:
// js部分 var bodyContent = document.body.innerHTMl; // 獲取頁面內(nèi)容 var pattern = new RegExp('關(guān)鍵字', 'g'); // 生成正則表達(dá)式 // 匹配關(guān)鍵詞并替換 document.body.innerHTMl = bodyContent.replace(pattern, '<span class="highlight">$&</span>');
在 innerHTML 匹配關(guān)鍵字,將關(guān)鍵字替換成高亮的 span,然后重新替換 innerHTML 這樣實(shí)現(xiàn)雖然非常簡單,但會導(dǎo)致 shadowRoot 和 iframe 失效。
于是我決定問問 GPT,看看它能不能給我一點(diǎn)驚喜
使用 GPT 編寫代碼
我:我要寫一段 js 代碼,傳入指定的文本,在整個頁面高亮
GPT: 你可以使用 JavaScript 中的 DOM 操作來實(shí)現(xiàn)這個功能。以下是一段示例代碼:
function highlightText(text) { const body = document.body; const regex = new RegExp(text, 'gi'); let match; while ((match = regex.exec(body.innerText))) { const span = document.createElement('span'); span.style.backgroundColor = 'yellow'; span.textContent = match[0]; const range = document.createRange(); range.setStart(body, match.index); range.setEnd(body, regex.lastIndex); range.deleteContents(); range.insertNode(span); } }
這段代碼會將頁面中所有匹配指定文本的文本節(jié)點(diǎn)用黃色高亮顯示。你可以將需要高亮的文本作為參數(shù)傳入 highlightText
函數(shù)中,例如:
highlightText('JavaScript');
點(diǎn)評:代碼是寫出來了,用了一個我還沒見過的 API。。。趕緊學(xué)習(xí)了一下 原理是:用 Range API 去選擇對應(yīng)的文本,然后使用 Range API 刪除和新增高亮節(jié)點(diǎn),思路看起來是沒問題的,而且跟網(wǎng)上找到的不同
撤銷高亮
我: 還要能夠撤銷高亮
GPT: 好的,為了實(shí)現(xiàn)撤銷高亮的功能,我們可以在高亮文本時將每個被高亮的文本節(jié)點(diǎn)保存到一個數(shù)組中,然后在需要撤銷高亮?xí)r,遍歷這個數(shù)組,將每個高亮的文本節(jié)點(diǎn)恢復(fù)到原來的狀態(tài)。
以下是修改后的代碼:
let highlightedNodes = []; function highlightText(text) { const body = document.body; const regex = new RegExp(text, 'gi'); let match; while ((match = regex.exec(body.innerText))) { const span = document.createElement('span'); span.style.backgroundColor = 'yellow'; span.textContent = match[0]; const range = document.createRange(); range.setStart(body, match.index); range.setEnd(body, regex.lastIndex); range.deleteContents(); range.insertNode(span); regex.lastIndex -= match[0].length - span.textContent.length; highlightedNodes.push(span); } } function clearHighlight() { while (highlightedNodes.length > 0) { const node = highlightedNodes.pop(); const parent = node.parentNode; parent.replaceChild(node.firstChild, node); parent.normalize(); } }
點(diǎn)評:牛逼,GPT 還是很聰明的,居然還會把 span 存起來,用于撤銷高亮,這個思路也是非常的正確的。
Range 范圍算不準(zhǔn)
但是實(shí)際運(yùn)行后,這段代碼是會報錯的,我排查了一下,發(fā)現(xiàn)計算出來的 Range 的 start
和 end
的位置是錯誤的,于是我跟 GPT 說一下,看看它如何糾正
我:算不準(zhǔn) start 和 end 的位置
GPT: 如果 setStart
和 setEnd
方法的位置計算不準(zhǔn)確,可能是因?yàn)樵谟嬎阄恢脮r沒有考慮到文本節(jié)點(diǎn)之間的空格和換行符。在這種情況下,你可以使用 createTreeWalker
方法來遍歷文檔樹中的所有文本節(jié)點(diǎn),并計算出每個文本節(jié)點(diǎn)的起始和結(jié)束位置。
以下是一段示例代碼:
function highlightText(text) { const body = document.body; const regex = new RegExp(text, 'gi'); let match; const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null, false); while (walker.nextNode()) { const node = walker.currentNode; while ((match = regex.exec(node.nodeValue))) { const span = document.createElement('span'); span.style.backgroundColor = 'yellow'; span.textContent = match[0]; const range = document.createRange(); range.setStart(node, match.index); range.setEnd(node, regex.lastIndex); range.deleteContents(); range.insertNode(span); } } }
在這個修改后的代碼中,我們使用 createTreeWalker
方法來遍歷文檔樹中的所有文本節(jié)點(diǎn)。對于每個文本節(jié)點(diǎn),我們使用 exec
方法來查找匹配指定文本的子串,并將每個匹配的子串用黃色高亮顯示。在計算位置時,我們使用 setStart
和 setEnd
方法來設(shè)置范圍的起始和結(jié)束位置,以確保高亮顯示的文本范圍正確。
點(diǎn)評:有點(diǎn)東西,我其實(shí)是不知道為什么算不準(zhǔn) start
和 end
的位置,但 GPT 自己也能發(fā)現(xiàn)問題,并且給出了另一個解決方案,并且非常貼心解析了這段代碼
根據(jù) GPT 的提示,是空字符的文本節(jié)點(diǎn)導(dǎo)致的問題,跟 GPT 提出的假設(shè)類似。
頁面卡死
但我把上面代碼運(yùn)行,發(fā)現(xiàn)頁面卡死了,原因是,在 TreeWalker 遍歷過程中,新增了高亮節(jié)點(diǎn),然后又進(jìn)入高亮節(jié)點(diǎn)進(jìn)行遍歷,然后又創(chuàng)建高亮節(jié)點(diǎn)。。。
因此應(yīng)該在 TreeWalker 遍歷節(jié)點(diǎn)之后再進(jìn)行高亮節(jié)點(diǎn)替換,于是我讓 GPT 繼續(xù)改
我:在 TreeWalker 遍歷過程中修改節(jié)點(diǎn),會遍歷到新節(jié)點(diǎn),需要延遲修改節(jié)點(diǎn)
GPT: 是的,你說的很對。在 TreeWalker
遍歷過程中修改節(jié)點(diǎn),會導(dǎo)致遍歷到新節(jié)點(diǎn),從而可能會出現(xiàn)意外的結(jié)果。為了避免這種情況,我們可以將需要修改的節(jié)點(diǎn)保存到一個數(shù)組中,在遍歷完成后再對這些節(jié)點(diǎn)進(jìn)行修改。
以下是一段示例代碼:
function highlightText(text) { const body = document.body; const regex = new RegExp(text, 'gi'); let match; const nodesToHighlight = []; const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null, false); while (walker.nextNode()) { const node = walker.currentNode; while ((match = regex.exec(node.nodeValue))) { const span = document.createElement('span'); span.style.backgroundColor = 'yellow'; span.textContent = match[0]; const range = document.createRange(); range.setStart(node, match.index); range.setEnd(node, regex.lastIndex); range.deleteContents(); range.insertNode(span); nodesToHighlight.push(span); } } nodesToHighlight.forEach(node => { const parent = node.parentNode; parent.replaceChild(node.firstChild, node); parent.normalize(); }); }
在這個修改后的代碼中,我們新增了一個名為 nodesToHighlight
的數(shù)組,用于保存需要高亮的節(jié)點(diǎn)。在遍歷過程中,每當(dāng)找到一個需要高亮的節(jié)點(diǎn)時,我們將這個節(jié)點(diǎn)添加到 nodesToHighlight
數(shù)組中。在遍歷完成后,我們遍歷 nodesToHighlight
數(shù)組,對每個節(jié)點(diǎn)進(jìn)行高亮處理。
這樣做可以確保在遍歷過程中不會修改節(jié)點(diǎn),從而避免出現(xiàn)意外的結(jié)果。
點(diǎn)評:改了,但沒完全改好,TreeWalker 中仍然有修改 DOM 的操作,于是我換了個說法
我:不能在遍歷過程中調(diào)用 deleteContents
和 insertNode
GPT: 是的,你說的很對。在遍歷過程中調(diào)用 deleteContents
和 insertNode
方法,會導(dǎo)致遍歷到新節(jié)點(diǎn),從而可能會出現(xiàn)意外的結(jié)果。為了避免這種情況,我們可以將需要修改的節(jié)點(diǎn)保存到一個數(shù)組中,在遍歷完成后再對這些節(jié)點(diǎn)進(jìn)行修改。
以下是一段示例代碼:
function highlightText(text) { const body = document.body; const regex = new RegExp(text, 'gi'); let match; const nodesToHighlight = []; const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if (node.parentNode.nodeName.match(/^(script|style)$/i)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } }, false); while (walker.nextNode()) { const node = walker.currentNode; while ((match = regex.exec(node.nodeValue))) { const span = document.createElement('span'); span.style.backgroundColor = 'yellow'; span.textContent = match[0]; const range = document.createRange(); range.setStart(node, match.index); range.setEnd(node, regex.lastIndex); nodesToHighlight.push({ range, span }); regex.lastIndex -= match[0].length - span.textContent.length; } } setTimeout(() => { nodesToHighlight.forEach(({ range, span }) => { range.deleteContents(); range.insertNode(span); }); }, 0); }
在這個修改后的代碼中,我們新增了一個名為 nodesToHighlight
的數(shù)組,用于保存需要高亮的節(jié)點(diǎn)和它們對應(yīng)的范圍。在遍歷過程中,每當(dāng)找到一個需要高亮的節(jié)點(diǎn)時,我們將這個節(jié)點(diǎn)和它對應(yīng)的范圍添加到 nodesToHighlight
數(shù)組中。在遍歷完成后,我們使用 setTimeout
方法將對節(jié)點(diǎn)的修改延遲到下一個事件循環(huán)中執(zhí)行。
這樣做可以確保在遍歷過程中不會修改節(jié)點(diǎn),從而避免出現(xiàn)意外的結(jié)果。同時,使用 setTimeout
方法可以將對節(jié)點(diǎn)的修改延遲到下一個事件循環(huán)中執(zhí)行,從而確保在修改節(jié)點(diǎn)時不會影響遍歷過程。
點(diǎn)評:這段代碼終于能運(yùn)行了,非常的優(yōu)秀!
支持 shadowRoot
我:但這樣還不能遍歷 webComponent shadowRoot 里面的元素
GPT:
function highlightText(text) { const body = document.body; const regex = new RegExp(text, 'gi'); let match; const nodesToHighlight = []; const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if (node.parentNode.nodeName.match(/^(script|style)$/i)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } }, false); while (walker.nextNode()) { const node = walker.currentNode; while ((match = regex.exec(node.nodeValue))) { const span = document.createElement('span'); span.style.backgroundColor = 'yellow'; span.textContent = match[0]; const range = document.createRange(); range.setStart(node, match.index); range.setEnd(node, regex.lastIndex); nodesToHighlight.push({ range, span }); regex.lastIndex -= match[0].length - span.textContent.length; } } const shadowHosts = document.querySelectorAll('*'); shadowHosts.forEach(shadowHost => { const shadowRoot = shadowHost.shadowRoot; if (shadowRoot) { const walker = document.createTreeWalker(shadowRoot, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if (node.parentNode.nodeName.match(/^(script|style)$/i)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } }, true); while (walker.nextNode()) { const node = walker.currentNode; while ((match = regex.exec(node.nodeValue))) { const span = document.createElement('span'); span.style.backgroundColor = 'yellow'; span.textContent = match[0]; const range = document.createRange(); range.setStart(node, match.index); range.setEnd(node, regex.lastIndex); nodesToHighlight.push({ range, span }); } } } }); setTimeout(() => { nodesToHighlight.forEach(({ range, span }) => { range.deleteContents(); range.insertNode(span); }); }, 0); }
在這個修改后的代碼中,我們使用 querySelectorAll
方法來獲取所有元素,然后對于每個元素,我們檢查它是否有 shadowRoot
。如果有,我們使用 createTreeWalker
方法來遍歷它的 shadowRoot
中的所有文本節(jié)點(diǎn)。在遍歷過程中,我們使用與之前相同的方式來查找需要高亮的節(jié)點(diǎn),并將它們添加到 nodesToHighlight
數(shù)組中。
這樣做可以確保遍歷到 shadowRoot
中的所有文本節(jié)點(diǎn),并將需要高亮的節(jié)點(diǎn)保存到 nodesToHighlight
數(shù)組中。然后,我們使用 setTimeout
方法將對節(jié)點(diǎn)的修改延遲到下一個事件循環(huán)中執(zhí)行,從而確保在修改節(jié)點(diǎn)時不會影響遍歷過程。
點(diǎn)評:這段代碼也是能運(yùn)行的,很牛逼,不過沒考慮代碼的復(fù)用性,shadowRoot 的處理,可以復(fù)用普通的 DOM 的高亮邏輯
撤銷操作支持 shadowRoot
我:撤銷操作也要作用到 shadowRoot
GPT: 是的,你說的很對。如果需要對 shadowRoot
中的高亮文本進(jìn)行撤銷操作,我們可以使用 Range
對象的 deleteContents()
方法來刪除高亮文本所在的節(jié)點(diǎn),并將原始文本重新插入到文檔中。
以下是一段示例代碼:
function removeHighlight() { const highlightedSpans = document.querySelectorAll('span[style="background-color: yellow;"]'); highlightedSpans.forEach(span => { const range = document.createRange(); range.selectNode(span); range.deleteContents(); range.insertNode(document.createTextNode(span.textContent)); }); const shadowHosts = document.querySelectorAll('*'); shadowHosts.forEach(shadowHost => { if (shadowHost.shadowRoot) { const highlightedSpans = shadowHost.shadowRoot.querySelectorAll('span[style="background-color: yellow;"]'); highlightedSpans.forEach(span => { const range = document.createRange(); range.selectNode(span); range.deleteContents(); range.insertNode(document.createTextNode(span.textContent)); }); } }); }
點(diǎn)評:代碼可以運(yùn)行,但有以下缺點(diǎn):
- 同樣是復(fù)用性不足,
- 高亮節(jié)點(diǎn)的選擇器
span[style="background-color: yellow;"]
不太優(yōu)雅,不夠健壯 但問題不大,我自己能改hhh
highligh 函數(shù)需要支持重復(fù)調(diào)用
目前的代碼會有一個問題,就是重復(fù)調(diào)用 highlightText
,已高亮的節(jié)點(diǎn),仍然會創(chuàng)建 span,這就會導(dǎo)致 span 重復(fù)嵌套。需要先判斷是否已經(jīng)高亮了。
我:highlightText
如果同一個值多次調(diào)用,會給文本節(jié)點(diǎn)再嵌套一層高亮節(jié)點(diǎn),要支持重復(fù)調(diào)用不會重復(fù)嵌套節(jié)點(diǎn)
GPT: 好的,如果需要支持重復(fù)調(diào)用 highlightText
函數(shù)時不會重復(fù)嵌套高亮節(jié)點(diǎn),我們可以在每次調(diào)用函數(shù)之前先檢查文本節(jié)點(diǎn)是否已經(jīng)被高亮過,如果已經(jīng)被高亮過,則不再重復(fù)嵌套高亮節(jié)點(diǎn)。
以下是修改后的 highlightText
函數(shù)代碼:
function highlightText(text) { const body = document.body; const regex = new RegExp(text, 'gi'); let match; const nodesToHighlight = []; const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if (node.parentNode.nodeName.match(/^(script|style)$/i)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } }, false); while (walker.nextNode()) { const node = walker.currentNode; if (node.parentNode.classList.contains('highlighted')) { continue; } while ((match = regex.exec(node.nodeValue))) { const span = document.createElement('span'); span.style.backgroundColor = 'yellow'; span.textContent = match[0]; const range = document.createRange(); range.setStart(node, match.index); range.setEnd(node, regex.lastIndex); nodesToHighlight.push({ range, span }); regex.lastIndex -= match[0].length - span.textContent.length; } } const shadowHosts = document.querySelectorAll('*'); shadowHosts.forEach(shadowHost => { if (shadowHost.shadowRoot) { const shadowRoot = shadowHost.shadowRoot; const walker = document.createTreeWalker(shadowRoot, NodeFilter.SHOW_TEXT, { acceptNode: function(node) { if (node.parentNode.nodeName.match(/^(script|style)$/i)) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; } }, true); while (walker.nextNode()) { const node = walker.currentNode; if (node.parentNode.classList.contains('highlighted')) { continue; } while ((match = regex.exec(node.nodeValue))) { const span = document.createElement('span'); span.style.backgroundColor = 'yellow'; span.textContent = match[0]; const range = document.createRange(); range.setStart(node, match.index); range.setEnd(node, regex.lastIndex); nodesToHighlight.push({ range, span }); regex.lastIndex -= match[0].length - span.textContent.length; } } } }); nodesToHighlight.forEach(nodeToHighlight => { const range = nodeToHighlight.range; const span = nodeToHighlight.span; range.deleteContents(); span.classList.add('highlighted'); range.insertNode(span); }); }
在這個代碼中,我們添加了一個名為 highlighted
的 CSS 類,用于標(biāo)記已經(jīng)被高亮過的文本節(jié)點(diǎn)。在每次遍歷文本節(jié)點(diǎn)時,我們檢查當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)是否已經(jīng)被標(biāo)記為 highlighted
,如果是,則跳過當(dāng)前節(jié)點(diǎn),不再重復(fù)嵌套高亮節(jié)點(diǎn)。在插入高亮節(jié)點(diǎn)時,我們還將其添加到 highlighted
類中,以便下次遍歷時可以正確地跳過已經(jīng)被高亮過的節(jié)點(diǎn)。
點(diǎn)評:完美符合要求~我只需要稍微復(fù)用以下代碼,進(jìn)行一些小修改即可。
最終還是成為了一名代碼美化工程師~~~
使用體驗(yàn)
GPT 確實(shí)非常強(qiáng)大,它知道的非常多,有我不知道的知識,如果不是 GPT 我的確不會知道可以使用 Range API 去操作 DOM 實(shí)現(xiàn)高亮。
它還能較好的根據(jù)提示,糾正自己的錯誤
我們可以讓 GPT 提供思路,寫一個簡單的 Demo,然后自己進(jìn)行代碼優(yōu)化。
目前這種使用方式的效率還有待提升。整個過程耗費(fèi)了 1 個多小時,寫了不到 100 行代碼。在對話的過程中,還需要將代碼復(fù)制到 IDE 中,這并沒有將 GPT 和 IDE 串聯(lián)起來。
不過,我們可以嘗使用 Cursor
這個 IDE,它可以直接在 IDE 中使用 GPT,這樣應(yīng)該會有更好的編碼體驗(yàn)。
以上就是使用GPT寫代碼實(shí)現(xiàn)高亮頁面關(guān)鍵字的詳細(xì)內(nèi)容,更多關(guān)于GPT 高亮頁面關(guān)鍵字的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
js 多種變量定義(對象直接量,數(shù)組直接量和函數(shù)直接量)
js 多種變量定義(對象直接量,數(shù)組直接量和函數(shù)直接量),大家可以參考下,對于以后學(xué)習(xí)js 面向?qū)τ谂cjson操作會有幫助。2010-05-05javascript 跨瀏覽器開發(fā)經(jīng)驗(yàn)總結(jié)(五) js 事件
javascript 跨瀏覽器開發(fā)之js 事件的兼容性問題,需要的朋友可以參考下。2010-05-05EXT中單擊button按鈕grid添加一行(光標(biāo)位置可設(shè)置)的實(shí)例代碼
這篇文章主要介紹了EXT中單擊button按鈕grid添加一行(光標(biāo)位置可設(shè)置)的實(shí)例代碼 的相關(guān)資料,需要的朋友可以參考下2016-06-06JavaScript String 對象常用方法總結(jié)
下面小編就為大家?guī)硪黄狫avaScript String 對象常用方法總結(jié)。小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考2016-04-04js+html+css實(shí)現(xiàn)簡單日歷效果
這篇文章主要為大家詳細(xì)介紹了js+html+css實(shí)現(xiàn)簡單日歷效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-06-06