HelloChunWei / blog

個人部落格,紀錄自己的知識
MIT License
7 stars 0 forks source link

如何不用 vue 的 v-html 渲染 HTML #10

Open HelloChunWei opened 3 years ago

HelloChunWei commented 3 years ago

前言

會有這一篇文章的原因是前陣子去面試的時候,面試官就問了我這個問題:「假設我們用Rich editor 編寫了一篇 blog 文章,他輸出的時候會一大串String,內容都是HTML tag,那你要如何去渲染這串String?」

我當下最直覺的回答是 V-html 直接做輸出。面試官接著問:「我們都知道v-html的實作是利用innerHTML,那可能會暴露出 xss 的危險,有辦法不利用V-html去做渲染嗎?」

其實在當下我是並沒有回答出這題的答案,想當然爾並沒有拿到offer,思考了幾個禮拜後,終於寫出這題的答案了。

vue的 render function

不知道有多少人有用到vue的render function? 大多時候運用 templates 就可以完成將近90%的需求,但是 vue 還有提供一個更為彈性的 render function

從文件上可以知道render function 可以完全不用寫template,以下面範例來說

<h1>{{ blogTitle }}</h1>

可以直接轉換成:

render() {
  return h('h1', {}, this.blogTitle)
}

以 Vuetify 來說,他們套件的主要寫法就是利用 render function 完成,看看他們的Button寫法。 VBtn

不過 element UI 的寫法就是我們比較熟悉的 single file component。 button

現在我們知道render function後,我們接下來就是要把 String 餵給render function 去渲染。

String HTML的轉換

起初我是在這裡被卡住,我有一段String <div><h1>title</h1></div> 要怎麼樣拆解? 我最一開始的想法是想直接用正規表達去拆,但後來發現超級困難,因為在這 html tag 中間有可能是有 class, id, style 這些屬性的, 如果單純用正規表達會相當的困難。

後來我找到了 DOMParser 他會把String 轉換成 HTML object,結果如下:

轉換後要用 web node API 下去操作dom。但實際上實作起來非常不順,例如我要拿到 div 的tag name 必須要透過 element.tagName 取得,接著我還要再一個一個去針對各個html tag,以及屬性轉換成我要的資訊。所以如果我最後要變成以下形式的話,我要處理非常多東西:

return  return h('div', {}, [h('h1', {}, 'title')])

不過我在看到上面這段code的時候靈機一動,「這怎麼看起來好像一個東西?」 「我如果能夠把string HTML 轉換成 virtual dom 的格式,然後再用遞迴去跑render function 不就好了?」

我們都知道 virtual dom 的基本格式長這樣:

var node = {
  tagName: 'div',
  attributes: [],
  childNodes: [
      {
        tagName: 'h1',
        attributes: [],
        childNodes: [
            {
                nodeName: "#text"
                value: "title"
            }
        ]
      }
  ],
};

那我們把寫成遞迴function,就可以利用vue render function 渲染出來了:

import { h } from "vue";
function renderNode (nodes) {
    if (!nodes.tagName) return
    let childNodes = nodes.childNodes.map((node) => {
        if (node.tagName) {
          return this.createEle(node);
        }
        // 如果他是純文字的話
        return node.value;
    });
    return h(nodes.tagName, {}, childNodes);
}

這樣就完成我們要的功能了~

把String HTML轉換成類似 virtual dom 的結構

現在知道我們的目標結構後,就是要來想怎麼做轉換了,不過這又回到上面所提到的,難道要用 DOMParser 然後再用 web API 一個一個自己手動轉換嗎?這好像是個方法,但應該有更方便的解法吧?

在這時候我想到一張圖:

對耶~其實 vue 本身就有做過這樣的事情,那他們怎麼轉的?從上面那張圖可以知道:vue 先把template的 html 轉換成 AST tree,然後再去做render。

ok~ 竟然知道關鍵字了那我們就來研究看看,提到 AST 就要提到 AST explorer,這網站可以線上將code轉換成AST tree,這次我是要轉換 HTML,就先切換成HTML。

切換到HTML後往右變看會發現,轉換後的結果不就是我們要的結構嗎?

那們接下來的工作就是把String HTML 轉換成 AST tree,再從中把我們要的東西拿出來,並放入遞迴涵式做渲染。 那 AST explorer 是利用 parse5 這套件去做轉換。

現在我們都知道步驟了,接著就是把它組起來。我就不再一一說明了,直接呈上結果。

成果在這

結論

這一次算是把之前被問倒的問題給解決,後來想想,其實要把這問題回答好會需要很多的知識點:

  1. 要知道 vue render function
  2. 要知道怎麼轉換String HTML
  3. 知道 vue 有利用 AST tree
  4. 大致上知道 AST tree 是在做什麼

當然如果沒有想到 AST tree的話,也可以自己土炮一個轉換功能。只是會比較複雜跟麻煩而已。(parse5 的實作code)