富文本關(guān)鍵字搜索高亮,解決方法及優(yōu)化(收藏?。?/h1>
大廠技術(shù) 高級(jí)前端 Node進(jìn)階
點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)Node交流群
作者:猿猴望月 原文:https://juejin.cn/post/7070688497929043998
去年在冬季面試的時(shí)候,被某廠面試官問了這個(gè)問題:
如果我們的數(shù)據(jù)是富文本,現(xiàn)在要加一個(gè)搜索功能,怎么樣才能完美的實(shí)現(xiàn)高亮呢?
當(dāng)時(shí)回答的很粗糙,只答了提取出文字進(jìn)行搜索,怎么回填原本的樣式并沒有說清楚。
今天坐在我旁邊的小哥開始做MarkDown搜索匹配了,又喚起了我塵封已久的記憶,于是今天就讓我們來(lái)一起震懾一下面試官吧!
首先能想到的思路是和leetcode的上車問題相關(guān)。
有一群乘客,當(dāng)中一個(gè)人在1號(hào)站臺(tái)下車,兩個(gè)人在2號(hào)上車上車,最后求N號(hào)站臺(tái)有多少乘客。
這個(gè)問題也可以那么去考慮:在富文本串之中,在遇到開標(biāo)簽就理解為當(dāng)前的文字“上車”,而遇到閉標(biāo)簽就理解為當(dāng)前的文字”下車“。
(開標(biāo)簽:<span>、閉標(biāo)簽:</span>)
第一版
首先我們開發(fā)一個(gè)頁(yè)面,其中僅包含搜索框和幾段富文本的數(shù)據(jù)

舉個(gè)??,數(shù)據(jù)第一項(xiàng)為:
今天真是個(gè)<span style="text-decoration:underline">好天氣</span>
這個(gè)時(shí)候我們想搜索“好天氣”,整體的思路是分為3個(gè)部分:
處理富文本,生成標(biāo)簽在字符串位置的映射關(guān)系 在處理好的文本中搜索key,對(duì)命中的詞在映射關(guān)系中增加命中樣式 根據(jù)映射關(guān)系生成新的富文本
在其中,我們需要記錄一些關(guān)鍵狀態(tài)值:
currentTag:當(dāng)前正處理標(biāo)簽 isOpen:當(dāng)前標(biāo)簽是否是開標(biāo)簽 needAdd: 文本需要增加的開標(biāo)簽 nowText:目前正在處理的文本 textToTagMap:文本與標(biāo)簽的映射,這里提供一下類型
type TextToStyleMap = Array<
{
key: string,
// 從這個(gè)字符增加的標(biāo)簽index
up: number[],
// 從這個(gè)字符結(jié)束的標(biāo)簽index
down: number[]
}
>
讓我們簡(jiǎn)單的畫個(gè)流程圖看一下怎么去做~

codesandbox.io/s/new-smoke…[1]
首版完成!(ps:現(xiàn)在我們搜索用的是正則,在大文本的情況下可能會(huì)有性能問題

優(yōu)化1
看起來(lái)好像很完美了?如果我們?cè)谶@個(gè)文本框中搜索“個(gè)好”呢?

咦,下劃線丟了?
因?yàn)槲覀兯阉鱾€(gè)好后的富文本結(jié)果是
今天真是<em>個(gè)<span style="text-decoration:underline">好</em>天氣</span>
這里生產(chǎn)出了一個(gè)錯(cuò)誤的標(biāo)簽不對(duì)應(yīng)的富文本
我們?cè)趺磻?yīng)對(duì)這個(gè)問題呢?
最簡(jiǎn)單的方式應(yīng)該是把強(qiáng)調(diào)的樣式加在每一個(gè)文字上
今天真是<em>個(gè)<span style="text-decoration:underline">好</em>天氣</span>
??
今天真是<em>個(gè)</em><span style="text-decoration:underline"><em>好</em>天氣</span>
codesandbox.io/s/upbeat-fe…[2]
簡(jiǎn)單的改了一下代碼之后,我們就修改了這個(gè)bug

