vue2與vue3的差異(總結)?

vue作者尤雨溪在開發 vue3.0 的時候開發的一個基於瀏覽器原生 ES imports 的開發服務器(開發構建工具)。那麼我們先來了解一下vite

Vite

Vite,一個基於瀏覽器原生 ES imports 的開發服務器。利用瀏覽器去解析 imports,在服務器端按需編譯返回,完全跳過了打包這個概念,服務器隨起隨用。同時不僅有 Vue 文件支持,還搞定了熱更新,而且熱更新的速度不會隨着模塊增多而變慢。針對生產環境則可以把同一份代碼用 rollup 打。雖然現在還比較粗糙,但這個方向我覺得是有潛力的,做得好可以徹底解決改一行代碼等半天熱更新的問題。它做到了本地快速開發啟動, 用 vite 文檔上的介紹,它具有以下特點:

  • 快速的冷啟動,不需要等待打包操作;
  • 即時的熱模塊更新,替換性能和模塊數量的解耦讓更新飛起;
  • 真正的按需編譯,不再等待整個應用編譯完成;

使用 npm:

# npm 7+,需要加上額外的雙短橫線
$ npm init vite@latest <project-name> -- --template vue

$ cd <project-name>
$ npm install
$ npm run dev

或者 yarn:

$ yarn create vite <project-name> --template vue
$ cd <project-name>
$ yarn
$ yarn dev

概覽

在這裡插入圖片描述

  • 速度更快
  • 體積減少
  • 更易維護
  • 更接近原生
  • 更易使用
  1. 重寫了虛擬Dom實現
    diff算法優化
<div>
  <span/>
  <span>{{ msg }}</span>
</div>

被編譯成:

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("span", null, "static"),
    _createVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

首先靜態節點進行提升,會提升到 render 函數外面,這樣一來,這個靜態節點永遠只被創建一次,之後直接在 render 函數中使用就行了。
Vue在運行時會生成number(大於0)值的PatchFlag,用作標記,僅帶有PatchFlag標記的節點會被真正追蹤,無論層級嵌套多深,它的動態節點都直接與Block根節點綁定,無需再去遍歷靜態節點,所以處理的數據量減少,性能得到很大的提升。
在這裡插入圖片描述

  1. 事件監聽緩存:cacheHandlers
<div>
  <span @click="onClick">
    {{msg}}
  </span>
</div>

優化前:

import { toDisplayString as _toDisplayString, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("span", { onClick: _ctx.onClick }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["onClick"])
  ]))
}

onClick會被視為PROPS動態綁定,後續替換點擊事件時需要進行更新。
優化後:

