原生 CSS Custom Highlight 終於來了

介紹一個比較前沿但是非常有用的新特性:一個瀏覽器原生支持的 CSS 文本高亮高亮功能,官方名稱叫做 CSS Custom Highlight API[1],有了它,可以在不改變 dom 結構的情況下自定義任意文本的樣式,例如

再例如搜索詞高亮

還可以輕易實現代碼高亮

多麼令人興奮的功能啊,現在在 Chrome 105 中已經正式支持了(無需開啓實驗特性),一起學習一下吧

一、僞元素 ::highlight()

要自定義任意文本樣式需要 CSSJS 的共同作用。

首先來看 CSS 部分,一個新的僞元素,非常簡單

::highlight(custom-highlight-name) {
  color: red
}

::selection這類僞元素比較類似,僅支持部分文本相關樣式,如下

注意,注意,注意不支持background-image,也就是漸變之類的也不支持

但是,僅僅知道這個僞類是沒用的,她還需要一個 “參數”,也就是上面的custom-highlight-name,表示高亮的名稱,那這個是怎麼來的呢?或者換句話說,如何去標識頁面中需要自定義樣式的那部分文本呢?

這就需要藉助下面的內容了,看看如何生成這個 “參數”,這纔是重點

二、CSS Custom Highlight API

在介紹之前,建議先仔細閱讀這篇文章:web 中的 “光標” 和“選區”

大部分操作其實和這個原理是相同的,只是把拿到的選區做了進一步處理,具體分以下幾步

1. 創建選區(重點)

首先,通過 Range[2] 對象創建文本選擇範圍,就像用鼠標滑過選區一樣,這也是最複雜的一部分,例如

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);

這樣可以得到選區對象range1range2

2. 創建高亮

然後,將創建的選區高亮實例化,需要用到 Highlight[3] 對象

const highlight = new Highlight(range1, range2, ...);

當然也可以根據需求創建多個

const highlight1 = new Highlight(user1Range1, user1Range2);
const highlight2 = new Highlight(user2Range1, user2Range2, user2Range3);

這樣可以得到高亮對象highlight1highlight2

3. 註冊高亮

接着,需要將實例化的高亮對象通過 [CSS.Highlight](HighlightRegistry - Web APIs | MDN (mozilla.org "CSS.Highlight")) 註冊到頁面

有點類似於Map對象的操作

CSS.highlights.set("highlight1", highlight1);
CSS.highlights.set("highlight2", highlight2);

目前兼容性比較差,所以需要額外判斷一下

if (CSS.highlights) {
  //...支持CSS.highlights
}

注意看,上面註冊的key名,highlight1就是上一節提到的高亮名稱,也就是 CSS 中需要的 “參數”

  1. 自定義樣式

最後,將定義的高亮名稱結合::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[10]

測試代碼可以查看以下任意鏈接:

測試效果如下

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[14]。

 hljs.highlight(pre.textContent, {
   language: 'css'
 })._emitter.rootNode.children

通過這個方法可以獲取到CSS語言的關鍵詞以及類型,如下

簡單解釋一下,這是一個數組,如果是純文本,表示普通的字符,如果是對象,表示是關鍵詞,例如第一個,children裏面的ul就是關鍵詞,類型是selector-tag,也就是選擇器,除此之外,還有attributenumberselector-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的使用方式以及應用示例了,下面再來回顧一下使用步驟:

  1. 創建選區,new Range

  2. 創建高亮,new Highlight

  3. 註冊高亮,CSS.highlights.set

  4. 自定義樣式,::highlight()

相比傳統使用標籤的方式而已,有很多優點

  1. 使用場景更廣泛,很多情況下不能修改dom或者成本極大

  2. 性能更高,避免了操作dom帶來的額外開銷,在dom較多情況下性能差異至少100

  3. 幾乎沒有副作用,能有效減少dom變化引起的其他影響,比如光標選區的處理

其實歸根結底,都是dom變化帶來的,而Highlight API恰好能有效避開這個問題。當然也有一些缺陷,由於僅僅能改變文本相關樣式,所以也存在一些侷限性,這個就需要權衡了,目前兼容性也還不足,僅適用於內部項目,敬請期待

參考資料

[1]

CSS Custom Highlight API: https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API

[2]

Range: https://developer.mozilla.org/en-US/docs/Web/API/Range

[3]

Highlight: https://developer.mozilla.org/en-US/docs/Web/API/Highlight

[4]

CSS Custom Highlight API (juejin.cn): https://code.juejin.cn/pen/7198496899391815736

[5]

CSS Custom Highlight API (codepen.io): https://codepen.io/xboxyan/pen/qByzGYr

[6]

CSS Custom Highlight API (runjs.work): https://runjs.work/projects/450431c8f0064298

[7]

CSS Highlight search (juejin.cn): https://code.juejin.cn/pen/7198488612801871929

[8]

CSS Highlight search (codepen.io): https://codepen.io/xboxyan/pen/eYjwoqo

[9]

CSS Highlight search (runjs.work): https://runjs.work/projects/a661feba3dad44c9

[10]

https://ffiori.github.io/highlight-api-demos/demo-performance.html: https://ffiori.github.io/highlight-api-demos/demo-performance.html

[11]

Highlight performance demo (juejin.cn): https://code.juejin.cn/pen/7198487962978353208

[12]

Highlight performance demo (codepen.io): https://codepen.io/xboxyan/pen/YzjoMmp

[13]

Highlight performance demo (runjs.work): https://runjs.work/projects/e5fe09f70d324d99

[14]

highlight.js: https://highlightjs.org/

[15]

CSS highlight editor (juejin.cn): https://code.juejin.cn/pen/7198487629262749756

[16]

CSS highlight editor (codepen.io): https://codepen.io/xboxyan/pen/RwBzOmK

[17]

CSS highlight editor (runjs.work): https://runjs.work/projects/9ff7ab8f12844ce1

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/XMvPKXcqSp0vZ7SPDtQjlA