優(yōu)化2
這么做了以后,我們會(huì)添加許多多余的強(qiáng)調(diào)樣式標(biāo)簽。比如還是在上面這個(gè)例子里,我們?nèi)绻阉鳌禾鞖狻坏脑?,結(jié)果會(huì)是這樣:
今天真是個(gè)<span style="text-decoration:underline">好<em>天</em><em>氣</em></span>
實(shí)際上,天氣兩個(gè)字本身就可以用同一個(gè)em標(biāo)簽來(lái)包裹,這樣可以減少頁(yè)面中的dom節(jié)點(diǎn)樹,從而提升性能。
那么,具體該怎么做呢?這里整體的思路就是原本是在同一個(gè)文本段里的文本,我們只用一個(gè)em標(biāo)簽包裹,只在出現(xiàn)標(biāo)簽的地方添加額外的命中樣式標(biāo)簽。
這里需要注意的是,添加命中樣式標(biāo)簽的時(shí)候,需要添加在最內(nèi)層,也就是命中樣式的開標(biāo)簽要放在所有其他開標(biāo)簽之后,而閉標(biāo)簽則要放在所有其他閉標(biāo)簽之前,這樣可以保證命中樣式的優(yōu)先級(jí)是最高的,不會(huì)被其他標(biāo)簽的樣式覆蓋。
核心代碼邏輯如下:
const match = [...strs.matchAll(reg)].forEach(({ index }) => {
for (let i = 0; i < word.length; i++) {
const letterIndex = i + index;
if (
i === 0 || // 匹配區(qū)域區(qū)間開始需要有命中樣式的開標(biāo)簽
textToTagMap[letterIndex].up.length > 0 || // 當(dāng)有新的開標(biāo)簽時(shí),需要在內(nèi)部有命中樣式的開標(biāo)簽
textToTagMap[letterIndex - 1].down.length > 0 // 當(dāng)上一個(gè)標(biāo)簽有閉標(biāo)簽時(shí),下一個(gè)標(biāo)簽需要有命中樣式的開標(biāo)簽
) {
textToTagMap[letterIndex].up.push(emStyleStart);
}
if (
i === word.length - 1 || // 匹配區(qū)域結(jié)束需要有命中樣式的閉標(biāo)簽
textToTagMap[letterIndex].down.length > 0 || // 當(dāng)有新的閉標(biāo)簽時(shí),需要在內(nèi)部有命中樣式的閉標(biāo)簽
textToTagMap[letterIndex + 1].up.length > 0 // 當(dāng)下一個(gè)標(biāo)簽有開標(biāo)簽時(shí),上一個(gè)標(biāo)簽需要有命中樣式的閉標(biāo)簽
) {
textToTagMap[letterIndex].down.unshift(emStyleEnd);
}
}
});
最后的成果??

大功告成!
codesandbox.io/s/restless-…[3]
結(jié)論
看似完成了?其實(shí)還有一些功能沒有做,比如局部匹配、多詞搜索、emoji匹配等功能,這些就留給大家自己去實(shí)現(xiàn)啦
并且,這里的搜索匹配沒有考慮轉(zhuǎn)義字符和不合法標(biāo)簽等問題,實(shí)際實(shí)現(xiàn)起來(lái)也需要多加判斷
剛剛也提到在大文本的情況下使用正則性能會(huì)有問題,那是不是可以考慮把textToTagMap換一種數(shù)據(jù)格式呢?像是字典樹之類
ps:做超大文本量的匹配時(shí)也可以選擇分片去做,先處理可視區(qū)的文字,保證搜索不卡頓
pss:做富文本相關(guān)的內(nèi)容一定要注意防范XSS攻擊哦!
參考資料
[1]https://codesandbox.io/s/new-smoke-w5hcjv?file=/index.html: https://link.juejin.cn?target=https%3A%2F%2Fcodesandbox.io%2Fs%2Fnew-smoke-w5hcjv%3Ffile%3D%2Findex.html
[2]https://codesandbox.io/s/upbeat-feynman-qch060?file=/index.html: https://link.juejin.cn?target=https%3A%2F%2Fcodesandbox.io%2Fs%2Fupbeat-feynman-qch060%3Ffile%3D%2Findex.html
[3]https://codesandbox.io/s/restless-pond-9b14j7?file=/index.html: https://link.juejin.cn?target=https%3A%2F%2Fcodesandbox.io%2Fs%2Frestless-pond-9b14j7%3Ffile%3D%2Findex.html
Node 社群
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
如果你覺得這篇內(nèi)容對(duì)你有幫助,我想請(qǐng)你幫我2個(gè)小忙:
1. 點(diǎn)個(gè)「在看」,讓更多人也能看到這篇文章 2. 訂閱官方博客 www.inode.club 讓我們一起成長(zhǎng) 點(diǎn)贊和在看就是最大的支持
瀏覽
92
大廠技術(shù) 高級(jí)前端 Node進(jìn)階
點(diǎn)擊上方 程序員成長(zhǎng)指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)Node交流群
作者:猿猴望月 原文:https://juejin.cn/post/7070688497929043998
去年在冬季面試的時(shí)候,被某廠面試官問了這個(gè)問題:
如果我們的數(shù)據(jù)是富文本,現(xiàn)在要加一個(gè)搜索功能,怎么樣才能完美的實(shí)現(xiàn)高亮呢?
當(dāng)時(shí)回答的很粗糙,只答了提取出文字進(jìn)行搜索,怎么回填原本的樣式并沒有說清楚。
今天坐在我旁邊的小哥開始做MarkDown搜索匹配了,又喚起了我塵封已久的記憶,于是今天就讓我們來(lái)一起震懾一下面試官吧!
首先能想到的思路是和leetcode的上車問題相關(guān)。
有一群乘客,當(dāng)中一個(gè)人在1號(hào)站臺(tái)下車,兩個(gè)人在2號(hào)上車上車,最后求N號(hào)站臺(tái)有多少乘客。
這個(gè)問題也可以那么去考慮:在富文本串之中,在遇到開標(biāo)簽就理解為當(dāng)前的文字“上車”,而遇到閉標(biāo)簽就理解為當(dāng)前的文字”下車“。
(開標(biāo)簽:<span>、閉標(biāo)簽:</span>)
第一版
首先我們開發(fā)一個(gè)頁(yè)面,其中僅包含搜索框和幾段富文本的數(shù)據(jù)