import { toDisplayString as _toDisplayString, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("span", {
      onClick: _cache[1] || (_cache[1] = $event => (_ctx.onClick($event)))
    }, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

會自動生成一個內聯函數,這個內聯函數裏面再去引用當前組件最新的onclick,然後把這個內聯函數cache起來,第一次渲染的時候會創建內聯函數並且緩存,後續的更新就直接從緩存裏面讀同一個函數,既然是同一個函數就沒有再更新的必要,就變成了一個靜態節點
3. SSR速度提高
當有大量靜態的內容時,這些內容會被當做純字符串推進一個buffer裏面,即使存在動態的綁定,會通過模板 插值嵌入進去,這樣會比通過虛擬dom來渲染的快很多。vue3.0 當靜態文件大到一定量的時候,會用_ceratStaticVNode方法在客戶端去生成一個static node, 這些靜態node,會被直接innerHtml,就不需要創建對象,然後根據對象渲染

  1. tree-shaking
    在這裡插入圖片描述

tree-shakinng 原理
主要依賴es6的模塊化的語法,es6模塊依賴關係是確定的,和運行時的狀態無關,可以進行可靠的靜態分析,
分析程序流,判斷哪些變量未被使用、引用,進而刪除對應代碼
前提是所有的東西都必須用ES6 module的import來寫

按照作者的原話解釋,Tree-shaking其實就是:把無用的模塊進行「剪枝」,很多沒有用到的API就不會打包到最後的包里
在Vue2中,全局 API 如 Vue.nextTick() 是不支持 tree-shake 的,不管它們實際是否被使用,都會被包含在最終的打包產物中。
而Vue3源碼引入tree shaking特性,將全局 API 進行分塊。如果你不使用其某些功能,它們將不會包含在你的基礎包中
5. compositon Api
在這裡插入圖片描述

沒有Composition API之前vue相關業務的代碼需要配置到option的特定的區域,中小型項目是沒有問題的,但是在大型項目中會導致後期的維護性比較複雜,同時代碼可復用性不高
compositon api提供了以下幾個函數:

  • setup (入口函數,接收兩個參數(props,context))

  • ref (將一個原始數據類型轉換成一個帶有響應式特性)

  • reactive (reactive 用來定義響應式的對象)

  • watchEffect

  • watch

  • computed

  • toRefs (解構響應式對象數據)

  • 生命周期的hooks

    如果用ref處理對象或數組,內部會自動將對象/數組轉換為reactive的代理對象
    ref內部:通過給value屬性添加getter/setter來實現對數據的劫持
    reactive內部:通過使用proxy來實現對對象內部所有數據的劫持,並通過Reflect反射操作對象內部數據
    ref的數據操作:在js中使用ref對象.value獲取數據,在模板中可直接使用

import { useRouter } from 'vue-router'
import { reactive, onMounted, toRefs } from 'vue'

// setup在beforeCreate 鉤子之前被調用
// setup() 內部,this是undefined,因為 setup() 是在解析其它組件選項之前被調用的,所以 setup() 內部的 this 的行為與其它選項中的 this 完全不同。這在和其它選項式 API 一起使用 setup() 時可能會導致混淆
// props 是響應式的,當傳入新的 prop 時,它將被更新(因為props是響應式的,所以不能使用 ES6 解構,因為它會消除 prop 的響應性。)

// props參數:包含組件props配置聲明且傳入了的所有props的對象
// attrs參數:包含沒有在props配置中聲明的屬性對象,相當於this.$attrs
// slots參數:包含所有傳入的插槽內容的對象,相當於this.$slots
// emit參數:可以用來分發一個自定義事件,相當於this.$emit
setup (props, {attrs, slots, emit}) {
  const state = reactive({
    userInfo: {}
  })

  const getUserInfo = async () => {
    state.userInfo = await GET_USER_INFO(props.id)
  }

  onMounted(getUserInfo) // 在 `mounted` 時調用 `getUserInfo`

// setup的返回值

// 一般都是返回一個對象,為模板提供數據,就是模板中可以直接使用此對象中所有屬性/方法
// 返回對象中的屬性會與data函數返回對象的屬性合併成為組件對象的屬性
// 返回對象中的方法會與methods中的方法合併成組件對象的方法
// 若有重名,setup優先
  return {
    ...toRefs(state),
    getUserInfo
  }
}

靈活的邏輯組合與復用
可與現有的Options API一起使用
與選項API最大的區別的是邏輯的關注點
選項API這種碎片化使得理解和維護複雜組件變得困難,在處理單個邏輯關注點時,我們必須不斷地上下翻找相關代碼的選項塊。
compositon API將同一個邏輯關注點相關代碼收集在一起
6. Fragment(碎片)
在這裡插入圖片描述

<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

Vue 3不再限於模板中的單個根節點,它正式支持了多根節點的組件,可純文字,多節點,v-for等
render 函數也可以返回數組
7. Teleport(傳送門)
在這裡插入圖片描述

這個組件的作用主要用來將模板內的 DOM 元素移動到其他位置。
允許我們控制在 DOM 中哪個父節點下渲染了 HTML

<teleport to="body">
  <div v-if="modalOpen" class="modal">
    <div>
      I'm a teleported modal!
      (My parent is "body")
      <button @click="modalOpen = false">
        Close
      </button>
    </div>
  </div>
</teleport>
  1. 更好的Typescript支持
    vue3是基於typescipt編寫的,可以享受到自動的類型定義提示

  2. 自定義渲染 API
    在這裡插入圖片描述

    vue官方實現的 createApp 會給我們的 template 映射生成 html 代碼,但是要是你不想渲染生成到 html ,而是要渲染生成到 canvas 之類的不是html的代碼的時候,那就需要用到 Custom Renderer API 來定義自己的 render 渲染生成函數了。
    意味着以後可以通過 vue, Dom 編程的方式來進行canvas、webgl 編程
    默認的目標渲染平台
    在這裡插入圖片描述

    自定義目標渲染平台
    在這裡插入圖片描述

  3. 響應原理的變化
    vue2對象響應化:遍歷每個key,通過 Object.defineProperty API定義getter,setter 進而觸發一些視圖更新
    數組響應化:覆蓋數組的原型方法,增加通知變更的邏輯
    vue2響應式痛點
    遞歸,消耗大
    新增/刪除屬性,需要額外實現單獨的API
    數組,需要額外實現
    Map Set Class等數據類型,無法響應式
    修改語法有限制
    vue3響應式方案: 使用ES6的Proxy進行數據響應化,解決上述vue2所有痛點,Proxy可以在目標對象上加一層攔截/代理,外界對目標對象的操作,都會經過這層攔截。Proxy可以在目標對象上加一層攔截/代理,外界對目標對象的操作,都會經過這層攔截,相比 Object.defineProperty ,Proxy支持的對象操作十分全面

一, 全局api

1. 全局 Vue API 已更改為使用應用程序實例

vue2使用全局api 如 Vue.component, Vue.mixin, Vue.use等,缺點是會導致所創建的根實例將共享相同的全局配置(從相同的 Vue 構造函數創建的每個根實例都共享同一套全局環境。這樣就導致一個問題,只要某一個根實例對 全局 API 和 全局配置做了變動,就會影響由相同 Vue 構造函數創建的其他根實例。)
vue3 新增了createApp,調用createApp返回一個應用實例,擁有全局API的一個子集,任何全局改變 Vue 行為的 API 現在都會移動到應用實例上
在這裡插入圖片描述

2. 組件掛載

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

createApp初始化後會返回一個app對象,裏面包含一個mount函數
mount函數是被重寫過的

  1. 處理傳入的容器並生成節點;
  2. 判斷傳入的組件是不是函數組件,組件里有沒有render函數,template屬性,沒有就用容器的innerHTML作為組件的template;
  3. 清空容器內容
  4. 運行緩存的mount函數實現掛載組件;

二, 模板指令

  • 組件上 v-model 用法更改,替換 v-bind.sync
    vue2默認會利用名為 value 的 prop 和名為 input 的事件
// ParentComponent
<ChildComponent v-model="pageTitle" />

<!-- 是以下的簡寫: -->

<ChildComponent :value="pageTitle" @input="pageTitle = $event" />

 // ChildComponent

<input type="text" :value="value" @input="$emit('input', $event.target.value)">

如果想要更改 prop 或事件名稱,則需要在組件中添加 model 選項:
model選項,允許組件自定義用於 v-model 的 prop 和事件

// ChildComponent
<input type="text" :value="title" @input="$emit('change', $event.target.value)">
export default {
  model: {
    prop: 'title',
    event: 'change'
  },
  props: {
    title: String
  }
}

使用 title 代替 value 作為 model 的 prop

vue2.3 新增.sync (對某一個 prop 進行「雙向綁定」,是update:title 事件的簡寫)

// ParentComponent
<ChildComponent :title.sync="name" />

<!-- 是以下的簡寫 -->

<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
 // ChildComponent
 <input type="text" :value="title" @input="$emit('update:title', $event.target.value)">

在 3.x 中,自定義組件上的 v-model 相當於傳遞了 modelValue prop 並接收拋出的 update:modelValue 事件
prop:value -> modelValue;
event:input -> update:modelValue
v-bind 的 .sync 修飾符和組件的 model 選項已移除,可用 v-model加參數 作為代替
vue3 可以將一個 argument 傳遞給 v-model:
<ChildComponent v-model:title="pageTitle" />
等價於
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
可使用多個model
在這裡插入圖片描述

  • 可以在template元素上添加 key
<template v-for="item in list" :key="item.id">
  <div>...</div>
</template>
  • 同一節點v-if 比 v-for 優先級更高
  • v-bind=”object” 現在排序敏感(綁定相同property,vue2單獨的 property 總是會覆蓋 object 中的綁定。vue3按順序決定如何合併)
<div id="red" v-bind="{ id: 'blue' }" ></div>
// vue2 id="red"
// vue3 id="blue"
  • 移除 v-on.native 修飾符
    Vue 2 如果想要在一個組件的根元素上直接監聽一個原生事件,需要使用v-on 的 .native 修飾符
    Vue3 現在將所有未在組件emits 選項中定義的事件作為原生事件添加到子組件的根元素中(除非子組件選項中設置了 inheritAttrs: false)。
    (強烈建議組件中使用的所有通過emit觸發的event都在emits中聲明)
<my-component @close="handleComponentEvent" @click="handleNativeClickEvent"/>
// mycomponent
<template>
  <div>
    <button @click="$emit('click')">click</button>
    <button @click="$emit('close')">close</button>
  </div>
</template>
<script>
  export default {
    emits: ['close']
  }
</script>
  • v-for 中的 ref 不再註冊 ref 數組
    vue2在 v-for 語句中使用ref屬性時,會生成refs數組插入$refs屬性中。由於當存在嵌套的v-for時,這種處理方式會變得複雜且低效。
    vue3在 v-for 語句中使用ref屬性 將不再會自動在$refs中創建數組。而是,將 ref 綁定到一個 function 中,在 function 中可以靈活處理ref。
<div v-for="item in list" :ref="setItemRef"></div>

export default {
 setup() {
   let itemRefs = []
   const setItemRef = el => {
     itemRefs.push(el)
   }
   return {
     setItemRef
   }
 }
}

三, 組件

  • 函數式組件
    在 Vue 2 中,函數式組件有兩個主要應用場景:
    作為性能優化,因為它們的初始化速度比有狀態組件快得多
    返回多個根節點
    然而Vue 3對有狀態組件的性能進行了提升,與函數式組件的性能相差無幾。此外,有狀態組件現在還包括返回多個根節點的能力。所以,建議只使用有狀態組件。

    結合<template>的函數式組件:

  1. functional 移除
  2. 將 props 的所有引用重命名為 $props,attrs 重命名為 $attrs。
<template>
  <component :is=`h${$props.level}`  v-bind='$attrs' />
</template>

<script>
  export default {
    props: ['level']
  }
</script>

函數寫法:
相較於 Vue 2.x 有三點變化:

  1. 所有的函數式組件都是用普通函數創建的,換句話說,不需要定義 { functional: true } 組件選項。
  2. export default導出的是一個函數,函數有兩個參數:
    props
    context(上下文):context是一個對象,包含attrs、slot、emit屬性
  3. h函數需要全局導入
import { h } from 'vue'

const DynamicHeading = (props, context) => {
  return h(`h${props.level}`, context.attrs, context.slots)
}

DynamicHeading.props = ['level']

export default DynamicHeading
  • 異步組件需要 defineAsyncComponent 方法來創建
    異步組件的導入需要使用輔助函數defineAsyncComponent來進行顯式聲明
import { defineAsyncComponent } from 'vue'
const child = defineAsyncComponent(() => import('@/components/async-component-child.vue'))

帶選項異步組件,component 選項重命名為 loader

const asyncPageWithOptions  = defineAsyncComponent({
  loader: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
})
  • (新增)組件事件需要在 emits 選項中聲明()
    強烈建議使用 emits 記錄每個組件所觸發的所有事件。
    因為移除了 v-on.native 修飾符。任何未聲明 emits 的事件監聽器都會被算入組件的 $attrs 並綁定在組件的根節點上。
    如果emit的是原生的事件(如,click),就會存在兩次觸發。
    一次來自於$emit的觸發;
    一次來自於根元素原生事件監聽器的觸發;
    (emits 1.更好的記錄已發出的事件,2.驗證拋出的事件)
 export default {
    props: ['text'],
    emits: ['accepted']
  }
emits: {
    click: null,
    submit: payload => {
      if (payload.email && payload.password) {
        return true
      } else {
        console.warn(`Invalid submit event payload!`)
        return false
      }
    }
  }

四, 渲染函數

  • 渲染函數API
    h是全局導入,而不是作為參數傳遞給渲染函數
    在 2.x 中,render 函數會自動接收 h 函數作為參數
    在 3.x 中,h 函數需要全局導入。由於 render 函數不再接收任何參數,它將主要在 setup() 函數內部使用。可以訪問在作用域中聲明的響應式狀態和函數,以及傳遞給 setup() 的參數
import { h, reactive } from 'vue'

export default {
  setup(props, { slots, attrs, emit }) {
    const state = reactive({
      count: 0
    })

    function increment() {
      state.count++
    }

    // 返回render函數
    return () =>
      h(
        'div',
        {
          onClick: increment
        },
        state.count
      )
  }
}
  • 移除$listeners整合到 $attrs
    包含了父作用域中的(不含emits的) v-on 事件監聽器。它可以通過 v-on=”$listeners” 傳入內部組件
{{$attrs}}
<grand-son v-bind="$attrs"></grand-son>
  • $attrs包含class&style
    在vue2中,關於父組件使用子組件有這樣一個原則:

默認情況下父作用域的不被認作 props 的 attribute 綁定 (attribute bindings) 將會「回退」且作為普通的 HTML attribute 應用在子組件的根元素上

這句話的意思是,父組件調用子組件時,給子組件錨點標籤添加的屬性中,除了在子組件的props中聲明的屬性,其他屬性會自動添加到子組件根元素上。
為此,vue添加了inheritAttrs = false,這些默認行為將會被去掉,通過實例 property $attrs 可以讓這些 attribute 生效,且可以通過 v-bind 顯性的綁定到非根元素上。

五, 自定義元素

  • 自定義元素檢測在編譯時執行
    自定義元素交互
    Vue 2中,通過 Vue.config.ignoredElements 配置自定義元素
Vue.config.ignoredElements = ['plastic-button']

Vue 3 通過app.config.isCustomElement

const app = Vue.createApp({})
app.config.isCustomElement = tag => tag === 'plastic-button'
  • Vue 3.x 對 is做了新的限制
    當在 Vue 保留的 component標籤上使用is時,它的行為將與 Vue 2.x 中的一致
    當在不同組件標籤上使用is時,is會被當做一個不同的prop;
    當在普通的 HTML 元素上使用is,is將會被當做元素的屬性。
    新增了v-is,專門來實現在普通的 HTML 元素渲染組件。

六, 其他

  • destroyed 生命周期選項被重命名為 unmounted
  • beforeDestroy 生命周期選項被重命名為 beforeUnmount
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-awWIzouv-1637307558259)(assets/vue3/img.png)]
    整體來看其實變化不大,使用setup代替了之前的beforeCreate和created,其他生命周期名字有些變化,功能都是沒有變化的
  • Props 的默認值函數不能訪問this
    替代方案:
    把組件接收到的原始 prop 作為參數傳遞給默認函數;
    inject API 可以在默認函數中使用。
