在 Nuxt3 使用 shadow DOM 取代 v-html 的歷程

Stephen Chen
7 min readAug 30, 2023

前言

因為遇到了後台圖文編輯器在前端無法正常的 UI 顯示 ( 比如無法顯示斜體 )以下紀錄為了解決此問題我所思考以及學習的歷程

TL;DR

最終使用 Shadow DOM 解決

前端環境

Nuxt3

先從 API 開始

每當前後端遇到問題,我都先會用 postman 檢查 API
我首先把後端吐回來的 html 使用 codepen render 看看,我發現該顯示的 UI 畫面如預期的都沒有問題,於是我知道問題可能發生在前端

v-html

<div v-html="data">
</div>

由於發生問題在於這行 code,當我打開 Chrome dev tool 的時候,發現這段 div 的 css 竟然 inherent global css style,於是我嘗試把 global css style 被影響的地方關掉,發現可以正常顯示

那更動或者關掉 global css

這樣方法確實可行,但影響的層面非常之廣,連帶影響其他的頁面,覺得不太妥

那如果在後台圖文編輯器寫 inline style 呢

此方法也不可行,因為對上架的人不友善

那有辦法影響 v-html 裡面的 css 嗎

我看了很多文章,找到了 Deep Selectors 這關鍵字,可以影響 v-html 裡面的 style 解決我目前的問題,但圖文編輯器就是可以讓使用者隨意編輯,這麼多種情境要處理感覺就不是一個正確的做法

備註:在找個解答的過程中,又學到 reset css 跟 normalize.css

思路轉變

上面的思考一直圍繞在一個想辦法影響 v-html 裡面 style 的圈圈裡面,走到最後發現還是不行

於是我沈寂了一天之後突然想到,如果讓此部分是一個獨立的 document 呢?有這想法之後馬上蹦出 iframe 來

iframe

<iframe :srcdoc="data"></iframe>

果然這樣的思維沒問題,後台圖文編輯器的畫面可以很順利的顯示,但衍伸而來的是高度問題

iframe 高度問題

我是參考這篇,作者透過 timer 跟 postMessage 來監聽 content 的高度,然後定期的更新 iframe 的高

但我沒真的實作,因為我在查資料的過程中發現了這篇

Upcoming Change to Embedded Tweet Display on Web

Tweet 說

Embedded Tweet display 要從 source-less iframe 改為 Shadow Dom

因為 lower memory,render times faster 以及更流暢的滑動 🤔🤔🤔

那具體為什麼更快呢?

詳情可看下方,簡單來說就是東西變少惹,要 traverse、update、create…就會更快

那其他人怎麼說

twitter 要換 iframe,那有沒有誰也想換,於是我找到這篇,作者在 BBC 工作

根據原文擷取一些重點,在處理 ifrmae 的時候需要一直跟 host page 溝通來溝通去,還要依據 content 處理高度以及 rwd 寬度…等

https://medium.com/bbc-product-technology/goodbye-iframes-6c84a651e137

新名詞 source-less iframe

<iframe id="myIframe"></iframe>

就是 iframe 一開始沒有 src 這 attribute,之後再透過 javascript assign,請看 ChatGPT 回答

what is source-less iframe

新名詞 2 :Shadow Dom

Shadow DOM allows hidden DOM trees to be attached to elements in the regular DOM tree

from https://developer.mozilla.org/

其核心價值就是 encapsulation

我自己的理解是在 Shadow DOM 裡有自己的 Scoped style ,且不會跟已有的 Dom Tree 有任何互相影響,所以解決我一開始的問題

後台圖文編輯器在前端無法正常的 UI 顯示

Nuxt3 Code

沒問題那就直接上 Code 拉

<!-- File name : ShadowDom.vue -->

<script setup lang="ts">
const element = ref(null)

const props = defineProps({
sourceDoc: {
type: String,
default: null,
},
})

onMounted(() => {
const shadowHost = computed(
() =>
element.value.shadowRoot || element.value.attachShadow({ mode: 'open' })
)

shadowHost.value.innerHTML = props.sourceDoc
})
</script>

<template>
<div ref="element"></div>
</template>

使用

<ClientOnly>
<ShadowDom :source-doc="data"></ShadowDom>
</ClientOnly>

Done

Ref

https://twittercommunity.com/t/upcoming-change-to-embedded-tweet-display-on-web/66215/1

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

https://medium.com/bbc-product-technology/goodbye-iframes-6c84a651e137

--

--