舉個(gè)??,數(shù)據(jù)第一項(xiàng)為:
今天真是個(gè)<span style="text-decoration:underline">好天氣</span>
這個(gè)時(shí)候我們想搜索“好天氣”,整體的思路是分為3個(gè)部分:
處理富文本,生成標(biāo)簽在字符串位置的映射關(guān)系 在處理好的文本中搜索key,對(duì)命中的詞在映射關(guān)系中增加命中樣式 根據(jù)映射關(guān)系生成新的富文本
在其中,我們需要記錄一些關(guān)鍵狀態(tài)值:
currentTag:當(dāng)前正處理標(biāo)簽 isOpen:當(dāng)前標(biāo)簽是否是開標(biāo)簽 needAdd: 文本需要增加的開標(biāo)簽 nowText:目前正在處理的文本 textToTagMap:文本與標(biāo)簽的映射,這里提供一下類型
type TextToStyleMap = Array<
{
key: string,
// 從這個(gè)字符增加的標(biāo)簽index
up: number[],
// 從這個(gè)字符結(jié)束的標(biāo)簽index
down: number[]
}
>
讓我們簡(jiǎn)單的畫個(gè)流程圖看一下怎么去做~

codesandbox.io/s/new-smoke…[1]
首版完成!(ps:現(xiàn)在我們搜索用的是正則,在大文本的情況下可能會(huì)有性能問題

優(yōu)化1
看起來(lái)好像很完美了?如果我們?cè)谶@個(gè)文本框中搜索“個(gè)好”呢?

咦,下劃線丟了?
因?yàn)槲覀兯阉鱾€(gè)好后的富文本結(jié)果是
今天真是<em>個(gè)<span style="text-decoration:underline">好</em>天氣</span>
這里生產(chǎn)出了一個(gè)錯(cuò)誤的標(biāo)簽不對(duì)應(yīng)的富文本
我們?cè)趺磻?yīng)對(duì)這個(gè)問題呢?
最簡(jiǎn)單的方式應(yīng)該是把強(qiáng)調(diào)的樣式加在每一個(gè)文字上
今天真是<em>個(gè)<span style="text-decoration:underline">好</em>天氣</span>
??
今天真是<em>個(gè)</em><span style="text-decoration:underline"><em>好</em>天氣</span>
codesandbox.io/s/upbeat-fe…[2]
簡(jiǎn)單的改了一下代碼之后,我們就修改了這個(gè)bug