import { inject } from 'vue'

export default {
  props: {
    theme: {
      default (props) {
        // `props` 是傳遞給組件的原始值。
        // 也可以使用 `inject` 來訪問注入的屬性
        return inject('theme', 'default-theme')
      }
    }
  }
}
  • 自定義指令 API 與組件生命周期一致
const MyDirective = {
  created(el, binding, vnode, prevVnode) {}, // 新增
  beforeMount() {},
  mounted() {},
  beforeUpdate() {}, // 新增
  updated() {},
  beforeUnmount() {}, // 新增
  unmounted() {}
}

綁定組件的實例從 Vue 2.x 的vnode.context移到了binding.instance中

  • data 選項應始終被聲明為一個函數
    data 組件選項聲明不再接收 js 對象,只接受函數形式的聲明。
<script>
  import { createApp } from 'vue'

  createApp({
    data() {
      return {
        apiKey: 'a1b2c3'
      }
    }
  }).mount('#app')
</script>

當合併來自 mixin 或 extend 的多個 data 返回值時,data現在變為淺拷貝形式(只合併根級屬性)。

const Mixin = {
  data() {
    return {
      user: {
        name: 'Jack',
        id: 1
      }
    }
  }
}

const CompA = {
  mixins: [Mixin],
  data() {
    return {
      user: {
        id: 2
      }
    }
  }
}

