談談 Vue 模板和 JSX

工具鏈

從學習曲線角度來講,結合我個人體會,React 學習路線是比 Vue 陡峭的,這個和 JSX、Template 有關嗎?當然有。在 React 中使用 JSX,眾所周知, JSX 需要經過 Transform 才能在瀏覽器中運行。馬上就有小夥伴反駁了,Vue 有官方的 Vue-cli, React 使用 create-react-app 初始化項目就好了呀,並不需要比 Vue 多學習其他工具呀。

我們從另一個角度來看這個問題,使用 JSX 還需要熟練 ESM,這是繞不開的,因為一個 React 組件就是一個 js 或 JSX,這可能會給新手帶來疑惑,組件是如何鏈接的?先別著急反駁,接著往下看。

僅僅掌握「三劍客」的新手就可以輕鬆上手 Vue,這在 Vue 官方文檔中有所體現。

image.png

巧妙之處就在與上圖中的 「易用」,僅僅學會了 HTML、CSS、JavaScript 就能上手。Vue 的「漸進式」不僅體現在跟隨項目的複雜度上的,還有很大程度上適應使用者的水平。這不是空穴來風,當年鄙人就僅學習了這「三劍客」,就用 Vue-cli 初始化並完成了小項目。如今我思考在當時 Vue 是如何幫我避開 ESM 的。答案在於 vue 的「三段式結構」。

<script>
export defalut  {
  // ...
} 
</script>

我的注意點完全在於導出的這個對象之中以及上方的 Template 和下方的 Style,只知道在這個對象裡面寫一些 Data、Methods 等等。甚至完全沒有在意 export defalut 這個關鍵字。

不僅如此,Vue 的 Template + Options API 基本就規範了處理一些邏輯時的大體形式,這在通過搜索引擎檢索一些遇到的問題時,通常能夠輕易看懂檢索到的程式碼,甚至可以「依葫蘆畫瓢」,這對新手無疑是一個優勢。相比 JSX, 寫法上,Template 雖不能夠從「優雅」上勝出,但 JSX 更容易帶入個人編碼風格,這應該能使大家有所認同,這也就加大了新手的理解難度。

回到問題,那麼 Vue 是如何引入組件的呢?作為新手,我只會「依葫蘆畫瓢」,通過一條 import 語句導入進來,然後在 components 裡面將組件註冊,這一切都被我自然得誤認為是 Vue 的能力。

這不代表使用 Vue 不需要學習 ESM,沒錯,這是究極必要的,熟悉 ESM 可以更好得組織項目程式碼。上面我只是在思考當年 Vue 是怎麼幫我避開 ESM 的,或者說我作為新手時是如何毫無察覺地避開它的,Template 「功不可沒」

題外話,Vue 的 Template 可以放到 Script 標籤下方,這可能更符合 React 用戶的直覺,看尤大在推上分享一些 Demo 時有時會這麼寫。如果您樂意,也可以這麼做,但在多人協作的項目中還要考慮小夥伴的感受。

<script>
export defalut  {
  // ...
} 
</script>

<template>
  
</template>

Vue + JSX = React?

所謂「蘿蔔白菜,各有所愛」。在 React 中只能使用 JSX,Vue 從 2.0 開始乾脆把 JSX 也支援完事了。有人說,在 Vue 中使用 JSX 為什麼不直接使用 react? 這個話題就大了,這不僅和項目的技術選型有關,且 Vue 和 React 設計上就有很大差別。在 Vue 中無論使用 Template 還是 JSX 都會被編譯成 h 函數(在 Vue 2 中稱為 render 函數)。所以,在 Vue 中使用 JSX,只是換了一種表現形式而已。

我們來看一下 Vue 將 Template 渲染到瀏覽器的過程。

<div>Hello</div>

這個HTML也可以通過一個虛擬節點 VNode 來表示,也就是用 JavaScript 對象的形式來表示。

{
    tag: 'div',
    children: [
        {
            text: 'Hello'
        }
    ]
}

Vue 知道如何將此虛擬節點並掛載到 DOM 上,它會更新我們在瀏覽器中看到的內容,但是 VNode 從哪裡來的呢?實際還有一個步驟,Vue 基於我們的 Template 創建一個渲染函數,返回一個虛擬 DOM 節點。

vue2.png

渲染函數可以是這樣的:

render(h) {
    return h('div', 'hello')
}

當組件更改時,Render 函數將重新運行,它將創建另一個虛擬節點。然後發送舊的 VNode 和新的 VNode 到 Vue中進行比較並以最高效的方式在我們的網頁上更新。例如下圖所示,僅僅更新了文本內容。
vue3.png

題外話,實際 Vue 有三個核心模組:

  1. Reactivity Module 響應式模組
  2. Compiler Module 編譯器模組
  3. Renderer Module 渲染模組

響應式模組允許創建 JavaScript 響應對象並可以觀察其變化。當使用對象的程式碼運行時,它們會被跟蹤,因此,它們可以在響應對象發生變化後運行。

編譯器模組獲取 HTML 模板並將它們編譯成渲染函數。這可能在運行時在瀏覽器中發生,但在構建 Vue 項目時更常見。這樣瀏覽器就可以只接收渲染函數。

渲染模組在網頁上渲染組件經歷三個不同階段:在渲染階段,將調用 render 函數,它返回一個虛擬 DOM 節點;在掛載階段,使用虛擬 DOM 節點並調用 DOM API 來創建網頁;在修補程式階段,渲染器將舊的虛擬節點和新的虛擬節點進行比較並只更新變化的部分。