優(yōu)化2
這么做了以后,我們會(huì)添加許多多余的強(qiáng)調(diào)樣式標(biāo)簽。比如還是在上面這個(gè)例子里,我們?nèi)绻阉鳌禾鞖狻坏脑?,結(jié)果會(huì)是這樣:
今天真是個(gè)<span style="text-decoration:underline">好<em>天</em><em>氣</em></span>
實(shí)際上,天氣兩個(gè)字本身就可以用同一個(gè)em標(biāo)簽來(lái)包裹,這樣可以減少頁(yè)面中的dom節(jié)點(diǎn)樹,從而提升性能。
那么,具體該怎么做呢?這里整體的思路就是原本是在同一個(gè)文本段里的文本,我們只用一個(gè)em標(biāo)簽包裹,只在出現(xiàn)標(biāo)簽的地方添加額外的命中樣式標(biāo)簽。
這里需要注意的是,添加命中樣式標(biāo)簽的時(shí)候,需要添加在最內(nèi)層,也就是命中樣式的開標(biāo)簽要放在所有其他開標(biāo)簽之后,而閉標(biāo)簽則要放在所有其他閉標(biāo)簽之前,這樣可以保證命中樣式的優(yōu)先級(jí)是最高的,不會(huì)被其他標(biāo)簽的樣式覆蓋。
核心代碼邏輯如下:
const match = [...strs.matchAll(reg)].forEach(({ index }) => {
for (let i = 0; i < word.length; i++) {
const letterIndex = i + index;
if (
i === 0 || // 匹配區(qū)域區(qū)間開始需要有命中樣式的開標(biāo)簽
textToTagMap[letterIndex].up.length > 0 || // 當(dāng)有新的開標(biāo)簽時(shí),需要在內(nèi)部有命中樣式的開標(biāo)簽
textToTagMap[letterIndex - 1].down.length > 0 // 當(dāng)上一個(gè)標(biāo)簽有閉標(biāo)簽時(shí),下一個(gè)標(biāo)簽需要有命中樣式的開標(biāo)簽
) {
textToTagMap[letterIndex].up.push(emStyleStart);
}
if (
i === word.length - 1 || // 匹配區(qū)域結(jié)束需要有命中樣式的閉標(biāo)簽
textToTagMap[letterIndex].down.length > 0 || // 當(dāng)有新的閉標(biāo)簽時(shí),需要在內(nèi)部有命中樣式的閉標(biāo)簽
textToTagMap[letterIndex + 1].up.length > 0 // 當(dāng)下一個(gè)標(biāo)簽有開標(biāo)簽時(shí),上一個(gè)標(biāo)簽需要有命中樣式的閉標(biāo)簽
) {
textToTagMap[letterIndex].down.unshift(emStyleEnd);
}
}
});
最后的成果??

大功告成!
codesandbox.io/s/restless-…[3]
結(jié)論
看似完成了?其實(shí)還有一些功能沒有做,比如局部匹配、多詞搜索、emoji匹配等功能,這些就留給大家自己去實(shí)現(xiàn)啦
并且,這里的搜索匹配沒有考慮轉(zhuǎn)義字符和不合法標(biāo)簽等問題,實(shí)際實(shí)現(xiàn)起來(lái)也需要多加判斷
剛剛也提到在大文本的情況下使用正則性能會(huì)有問題,那是不是可以考慮把textToTagMap換一種數(shù)據(jù)格式呢?像是字典樹之類
ps:做超大文本量的匹配時(shí)也可以選擇分片去做,先處理可視區(qū)的文字,保證搜索不卡頓
pss:做富文本相關(guān)的內(nèi)容一定要注意防范XSS攻擊哦!
參考資料
https://codesandbox.io/s/new-smoke-w5hcjv?file=/index.html: https://link.juejin.cn?target=https%3A%2F%2Fcodesandbox.io%2Fs%2Fnew-smoke-w5hcjv%3Ffile%3D%2Findex.html
[2]https://codesandbox.io/s/upbeat-feynman-qch060?file=/index.html: https://link.juejin.cn?target=https%3A%2F%2Fcodesandbox.io%2Fs%2Fupbeat-feynman-qch060%3Ffile%3D%2Findex.html
[3]https://codesandbox.io/s/restless-pond-9b14j7?file=/index.html: https://link.juejin.cn?target=https%3A%2F%2Fcodesandbox.io%2Fs%2Frestless-pond-9b14j7%3Ffile%3D%2Findex.html
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
如果你覺得這篇內(nèi)容對(duì)你有幫助,我想請(qǐng)你幫我2個(gè)小忙:
點(diǎn)贊和在看就是最大的支持
