找回密码
 立即注册
首页 业界区 业界 利用自定义html元素实现支持实时修改的高亮代码块 ...

利用自定义html元素实现支持实时修改的高亮代码块

咳镘袁 2026-2-5 20:10:01
利用自定义html元素实现支持实时修改的高亮代码块

代码块高亮是前端开发中常见的需求,尤其是在展示代码片段的博客、文档等场景中。市面上有很多成熟的代码高亮库,比如Highlight.js、Prism.js等,它们都能很好地实现代码高亮功能。
通常的高亮代码块是“静态”的,修改代码内容后需要对DOM元素重新应用高亮样式。由于涉及DOM操作,在Vue等前端框架中使用必须谨慎处理,否则会出现DOM树和虚拟DOM不一致的问题,造成很多麻烦。
那么有没有办法让代码高亮不改变DOM结构呢?答案是有的,我们可以利用自定义HTML元素和Shadow DOM来实现这一点。
Shadow DOM和自定义HTML元素

Shadow DOM允许我们创建封闭的DOM树,Shadow DOM内可以使用自己的样式,并封装复杂的逻辑,而不会影响到外部的DOM结构。现代浏览器的(特别是、等复杂控件)元素就是利用Shadow DOM实现的。
要想使用Shadow DOM,我们需要创建一个自定义HTML元素,并在其中通过attachShadow方法创建Shadow DOM。
  1. class MyElement extends HTMLElement {
  2.     constructor() {
  3.         super()
  4.         const shadow = this.attachShadow({ mode: 'open' })
  5.         shadow.innerHTML = `<p>Hello, Shadow DOM!</p>`
  6.     }
  7. }
  8. customElements.define('my-element', MyElement)
复制代码
之后,我们就可以在HTML中使用来插入这个自定义元素。
  1. [/code]在DevTools中,我们可以看到的渲染结果,其中包括元素内部的Shadow DOM:
  2. [code]<my-element>
  3.   #shadow-root (open)
  4.     <p>Hello, Shadow DOM!</p>
  5. </my-element>
复制代码
在自定义元素中获取内容

我们希望在自定义元素中获取标签之间的内容。这可以通过插槽(slot)机制实现。插槽机制允许我们在自定义元素中定义占位符,外部传入的内容会被插入到这些占位符中。
为了使用插槽,我们需要在Shadow DOM中添加一个元素:
  1. class MyElement extends HTMLElement {
  2.     constructor() {
  3.         super()
  4.         const shadow = this.attachShadow({ mode: 'open' })
  5.         shadow.innerHTML = `<slot></slot>`
  6.         const slot = shadow.querySelector('slot')
  7.         slot.addEventListener('slotchange', this.handleSlotChange.bind(this))
  8.     }
  9.     handleSlotChange(event) {
  10.         const slot = event.target
  11.         console.log('Slot content changed:', slot.assignedNodes({ flatten: true }))
  12.     }
  13. }
  14. customElements.define('my-element', MyElement)
复制代码
对于HTML片段
  1. <my-element id="my-el"><p>This is slotted content.</p></my-element>
复制代码
当页面第一次加载时,控制台会显示
  1. Slot content changed: [p]
复制代码
其中p就是元素内部的节点。
如果我们动态修改内的内容,比如通过JavaScript:
  1. document.getElementById('my-el').innerHTML = '<pre>New slotted content1.</pre><pre>New slotted content2.</pre>'
复制代码
控制台会显示
  1. Slot content changed: (2) [pre, pre]
复制代码
两个pre节点就是我们新修改的内容。
通过这种方法,我们可以在自定义元素中实时获取内容的变化。
利用自定义元素实现高亮代码块

结合前面的内容,我们可以创建一个自定义元素,用于实现高亮代码块的功能。只需要监听插槽内容的变化,将内容传递给高亮库进行处理,然后将处理后的结果显示出来即可。
  1. class PreHighlightElement extends HTMLElement {
  2.     constructor() {
  3.         super()
  4.         const shadow = this.attachShadow({ mode: 'open' })
  5.         shadow.innerHTML = `
  6. <link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets/styles/github.min.css">
  7. <pre id="code"></pre>
  8. <pre hidden><slot></slot></pre>
  9. `
  10.         this.__code = this.shadowRoot.querySelector('#code')
  11.         this.__slot = this.shadowRoot.querySelector('slot')
  12.         this.__slot.addEventListener('slotchange', this.highlightContent.bind(this))
  13.     }
  14.     highlightContent() {
  15.         if (typeof hljs === 'undefined') return
  16.         let text = this.__slot.assignedNodes({ flatten: true }).map(n => n.textContent).join("")
  17.         const code = document.createElement('code')
  18.         const result = hljs.highlightAuto(text)
  19.         code.innerHTML = result.value
  20.         if (result.language) code.classList.add(`language-${result.language}`)
  21.         this.__code.replaceChildren(code)
  22.     }
  23. }
  24. customElements.define('pre-highlight', PreHighlightElement)
复制代码
使用方法:
  1. <pre-highlight id="my-el">
  2. function helloWorld() {
  3.     console.log("Hello, world!")
  4. }
  5. </pre-highlight>