上面是 Vue 將 Template 渲染到瀏覽器的過程,和 Template 類似,在 Vue 中使用的 JSX 也會轉化成 h 函數,眼見為實。

image.png

所以,了解這個過程,您可能不是那麼排斥在 Vue 中使用 JSX 了,或者或許對它在 Vue 中的表現不是那麼陌生了。

從另一個方面來講,Vue 中的 JSX 和 React 中的 JSX 在寫法上也有差別。在 Vue JSX 中您可以直接使用熟悉的指令(directives),例如 Vue 內置的 v-show 、 v-model 甚至 v-models 和自定義指令,但例如 slots 在 Vue JSX 中或許沒有在 React 中那麼方便。

「聯合」的能力

在 Vue sfc(single file component)中同時編輯 script、template、style,這提供了「聯合」的能力,即在一個文件中使 Style 和 Template 共享 Script,幫助開發者更容易實現業務。例如:rfcs-style-variables,該提案雖然還沒有最終確定下來,但 Vue 展示了這種能力。基礎示例:

<template>
  <div class="text">hello</div>
</template>

<script>
export default {
  data:()=> ({
    color: 'red',
    font: {
      size: '2em'
    }
  })
}
</script>

<style>
.text {
  color: v-bind(color);

  /* expressions (wrap in quotes) */
  font-size: v-bind('font.size');
}
</style>

通過 style-variables 寫有一個有趣的示例,此處為部分程式碼。

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent({
    setup() {
        const inputBorderColor = ref('')
        
        const onEngineChange = engine => {
            for (const { name, color } of enginesData) {
                if (name === engine) {
                    inputBorderColor.value = color
                    return
                }
            }
        }
        
        return {
            inputBorderColor,
        }
    },
})
</script>

<style lang="scss">
input {
  &:focus {
    border-color: v-bind(inputBorderColor);
  }
}
</style>

Video_2021-01-03_112916.gif
如果在 Vue 中使用 JSX, 就無法利用這個優勢了。題外話,在 React 中類似的 css-in-js 方案是 styled-component。

業務組件庫

Vue 幾乎所有面向業務構建的生態內容給出的示例程式碼都是基於 Template 的。我們通常在項目中安裝 UI 組件庫依賴,組件庫中給出的示例程式碼也是基於 Template 的。當前 Vue 組件庫文檔示例程式碼正在由 Options API 向 Composition API 過渡,但幾乎沒有可能從 Template 向 JSX 過渡,或者向用戶提供 JSX 的組件示例程式碼。這非常重要,如果您的項目依賴組件庫,查看文檔後還需要將 Template 轉到 JSX,這無疑增加開發成本。

TypeScript 支援

在 script 中,vue3 相比 vue2 已經出現質的飛躍。從 Template 來看,Vue 還有待完善,但這個可行的。JSX 一開始也沒有類型支援,完全是 TS 給加了一套針對 JSX 的推導機制。

模版和類型推導,表面上看,隔了一層模版語法 + 編譯,似乎確實存在 「斷層」,但其實裡面沒您想的差那麼遠。Vue 的模版是編譯成 virtual dom 渲染函數的,生成的 js 跟 React 的渲染函數一樣可以類型推導,而模版跟生成的 js 之間是完整的邏輯映射,所以這裡其實主要是需要做一些工具鏈上的銜接,把對生成的 js 分析出來的 intellisense 回饋到 IDE 里的模版上。技術上是完全可行的。

vscode 插件 Volar 已經支援 Template 表達式的類型檢查。Vetur 雖然不支援還未定稿的 RFC,也已經支援基於現有 API 的模板類型檢查和組件 props 類型檢查了。另外,在 vue3 中無論是否使用 TS,通過 defineComponent 定義組件都能獲得更好的提示。
image.png
可以預見的是,Vue 從升級 3.0 後,對於 TypeScript 的支援將會越來越好。

適用場景

在大多數場景下(尤其是業務場景)使用 Template 可能是更好的選擇, vue3 基於 Template 分析做了大量的優化,而使用 JSX 需要手動做一些優化。

JSX 由於更具靈活性,通常一些組件庫的不二之選,例如:Ant Design Vue、Vant、Element Plus(部分使用)。看到這麼個例子: 根據 props 上的 reverse 屬性,決定是否要調換兩塊內容的順序。使用 JSX 輕易就能實現,且可讀性也很高。

const renderContent = () => {
  const Content = [
    <div class="foo">Foo DOM...</div>,
    <div class="bar">Bar DOM...</div>,
  ];
  if (props.reverse) Content.reverse();
  return <div>{Content}</div>;
}

如果通過模板來實現,在不抽象子組件的情況下,foo 和 bar 的模板結構需要重複寫兩遍,才能滿足這個需求:

<template>
  <div>
    <template v-if="reverse">
      <div class="bar">Bar DOM...</div>
      <div class="foo">Foo DOM...</div>
    </template>
    <template v-else>
      <div class="foo">Foo DOM...</div>
      <div class="bar">Bar DOM...</div>
    </template>
  </div>
</template>

因此,在動態性強的場景下,JSX 會有一定優勢。Composition API + JSX 是某些場景下追求極致的選擇,相應地需要付出更多開發成本。

參考資料:

[1] //www.zhihu.com/question/436260027/answer/1647182157
[2] //www.zhihu.com/question/310485097/answer/591869966
[3] //www.cnblogs.com/guangzan/p/13358322.html