歡迎關注我的公眾號:前端偵探
介紹一個比較前沿但是非常有用的新特性:一個瀏覽器原生支援的 CSS
文字高亮高亮功能,官方名稱叫做 CSS Custom Highlight API,有了它,可以在不改變 dom
結構的情況下自定義任意文字的樣式,例如
再例如搜尋詞高亮
還可以輕易實現程式碼高亮
多麼令人興奮的功能啊,現在在 Chrome 105
中已經正式支援了(無需開啟實驗特性),一起學習一下吧
一、偽元素 ::highlight()
要自定義任意文字樣式需要 CSS
和 JS
的共同作用。
首先來看 CSS
部分,一個新的偽元素,非常簡單
::highlight(custom-highlight-name) {
color: red
}
和::selection
這類偽元素比較類似,僅支援部分文字相關樣式,如下
- 文字顏色
color
- 背景顏色
background-color
- 文字修飾
text-decoration
- 文字陰影
text-shadow
- 文字描邊
-webkit-text-stroke
- 文字填充
-webkit-text-fill-color
注意,注意,注意不支援background-image
,也就是漸變之類的也不支援
但是,僅僅知道這個偽類是沒用的,她還需要一個“引數”,也就是上面的custom-highlight-name
,表示高亮的名稱,那這個是怎麼來的呢?或者換句話說,如何去標識頁面中需要自定義樣式的那部分文字呢?
這就需要藉助下面的內容了,看看如何生成這個“引數”,這才是重點
二、CSS Custom Highlight API
在介紹之前,建議先仔細閱讀這篇文章:web 中的“游標”和“選區”
大部分操作其實和這個原理是相同的,只是把拿到的選區做了進一步處理,具體分以下幾步
1. 建立選區(重點)
首先,透過Range物件建立文字選擇範圍,就像用滑鼠滑過選區一樣,這也是最複雜的一部分,例如
const parentNode = document.getElementById("foo");
const range1 = new Range();
range1.setStart(parentNode, 10);
range1.setEnd(parentNode, 20);
const range2 = new Range();
range2.setStart(parentNode, 40);
range2.setEnd(parentNode, 60);
這樣可以得到選區物件range1
、range2
2. 建立高亮
然後,將建立的選區高亮例項化,需要用到Highlight物件
const highlight = new Highlight(range1, range2, ...);
當然也可以根據需求建立多個
const highlight1 = new Highlight(user1Range1, user1Range2);
const highlight2 = new Highlight(user2Range1, user2Range2, user2Range3);
這樣可以得到高亮物件highlight1
、highlight2
3. 註冊高亮
接著,需要將例項化的高亮物件透過CSS.Highlight](https://developer.mozilla.org...))註冊到頁面
有點類似於Map
物件的操作
CSS.highlights.set("highlight1", highlight1);
CSS.highlights.set("highlight2", highlight2);
目前相容性比較差,所以需要額外判斷一下
if (CSS.highlights) {
//...支援CSS.highlights
}
注意看,上面註冊的key
名,highlight1
就是上一節提到的高亮名稱,也就是 CSS
中需要的“引數”
4. 自定義樣式
最後,將定義的高亮名稱結合::highlight
,這樣就可以自定義選中樣式了
::highlight(highlight1) {
background-color: yellow;
color: black;
}
以上就是全部過程了,稍顯複雜,但是還是比較好理解的,關鍵是第一步建立選區的過程,最為複雜,再次推薦仔細閱讀這篇文章:web 中的“游標”和“選區”,下面用一張圖總結一下
原理就是這樣,下面看一些例項
三、彩虹文字
現在來實現文章開頭圖示效果,彩虹文字效果。總共7種顏色,文字依次變色,不斷迴圈,而且僅有一個標籤
<p id="rainbow-text">CSS Custom Highlight API</p>
這裡總共有7
種顏色,所以需要建立7
個高亮區域,可以先定義高亮 CSS
,如下
::highlight(rainbow-color-1) { color: #ad26ad; text-decoration: underline; }
::highlight(rainbow-color-2) { color: #5d0a99; text-decoration: underline; }
::highlight(rainbow-color-3) { color: #0000ff; text-decoration: underline; }
::highlight(rainbow-color-4) { color: #07c607; text-decoration: underline; }
::highlight(rainbow-color-5) { color: #b3b308; text-decoration: underline; }
::highlight(rainbow-color-6) { color: #ffa500; text-decoration: underline; }
::highlight(rainbow-color-7) { color: #ff0000; text-decoration: underline; }
現在肯定不會有什麼變化,因為還沒建立選區
先建立一個高亮區域試試,比如第一個文字
const textNode = document.getElementById("rainbow-text").firstChild;
if (CSS.highlights) {
const range = new Range();
range.setStart(textNode, 0); // 選區起點
range.setEnd(textNode, 1); // 選區終點
const Highlight = new Highlight(range);
CSS.highlights.set(`rainbow-color-1`, Highlight);
}
效果如下
下面透過迴圈,建立7
個高亮區域
const textNode = document.getElementById("rainbow-text").firstChild;
if (CSS.highlights) {
const highlights = [];
for (let i = 0; i < 7; i++) {
// 給每個顏色例項化一個Highlight物件
const colorHighlight = new Highlight();
highlights.push(colorHighlight);
// 註冊高亮
CSS.highlights.set(`rainbow-color-${i + 1}`, colorHighlight);
}
// 遍歷文字節點
for (let i = 0; i < textNode.textContent.length; i++) {
// 給每個字元建立一個選區
const range = new Range();
range.setStart(textNode, i);
range.setEnd(textNode, i + 1);
// 新增到高亮
highlights[i % 7].add(range);
}
}
這樣就在不改變dom
的情況下實現了彩虹文字效果
完整程式碼可以檢視以下任意連結:(注意需要Chrome 105+)
四、文字搜尋高亮
大家都知道瀏覽器的搜尋功能,ctrl+f
就可以快速對整個網頁就行查詢,查詢到的關鍵詞會新增黃色背景的高亮,如下
以前一直很疑惑這個顏色是怎麼新增的,畢竟沒有任何包裹標籤。現在有了CSS Custom Highlight API
,完全可以手動實現一個和原生瀏覽器一模一樣的搜尋高亮功能。
到目前為止,還無法自定義原生搜尋高亮的黃色背景,以後可能會開放
假設HTML
結構是這樣的,一個搜尋框和一堆文字
<label>搜尋 <input id="query" type="text"></label>
<article>
<p>
閱文旗下囊括 QQ 閱讀、起點中文網、新麗傳媒等業界知名品牌,匯聚了強大的創作者陣營、豐富的作品儲備,覆蓋 200 多種內容品類,觸達數億使用者,已成功輸出《慶餘年》《贅婿》《鬼吹燈》《全職高手》《斗羅大陸》《琅琊榜》等大量優秀網文 IP,改編為動漫、影視、遊戲等多業態產品。
</p>
<p>
《盜墓筆記》最初連載於起點中文網,是南派三叔成名代表作。2015年網劇開播首日點選破億,開啟了盜墓文學 IP 年。電影於2016年上映,由井柏然、鹿晗、馬思純等主演,累計票房10億元。
</p>
<p>
慶餘年》是閱文集團白金作家貓膩的作品,自2007年在起點中文網連載,持續保持歷史類收藏榜前五位。改編劇整合為2019年現象級作品,播出期間登上微博熱搜百餘次,騰訊影片、愛奇藝雙平臺總播放量突破160億次,並榮獲第26屆白玉蘭獎最佳編劇(改編)、最佳男配角兩項大獎。
</p>
<p>《鬼吹燈》是天下霸唱創作的經典懸疑盜墓小說,連載於起點中文網。先後進行過漫畫、遊戲、電影、網路電視劇的改編,均取得不俗的成績,是當之無愧的超級IP。</p>
</article>
簡單美化一下後效果如下
然後就是監聽輸入框,遍歷文字節點(推薦使用原生的treeWalker
,當然普通的遞迴也可以),根據搜尋詞建立選區,詳細程式碼如下
const query = document.getElementById("query");
const article = document.querySelector("article");
// 建立 createTreeWalker 迭代器,用於遍歷文字節點,儲存到一個陣列
const treeWalker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
const allTextNodes = [];
let currentNode = treeWalker.nextNode();
while (currentNode) {
allTextNodes.push(currentNode);
currentNode = treeWalker.nextNode();
}
// 監聽inpu事件
query.addEventListener("input", () => {
// 判斷一下是否支援 CSS.highlights
if (!CSS.highlights) {
article.textContent = "CSS Custom Highlight API not supported.";
return;
}
// 清除上個高亮
CSS.highlights.clear();
// 為空判斷
const str = query.value.trim().toLowerCase();
if (!str) {
return;
}
// 查詢所有文字節點是否包含搜尋詞
const ranges = allTextNodes
.map((el) => {
return { el, text: el.textContent.toLowerCase() };
})
.map(({ text, el }) => {
const indices = [];
let startPos = 0;
while (startPos < text.length) {
const index = text.indexOf(str, startPos);
if (index === -1) break;
indices.push(index);
startPos = index + str.length;
}
// 根據搜尋詞的位置建立選區
return indices.map((index) => {
const range = new Range();
range.setStart(el, index);
range.setEnd(el, index + str.length);
return range;
});
});
// 建立高亮物件
const searchResultsHighlight = new Highlight(...ranges.flat());
// 註冊高亮
CSS.highlights.set("search-results", searchResultsHighlight);
});
最後,透過CSS
設定高亮的顏色
::highlight(search-results) {
background-color: #f06;
color: white;
}
實時搜尋效果如下
完整程式碼可以檢視以下任意連結:(注意需要Chrome 105+)
還可以將高亮效果改成波浪線
::highlight(search-results) {
text-decoration: underline wavy #f06;
}
效果如下,是不是也可用作錯別字標識呢?
除了避免dom
操作帶來的便利外,效能也能得到極大的提升,畢竟建立、移除dom
也是效能大戶,下面是一個測試 demo,搬運自
https://ffiori.github.io/highlight-api-demos/demo-performance.html
測試程式碼可以檢視以下任意連結:
測試效果如下
在10000
個節點的情況下,兩者相差100
倍的差距!而且數量越大,效能差距越明顯,甚至直接導致瀏覽器卡死!
五、程式碼高亮編輯器
最後再來看一個非常實用的例子,可以輕易實現一個程式碼高亮的編輯器。
假設 HTML
結構是這樣的,很簡單,就一個純文字的標籤
<pre class="editor" id="code">ul{
min-height: 0;
}
.sub {
display: grid;
grid-template-rows: 0fr;
transition: 0.3s;
overflow: hidden;
}
:checked ~ .sub {
grid-template-rows: 1fr;
}
.txt{
animation: color .001s .5 linear forwards;
}
@keyframes color {
from {
color: var(--c1)
}
to{
color: var(--c2)
}
}</pre>
簡單修飾一下,設定為可編輯元素
.editor{
white-space: pre-wrap;
-webkit-user-modify: read-write-plaintext-only; /* 讀寫純文字 */
}
效果如下
那麼,如何讓這些程式碼高亮呢?
這就需要對內容進行關鍵詞分析提取了,我們可以用現有的程式碼高亮庫,比如highlight.js。
hljs.highlight(pre.textContent, {
language: 'css'
})._emitter.rootNode.children
透過這個方法可以獲取到CSS
語言的關鍵詞以及型別,如下
簡單解釋一下,這是一個陣列,如果是純文字,表示普通的字元,如果是物件,表示是關鍵詞,例如第一個,children
裡面的ul
就是關鍵詞,型別是selector-tag
,也就是選擇器,除此之外,還有attribute
、number
、selector-class
等各種型別。有了這些關鍵詞,我們就可以把這些文字單獨選取出來,然後高亮成不同的顏色。
接下來,就需要對程式碼內容進行遍歷了,方法也是類似的,如下
const nodes = pre.firstChild
const text = nodes.textContent
const highlightMap = {}
let startPos = 0;
words.filter(el => el.scope).forEach(el => {
const str = el.children[0]
const scope = el.scope
const index = text.indexOf(str, startPos);
if (index < 0) {
return
}
const item = {
start: index,
scope: scope,
end: index + str.length,
str: str
}
if (highlightMap[scope]){
highlightMap[scope].push(item)
} else {
highlightMap[scope] = [item]
}
startPos = index + str.length;
})
Object.entries(highlightMap).forEach(function([k,v]){
const ranges = v.map(({start, end}) => {
const range = new Range();
range.setStart(nodes, start);
range.setEnd(nodes, end);
return range;
});
const highlight = new Highlight(...ranges.flat());
CSS.highlights.set(k, highlight);
})
}
highlights(code)
code.addEventListener('input', function(){
highlights(this)
})
最後,根據不同的型別,定義不同的顏色就行了,如下
::highlight(built_in) {
color: #c18401;
}
::highlight(comment) {
color: #a0a1a7;
font-style: italic;
}
::highlight(number),
::highlight(selector-class){
color: #986801;
}
::highlight(attr) {
color: #986801;
}
::highlight(string) {
color: #50a14f;
}
::highlight(selector-pseudo) {
color: #986801;
}
::highlight(attribute) {
color: #50a14f;
}
::highlight(keyword) {
color: #a626a4;
}
這樣就得到了一個支援程式碼高亮的簡易編輯器了
相比傳統的編輯器而言,這個屬於純文字編輯,非常輕量,在高亮的同時也不會影響游標,因為不會生成新的dom
,效能也是超級棒??
完整程式碼可以檢視以下任意連結:
六、最後總結一下
以上就是關於CSS Custom Highlight API
的使用方式以及應用示例了,下面再來回顧一下使用步驟:
- 建立選區,
new Range
- 建立高亮,
new Highlight
- 註冊高亮,
CSS.highlights.set
- 自定義樣式,
::highlight()
相比傳統使用標籤的方式而已,有很多優點
- 使用場景更廣泛,很多情況下不能修改
dom
或者成本極大 - 效能更好,避免了操作
dom
帶來的額外開銷,在dom
較多情況下效能差異至少100
倍 - 幾乎沒有副作用,能有效減少
dom
變化引起的其他影響,比如游標選區的處理
其實歸根結底,都是dom
變化帶來的,而Highlight API
恰好能有效避開這個問題。當然也有一些缺陷,由於僅僅能改變文字相關樣式,所以也存在一些侷限性,這個就需要權衡了,目前相容性也還不足,僅適用於內部專案,敬請期待
最後,如果覺得還不錯,對你有幫助的話,歡迎點贊、收藏、轉發❤❤❤
歡迎關注我的公眾號:前端偵探
本文參與了「SegmentFault 思否寫作挑戰賽」,歡迎正在閱讀的你也加入。