复制代码
渲染结果为
  1. <pre-highlight id="my-el">
  2.   #shadow-root (open)
  3.     <link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets/styles/github.min.css">
  4.     <pre id="code">
  5.       
  6.         function
  7.         helloWorld
  8.         "("
  9.         )
  10.         "{"
  11.         console
  12.         "."
  13.         log
  14.         "("
  15.         "Hello, world!"
  16.         ") }"
  17.       
  18.     </pre>
  19.     <pre hidden="">
  20.       <slot>
  21.         #text
  22.       </slot>
  23.     </pre>
  24.   " function helloWorld() { console.log("Hello, world!") } "
  25. </pre-highlight>
复制代码
修改内的内容后,高亮效果会自动更新。
  1. document.getElementById('my-el').textContent = `void helloWorld(void) {
  2.     printf("Hello, World!");
  3. }`
复制代码
渲染结果为
  1. <pre-highlight id="my-el">
  2.   #shadow-root (open)
  3.     <link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets/styles/github.min.css">
  4.     <pre id="code">
  5.       
  6.         
  7.           void
  8.           helloWorld
  9.          
  10.             "("
  11.             void
  12.             ")"
  13.          
  14.         
  15.         "{"
  16.         printf
  17.         "("
  18.         "Hello, World!"
  19.         "); }"
  20.       
  21.     </pre>
  22.     <pre hidden="">
  23.       <slot>
  24.         #text
  25.       </slot>
  26.     </pre>
  27.   "void helloWorld(void) { printf("Hello, World!"); }"
  28. </pre-highlight>
复制代码
一些改进

为了避免高亮库加载和高亮处理过程中的闪烁,我们可以在Shadow DOM中使用两个元素:一个用于显示原始内容,另一个用于显示高亮后的内容。初始时只显示原始内容,高亮处理完成后再切换显示。
此外,我们还可以添加一个lang属性,允许用户指定代码语言,以提高高亮的准确性。
最终结果如下:
  1. class PreHighlightElement extends HTMLElement {
  2.     constructor() {
  3.         super()
  4.         const shadow = this.attachShadow({ mode: 'open' })
  5.         shadow.innerHTML = `
  6. <link rel="stylesheet" href="https://unpkg.com/@highlightjs/cdn-assets/styles/github.min.css">
  7. <pre id="raw"><slot></slot></pre>
  8. <pre id="cooked" hidden></pre>
  9. `
  10.         this.__raw = this.shadowRoot.querySelector('#raw')
  11.         this.__cooked = this.shadowRoot.querySelector('#cooked')
  12.         this.__slot = this.shadowRoot.querySelector('slot')
  13.         this.__slot.addEventListener('slotchange', this.highlightContent.bind(this))
  14.     }
  15.     highlightContent() {
  16.         this.__raw.hidden = false
  17.         this.__cooked.hidden = true
  18.         if (typeof hljs === 'undefined') return
  19.         let text = this.__slot.assignedNodes({ flatten: true }).map(n => n.textContent).join("")
  20.         const lang = this.getAttribute('lang')
  21.         const code = document.createElement('code')
  22.         if (lang) {
  23.             const result = hljs.highlight(text, { language: lang, ignoreIllegals: true })
  24.             code.innerHTML = result.value
  25.             code.classList.add(`language-${lang}`)
  26.         } else {
  27.             const result = hljs.highlightAuto(text)
  28.             code.innerHTML = result.value
  29.             if (result.language) code.classList.add(`language-${result.language}`)
  30.         }
  31.         this.__cooked.replaceChildren(code)
  32.         this.__raw.hidden = true
  33.         this.__cooked.hidden = false
  34.     }
  35. }
  36. customElements.define('pre-highlight', PreHighlightElement)
复制代码
用例:
  1. [/code]在这个例子中,我们创建了一个滑动条,可以动态修改内的代码内容,内容修改后会实时显示高亮效果。
  2. [size=4]在Vue中使用[/size]
  3. 通过自定义元素的方法,我们可以轻松地在Vue项目中使用高亮代码块,而无需担心DOM和虚拟DOM的不一致问题。
  4. 为了避免自定义元素和Vue组件名冲突,我们需要在配置中制定isCustomElement选项:
  5. [code]// vite.config.js
  6. export default defineConfig({
  7.   plugins: [
  8.     vue({
  9.       template: {
  10.         compilerOptions: {
  11.           // 将所有含"-"的标签视为自定义元素
  12.           // Vue3中通常使用帕斯卡命名法(单词首字母大写)作为组件标签
  13.           isCustomElement: (tag) => tag.includes('-')
  14.         }
  15.       }
  16.     })
  17.   ]
  18. })
复制代码
之后就可以在组件或页面中直接使用元素,内部可以使用Vue的数据绑定而不用担心虚拟DOM冲突的问题:
  1. <template>
  2.     <pre-highlight lang="javascript">
  3. function greet({{arg}}) {
  4.     console.log("Hello, " + {{arg}} + "!")
  5. }
  6.     </pre-highlight>
  7. </template>
复制代码
附:完整的单页html演示代码

原生html
  1. <html>
  2. <head>
  3.    
  4.    
  5. </head>
  6. <body>
  7.     <pre-highlight id="code" lang="html"></pre-highlight>
  8.     <input type="range" id="input" value="10" />
  9.    
  10. </body>
  11. </html>
复制代码
使用Vue
  1. <html>
  2. <head>
  3.    
  4.    
  5. </head>
  6. <body>
  7.     <pre-highlight id="code" lang="html"></pre-highlight>
  8.     <input type="range" id="input" value="10" />
  9.    
  10. </body>
  11. </html>
复制代码
渲染效果:
1.gif


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册