前言#
こんにちは、ここは三咲智子、Vue コアチームのメンバーです。Vue 3.3 のこのリリースは主に DX(開発者体験)を改善するためのもので、いくつかのシンタックスシュガーとマクロ、そして TypeScript の改善が追加されました。
- ジェネリックコンポーネント
- SFC(単一ファイルコンポーネント)で外部 TS タイプをインポート
defineSlots
でスロットのタイプを定義defineEmits
より便利なシンタックスdefineOptions
コンポーネントオプションの定義- (実験的)リアクティブな props デストラクチャリング
- (実験的)
defineModel
シンタックスシュガー - リアクティビティトランスフォームの廃止
昨年、Vue に参加したばかりの頃、私は Vue 3 に PR を貢献していましたが、最近やっと Vue 3.3 に実装されました。Vue 3.3 は Vue Macros から合計五六個の機能を吸収しました。今日は私が貢献した部分についてお話しします。
defineOptions マクロ#
- PR: https://github.com/vuejs/core/pull/5738
- RFC: https://github.com/vuejs/rfcs/discussions/430
- 開発実録:https://www.bilibili.com/video/BV1uu411y7WE
前情提要#
<script setup>
がない頃を思い出してみてください。props
や emits
を定義するために、setup
と同じレベルのプロパティを簡単に追加できました。しかし、<script setup>
を使うようになってからは、そうすることができなくなりました —— setup
プロパティはもう存在しないので、同じレベルのプロパティを追加することはできません。この問題を解決するために、defineProps
と defineEmits
という二つのマクロを導入しました。
しかし、これは props
と emits
の二つのプロパティだけを解決しました。コンポーネントの name
、inheritAttrs
または他のカスタムプロパティを定義する必要がある場合、最初の方法に戻らなければなりません —— 普通の <script>
タグを追加することです。これでは二つの <script>
タグが存在することになります。私にとっては、これは受け入れられません。
- 二つの
script
タグは ESLint プラグインや Volar に予期しない問題を引き起こす可能性があります; - 両方の
script
タグにインポート文が存在する場合、Vue は奇妙な特別処理を行います; - DX(開発者体験)にとっては、面倒で理解しにくいです。
现状#
そこで、Vue 3.3 では新たに defineOptions
マクロを導入しました。名前の通り、主に Options API のオプションを定義するために使用されます。defineOptions
を使用して任意のオプションを定義できますが、props
、emits
、expose
、slots
は除外されます(これらは defineXXX を使用して実現できます)。<template>
タグを使わずに、defineOptions
内で h 関数や JSX を使ってレンダリング関数 render
を記述することもできます(もちろん、これは推奨されません)。
🌰 例子#
<script setup>
defineOptions({
name: 'Foo',
inheritAttrs: false,
// ... その他のカスタムプロパティ
})
</script>
背后的故事#
この機能の起源は、Element Plus のコンポーネントを <script setup>
にリファクタリングしているときに生まれました。コンポーネントライブラリとして、私たちはカスタムコンポーネントの名前(name
プロパティ)をデフォルトのファイル名ではなく、指定したいと考えました。しかし、元の書き方に戻りたくはなかったので、プラグイン(夢の始まり 🤣)unplugin-vue-define-options
を作成しました。何度も修正を重ねて、現在の Vue 3.3 にある defineOptions
マクロが確定し、実装されました。
提升静态常量#
この機能は SFC(単一ファイルコンポーネント)コンパイラの最適化です。script
部分に hoistStatic
オプションが新たに追加されました。
模板中的 hoistStatic
#
template
下の hoistStatic
は基本的に同じです。Vue コンパイラには最適化があり、静的な要素ノードをトップレベルスコープに昇格させることができます。これにより、コードが読み込まれるときに一度だけ実行され、render
関数が実行されるたびに繰り返し実行されることはありません(極端な場合には欠点もあります)。
例を見てみましょう 🌰。
<template>
<div id="title">Hello World</div>
</template>
このコードは以下の JavaScript にコンパイルされます(重要でないコードは省略されています)。
const _hoisted_1 = { id: 'title' }
function render(_ctx, _cache) {
return _openBlock(), _createElementBlock('div', _hoisted_1, 'Hello World')
}
ここで、_hoisted_1
変数はコンパイラによって意図的にトップレベルに昇格されたコードです。この機能を無効にすると、render
関数内で実行されます。
script タグの hoistStatic#
Vue 3.3 以前は、上記の機能だけでした。Vue 3.3 では、同様の最適化を行いました —— もし定数の値がプリミティブ値(基本型、すなわち string, number, boolean, bigint, symbol, null, undefined)であれば、その定数宣言はトップレベルに昇格されます。(注:symbol は現在実装されていません)
これらの型の定数は変更できないため、どこで宣言されても効果は同じです。
作用#
この機能は性能の最適化だけでなく、さらに有用な点があります。Vue 3.3 以前は、マクロを使用する際に <script setup>
ブロック内で定義された変数を渡すことができませんでした。以下の例を見てみましょう。
<script setup>
const name = 'Foo'
defineOptions({
name,
})
</script>
エラーが発生します。
[@vue/compiler-sfc] `defineOptions()` in <script setup> cannot reference locally declared variables because it will be hoisted outside of the setup() function. If your component options require initialization in the module scope, use a separate normal <script> to export the options instead.
これは、前述の理由によるもので、defineProps
は setup
と同じレベルの props
プロパティを追加し、name
定数は setup
関数内で宣言されているためです。関数外でその内部の変数を参照することはできません。以下のコードは間違いなくエラーです。
const __sfc__ = {
props: [propName],
setup(__props) {
const propName = 'foo'
},
}
Vue 3.3 以降、4 行目の propName
は最初の行に昇格されるため、コードは完全に理解可能になります。この機能はデフォルトで有効になっています。通常、開発者はこの機能が存在することを意識する必要はありません。
背后的故事#
この機能を開発した理由も前のものと似ています。Element Plus では defineOptions
で name
を設定した後、特定の条件下で例外をスローする必要がありました。例外にはコンポーネントの名前が必要で、ユーザーがデバッグしやすくするためです。
<script setup>
const name = 'ElButton'
defineOptions({
name,
})
// ...
if (condition) {
throw new Error(`${name}: something went wrong.`)
}
</script>
そのため、重複したコードを避け、コンポーネント名を定数として抽出し、defineOptions
と例外をスローする際にそれを参照したいと考えました。
defineModel マクロ#
- PR: https://github.com/vuejs/core/pull/8018
- RFC: https://github.com/vuejs/rfcs/discussions/503
- 開発実録:第一期、第二期
- Twitter: https://twitter.com/sanxiaozhizi/status/1644564064931307522
动机#
このマクロは純粋なシンタックスシュガーです。Vue 3.3 以前は、双方向バインディングの prop を定義するのは非常に面倒なことでした。
<script setup lang="ts">
const props = defineProps<{
modelValue: number
}>()
const emit = defineEmits<{
(evt: 'update:modelValue', value: number): void
}>()
// 値を更新
emit('update:modelValue', props.modelValue + 1)
</script>
props
を定義し、次に emits
を定義する必要があります。その中には多くの重複したコードがあります。この値を変更する必要がある場合は、手動で emit
関数を呼び出す必要があります。
私は考えました:このステップを簡素化するために関数(マクロ)をラップしない理由は何ですか?それが defineModel
マクロの誕生です。
🌰 例子#
<script setup>
const modelValue = defineModel()
modelValue.value++
</script>
上記の煩雑な 7 行のコードは、Vue 3.3 では 2 行に短縮できます!
与 useVModel
的区别#
VueUse には useVModel
関数もあり、類似の効果を実現できます。なぜこのマクロを導入する必要があるのでしょうか?
これは、VueUse が単に props
と emit
関数を組み合わせて Ref
にラップしただけであり、コンポーネントの props
と emits
を定義することはできないからです。つまり、開発者は依然として手動で defineProps
と defineEmits
を呼び出す必要があります。
背后的故事#
😛 この背後には特にストーリーはありません。単純に書くのが面倒だと感じたからです。最初は Vue Macros で defineModel
のマクロを作成しました(現在は defineModels
に改名され、公式と区別されています)。使ってみたところ、なかなか良い結果が得られました。
在 SFC 导入外部类型#
背景#
Vue 3.2 のリリース以来、Vue には最も注目されている issue がありました ——defineProps で外部の型を使用する方法。
この問題を解決するために、私たちの前には二つの道がありました。
- Vue SFC コンパイラが TypeScript コンパイラを呼び出し、最終的な型を計算し、それがどの型を含むかを特定する(
String
,Number
,Boolean
,Function
など)。 - 簡易的な TypeScript 分析器を自分たちで実装し、大部分のシナリオを解決する。
ご存知の通り、TypeScript の型体操は非常に恐ろしいものです。この issue を完璧に解決するためには、Vue の SFC コンパイラが TypeScript コンパイラのようにすべての型を解析し、計算する必要があります。最初のアプローチは良いですが、巨大で肥大化した TypeScript コンパイラに依存することになり、ビルド時間が大幅に遅くなるという大きな欠点があります。
最終的に、Vue Macros では第二のアプローチを選択しました。そのため、現在はマクロ内で複雑な型を渡すことは適していません。しかし、これは今後徐々に改善される予定です。
Vue 3.3 与 Vue Macros#
Vue Macros がこの簡易分析器を実装した後、Vue コアも同様の実装を行いました。
しかし、現時点では依然として違いがあります。Vue Macros のイテレーション速度は Vue コア自体を遥かに上回っており、Vue Macros は現在、より多くの奇妙なシンタックスをサポートしていますが、Vue 3.3 では一部のシンタックスがまだサポートされていません。
したがって、将来的に Vue が解析できない型に遭遇した場合は、Vue Macros を試してみると良いでしょう。もし Vue Macros でもうまくいかない場合は、最小限の再現コードを添えて、Vue Macros リポジトリに issue を提起するか、よりシンプルで二次推論を避けるシンタックスを試してみてください。
defineSlots マクロ#
- PR: https://github.com/vuejs/core/pull/7982
- Twitter: https://twitter.com/sanxiaozhizi/status/1641378248448937984
背景#
Vue 3.3 では defineSlots
マクロが新たに追加されました。defineSlots
を使用して、自分でスロットの型を定義できます。このマクロはシンプルなコンポーネントではほとんど使われませんが、複雑なコンポーネントには非常に役立ちます。特に、この機能はジェネリックコンポーネントと一緒に使用する場合や、Volar が型を正しく推測できない場合に手動で指定することができます。
🌰 例子#
<script setup lang="ts">
const slots = defineSlots<{
default(props: { foo: string; bar: number }): any
}>()
</script>
私たちは default
コンポーネントのスロットスコープの型を手動で定義しました。
🌰 真的例子#
例えば、ページネーターコンポーネントがある場合、スロットを通じて具体的な item
のレンダリング方法を制御できます。
<script setup lang="ts" generic="T">
// 子コンポーネント Paginator
defineProps<{
data: T[]
}>()
defineSlots<{
default(props: { item: T }): any
}>()
</script>
<template>
<!-- 親コンポーネント -->
<Paginator :data="[1, 2, 3]">
<template #default="{ item }">{{ item }}</template>
</Paginator>
</template>
data
パラメータを渡すと、それは number[]
であるため、item
も number
として推測されます。item
の型は data
パラメータに渡された型に応じて変化します。
defineEmits 更便捷的语法#
- PR: https://github.com/vuejs/core/pull/7992
- Twitter: https://twitter.com/youyuxi/status/1641403989026820098
この機能も純粋なシンタックスシュガーです。
例子#
<script setup lang="ts">
const emits = defineEmits<{
(evt: 'update:modelValue', value: string): void
(evt: 'change'): void
}>()
// ⬇️ Vue 3.3 以降
const emits = defineEmits<{
'update:modelValue': [value: string],
'change': []
}>()
</script>
Vue 3.3 以前は、もう少し文字を多く書く必要がありましたが、今は省ける部分は省けます。
废弃 Reactivity Transform 语法糖#
早い段階で、Vue チームは Reactivity Transform シンタックスを廃止することを発表しました。具体的には、Vue 3.3 で廃止(deprecated)され、使用時に警告が表示され、Vue 3.4 では完全に削除される予定です。
私個人としては、Reactivity Transform にはまだ使い道があると思っています。
公式に廃止されましたが、その機能は Vue Macros に移動されました。これは、古いシンタックスに急いで移行する必要がなく、プラグイン を使用することで、正常に使用し続け、今後のバグ修正も受けられることを意味します。
具体的に廃止された理由については、こちらのコメントを読むことができます。
后记#
実際のところ、Vue がコミュニティからの提案や提案を受け入れる姿勢を見られることに非常に嬉しく思っています。これが私が Vue 3.3 に貢献した内容です。その他の機能については、Vue ブログの記事を読むことができます。
P.S. もし誰かがこの記事を英語に翻訳してくれるなら、翻訳が完了したら sxzz/articles リポジトリに内容を提出していただけると大変感謝します!
关于 Vue Macros#
Vue Macros は現在、Vue 公式とは独立したプロジェクトです。Vue 公式とは異なり、その目的は異なる可能性を探求することです。
私は、たとえそのアイデアが成熟していなくても、より過激な考えを見たいと思っています。私たちは Vue Macros でさらに実験を行い、成熟したら公式リポジトリに統合することを試みることができます。
現在、Vue Macros は私一人でメンテナンスしており、コミュニティからの仲間の参加をもっと引き寄せたいと思っています! 💕