vue2

{
  "user": {
    "id": 2,
    "name": "Jack"
  }
}

vue3

{
  "user": {
    "id": 2
  }
}
  • 過渡的 class 名更改(過渡類名 v-enter 修改為 v-enter-from、過渡類名 v-leave 修改為 v-leave-from。)
  • transition-group 不再需要設置根元素( 不再默認渲染根元素,但仍可以使用 tag prop創建一個根元素。)
  • 偵聽數組(當偵聽一個數組時,只有當數組被替換時才會觸發回調。如果你需要在數組改變時觸發回調,必須指定 deep 選項。)
  • 已掛載的應用不會取代它所掛載的元素(在vue2中,當掛載一個具有 template 的應用時,被渲染的內容會替換我們要掛載的目標元素。在 Vue 3.x 中,被渲染的應用會作為子元素插入,從而替換目標元素的 innerHTML)
  • 生命周期 hook: 事件前綴改為 vnode-(監聽子組件和第三方組件的生命周期)

移除API

  • 不再支持使用數字 (即鍵碼) 作為 v-on 修飾符,vue3建議使用按鍵alias(別名)作為v-on的修飾符。
<input v-on:keyup.delete="confirmDelete" />
  • vue3將移除且不再支持 filters,如果需要實現過濾功能,建議通過method或computed屬性來實現(如果需要使用全局過濾器vue3提供了globalProperties。我們可以藉助globalProperties來註冊全局過濾, 全局過濾器裏面定義的只能是method。)
