Vue3に触れてみる
だいぶ昔にVue2にちょろっと触れて、今はVueのこと完全に忘れてReactばかり触ってるからVue3にも少し触れてみる。
食べ比べ。
ここから読み進める。
Vue の他のインストール方法については、インストール ページで紹介しています。注意点として、初心者が vue-cli で始めることは推奨しません(特に Node.js ベースのビルドツールに慣れていない場合)。
こういうスタンス好き。
近年のWeb界隈は「最初から正しい方法でやるべき」と無駄に敷居を高くしてるから嫌だ。
初心者はスクリプトタグでCDNのリンク貼るだけで良いじゃん。まず手軽に体験してみることが大切。
今回自分はvue-cli使うけど。
npm install -g @vue/cli
まずはcliツールをインストール。
グローバルなのが若干気持ち悪いが、入門する時はお手本通りにやるのがベストかな。
4 vulnerabilities (2 moderate, 2 high)
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
メジャーなツールでこんだけnpm auditの警告でるの大丈夫かな…。
「はじめに」を呼んだ感じだと、Vue2との違いがあまりわからなかった。(Vue2ほとんど忘れてるけど)
この次の章から本気だす感じなのだろうか。
とりあえず、 ルートコンポーネントは他のコンポーネントとはなにも違いはないことを認識しておいてください。設定オプションは同じで、対応するコンポーネントインスタンスの振る舞いも同じです。
美しい。
//
this
は vm インスタンスを指す
アロー関数 (opens new window)をオプションのプロパティやコールバックに使用しないでください。これは例えば、created: () => console.log(this.a) や vm.$watch('a', newValue => this.myMethod()) のようなことです。アロー関数は this を持たないため、this は他の変数のように親のスコープ内を辞書探索され、しばしば Uncaught TypeError: Cannot read property of undefined や Uncaught TypeError: this.myMethod is not a function のようなエラーを起こします。
これハマりそう。
以前Vueを触った時の記憶がおぼろげなので「こんなに表記面倒くさかったっけ?」と思ったけど、省略形みて「あーこれこれ」と思った。
<!-- 完全な構文 -->
<a v-bind:href="url"> ... </a>
<!-- 省略記法 -->
<a :href="url"> ... </a>
<!-- 完全な構文 -->
<a v-on:click="doSomething"> ... </a>
<!-- 省略記法 -->
<a @click="doSomething"> ... </a>
/*
目次見てみたけどお目当てのComposition APIはだいぶ後の説明になりそうだった。そこだけ見るのもありではあるけど、とりあえず今は目次通り読み進めてみる。
*/
テンプレートから呼び出されたメソッドは、データの変更や非同期処理の発火などの副作用があってはなりません。もしそのようなことをしたくなったら、代わりに ライフサイクルフック を使うべきです。
同じ関数を算出プロパティではなくメソッドとして定義することもできます。結果だけ見れば、この 2 つのアプローチはまったく同じになりますが、算出プロパティはリアクティブな依存関係に基づいてキャッシュされるという違いがあります。算出プロパティはリアクティブな依存関係の一部が変更された場合にのみ再評価されるのです。つまり、author.books が変更されなければ、算出プロパティの publishedBooksMessage に複数回アクセスしても関数は再実行されず、前回計算した結果がすぐに返されます。
これすき。
算出プロパティはデフォルトでは getter 関数のみですが、必要に応じて setter 関数を設定することもできます:
これ知らなかった。
ほとんどの場合、算出プロパティの方が適切ですが、カスタムウォッチャが必要な場合もあります。そのため Vue は、データの変更に反応するためのより汎用的な方法を、watch オプションによって提供しています。これはデータを変更するのに応じて非同期処理や重い処理を実行したい場合に最も便利です。
あんまり歓迎されてない機能の匂いがする。
テキストボックスの値を監視する場合、onChangeとどっちがよく使われてるんだろう。
<div :class="{ active: isActive }"></div>
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
Reactとだいたい同じ感じか。
:style で ベンダープレフィックス (opens new window)が必要な CSS プロパティを使用するとき、Vue は自動的に適切なプレフィックスを追加します。
べんり。
React書いたあとだとv-ifみたいな書き方あまり好きじゃないなぁ。
v-if は、イベントリスナと子コンポーネント内部の条件ブロックが適切に破棄され、そして切り替えられるまでの間再作成されるため、”リアル”な条件レンダリングです。
v-if は 遅延レンダリング (lazy) です。 初期表示において false の場合、何もしません。条件付きブロックは、条件が最初に true になるまでレンダリングされません。
一方で、v-show はとてもシンプルです。要素は初期条件に関わらず常にレンダリングされ、シンプルな CSS ベースの切り替えとして保存されます。
ここだいじ。
<ul id="array-with-index">
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
</ul>
お好みでインデックスにもアクセスできると。
v-for
という表記を見るたびに『V for Vendetta』という映画を思い出す。
<div v-for="item in items" :key="item.id">
<!-- content -->
</div>
繰り返される DOM の内容が単純な場合や、性能向上のためにデフォルトの動作を意図的に頼る場合を除いて、可能なときはいつでも v-for に key 属性を与えることが推奨されます。
やはりkeyがないと罠があるのか。
<!-- クリックイベントの伝搬が止まります -->
<a @click.stop="doThis"></a>
<!-- submit イベントによってページがリロードされません -->
<form @submit.prevent="onSubmit"></form>
<!-- 修飾子は繋げることができます -->
<a @click.stop.prevent="doThat"></a>
<!-- 値を指定せず、修飾子だけ利用することもできます -->
<form @submit.prevent></form>
<!-- イベントリスナを追加するときにキャプチャモードで使います -->
<!-- 言い換えれば、内部要素を対象とするイベントは、その要素によって処理される前にここで処理されます -->
<div @click.capture="doThis">...</div>
<!-- event.target が要素自身のときだけ、ハンドラが呼び出されます -->
<!-- 言い換えると子要素のときは呼び出されません -->
<div @click.self="doThat">...</div>
気が利いてる。
<!-- `key` が `Enter` のときだけ、`vm.submit()` が呼ばれます -->
<input @keyup.enter="submit" />
ちょっとやり過ぎ感もあるけど便利ではある。
IME (opens new window)を必要とする言語 (中国語、日本語、韓国語など) においては、IME による入力中に v-model が更新を行わないことに気づくでしょう。このような更新にも対応したい場合、 v-model をつかう代わりに input イベントリスナと value のバインディングを使ってください。
さすがCJK圏の人が作ったライブラリなだけある。ありがたい。
<input v-model.number="age" type="text" />
<input v-model.trim="msg" />
気が利いてる。
エクスキューズ。
ここでは単純な例を示していますが、典型的な Vue アプリケーションでは文字列テンプレートではなく単一ファイルコンポーネントを使用します。詳しくはこちらで解説しています。
カスタムイベントふむふむ。
<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>
<button @click="$emit('enlargeText', 0.1)">
Enlarge text
</button>
コンポーネントに付ける名前は使用箇所によって異なります。DOM を直接操作する場合 (文字列テンプレートや 単一ファイルコンポーネントを除く) は、W3C rules (opens new window)に従ったカスタムタグ名を強く推奨します:
全て小文字
ハイフンを含める (複数の単語をハイフンを用いて繋げる)
コンポーネントをパスカルケースで定義する場合は、そのカスタム要素を参照する際どちらのケースも用いることができます。<my-component-name> と <MyComponentName> のどちらも利用可能です。 ですが、DOM 内で直接使用する場合 (つまり、文字列テンプレート以外の場合) はケバブケースの方が適している点に注意してください。
結局ケバブケースの方がいいのかな?
でもローカル登録のほうではパスカルケース使ってるしなぁ。
このような場合には、プロパティをオブジェクトとして列挙し、プロパティのキーと値にそれぞれプロパティの名前と型を設定します:
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // またはその他のコンストラクタ
}
このオレオレTypeScript、Typescriptとの親和性はどうなんだろう。
TSでやるときは普通にTypeで定義したいよなぁ。
すべてのプロパティは、子プロパティと親プロパティの間に 単方向のバインディング を形成します: 親のプロパティが更新される場合は子へと流れ落ちますが、その逆はありません。これにより、子コンポーネントが誤って親の状態を変更すること(アプリのデータフローを理解しづらくすることがあります)を防ぎます。
👍
ただ一つのルート要素をもつコンポーネントの場合、プロパティでない属性はルート要素にそのまま追加されます。例えば date-picker コンポーネントの場合は次のような形になります。
べんり。
コンポーネントのルート要素が 1 つでなく複数のルート要素からなる場合には、暗黙の属性の継承は行われません。$attrs を用いた明示的なアサインを行わない場合、ランタイム上で warning が発行されます。
// <main> 要素に $attrs で属性を渡しているため、 warnings は発行されません
app.component('custom-layout', {
template: `
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
`
})
Reactではルート要素は1つにしないと怒られるけど、Vueでは許容しているのか。
コンポーネントやプロパティと同じように、イベント名は大文字と小文字を自動的に変換します。子コンポーネントからキャメルケース(camelCase)でイベントを発行すると、親コンポーネントではケバブケース(kebab-case)のリスナを追加できるようになります:
ややこしい。もう全部ケバブケースで良いじゃん。
プロパティの型検証と同様に、発行されたイベントは、配列構文ではなくオブジェクト構文で定義されている場合に検証できます。
submitの例はわかりやすい。
ちょっと難しくなってきた。
<my-component v-model:title="bookTitle"></my-component>
app.component('my-component', {
props: {
title: String
},
emits: ['update:title'],
template: `
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)">
`
})
これは暗黙にはやってくれないのかな。
v-model 修飾子の処理
これ難しい。
modelModifiers
とか***Modifiers
とか約束事が増えてきた。
Vue には Web Components spec draft (opens new window)にヒントを得たコンテンツ配信 API が実装されており、 <slot> 要素をコンテンツ配信の受け渡し口として利用します。
Web Componentsと親和性が高いのは素晴らしい。
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
複数スロット持てるのはとても良い。
<ul>
<li v-for="( item, index ) in items">
<slot :item="item"></slot>
</li>
</ul>
<todo-list v-slot="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</todo-list>
スロットに子供からプロパティを渡す。
ちょっと難しくなってきた…でもこれは便利だ。覚えよう。
v-on や v-bind と同様に v-slot にも省略記法があり、引数の前のすべての部分 (v-slot:) を特別な記号 # で置き換えます。例えば、v-slot:header は #header に書き換えることができます:
便利。
<todo-list #default="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}<span>
</todo-list>
これも便利。
でも覚えること多いなぁ。
Provide /inject便利…ではあるけれど、
app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
こう書かないと罠があるって、なんか複雑だなぁ。
色々ある。パンクしそうなので「こういうのがある」という事実だけ頭の片隅に入れておこう。
const app = Vue.createApp({})
app.component('base-input', {
template: `
<input ref="input" />
`,
methods: {
focusInput() {
this.$refs.input.focus()
}
},
mounted() {
this.focusInput()
}
})
useRef相当。
後で読む
まぁ普通にCSS。
普通にCSSを適用するんじゃ駄目なんだろうか。
このあと愚直にCSSで実装して罠にハマってから「あーこれのためかー」ってなる展開か。
もうこの辺は後で読もう。
Composition API やる。
setup肥大化しただけじゃん…と思ったら、最後はきれいに分離できててほぇ~となった。
Vueもuse何々という名前で命名するんだ。
でもまだこれからの章でもっとシンプルになるんじゃないかと期待してる。
setup が実行されるとき、以下のプロパティにのみアクセスできるようになります:
props
attrs
slots
emit
言い換えると、以下のコンポーネントオプションには アクセスできません:data
computed
methods
refs (template refs)
これは後で「setupがあればdataとかmethodsとか全部いらなくなります。そういうのは過去の遺物です」という展開になるフラグかな?
いろんなものを置き換えてる。
Provide / Injectの使い方はだいぶシンプルになった。
リアクティブな provide / inject の値を使う場合、リアクティブなプロパティに対しての変更は可能な限り 提供する側 の内部に留めることが推奨されます
しかし、データが注入されたコンポーネント側でデータを更新する必要がある場合もあります。そのような場合、リアクティブなプロパティを更新する責務を持つメソッドを提供することを推奨します。
まぁそりゃそうだよね。
JSX での使用例
export default {
setup() {
const root = ref(null)
return () =>
h('div', {
ref: root
})
// JSX 記法
return () => <div ref={root} />
}
}
いきなりJSX出てきた。
これでかなりReactに近づいた。
VueはJSXどのくらい推してるんだろう。
「JSXも使えるよ」なのか「これからはJSX使おう」なのか。
これは悪名高き複数継承に近いのでは…
これは後でやろう。
<teleport>
べんり!
JSX使わない場合、render関数ってかなり苦行に見える。
import AnchoredHeading from './AnchoredHeading.vue'
const app = createApp({
render() {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
app.mount('#demo')
うん、JSXはすっきり。
関数型コンポーネント
これもうほとんどReactだ。
でもこのやり方を推奨してるのか、あんまりオススメしてないのか、いまいち分からない。後者かな。
なかなか難しい。
プラグインを作りたくなったら読み返す。
これは後で読む。
うーん
「最近のvueは<script setup>
で書くのが主流」とどっかで見たような気がするけど、そのへんの記述が公式の入門には見当たらない。
結局何がベストプラクティスなんだろ。
このあたりかな。
APIリファレンスにあった。
<script setup> を使用する場合、<script setup> 内で宣言されたトップレベルのバインディング(変数、関数宣言、インポートを含む)は、テンプレートで直接使用できます:
これこれ。
これで書き味がReactに近づく。
なんかもうscript setupだけ別フレームワークな印象がある。
Typescriptとの組み合わせは…ちょっと違和感あるなぁ。
TypeScriptに関しては以下のようにかけるらしい。これなら良し。
interface Props {
value: string;
label?: string;
type?: "text" | "password" | "email" | "number";
placeholder?: string;
disabled?: boolean;
}
const props = defineProps<Props>();
実践
viteでプロジェクトを作ってみる
npm create vite@latest
Need to install the following packages:
create-vite@latest
Ok to proceed? (y) y
√ Project name: ... minesweeper_tag
√ Select a framework: » Vue
√ Select a variant: » TypeScript
vue-cliと違ってこっちは<script setup>
使ってて好み。
main.tsでこんなエラーがでる。
Cannot find module './App.vue' or its corresponding type declarations.
You should install TypeScript Vue Plugin and Vue Language Features.
これ入れたらエラーが消えた。
1行=1行でReactの既存のWebアプリケーションを移植できそう。
これは良い書き味。
過去にReactで作ったマインスイーパーを移植してWebCompornents化した。
やはり書き味はReactに似ている。
Composition APIとscript setupだけ使っていれば、Reactとの文化ギャップは最小限で済みそう。
ただWeb Compornents化では以下の点に躓いた。
子コンポーネント内で定義したCSSが無視される
これ将来のバージョンで改善されてくれないかなぁ。
型定義を厳密に定めていても、問答無用で文字列型が来る
Web Compornentに型定義なんて無いから、型を無視してパラメータが渡ってくる。
そして
"100" + 20 = "120"
のような分かりにくい不具合が起きて頭を抱えた。
TypeScriptはあくまでコンパイル時のみのチェックしかしてくれない。
気になった記事
Vapor コンポーネントのみでアプリを構築すると、バンドルから Virtual DOM ランタイムを削除でき、ベースラインのランタイムサイズを大幅に削減することができます。
最高のパフォーマンスを達成するために、Vapor ModeはVueの機能のサブセットのみをサポートします。特に、Vapor Modeのコンポーネントは、Composition APIと<script setup>のみをサポートします。しかし、このサポートされるサブセットは、Vaporと非Vaporのコンポーネント間で全く同じように動作します。
将来このモードが実現すると、VueでWeb Compornentsを作る上でネックだったバンドルサイズの問題がなくなるのかな。
へーこういう流れが来てるのか。
たしかに開発者の目からDOMが十分に隠蔽されていれば、仮想DOMが使われようが直接DOMが使われようがどっちでも良いしなぁ。