Closed NaokiHaba closed 1 month ago
2024年9月1日に Vue 3.5 がリリースされました。
この記事では、Vue 3.5 の主な機能改善・追加とその背景について解説します。
https://blog.vuejs.org/posts/vue-3-5
Lazy Hydration
useId()
data-allow-mismatch
useTemplateRef()
Deferred Teleport
onWatcherCleanup()
<script setup>
内で defineProps
から分割代入された変数が自動的にリアクティブになる機能が追加されました。これにより、コードがより簡潔になり、可読性が向上します。
より詳細な内部実装を知りたい方は以下の記事を参照してください。
https://zenn.dev/comm_vue_nuxt/articles/reactive-props-destructure
https://github.com/vuejs/rfcs/discussions/502
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(defineProps<{
count?: number
}>(), {
count: 0
})
const double = computed(() => props.count * 2)
</script>
<template>
<div>Count: {{ props.count }}</div>
<div>Double: {{ double }}</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const { count = 0 } = defineProps<{
count?: number
}>()
const double = computed(() => count * 2)
</script>
<template>
<div>Count: {{ count }}</div>
<div>Double: {{ double }}</div>
</template>
この新しい書き方では、withDefaults
を使用せずにデフォルト値を設定できるようになりました。また、プロパティへのアクセスが直接的になり、コードの可読性が向上しています。
Vue 3.5の新機能により、コンパイル後のコードにも変更が加えられています。以下に、コンパイル前後のコードの違いを示します。
入力:
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
count?: number
}>()
const double = computed(() => props.count * 2)
</script>
<template>
<div>Count: {{ count }}</div>
<div>Double: {{ double }}</div>
</template>
出力(一部抜粋):
const __sfc__ = /*#__PURE__*/_defineComponent({
// ...
setup(__props, { expose: __expose }) {
__expose();
const props = __props
const double = computed(() => props.count * 2)
const __returned__ = { props, double }
// ...
return __returned__
}
});
入力:
<script setup lang="ts">
import { computed } from 'vue'
const { count = 0 } = defineProps<{
count?: number
}>()
const double = computed(() => count * 2)
</script>
<template>
<div>Count: {{ count }}</div>
<div>Double: {{ double }}</div>
</template>
出力(一部抜粋):
const __sfc__ = /*#__PURE__*/_defineComponent({
// ...
props: {
count: { type: Number, required: false, default: 0 }
},
setup(__props, { expose: __expose }) {
__expose();
const double = computed(() => __props.count * 2)
const __returned__ = { double }
// ...
return __returned__
}
});
この変更により、プロパティのデフォルト値がコンポーネントの props
オプション内で直接定義されるようになり、setup
関数内でのプロパティの取り扱いが簡略化されています。
ただし、RFCで言及されている通り、props
と通常の変数を視覚的に区別するのが難しくなっているため、@vue/language-tools 2.1
以降では、オプトイン設定でインレイヒント(inlay hints)を有効にできるようになりました。
<script setup lang="ts">
import { defineProps } from 'vue'
const { count = 0, msg = 'hello' } = defineProps<{
count?: number
message?: string
}>()
// props.count と props.msg の両方がインレイヒントでハイライトされる
console.log(count, msg)
</script>
https://github.com/vuejs/core/pull/11530
https://github.com/vuejs/core/pull/11458
requestIdleCallback
を使用して、ブラウザがアイドル状態のときにコンポーネントをハイドレーションします。使用例:
import { defineAsyncComponent, hydrateOnIdle } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
hydrate: hydrateOnIdle(5000) // 最大5秒待機
})
IntersectionObserver
を使用して、コンポーネントが画面に表示されたときにハイドレーションします。使用例:
import { defineAsyncComponent, hydrateOnVisible } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./LazyLoadedComponent.vue'),
hydrate: hydrateOnVisible({ rootMargin: '200px' })
})
使用例:
import { defineAsyncComponent, hydrateOnMediaQuery } from 'vue'
const MobileComp = defineAsyncComponent({
loader: () => import('./MobileComponent.vue'),
hydrate: hydrateOnMediaQuery('(max-width: 768px)')
})
使用例:
import { defineAsyncComponent, hydrateOnInteraction } from 'vue'
const InteractiveComp = defineAsyncComponent({
loader: () => import('./InteractiveComponent.vue'),
hydrate: hydrateOnInteraction(['click', 'mouseover'])
})
forEachElement
ヘルパー関数を使用して、コンポーネントのルート要素にアクセスできます。使用例:
import { defineAsyncComponent, type HydrationStrategy } from 'vue'
const customStrategy: HydrationStrategy = (hydrate, forEachElement) => {
let shouldHydrate = false
forEachElement(el => {
// カスタムロジックを実装
if (someCondition(el)) {
shouldHydrate = true
}
})
if (shouldHydrate) {
hydrate()
}
return () => {
// 必要に応じてクリーンアップロジックを実装
}
}
const CustomComp = defineAsyncComponent({
loader: () => import('./CustomComponent.vue'),
hydrate: customStrategy
})
これらの戦略を適切に使用することで、アプリケーションのパフォーマンスを最適化し、必要なときにのみコンポーネントをハイドレーションすることができます。各戦略はdefineAsyncComponent
関数のhydrate
オプションとして指定します。
https://github.com/vuejs/core/pull/11404
useId()
は、Vue 3.5で導入された新しいコンポジションAPIで、Reactの useId
と類似した機能を提供します。このAPIは、フォーム要素やアクセシビリティ属性に使用できるユニークなIDを生成します。
入力:
<script setup>
import { useId } from 'vue'
const id = useId()
</script>
<template>
<form>
<label :for="id">Name:</label>
<!-- <input id="v:0" type="text"> -->
<input :id="id" type="text" />
</form>
</template>
出力(一部抜粋):
<script setup>
で 呼び出した useId
の値が、render
関数内で id
として使用されていることがわかります。
/* Analyzed bindings: {
"useId": "setup-const",
"id": "setup-maybe-ref"
} */
setup(__props, { expose: __expose }) {
__expose();
const id = useId()
const __returned__ = { id, useId }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("form", null, [
_createElementVNode("label", { for: $setup.id }, "Name:", 8 /* PROPS */, _hoisted_1),
_createElementVNode("input", {
id: $setup.id,
type: "text"
}, null, 8 /* PROPS */, _hoisted_2)
]))
}
サーバーサイドレンダリング(SSR)とクライアントサイドのハイドレーションの間に発生する可能性のある不一致警告を抑制できる
<span data-allow-mismatch>{{ data.toLocaleString() }}</span>
また、この属性に値を指定することで、許可する不一致のタイプを制限することもできます。指定可能な値は以下の通りです:
text
: テキストコンテンツの不一致を許可children
: 子コンテンツの不一致を許可class
: クラスの不一致を許可style
: スタイルの不一致を許可attribute
: 属性の不一致を許可Vue 3.5 では、カスタム要素(Custom Elements)の機能が大幅に改善されました。これらの改良により、開発者はより柔軟にカスタム要素を設定し、制御できるようになりました。
新たに導入されたconfigureApp
オプションにより、カスタム要素内でVueアプリケーションの詳細な設定が可能になりました。これにより、エラーハンドリングなどのアプリケーションレベルの設定をカスタム要素に適用できます。
カスタム要素とその環境へのアクセスを容易にする新しいAPIが追加されました:
useHost()
: ホスト要素(カスタム要素自体)にアクセスするための関数useShadowRoot()
: シャドウルートにアクセスするための関数。シャドウDOM内の要素操作やスタイリングに有用です。this.$host
: コンポーネント内でホスト要素にアクセスするためのプロパティこれらの新APIにより、開発者はシャドウDOMとホスト要素をより効果的に操作できるようになりました。
shadowRoot
オプションの追加shadowRoot: false
オプションを使用することで、シャドウDOMを使用せずにカスタム要素をマウントできるようになりました。これにより、以下のような状況での柔軟性が向上します:
この機能により、開発者はシャドウDOMの利点とグローバルスコープの柔軟性のバランスを取ることができます。シャドウDOMを使用しない場合、カスタム要素の内部構造は通常のDOM構造として扱われ、外部からのアクセスやスタイリングが可能になります。
nonce
オプションの導入により、カスタム要素によって注入される<style>
タグにnonceを付与できるようになりました。これにより、Content Security Policy (CSP)の実装が容易になり、全体的なセキュリティが向上します。
nonceは、スクリプトやスタイルシートの実行を制御するためのセキュリティ機能で、特定のリソースが信頼できるソースからのものであることを確認するのに役立ちます。
以下は、これらの新機能を活用したカスタム要素の定義例です:
import MyElement from './MyElement.ce.vue'
defineCustomElement(MyElement, {
shadowRoot: false, // シャドウDOMを使用しない
nonce: 'xxx',
configureApp(app) {
app.config.errorHandler = // エラーハンドラーの設定
}
})
この例では、シャドウDOMを使用せず、nonceを設定し、アプリケーションレベルのエラーハンドラーを構成しています。シャドウDOMを使用する場合は、shadowRoot: false
を省略するか、true
に設定します。
これらの改善により、Vue 3.4でのカスタム要素の使用がより柔軟かつ強力になり、さまざまなユースケースに対応できるようになりました。開発者は、シャドウDOMの利点を活かしつつ、必要に応じてグローバルスコープとの連携も選択できるようになりました。
useTemplateRef()
を使ってテンプレート参照を取得する新しい方法が導入されました。
3.5以前では、静的な ref
属性と一致する変数名を持つプレーンな参照(ref
)を使用することを推奨していました。
この古い方法では、ref
属性がコンパイラによって解析可能である必要があったため、静的な ref
属性に限定されていました。
対照的に、useTemplateRef()
は実行時の文字列 ID を介して参照をマッチングするため、動的に変更される ID への ref
バインディングをサポートしています。
新しい方法:
<script setup>
import { useTemplateRef } from 'vue'
const inputRef = useTemplateRef('input')
</script>
<template>
<input ref="input">
</template>
以前の書き方:
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
// inputRef.value は対応する DOM 要素を参照します
console.log(inputRef.value)
})
</script>
<template>
<input ref="inputRef">
</template>