const app = createApp(App)

app.config.globalProperties.$filters = {
  currencyUSD(value) {
    return '$' + value
  }
}
<template>
  <p>{{ $filters.currencyUSD(accountBalance) }}</p>
</template>
  • 內聯模板 (inline-template attribute移除)
  • $children(如果需要訪問子組件實例,建議使用 $refs)
  • propsData 選項之前用於在創建 Vue 實例的過程中傳入 prop,現在它被移除了。如果想為 Vue 3 應用的根組件傳入 prop,使用 createApp 的第二個參數。
  • 全局函數 set 和 delete 以及實例方法 $set 和 $delete。基於代理的變化檢測不再需要它們了。

用於遷移的構建版本

@vue/compat (即「遷移構建版本」) 是一個 Vue 3 的構建版本,提供了可配置的兼容 Vue 2 的行為。

該構建版本默認運行在 Vue 2 的模式下——大部分公有 API 的行為和 Vue 2 一致,僅有一小部分例外。使用在 Vue 3 中發生改變或被廢棄的特性時會拋出運行時警告。一個特性的兼容性也可以基於單個組件進行開啟或禁用。

已知的限制:

  • 基於vue2內部API或文檔中未記載行為的依賴。最常見的情況就是使用 VNodes 上的私有 property。如果你的項目依賴諸如 Vuetify、Quasar 或 Element UI 等組件庫,那麼最好等待一下它們的 Vue 3 兼容版本。

  • 對IE11的支持:Vue 3 已經官方放棄對 IE11 的支持。如果仍然需要支持 IE11 或更低版本,那你仍需繼續使用 Vue 2。

  • 服務端渲染:該遷移構建版本可以被用於服務端渲染,但是遷移一個自定義的服務端渲染設置有更多工作要做。大致的思路是將 vue-server-renderer 替換為 @vue/server-renderer。Vue 3 不再提供一個包渲染器,推薦使用 Vite 以支持 Vue 3 服務端渲染。

Tags: