
么有没有办法让代码高亮不改变DOM结构呢答案是有的我们可以利用自定义HTML元素和Shadow DOM来实现这一点。Shadow DOM和自定义HTML元素Shadow DOM允许我们创建封闭的DOM树Shadow DOM内可以使用自己的样式并封装复杂的逻辑而不会影响到外部的DOM结构。现代浏览器的input特别是input typerange、input typedate等复杂控件元素就是利用Shadow DOM实现的。要想使用Shadow DOM我们需要创建一个自定义HTML元素并在其中通过attachShadow方法创建Shadow DOM。class MyElement extends HTMLElement {constructor() {super()const shadow this.attachShadow({ mode: open })shadow.innerHTML pHello, Shadow DOM!/p}}customElements.define(my-element, MyElement)之后我们就可以在HTML中使用my-element/my-element来插入这个自定义元素。my-element/my-element在DevTools中我们可以看到my-element的渲染结果其中包括元素内部的Shadow DOMmy-element#shadow-root (open)pHello, Shadow DOM!/p/my-element在自定义元素中获取内容我们希望在自定义元素中获取标签之间的内容。这可以通过插槽slot机制实现。插槽机制允许我们在自定义元素中定义占位符外部传入的内容会被插入到这些占位符中。为了使用插槽我们需要在Shadow DOM中添加一个slot元素class MyElement extends HTMLElement {constructor() {super()const shadow this.attachShadow({ mode: open })shadow.innerHTML slot/slotconst slot shadow.querySelector(slot)slot.addEventListener(slotchange, this.handleSlotChange.bind(this))}handleSlotChange(event) {const slot event.targetconsole.log(Slot content changed:, slot.assignedNodes({ flatten: true }))}}customElements.define(my-element, MyElement)对于HTML片段my-element idmy-elpThis is slotted content./p/my-element当页面第一次加载时控制台会显示Slot content changed: [p]其中p就是元素内部的p节点。如果我们动态修改my-element内的内容比如通过JavaScriptdocument.getElementById(my-el).innerHTML preNew slotted content1./prepreNew slotted content2./pre控制台会显示Slot content changed: (2) [pre, pre]两个pre节点就是我们新修改的内容。通过这种方法我们可以在自定义元素中实时获取内容的变化。利用自定义元素实现高亮代码块结合前面的内容我们可以创建一个自定义元素pre-highlight用于实现高亮代码块的功能。只需要监听插槽内容的变化将内容传递给高亮库进行处理然后将处理后的结果显示出来即可。class PreHighlightElement extends HTMLElement {constructor() {super()const shadow this.attachShadow({ mode: open })shadow.innerHTML link relstylesheet hrefhttps://unpkg.com/highlightjs/cdn-assets/styles/github.min.csspre idcode/prepre hiddenslot/slot/prethis.__code this.shadowRoot.querySelector(#code)this.__slot this.shadowRoot.querySelector(slot)this.__slot.addEventListener(slotchange, this.highlightContent.bind(this))}highlightContent() {if (typeof hljs undefined) returnlet text this.__slot.assignedNodes({ flatten: true }).map(n n.textContent).join()const code document.createElement(code)const result hljs.highlightAuto(text)code.innerHTML result.valueif (result.language) code.classList.add(language-${result.language})this.__code.replaceChildren(code)}}customElements.define(pre-highlight, PreHighlightElement)使用方法pre-highlight idmy-elfunction helloWorld() {console.log(Hello, world!)}/pre-highlight渲染结果为pre-highlight idmy-el#shadow-root (open)link relstylesheet hrefhttps://unpkg.com/highlightjs/cdn-assets/styles/github.min.csspre idcodecode classlanguage-javascriptspan classhljs-keywordfunction/spanspan classhljs-title function_helloWorld/span(span classhljs-params/span){span classhljs-variable language_console/span.span classhljs-title function_log/span(span classhljs-stringHello, world!/span) }/code/prepre hiddenslot#text/slot/pre function helloWorld() { console.log(Hello, world!) } /pre-highlight修改pre-highlight内的内容后高亮效果会自动更新。document.getElementById(my-el).textContent void helloWorld(void) {printf(Hello, World!);}渲染结果为pre-highlight idmy-el#shadow-root (open)link relstylesheet hrefhttps://unpkg.com/highlightjs/cdn-assets/styles/github.min.csspre idcodecode classlanguage-cppspan classhljs-functionspan classhljs-typevoid/spanspan classhljs-titlehelloWorld/spanspan classhljs-params(span classhljs-typevoid/span)/span/span{span classhljs-built_inprintf/span(span classhljs-stringHello, World!/span); }/code/prepre hiddenslot#text/slot/prevoid helloWorld(void) { printf(Hello, World!); }/pre-highlight一些改进为了避免高亮库加载和高亮处理过程中的闪烁我们可以在Shadow DOM中使用两个pre元素一个用于显示原始内容另一个用于显示高亮后的内容。初始时只显示原始内容高亮处理完成后再切换显示。此外我们还可以添加一个lang属性允许用户指定代码语言以提高高亮的准确性。最终结果如下class PreHighlightElement extends HTMLElement {constructor() {super()const shadow this.attachShadow({ mode: open })shadow.innerHTML link relstylesheet hrefhttps://unpkg.com/highlightjs/cdn-assets/styles/github.min.csspre idrawslot/slot/prepre idcooked hidden/prethis.__raw this.shadowRoot.querySelector(#raw)this.__cooked this.shadowRoot.querySelector(#cooked)this.__slot this.shadowRoot.querySelector(slot)this.__slot.addEventListener(slotchange, this.highlightContent.bind(this))}highlightContent() {this.__raw.hidden falsethis.__cooked.hidden trueif (typeof hljs undefined) returnlet text this.__slot.assignedNodes({ flatten: true }).map(n n.textContent).join()const lang this.getAttribute(lang)const code document.createElement(code)if (lang) {const result hljs.highlight(text, { language: lang, ignoreIllegals: true })code.innerHTML result.valuecode.classList.add(language-${lang})} else {const result hljs.highlightAuto(text)code.innerHTML result.valueif (result.language) code.classList.add(language-${result.language})}this.__cooked.replaceChildren(code)this.__raw.hidden truethis.__cooked.hidden false}}customElements.define(pre-highlight, PreHighlightElement)用例pre-highlight idcode langhtml/pre-highlightinput typerange idinput value10 /scriptconst input document.getElementById(input)const preHighlight document.getElementById(code)input.oninput function(e) {preHighlight.textContent textarea rows${this.value} cols50Hello, world!/textarea}input.oninput()/script在这个例子中我们创建了一个滑动条可以动态修改pre-highlight内的代码内容内容修改后会实时显示高亮效果。在Vue中使用pre-highlight通过自定义元素的方法我们可以轻松地在Vue项目中使用高亮代码块而无需担心DOM和虚拟DOM的不一致问题。为了避免自定义元素和Vue组件名冲突我们需要在配置中制定isCustomElement选项// vite.config.jsexport default defineConfig({plugins: [vue({template: {compilerOptions: {// 将所有含-的标签视为自定义元素// Vue3中通常使用帕斯卡命名法单词首字母大写作为组件标签isCustomElement: (tag) tag.includes(-)}}})]})之后就可以在组件或页面中直接使用pre-highlight元素内部可以使用Vue的数据绑定而不用担心虚拟DOM冲突的问题templatepre-highlight langjavascriptfunction greet({{arg}}) {console.log(Hello, {{arg}} !)}/pre-highlight/template