とほほのVue 3入門

目次

Vue 3とは

インストール

オンライン環境で試す

インストールすることなくオンラインで Vue を試す環境がいくつか用意されています。

CDNを使用する方法

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">
  {{ message }}
</div>
<script>
const { createApp, ref } = Vue
createApp({
  setup() {
    const message = ref("Hello world!")
    return { message }
  }
}).mount("#app")
</script>

もしくは ES modules を用いて次のように書くこともできます。

<div id="app">
  {{ message }}
</div>
<script type="module">
import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
createApp({
  setup() {
    const message = ref("Hello world!")
    return { message }
  }
}).mount('#app')
</script>

インポートマップを用いて 'https://unpkg...''vue' とだけ記述できるようになります。

<script type="importmap">
  { "imports": { "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js" } }
</script>
<script type="module">
  import { createApp, ref } from 'vue'
    :

npmを使用する方法

$ podman run -dit --name vue -p 80:80 -v `pwd`/vue:/opt/vue almalinux:9
$ podman exec -it --detach-keys=ctrl-\\ vue /bin/bash
$ dnf -y install git
$ git clone https://github.com/creationix/nvm.git ~/.nvm
$ echo "source ~/.nvm/nvm.sh" >> ~/.bashrc
$ source ~/.bashrc
$ nvm ls-remote
$ nvm install v20.18.0
$ npm create vue@latest
※すべてデフォルト値を選択
$ cd vue-project
$ npm install
$ npm run dev

ホストアドレスやポート番号を変更するには vite.config.js ファイルに下記を追記します。

export default defineConfig({
   :
  server: {
     host: "0.0.0.0",
     port: 80,
  }
})

ビルドするには下記を実行します。ビルドした結果が dist フォルダに作成されます。

$ npm run build

Apache (httpd) から dist フォルダを参照します。

# dnf -y install httpd
# sed -i 's/#ServerName/ServerName/' /etc/httpd/conf/httpd.conf
# vi /etc/httpd/conf.d/vue.conf
# /usr/sbin/httpd -DFORGROUND
<VirtualHost *:80>
  ServerName www.example.com
  DocumentRoot /opt/vue/vue-project/dist
  <Directory "/opt/vue/vue-project/dist">
    Require all granted
  </Directory>
</VirtualHost>

チュートリアル

main.js

src/main.js を下記の様に書き換えます。App.vue を id="app" の要素に割り当てます。

import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')

App.vue

*.vue ファイルは 単一ファイルコンポーネント (SFC: Single File Component) と呼ばれるもので、JavaScript (または TypeScript) を記述する script ブロック、HTML を記述する template ブロック、CSS を記述する style ブロックから構成されます。src/App.vue を下記の様に書き換えてみましょう。

<script setup>
  import { ref } from 'vue'
  const message = ref("Hello world!")
</script>

<template>
  <h1>{{ message }}</h1>
</template>

<style scoped>
  h1 { font-size: 40pt; }
</style>

npm run dev でサーバーを起動し、ブラウザからアクセスし、Hello world! が表示されれば成功です。

テキスト展開({{...}})

下記の様に値 "Hello world!" を持つリアクティブなオブジェクト(ref)を定義します。

const message = ref("Hello world!")

テンプレートで {{ ... }} を記述することで ref の値を表示します。

<h1>{{ message }}</h1>

下記の様に宣言しても {{ ... }} で表示することはできますが、message の値を変更してもリアクティブに表示に反映することができません。

var message = "Hello world!"

ref の値を変更するには message を直接変更するのではなく、message.value の値を変更します。.valuesetter が値の変更を感知し、変更を表示に反映してくれます。

message.value = "Bye!"

{{ ... }} の中では JavaScript を記述することができます。

<h1>{{ message.toUpperCase() + "!" }}</h1>

イベントハンドラー(@click)

@eventName でイベントハンドラーを呼び出すことができます。Up ボタンを押すたびにカウントアップしていきます。

<script setup>
  import { ref } from 'vue'
  const count = ref(0)
  function countUp() { count.value++ }
</script>
<template>
  {{ count }}
  <button @click="countUp">Up</button>
</template>
{{ count }}

子コンポーネント

子コンポーネントを呼び出す

まず、子コンポーネント (例: Child.vue) を用意します。

Child.vue
<template>
  <div>THIS IS CHILD</div>
</template>
<style scoped>
  div { color: #f00 }
</style>

親コンポーネントからこれを読み込みます。

App.js
<script setup>
  import Child from './Child.vue'
</script>
<template>
  <Child />
</template>

子コンポーネントに値を渡す

親コンポーネントから子コンポーネントに属性を渡すには次のようにします。

App.js
<script setup>
  import Child from './Child.vue'
</script>
<template>
  <Child message="MY MESSAGE" />
</template>
Child.vue
<script setup>
  const props = defineProps(['message'])
</script>
<template>
  <div>{{ props.message }}</div>
</template>

子コンポーネントにコンテンツを渡す

スロット を用いることで子コンポーネントにコンテンツを渡すことができます。

App.vue
<script setup>
  import Child from './Child.vue'
</script>
<template>
  <Child>Hello!!</Child>
</template>
Child.vue
<template>
  <slot></slot>    // Hello!!
</template>

子コンポーネントからイベントを受け取る

下記の様にして子コンポーネントからカスタムイベントを受け取ることができます。

Child.vue
<template>
  <button @click="$emit('my-click')">OK</button>
</template>
App.vue
<script setup>
  import { ref } from 'vue'
  import Child from './Child.vue'
  const count = ref(0)
</script>
<template>
  <Child @my-click="count++" />
  {{ count }}
</template>

汎用コンポーネント

下記の様にコンポーネントを登録しておくことで、アプリケーション内部のどの場所でも、登録したコンポーネントを import 無しに利用することができます。

-- main.js --
import { createApp } from 'vue'
import App from './App.vue'
import MyButton from './MyButton.vue'
const app = createApp(App)
app.component("MyButton", MyButton)
app.mount('#app')

ディレクティブ

テキストを表示する(v-text, {{...}})

指定したプロパティの値を表示します。v-text="..." は {{ ... }} と同じ意味を持ちます。

-- script --
const message = ref("Hello world!")
-- template --
<div v-text="message"></div>
<div>{{ message }}</div>

HTMLを表示する(v-html)

&, <, > をサニタイジングせず生の HTML として表示します。利用者が入力した文字列を HTML として表示してしまうとクロスサイトスクリプト問題(XSS) が発生してしまうため、使用には十分注意してください。

-- script --
const message = ref("<b>Hello world!</b>")
-- template --
<div v-html="message"></div>

表示/非表示を切り替える(v-show)

真偽値によって表示する・しないを切り替えます。

-- script --
const flag = ref(true)
function toggle() { flag.value = !flag.value }
-- template --
<button @click="toggle">Toggle</button>
<div v-show="flag">Hello world!</div>
Hello world!

条件により表示を切り替える(v-if/v-else-if/v-else)

if / else if / else の条件判断により表示を制御します。

-- script --
const num = ref(5)
const ans = ref(5)
-- template --
<div v-if="num < ans">Too small.</div>
<div v-else-if="num > ans">Too big.</div>
<div v-else>Correct!</div>

v-showv-if は見た目上同じような動作になりますが、v-show は条件の真偽に関わらずレンダリングを行いその表示・非表示を CSS で切り替えるのに対し、v-if は偽の時はレンダリング自体をスキップし、真になった時点でレンダリングを行います。

<div v-show="flag">...</div>
<div v-if="flag">...</div>

繰り返し(v-for)

リストやイテラブルオブジェクトに対して繰り返し処理を行います。

-- script --
const users = ref([
  { name: "Yamada", age: 36 },
  { name: "Suzuki", age: 42 },
  { name: "Tanaka", age: 53 },
])
-- template --
<div v-for="user in users">
  {{ user.name }} {{ user.age }}
</div>

下記の様にして「10回繰り返す」を実装することもできます。

<div v-for="n in 10">...</div>

オブジェクトに対して key, value を列挙することもできます。

-- script --
const user = ref({ name: "Yamada", age: 36 })
-- template --
<div v-for="(value, key) in user">
  {{ key }}: {{ value }}
</div>

下記の様にループのインデックス番号(index)を参照することができます。

<div v-for="(user, index) in users">
  {{ index }}: {{ user.name }}({{ user.age }})
</div>
<div v-for="(value, key, index) in user">
  {{ index }}: {{ key }}: {{ value }}
</div>

イベントをハンドリングする(v-on, @)

イベントハンドラーを呼び出します。@v-on: の省略形です。

-- script --
const count = ref(0)
function onClick() { count.value++ }
-- template --
<button v-on:click="onClick">Up</button>
<button @click="onClick">Up</button>
<div>{{ count }}</div>

インラインハンドラーとメソッドハンドラー

イベントハンドラーには メソッドハンドラーインラインハンドラー 形式があります。メソッドハンドラー形式はメソッド名のみを記述します。ハンドラーは暗黙の引数 event を受け取ります。

-- script --
function onClick(event) { console.log(event.target); count.value++ }
-- template --
<button @click="onClick">Click</button>

インラインハンドラー形式は JavaScript 構文を記述します。下記の様にメソッドに引数を渡すことができます。event を引数として渡したい時は $event と記述します。

<button @click="onClick(123)">Click</button>
<button @click="onClick(123, $event)">Click</button>

インラインハンドラーの中で直接 ref を書き換えることもできます。この場合 .value は不要です。

<button @click="count++">Up</button>

イベント修飾子

下記の様にイベント名の後に .once.prevent などの修飾子を加えることができます。

<button @click.once="onClick">Click</button>

キー修飾子

@keyup などのキーイベントに対してはキーを指定することができます。キー名は KeyboardEvent.key(↗) 値 (例: PageDown) をケバブケース (例: page-down) に置換したもの、または省略形 (.enter, .tab, .delete, .esc, .space, .up, .down, .left, .right, .ctrl, .alt, .shift, .meta) を指定します。

<input @keyup.page-down="onPageDown">
<input @keyup.alt.enter="onAltEnter">

.exact は Ctrl, Alt, Shift, Meta キーの押下を厳密に判断します。

<!-- Ctrl, Alt, Shift, Meta キーが押されていても発火 -->
<button @click.ctrl="onClick">OK</button>
<!-- Ctrl, Alt, Shift, Meta キーが押されてないときだけ発火 -->
<button @click.exact="onClick">OK</button>
<!-- Ctrl キーのみが押されている場合に発火 -->
<button @click.ctrl.exact="onCtrlClick">OK</button>

マウスボタン修飾子

@click は通常マウスの左ボタンクリックに反応しますが、右ボタンや中央ボタンに反応させることができます。

<button @click="onLeftButtonClick">OK</button>
<button @click.left="onLeftButtonClick">OK</button>
<button @click.right="onRightButtonClick">OK</button>
<button @click.middle="onMiddleButtonClick">OK</button>

動的イベント名

@[eventName] でイベント名を変数で指定することができます。下記のサンプルでは最初は @click に反応しますが、一度クリックすると @dblclick に反応するようになります。

-- script --
const eventName = ref("click")
function onEvent() { console.log(eventName.value); eventName.value = "dblclick" }
-- template --
<button @[eventName]="onEvent">{{ eventName }}</button>

属性バインド(v-bind)

属性値のバインド(:attr)

v-bind:attrName は属性値をバインドします。省略形は :attrName です。

-- script --
const url = ref("https://www.example.com")
-- template --
<a v-bind:href="url">Link</a>
<a :href="url">Link</a>

イメージ名のバインド(:src)

画像は /public に配置する方法、/assets に配置する方法があります。

<img src="/public/img/sample.png">
<img src="./assets/img/sample.png">
<img src="@/assets/img/sample.png">
<img src="~@/assets/img/sample.png">

:src でバインドするには下記の様にします。古い書き方で require() を用いる例も紹介されていますが、require() はブラウザ上の JavaScript ではサポートされておらず、Native ES Modules を使用する Vite では使用することができません。

-- script --
import sampleImg1 from "/public/img/sample.png"
import sampleImg2 from "@/assets/img/sample.png"
-- template --
<img :src="sampleImg1">
<img :src="sampleImg2">

クラス名のバインド(:class)

下記は v-bind を使用せずに普通にクラス名を割り当てる例です。

-- template --
<div class="red">Hello</div>
-- style --
.red { color: red }

v-bind:class (省略形 :class) を用いると次のようになります。colorClass の値が変わるとリアクティブに反映されます。

-- script --
const colorClass = ref("red")
-- template --
<div :class="colorClass">Hello</div>

isError が true の時だけ red クラスを割り当てるには次のようにします。

<div :class="{'red': isError}">Hello</div>
<div :class="{'red': isError == true}">Hello</div>

複数のクラスを指定する場合は [...] を用います。

<div :class="['red', 'bold']">Hello</div>
<div :class="[colorClass, boldClass]">Hello</div>

[...] 内では三項演算子を使用することもできます。

<div :class="[isError ? 'red' : 'normal']">Hello</div>
<div :class="[isError ? redClass : normalClass]">Hello</div>

下記の様にして適用するクラスを動的に変更することができます。

-- script --
import { ref, computed } from 'vue'
const textClass = ref({'red':false, 'bold':false})
function onRed() { textClass.value.red = !textClass.value.red }
function onBold() { textClass.value.bold = !textClass.value.bold }
-- template --
<div :class="textClass">Hello</div>
<button @click="onRed">Red</button>
<button @click="onBold">Bold</button>
-- style --
.red { color: red; }
.bold { font-weight: bold; }

スタイルのバインド(:style)

スタイルを指定する際は次のように指定します。ハイフンを含む CSS プロパティ名はキャメルケースにするか、'...' で囲みます。

<div :style="{color: 'red', fontWeight: 'bold'}">Hello</div>
<div :style="{color: 'red', 'font-weight': 'bold'}">Hello</div>

属性値をリアクティブに変更することもできます。

-- script --
const textColor = ref("red")
-- template --
<div :style="{color: textColor}">Hello</div>

下記の様に指定することもできます。

-- script --
const textStyle = ref({color: 'red', fontWeight: 'bold'})
-- template --
<div :style="textStyle">Hello</div>

[...] で複数のスタイルオブジェクトを指定することもできます。

-- script --
const redStyle = ref({color: 'red'})
const boldStyle = ref({fontWeight: 'bold'})
-- template --
<div :style="[redStyle, boldStyle]">Hello</div>

プロパティ名が -webkit- などのベンダプレフィックスを持つ場合、これを省略することができます。下記の例で line-clamp'-webkit-line-clamp' と記述すべきですが、省略しても Vue が適切に保管してくれます。

const textStyle = ref({
  display: '-webkit-box',
  width: '10rem',
  lineClamp: 3,
  boxOrient: 'vertical',
  overflow: 'hidden',
})

値に関しては補完してくれませんが、'box' がサポートされていなければ '-webkit-box' としたい場合、次の様に記述することができます。

const textStyle = ref({
  display: ['box', '-webkit-box'],
    :

複数属性のバインド

下記の様にして複数の属性をまとめてバインドすることができます。

-- script --
const attrs = { id: "div1", class: "classA", style: "color:red" }
-- template --
<div v-bind="attrs">...</div>

バンドル修飾子(.camel, ...)

.camel は属性名をキャメルケースに変換します。下記の例は viewBox="0 0 100 100" に変換します。

<svg :view-box.camel="'0 0 100 100'"></svg>

モデル(v-model)

v-model は JavaScript から HTML、HTML から JavaScript への相互変換を行います。JavaScript の値が変わると画面上の表示が変わり、画面上の表示を編集すると JavaScript の値に反映されます。<input>, <select>, <textarea> で利用できます。

-- script --
const message = ref("Hello!")
-- template --
<div><input type="text" v-model="message"></div>
<div>{{ message }}</div>
{{ message }}

チェックボックスや複数選択(select multiple)の場合、下記の様に配列に割り当てることができます。

-- script --
const animals = ref([])
-- template --
<label><input type="checkbox" value="Cat" v-model="animals">Cat</label>
<label><input type="checkbox" value="Dog" v-model="animals">Dog</label>
<label><input type="checkbox" value="Fox" v-model="animals">Fox</label>
<div>{{ animals }}</div>
{{ animals }}

ラジオボタンの場合は文字列に割り当てます。

-- script --
const animal = ref("")
-- template --
<label><input type="radio" value="Cat" v-model="animal">Cat</label>
<label><input type="radio" value="Dog" v-model="animal">Dog</label>
<label><input type="radio" value="Fox" v-model="animal">Fox</label>
<div>{{ animal }}</div>
{{ animal }}

レイジー修飾子(.lazy)

.lazy 修飾子をつけると @input の代わりに @change イベント発行の際に値を更新します。キー入力の際ではなく、フォーカスが他の部品に移動した際などに値が変更されるようになります。

<div><input v-model.lazy="message"></div>

ナンバー修飾子(.number)

.number 修飾子をつけると JavaScript 値を更新する際に数値として更新します。

-- script --
const count = ref(123)
-- template --
<div><input v-model.number="count"></div>
<div>{{ typeof count }} {{ count }}</div>

トリム修飾子(.trim)

.trim 修飾子をつけると JavaScript 値を更新する際に前後の空白とトリムします。

-- script --
const message = ref("Hello!")
-- template --
<div><input v-model.trim="message"></div>
<pre>[{{ message }}]</pre>

スロット(v-slot)

スロットを定義します。詳細は スロット を参照してください。

子要素のコンパイルをスキップ(v-pre)

子要素の {{...}} などの記述のコンパイルをスキップします。

<div>{{ message }}</div>         ... messageの値を表示
<div v-pre>{{ message }}</div>   ... {{ message }} という文字列を表示

一度だけレンダリング(v-once)

一度だけレンダリングを行い、その後は更新を行いません。下記の例では count の値をカウントアップすると、1行目はカウントアップしますが、2行目はカウントアップしません。無駄なリアクティブ計算を抑制することで高速化に役立ちます。

<div>{{ count }}</div>         ... カウントアップする
<div v-once>{{ count }}</div>  ... カウントアップしない

メモ化(v-memo)

v-memo=[value1, value2, ...] のように記述し、前回のループの時と value1, value2, ... の値に変動があったアイテムのみを再描画します。下記の例では、item.count の値はすべてのアイテムでカウントアップしますが、item.selected の値が変動したアイテムのみ再描画し、他のアイテムの再描画をスキップします。行数の多いループを高速化するのに有効です。

<script setup>
  import { ref } from 'vue'
  const items = ref([])
  for (let i = 0; i < 10; i++) {
    items.value.push({ id: i, count: 0, selected: "" })
  }
  items.value[5].selected = "←"
  function update() {
    for (let i = 0; i < 10; i++) {
      items.value[i].count++
      items.value[i].selected = ""
    }
    items.value[Math.floor(Math.random(10) * 10)].selected = "←"
  }
</script>
<template>
  <ul v-for="item in items" v-memo="[item.selected]">
    <li>{{ item.id }}: {{ item.count }} {{ item.selected }}</li>
  </ul>
  <button @click="update">Update</button>
</template>

準備が整うまで非表示(v-cloak)

v-cloak はコンパイルの準備が整うまで要素を非表示にするために用いられます。ビルドを行う場合には必要ありません。

-- template --
<div v-cloak>
  {{ message }}
</div>
-- style --
[v-cloak] { display: none; }

グローバルAPI

アプリケーションAPI

createApp()

アプリケーションインスタンスを作成します。

import { createApp } from 'vue'
const app = createApp({ ... })

createSSRApp()

SSR hydration モードのアプリケーションインスタンスを作成します。

import { createSSRApp } from 'vue'
const app = createSSRApp({ ... })

app.mount()

DOM要素にアプリケーションインスタンスをマウントします。

const app = createApp(...)
app.mount("#app")

app.unmount()

DOM要素からアプリケーションインスタンスをアンマウントします。

app.unmount()

app.onUnmount()

DOM要素からアプリケーションインスタンスがアンマウントされた時に呼び出すコールバックを定義します。

app.onUnmount(() => { ... })

app.component()

app.component(key, value)keyvalue を割り当てます。app.component(key)key に割り当てた value を取り出します。

app.component("mykey", "myvalue")
const value = app.component("mykey")    // "myvalue"

app.directive()

カスタムディレクティブを定義します。下記の例では v-demo を作成しています。

app.directive("demo", {
  created(el, binding, vnode) { ... },
  beforeMount(el, binding, vnode) { ... },
  mounted(el, binding, vnode) { ... },
  beforeUpdate(el, binding, vnode, prevVnode) { ... },
  updated(el, binding, vnode, prevVnode) { ... },
  beforeUnmount(el, binding, vnode) { ... },
  unmounted(el, binding, vnode) { ... },
})

mountedupdated の場合に同じ動作を指定するだけでよい場合は、下記の様に簡略に記述することも可能です。

-- main.js --
app.directive("demo", (el, binding) => { el.style.color = binding.value })
-- App.vue --
<div v-color="'red'">Hello</div>

app.use()

プラグインを読み込みます。

import MyPlugin from './plugins/MyPlugin'
app.use(MyPlugin)

app.mixin()

ミックスインは非推奨となりました。

app.provide()

アプリケーションから inject() で参照可能な key-value を定義します。provide() も参照してください。

-- main.js --
app.provide("my-key", "my-value")
-- App.vue --
import { inject } from 'vue'
const myValue = inject("my-key")      // "my-value"

app.runWithContext()

アプリケーションインスタンスの様なコンテキストでコールバック関数を即座に実行します。

import { inject } from 'vue'
app.provide("message", "Hello!!")
const message = app.runWithContext(() => { return inject("message") })
console.log(message)

app.version

Vue のバージョンを返します。

console.log(app.version)

app.config.errorHandler

アプリケーションで発生するエラーをハンドリングします。テンプレートの記述誤りなどのエラーは拾ってくれないようです。

-- main.js --
app.config.errorHandler = (err, instance, info) => { console.log(err) }
-- App.vue --
function func() { throw new Error("ERROR!!!!") }

app.config.warnHandler

アプリケーションで発生する警告をハンドリングします。開発モードでのみ動作し、プロダクションモードでは無視されます。

-- main.js --
app.config.warnHandler = (msg, instance, trace) => { console.log(msg) }
-- App.vue --
{{ notExistingValue }}

app.config.performance

ブラウザの開発ツール(DevTools)によるパフォーマンストレースを有効にします。開発モードでのみ動作し、プロダクションモードでは無視されます。

app.config.performance = true

app.config.compilerOptions

実行時コンパイルのオプションを指定します。

app.config.compilerOptions.isCustomElment = tag => tag.startWith("ion-")  // ion-* をカスタムエレメントと見なす
app.config.compilerOptions.whitespace = "condense"                        // "condense" または "preserve"
app.config.compilerOptions.delimiters = ["{{", "}}"]                      // {{...}} のデリミタを指定
app.config.compilerOptions.comments = true                                // コンパイル時にコメントを残す

app.config.globalProperties

アプリケーションから参照可能なプロパティを定義します。定義したプロパティはコンポーネントインスタンスで this.propertyName で参照できます。

-- main.js --
app.config.globalProperties.myMessage = "Hello!"
-- App.vue --
<script>
  export default { data() { return { myMessage: this.myMessage } } }
</script>
<template>
  {{ myMessage }}
</template>

app.config.optionMergeStrategies

カスタムコンポーネントのオプションマージ戦略を定義します。詳細は app.config.optionMergeStrategies↗ を参照してください。

app.config.idPrefix

useId() で生成される ID のプレフィックスを指定します。

-- main.js --
app.config.idPrefix = "my-app"
-- App.vue --
console.log(useId())    // my-app-0
console.log(useId())    // my-app-1
console.log(useId())    // my-app-2

app.config.throwUnhandledErrorInProduction

開発モードでは開発者がエラーに気づくように未処理の例外がスローされますが、プロダクションモードでは利用者への影響を減らすためにログ出力のみとなります。この値を true とすることでプロダクションモードでも未処理例外をスローするようになります。

app.config.throwUnhandledErrorInProduction = true

汎用API

version

Vue のバージョン情報を返します。

import { version } from 'vue'
console.log(version)

nextTick()

リアクティブな値が変化した時、それが DOM に描画されるのを待ちます。下記の例では1つ目の console.log() ではカウントアップ前の値、2つ目の console.log() でカウントアップ後の値が表示されます。

-- script --
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
  count.value++
  console.log(document.getElementById('counter').textContent)
  await nextTick()
  console.log(document.getElementById('counter').textContent)
}
-- template --
<button id="counter" @click="increment">{{ count }}</button>

defineComponent()

<script setup> ではなく <script> を用いるケースで JavaScript (または TypeScript で)コンポーネントを定義します。本書では説明を省略します。

import { defineComponent } from 'vue'
export default defineComponent({
  props: { ... },
  setup(props) { ... },
})

defineAsyncComponent()

コンポーネントを非同期に読み込みます。デフォルトでは読み込まれず、必要になった時に読み込まれるコンポーネントを定義することができます。

import { defineAsyncComponent } from 'vue'
const AsyncChild = defineAsyncComponent(() =>
  import('./AsyncChild.vue')
)

Composition API

リアクティビティ:コアAPI

ref()

リアクティブな Ref オブジェクトを生成します。

import { ref } from 'vue'
const message = ref("Hello world!")

reactive()

リアクティブなオブジェクトを生成します。

-- script --
import { reactive } from 'vue'
const user = reactive({ name: "Yamada", age: 36 })
function increment() { user.age++ }
-- template --
{{ user.name }}({{ user.age }})      // Yamada(36)
<button @click="increment">Increment</button>

ref() と同じように利用できますが下記などの違いがあります。最近では reactive() より ref() を使用する方が推奨されているようです。

computed()

JavaScript を用いて新しい Ref オブジェクトを生成します。{{ ... }} の中にも JavaScript を記述することができますが、複雑になってきた場合は computed() でロジックを script ブロックに移します。

-- script --
import { ref, computed } from 'vue'
const num = ref(0)
const doubleNum = computed(() => num.value * 2)
-- template --
<button @click="num++">Up</button>
{{ num }} / {{ doubleNum }}

下記の様に get()set() を実装することで、num が変動した時に doubleNum を計算し、逆に、doubleNum が変動した時に num を逆計算することができます。

-- script --
import { ref, computed } from 'vue'
const num = ref(0)
const doubleNum = computed({
  get() { return num.value * 2 },
  set(newValue) { num.value = newValue / 2 }
})
-- template --
<button @click="num++">Up</button>
<button @click="doubleNum = doubleNum + 2">Up2</button>
{{ num }} / {{ doubleNum }}

computed() の第一引数には前回の値が渡されます。

computed((prev) => { ... })

readonly()

読み取り専用の ref を生成します。リファレンスなので元の値が変わると読み取り専用オブジェクトの値も変わります。読み取り専用オブジェクトの値を変更しようとすると変更は失敗し、警告が発生します。

-- script --
import { ref, readonly } from 'vue'
const count = ref(123)
const readOnlyCount = readonly(count)
function countUp1() { read.value++ }          // 両方の値がカウントアップする
function countUp2() { readOnlyCount.value++ }  // 警告が発生しカウントアップできない
-- template --
<button @click="countUp1">countUp1</button>
<button @click="countUp2">countUp2</button>

watch()

リアクティブオブジェクトの値が変更されたときに呼ばれるハンドラーを設定します。下記の例では、リアクティブオブジェクト count の値が変更された時に、変更前の値と変更後の値をコンソールに書き出します。

-- script --
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newValue, oldValue) => { console.log(`${oldValue} -> ${newValue}`) })
function countUp() { count.value++ }
-- template --
{{ count }}
<button @click="countUp">countUp</button>

複数の値を監視する場合は第一引数を配列にします。

watch([var1, var2], ...)

戻り値として監視を停止、一時停止、再開させるための関数を返却します。

const { stop, pause, resume } = watch(count, (newValue, oldValue) => { ... })
pause()    // 監視を一時停止する
resume()   // 監視を再開する
stop()     // 監視を停止する

第3引数にはオプションを指定します。flush:'post' はコンポーネントのレンダリングの後でウォッチャーが呼ばれます。

watch(count, (newValue, oldValue) => { ... }, { flush:"post" })

オプションには次のようなものがあります。

flush: post  -- コンポーネントをレンダリングした後でウォッチャーを呼び出す
flush: sync  -- オブジェクトが変動すると直ちにウォッチャーを呼び出す
immediate: true -- オブジェクトの変動に関わらずwatch()呼び出し時にもウォッチャーを呼び出す
onec: true -- 最初の1回だけウォッチャーを呼び出す

watchEffect()

watch() と同様リアクティブオブジェクトの値が変更されたときに呼ばれるハンドラーを設定します。watch() は第1引数で監視するオブジェクトを明示的に指定するのに対して、watchEffect() はハンドラの中で使用するオブジェクトが自動的に監視対象となります。wach() は変更前の値を受け取れるのに対して watchEffect() は受け取ることができません。停止、一時停止、再開関数やオプションは watch() と同様です。

import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => { console.log(count.value) })

watchPostEffect()

watchEffect() に { flush:'post' } オプションをつけて呼び出すのと同等です。

watchPostEffect(() => { ... })

watchSyncEffect()

watchEffect() に { flush:'sync' } オプションをつけて呼び出すのと同等です。

watchSyncEffect(() => { ... })

onWatcherCleanup()

ウオッチャー内で fetch() などの非同期関数を呼び出していると、非同期関数が終了するよりも前に再度ウォッチャーが呼び出されることがあります。その際に、前回のウォッチャーの非同期呼び出しをキャンセルしたい場合のハンドラーを設定します。下記の例では値が変動して1秒後にその値をロギングしますが、[Up] ボタンを何度も押した場合は setTimeout() の非同期関数がキャンセルされて、最後の値のみが書き出されます。

import { ref, watch, onWatcherCleanup } from 'vue'
const count = ref(0)
watch(count, (newValue, oldValue) => {
  const timeoutId = setTimeout(() => { console.log(`${oldValue} -> ${newValue}`) }, 1000)
  onWatcherCleanup(() => { clearTimeout(timeoutId) })
})
function countup() { count.value++ }

リアクティビティ:ユーティリティAPI

isRef()

引数が Ref オブジェクトか否かをチェックします。TypeScript の場合は 型述語 型が返却されます。

const msg = ref("Hello!")
console.log(isRef(msg))

unref()

引数が Ref オブジェクトであれば .value の値を、さもなくば値を返します。下記の2行は同じ意味を持ちます。

console.log(unref(msg))
console.log(isRef(msg) ? msg.value : msg)

toRefs()

reactive() で生成したリアクティブオブジェクトを Ref オブジェクトを要素に持つオブジェクトに変換します。元のオブジェクトの値を変更すると、toRefs() で生成したオブジェクトの値も変わります。

-- script --
const user = reactive({name: "Yamada", age: 36})
const userRefs = toRefs(user)
-- template --
{{ userRefs.name }} : {{ userRefs.age }}

toRef()

toRefs() はリアクティブオブジェクトのすべての値を Ref 化しますが、toRef() は単一の値のみを Ref 化します。上記の toRefs() は次の様に記述できます。

const userRefs = { name: toRef(user, "name"), age: toRef(user, "age") }

toValue()

Ref をデリファレンスして値を取り出します。

const count = ref(123)
let countValue = toValue(count)     // 123

isProxy()

引数が reactive(), readonly(), shallowReactive() または shallowReadonly() によって生成されたプロキシオブジェクトか否かをチェックします。

console.log(isProxy(value))

isReactive()

引数が reactive() または shallowReactive() によって生成されたオブジェクトか否かをチェックします。

console.log(isReactive(value))

isReadonly()

引数が readonly() または shallowReadonly() によって生成された読込専用オブジェクトか否かをチェックします。

console.log(isReadonly(value))

リアクティビティ:上級編

shallowRef()

shallow は「浅い」を意味します。ref() はオブジェクト自体や、その子プロパティなど子孫プロパティをすべてリアクティブにしますが、shallowRef() の場合はオブジェクトのみをリアクティブ化します。複雑な構造を持つ巨大なオブジェクトをリアクティブ化する際の効率を高めます。

const state = shallowRef({ count: 1 })
state.value = { count: 2 }    // 変更を反映する(リアクティブ)
state.value.count = 3         // 変更を反映しない(非リアクティブ)

triggerRef()

shallowRef() で作成した浅い ref の子孫プロパティを変動させた際、ref を強制的にトリガーします。

state.value.count = 3
triggerRef(state)

customRef()

リアクティブオブジェクトを参照・設定する際の動作をカスタマイズした ref を作成することができます。参照(get)処理では track()、設定(set)処理では trigger() を呼び出します。

<script setup>
  import { customRef } from 'vue'
  const MyCustomRef = (value) => {
    return customRef((track, trigger) => {
      return {
        get() {
          console.log(`Get ${value}`)
          track()
          return value
        },
        set(newValue) {
          console.log(`Set ${value} -> ${newValue}`)
          value = newValue
          trigger()
        }
      }
    })
  }
  const count = MyCustomRef(0)
  function countUp() { count.value++ }
</script>
<template>
  <button @click="countUp">Up</button>
  {{count}}
</template>

shallowReactive()

ref() の浅いバージョンが shallowRef() で、reactive() の浅い版が shallowReactive() です。

shallowReadonly()

readonly() の浅いバージョンです。

toRaw()

reactive(), readonly(), shallowReactive(), shallowReadonly() で作成したオブジェクトからリアクティブ化前のオブジェクトを取り出します。

const count = reactive({count: 0})
console.log(toRaw(count))    // {count: 0}

markRaw()

オブジェクトに対してリアクティブ化されないようにマークします。巨大なデータを扱う際にリアクティブ化されないデータを制御するのに有効です。

const foo = markRaw({ foo: "foo" })
const baa = { baa: "baa" }
const data = ref({ foo: foo, baa: baa })
console.log(isReactive(data.value.foo))    // false
console.log(isReactive(data.value.baa))    // true

effectScope()

エフェクトスコープを作成することで、watch() などのウォッチャーや computed() などのリアクティブエフェクトをまとめて破棄することができます。詳細は RFC↗ を参照してください。

const scope = effectScope()
scope.run(() => {
  const doubled = computed(() => counter.value * 2)
  watch(doubled, () => console.log(doubled.value))
  watchEffect(() => console.log('Count: ', doubled.value))
})
scope.stop()    // まとめて破棄

getCurrentScope()

現在のアクティブなエフェクトスコープがあればそれを返します。

const scope = getCurrentScope()

onScopeDispose()

現在のアクティブなエフェクトスコープが破棄された時のコールバック関数を登録します。

scope.run(() => {
  watch(count, () => console.log(count.value))
  watchEffect(() => console.log(count.value))
  onScopeDispose(() => console.log("DISPOSED!"))
})

ライフサイクルフック

onXxxxx()

下記のフック定義関数がサポートされています。詳細は「ライフサイクルフック(↗)」を参照してください。

import { onUpdated } from 'vue'
onUpdated(() => { console.log("onUpdated") })

依存関係のインジェクション

provide()

key-value を記憶します。記憶した情報は自コンポーネントや、子孫コンポーネントから inject() で参照することができます。

import { provide } from 'vue'
provide("my-key", "my-value")

inject()

app.provide()provide() で提供された key-value を参照します。provide() は自分自身または祖先コンポーネントが提供した値のみ取り出すことができます。

import { provide } from 'vue'
provide("my-key", "my-value")

hasInjectionContext()

inject() を呼び出してよいコンテキストであるか否かを true/false で返します。例えば setup の外部などでは false を返します。ライブラリが inject() を呼び出してよいか判断する場合などに利用されます。

import { hasInjectionContext } from 'vue'
if (hasInjectionContext()) { ... }

ヘルパー

useAttrs()

スロット の親コンポーネントから class, id, style を含む属性を受け取ります。

-- Parent.vue --
<Child class="..." id="..." style="..." />
-- Child.vue --
import { useAttrs } from 'vue'
const attrs = useAttrs()    // { "class": "...", "id": "...", "style": { ... } }

useSlots()

スロット の親コンポーネントからスロット情報を受け取ります。スロット情報にスロット名のメソッドを呼び出すと、親コンポーネントの要素リストを得ることができます。

-- Parent.vue --
<Child v-slot:foo>
  <div>AAA</div>
  <div>BBB</div>
</Child>
-- Child.vue --
import { useSlots } from 'vue'
const slots = useSlots()
slots.foo().forEach((e) => { console.log(e.children) })    // AAA, BBB

useModel()

スロット の親コンポーネントからモデル情報を受け取ります。setup をつけない <script> で使用されます。

-- Parent.vue --
<Child :count=123 />
-- Child.vue --
<script>
  import { useModel } from 'vue'
  export default {
    props: ['count'],
    setup(props) {
      const msg = useModel(props, 'count')
      console.log(msg.value)                 // 123
    }
  }
</script>

useTemplateRef()

ref=name を持つ要素の ref を受け取ります。下記の例では ref="myDiv" を指定した要素の ref を受け取り、マウント時にそのスタイルを赤色にしています。

<script setup>
  import { useTemplateRef, onMounted } from 'vue'
  const myDiv = useTemplateRef("myDiv")
  onMounted(() => { myDiv.value.style.color = "red" })
</script>

<template>
  <div ref="myDiv">Hello</div>
</template>

useId()

DOM 要素の id 属性に使用できるような一意の ID を払い出します。デフォルトは "v-0", "v-1", "v-2", ... ですがプレフィックスは app.config.idPrefix() で変更することができます。

import { useId } from 'vue'
console.log(useId())    // "v-0"
console.log(useId())    // "v-1"
console.log(useId())    // "v-2"

ビルトイン

コンポーネント

<Transition>

基本的なサンプル

v-ifv-show<component> による表示の切り替え、key 属性の変更のタイミングでトランジションをかけることができます。下記の例では show により表示・非表示を切り替える際に 1秒 で透明度を 0~100% に変動させます。.v-enter-active, .v-leave-active には表示・非表示時の transition を、.v-enter-from, .v-enter-to, .v-leave-from, .v-leave-to にはそれぞれ、表示の始め/終わり、非表示の始め/終わりにおける CSS プロパティを指定します。

<script setup>
  import { ref } from 'vue'
  const show = ref(true)
</script>
<template>
  <button @click="show = !show">Toggle</button>
  <Transition>
    <div v-if="show">HELLO!</div>
  </Transition>
</template>
<style scoped>
  .v-enter-active, .v-leave-active { transition: 1s; }
  .v-enter-from, .v-leave-to { opacity: 0; }
</style>
HELLO!
名前付きトランジション

トランジションには名前を付けることができます。名前付きトランジションの場合 CSS クラス名の v が指定した名前に代わります。

<Transition name="foo">...</Transition>
.foo-enter-active, .foo-leave-active { ... }
.foo-enter-from, .foo-leave-to { ...  }
アニメーション

.v-enter-active, .v-leave-activeanimation を指定することもできます。

.v-enter-active { animation: bounce-in 0.5s }
.v-leave-active { animation: bounce-in 0.5s reverse }
@keyframes bounce-in {
  0% { transform: scale(0) }
  50% { transform: scale(1.5) }
  100% { transform: scale(1) }
}
HELLO!
カスタムトランジションクラス

.v-enter-active などのCSSクラス名は enter-active-class, leave-active-class, enter-from-class, enter-to-class, leave-from-class, leave-to-class 属性で変更することができます。これは、Animate.css で定義された animated__bounce などのアニメーションを適用するのに役立ちます。

<Transition
    enter-active-class="animate__animated animate__bounce"
    leave-active-class="animate__animated animate__swing">
  <div v-if="show">HELLO!</div>
</Transition>
HELLO!
JavaScriptフック

トランジションの開始時や終了時にフック関数を呼び出すことができます。

<Transition
  @before-enter="onBeforeEnter"
  @enter="onEnter"
  @after-enter="onAfterEnter"
  @enter-cancelled="onEnterCancelled"
  @before-leave="onBeforeLeave"
  @leave="onLeave"
  @after-leave="onAfterLeave"
  @leave-cancelled="onLeaveCancelled"
>...</Transition>

function onBeforeEnter(elm) { ... }
トランジションの再利用

スロット と組み合わせて、トランジションを再利用可能なコンポーネントとして定義することができます。

MyTransition.vue
<template>
  <Transition name="MyTransition">
    <slot></slot>
  </Transition>
</template>
<style>
.MyTransition-enter-active, .MyTransition-leave-active { transition: 1s }
.MyTransition-enter-from, .MyTransition-leave-to { opacity: 0 }
</style>
App.vue
<script setup>
  import { ref } from 'vue'
  import MyTransition from './MyTransition.vue'
  const show = ref(true)
</script>
<template>
  <button @click="show = !show">Toggle</button>
  <MyTransition>
    <div v-if="show">HELLO!</div>
  </MyTransition>
</template>

<TransitionGroup>

リストの各要素にトランジションを適用する場合は <Transition> の代わりに <TransitionGroup> を使用します。tag 属性に ul などの親要素を指定します。各要素には :key 属性に一意となるキーを設定する必要があります。

<script setup>
  import { ref } from 'vue'
  const items = ref([1, 2, 3, 4, 5])
  function add() { items.value.push(items.value.length + 1) }
  function del() { items.value.pop() }
</script>
<template>
  <button @click="add">Add</button>
  <button @click="del">Del</button>
  <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item">{{ item }}</li>
  </TransitionGroup>
</template>
<style scoped>
.list-enter-active, .list-leave-active { transition: all 0.5s ease }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px) }
</style>
  • {{ item }}
  • <KeepAlive>

    <component> でコンポーネントを切り替える際などに、非アクティブとなったコンポーネントをキャッシュして、再表示の際の効率を上げることができます。

    <KeepAlive>
      <component :is="activeComponent"></component>
    </KeepAlive>
    

    <Teleport>

    要素を to で指定した CSS セレクタの子要素として表示します。下記を開発ツールの [Elements] で確認すると、div2 は div1 の子要素として配置されるのに対し、div3 は body 要素の子要素として配置されていることがわかります。モーダルダイアログを親要素の影響を受けずに body 直下に表示したい時などに有効です。

    <div id="div1">
      <div id="div2">FOO</div>
      <Teleport to="body">
        <div id="div3">BAA</div>
      </Teleport>
    </div>
    

    <Suspense>

    fetch() などを利用した非同期のコンポーネントがある場合、それらがすべてローでイング完了状態になるまでスピナーなどのフォールバックコンテンツを表示することができます。詳細は Suspense↗ を参照してください。

    <Suspense>
      <Dashbord />     // ネストされた非同期コンポーネントを含むコンポーネント
      <template #fallback>
        Loading...
      </template>
    </Suspense>
    

    特別要素

    <component>

    <component> は、:is で指定したコンポーネントに切り替わります。コンポーネントをリアクティブ化する際には ref() ではなく shallowRef() を用います。

    <script setup>
      import { shallowRef } from 'vue'
      import Foo from './Foo.vue'
      import Baa from './Baa.vue'
      const activeComponent = shallowRef(Foo)
    </script>
    <template>
      <button @click="comp = Foo">Foo</button>
      <button @click="comp = Baa">Baa</button>
      <component :is="activeComponent"></component>
    </template>
    

    <slot>

    スロットを定義します。詳細は スロット を参照してください。

    <template>

    HTML の DOM に変換されることのない疑似的な空要素です。複数の要素に v-ifv-for を適用する場合や v-slot で名前付きスロットを定義したい場合に使用します。

    <template v-if="show">
      <div>...</div>
      <div>...</div>
    </template>
    
    <template v-for="item is items">
      <div>...</div>
      <div>...</div>
    </template>
    
    <MyComponent>
      <template v-slot:header>Header</template>
    </MyComponent>
    

    特別属性

    key

    リストの子要素をリアクティブに扱う際、Vue が子要素の変動を感知できるよう、子要素に一意のキーを設定します。

    <ul>
      <li v-for="item in items" :key="item.id">...</li>
    </ul>
    

    ref

    useTemplateRef() で参照する要素に指定します。

    <div ref="myDiv">Hello</div>
    

    is

    <component> でコンポーネントを指定する属性として使用します。

    <component :is="activeComponent"></component>
    

    また、通常の is 属性は HTML 標準における カスタム要素 を指定する属性ですが、値に vue: をつけると Vue コンポーネントとみなされます。

    <div is="vue:MyComponent"></div>
    

    単一ファイルコンポーネント

    マクロ関数

    <script setup> 内部のみで使用可能なマクロ関数です。インポートは不要で <script setup> を処理する際に自動的にコンパイルされます。

    defineProps()

    親コンポーネントから属性を受け取ります。

    Parent.vue
    <Child foo="FOO" baa=123 />
    
    Child.vue
    -- script --
    const props = defineProps(['foo', 'baa'])
    -- template --
    {{ props }}   // { "foo": "FOO", "baa": "123" }
    {{ foo }}     // FOO
    {{ baa }}     // 123
    

    下記の様に型を指定することもできます。文字列以外を渡す場合は親コンポーネントで baa=123:baa=123 と記述する必要があります。型には String, Number, Boolean, Array, Object, Date, Function, Symbol, Error を指定できます。

    defineProps({ foo: String, baa: Number })
    

    TypeScript の場合はジェネリクスで表現することもできます。

    defineProps<{ foo: String, baa: Number }>()
    

    デフォルト値や必須属性を指定することもできます。

    defineProps({
      foo: { type: String, default: "Unknown" },
      baa: { type: Number, required: true }
    })
    

    必須だけれども null 値を許可する場合は下記の様に記述します。

    defineProps({ foo: { type: [String, null], required: true } })
    

    defineEmits()

    子コンポーネントから親コンポーネントに渡すイベントを定義します。

    Parent.vue
    -- script --
    function onMyEvent(e) { console.log(e) }     // { message: "Hello!" }
    -- template --
    <Child @my-event="onMyEvent" />
    
    Child.vue
    -- script --
    const emit = defineEmits(["my-event"])
    function onClick() { emit("my-event", { message: "Hello!" }) }
    -- template --
    <button @click="onClick">Send</button>
    

    defineModel()

    親コンポーネントから v-model を受け取ります。

    Parent.vue
    -- script --
    import { ref } from 'vue'
    import Child from './Child.vue'
    const message = ref("Hello!")
    -- template --
    <Child v-model="message" />
    <div>{{ message }}</div>
    
    Child.vue
    -- script --
    const model = defineModel()
    -- template --
    <input type="text" v-model="model" />
    

    defineExpose()

    子コンポーネントからメソッドや関数を親に公開します。下記の例では子コンポーネントから公開された countUp メソッドや count オブジェクトを親コンポーネントから参照しています。

    Parent.vue
    -- script --
    import { ref } from 'vue'
    import Child from './Child.vue'
    const child = ref()
    -- template --
    <Child ref="child" />
    <div>Parent</div>
    <div v-if="child">{{ child.count }}</div>
    <button @click="child.countUp">countUp</button>
    
    Child.vue
    -- script --
    import { ref } from 'vue'
    const count = ref(0)
    function countUp() { count.value++ }
    defineExpose({ count, countUp })
    -- template --
    <div>Child</div>
    {{ count }}
    <button @click="countUp">Up</button>
    

    defineOptions()

    nameinheritAttrs などのオプションを設定します。

    Child.vue
    -- script --
    defineOptions({ inheritAttrs: false })  // 親から id, class, style などの暗黙属性を受け取らなくなる
    

    defineSlots()

    TypeScript を用いた開発で slot と props の型チェック情報を IDE に渡すために使用します。詳細は defineSlots()↗ を参照してください。

    <script setup lang="ts">
    const slots = defineSlots<{
      default(props: { msg: string }): any
    }>()
    </script>
    

    Options API

    Vue 2 では Options API と呼ばれる書き方が主流でした。

    <script>
    export default {
      data() { return { count: 0 } },
      methods: { countUp() { this.count++ } }
    }
    </script>
    <template>
      <div>
        <div class="count">{{ count }}</div>
        <button @click="countUp">Up</button>
      </div>
    </template>
    <style>
      .count { font-size: 200% }
    </style>
    

    Vue 3 になって Composition API と呼ばれる API 群がサポートされました。

    <script>
      import { defineComponent, ref } from 'vue'
      export default defineComponent({
        name: "App",
        setup() {
          const count = ref(0)
          function countUp() { count.value++ }
          return { count, countUp }
        }
      })
    </script>
    <template>
      <div class="count">{{ count }}</div>
      <button @click="countUp">Up</button>
    </template>
    <style scoped>
      .count { font-size: 200% }
    </style>
    

    Vue 3.2 以降では <script>setup パラメータがサポートされ、次の様に記述できるようになりました。

    <script setup>
      import { ref } from 'vue'
      const count = ref(0)
      function countUp() { count.value++ }
    </script>
    <template>
      <div class="count">{{ count }}</div>
      <button @click="countUp">Up</button>
    </template>
    <style scoped>
      .count { font-size: 200% }
    </style>
    

    今後は setup 付きの SFC + Composition API が主流になっていくと思われます。本書では Options API の説明は省略しています。

    スロット

    スロットコンテンツとスロットアウトレット

    slot は「差し込む」という意味を持ちます。親コンポーネントから子コンポーネントに HTML 要素を差し込むことができます。子コンポーネントで <slot></slot> を記述すると、<slot></slot> が親コンポーネントの <Child>~</Child> の間に記述した要素に置換されます。ここで、<h1>Hello!</h1>スロットコンテンツ<slot></slot>スロットアウトレット と呼びます。

    -- App.vue --
    <script setup>
      import Child from './Child.vue'
    </script>
    <template>
      <Child>
        <h1>Hello!</h1>
      </Child>
    </template>
    -- Child.vue --
    <template>
      <slot></slot>    ... <h1>Hello!</h1> に置換される
    </template>
    

    フォールバックコンテンツ

    <slot>~</slot> の間には、親コンポーネントからスロットコンテンツが渡されなかった場合のデフォルトコンテンツを記述することができます。

    -- Child.vue --
    <slot>No data</slot>
    

    名前付きスロット

    親コンポーネントは v-bind で名前をつけることにより、複数の名前付きスロットコンテンツを子コンポーネントに渡すことができます。子コンポーネントではこれを name 属性で受け取ります。v-bind を指定しないコンテンツは default という名前のコンテンツとみなされます。

    -- App.vue --
    <Child>
      <template v-slot:header>Header</template>
      <template v-slot:body>Body</template>
      <template v-slot:footer>Footer</template>
    </Child>
    -- Child.vue --
    <slot name="header"></slot>
    <hr>
    <slot name="body"></slot>
    <hr>
    <slot name="footer"></slot>
    

    v-bind:name は省略して #name と記述することができます。

    <template #header>Header</template>
    

    条件付きスロット

    スロットコンテンツが存在する場合のみ表示したい場合は $slots.name で判断します。

    -- Child.vue --
    <div v-if="$slots.footer">
      <hr>
      <slot name="footer" />
    </div>
    

    動的スロット名

    スロット名も動的に割り当てることができます。[slotName] とすることでスロット名をリアクティブに指定することができます。

    -- App.vue --
    const cardName = ref("CardC")
    <template #[cardName]>My Card</template>
    

    スコープ付きスロット

    下記の様にして、子コンポーネントから親コンポーネントに値を渡すことができます。(※ Vue のガイドには :text="..." と書かれているのですが、:text="'...'" のようにシングルクォートで囲まないと文字列をうまく渡すことができませんでした)

    -- App.vue --
    <Child v-slot="slotProps">
      {{ slotProps.text }} {{ slotProps.count }}
    </Child>
    -- Child.vue --
    <slot :text="'hello!'" :count="1"></slot>
    

    サーバーサイドレンダリング(SSR)

    Vue3 ではサーバーサイドレンダリング(SSR)もサポートしています。クライアント側 JavaScript がサーバー側でレンダリングされた HTML をイベントハンドリング付きでマッピングするハイドレーション(Hydration)と呼びます。サーバー側とクライアント側で同一のコードが動作するアイソモーフィック(Isomorphic)を実現しています。詳細は サーバーサイドレンダリング(SSR)↗ を参照してください。

    mkdir ssr-test
    cd ssr-test
    npm init -y
    npm install vue
    npm install express
    

    ES modules を使用するため package.json に "type": "module" を加えます。

    {
      "name": "ssr-test",
      "type": "module",
        :
    

    下記のファイルを作成します。サーバーにアクセスすると server.js が client.js を含む HTML を返します。client.js は app.js を読み込み、これを #app にマウントします。

    app.js
    import { createSSRApp } from 'vue';
    export function createApp() {
      return createSSRApp({
        data: () => ({ count: 1 }),
        template: `<button @click="count++">{{ count }}</button>`,
      });
    }
    
    server.js
    import express from 'express';
    import { renderToString } from 'vue/server-renderer';
    import { createApp } from './app.js';
    const server = express();
    server.get('/', (req, res) => {
      const app = createApp();
      renderToString(app).then((html) => {
        res.send(`
        <!DOCTYPE html>
        <html>
          <head>
            <title>Vue SSR Example</title>
            <script type="importmap">
              { "imports": { "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js" } }
            </script>
            <script type="module" src="/client.js"></script>
          </head>
          <body>
            <div id="app">${html}</div>
          </body>
        </html>
        `);
      });
    });
    server.use(express.static('.'));
    server.listen(8080, () => {
      console.log('ready');
    });
    
    client.js
    import { createApp } from './app.js';
    createApp().mount('#app');
    

    サーバーを起動して http://{サーバアドレス}:8080 にアクセスしてみます。

    node ./server.js
    

    Vue は下記などの関連プロジェクトやエコシステムと連携することができます。

    Vite

    Vue.js の開発者である尤雨溪が開発した開発用Webサーバーです。Vue.js や React に組み込まれています。Vite(ヴィート/ヴィット)はフランス語で素早いという意味を持ち、起動が早いのが特徴。TypeScript, JSX, SSR などもサポートしています。

    Nuxt.js

    React ベースのフレームワークとして Next.js があるように、Vue.js ベースのフレームワークとして Nuxt.js が開発されています。クライアント上の SPA インタフェースと、サーバーサイドの SSR やロジックをシームレスに結合することができます。

    コマンドラインツール(Vue CLI, create-vue)

    コマンドラインから Vue に関する操作を行うツールです。Vue CLI は現在はすでにメンテナンスモードに入っており、代わりに create-vue が推奨されています。コマンドラインから プロジェクトスキャフォールディング(create)、ビルド(build)、Vite起動などの Vue に関する操作を行うことができます。

    npm create vue@latest
    npm install
    npm run dev
    npm run build
    

    Vue Router

    どの URL にアクセスしたらどのページを表示するといった、URL と表示ページ(コンポーネント)の関係を管理します。詳細は「Vue Router」を参照してください。

    Vue Routerの基本的な使い方

    Vue にルーティング機能を追加する Vue の公式ライブラリです。URL のパス名とコンポーネントのマッピングを提供します。Vue Router 4 から Vue 3 に対応しています。

    npm install vue-router@4
    npm list | grep vue-router
      vue-router@4.4.5
    
    main.js
    import { createApp } from 'vue'
    import { createRouter, createMemoryHistory } from 'vue-router'
    import App from './App.vue'
    import Home from './Home.vue'
    import Page from './Page.vue'
    
    const router = createRouter({
      history: createMemoryHistory(),
      routes: [
        { path: "/", component: Home },
        { path: "/page", component: Page },
      ]
    })
    
    createApp(App)
      .use(router)
      .mount('#app')
    
    App.vue
    <template>
      <Router-View />
    </template>
    
    Home.vue
    <template>
      <h1>Home</h1>
      <Router-Link to="/page">Page</Router-Link>
    </template>
    
    Page.vue
    <template>
      <h1>Page</h1>
      <Router-Link to="/">Home</Router-Link>
    </template>
    

    パラメータの渡し方

    上記のプログラムに下記を追記します。

    main.js
    { path: "/page/:page_id", component: Page },
    
    Home.vue
    <Router-Link to="/page/p123">Page</Router-Link>
    
    Page.vue
    -- script --
    import { useRoute } from 'vue-router'
    const route = useRoute()
    console.log(route.params.page_id)
      
    -- template --
    <div>{{ $route.params.page_id }}</div>
    

    状態管理ライブラリ(Vuex, Pinia)

    状態管理ライブラリを用いることでコンポーネント間でストア(状態変数の集合)を共有することができます。以前は Vuex が使用されていました。Vuex 3 と Vuex 4 のメンテナンスは継続されていますが、新機能の開発は停止しています。現在では代わりに Pinia への移行が薦められています。

    npm install pinia
    npm list | grep pinia
      pinia@2.2.4
    
    main.js
    import { createPinia } from 'pinia'
    createApp()
      .use(createPinia())
      .mount("#app")
    
    MyStore.js
    import { defineStore } from 'pinia'
    export const MyStore = defineStore("MyStore", {
      state: () => ({ count: 0 }),
      actions: { increment() { this.count++ } },
    })
    
    App.vue
    -- script --
    import { MyStore } from './MyStore'
    const mystore = MyStore()
    -- template --
    <div>{{ mystore.count }}</div>
    <button @click="mystore.increment">Increment</button>
    

    Vue DevTools

    Vue のデバッグなどを支援する開発者支援ツールです。Viteプラグイン、スタンドアロンアプリ、Chrome拡張機能(ベータ版)などとして機能します。

    VS Code拡張機能(Vetur, Vue - Official拡張機能)

    Vue 開発のための VS Code 拡張機能です。かつでは Vetur が使用されていましたが Vue 2 にしか対応していません。Vue 3 では Volar を使用していましたが、Volar は現在では Vue - Official拡張機能 の Vue Language Features に統合されました。

    Vuetify

    Vue.js をベースとしたフレームワークです。デザインスキルが無くても美しい画面を簡単に作成することができます。多くの全体レイアウト(ワイヤフレーム)、メニュー、ボタン、ダイアログ、ページネーション、ツールバーなどの UI コンポーネントが提供されています。

    npm install vuetify
    npm list | grep vuetify
      vuetify@3.7.3
    
    main.js
    import { createVuetify } from 'vuetify'
    import * as components from 'vuetify/components'
    import * as directives from 'vuetify/directives'
    
    createApp(App)
      .use(createVuetify({components, directives}))
      .mount('#app')
    
    App.js
    <script setup>
      const users = [
        { name: "Yamada", age: 36 },
        { name: "Suzuki", age: 48 },
        { name: "Tanaka", age: 52 },
      ]
    </script>
    <template>
      <v-container>
        <v-table>
          <thead>
            <tr><th>Name</th><th>Age</th></tr>
          </thead>
          <tbody>
            <tr v-for="user in users" :key="user.name">
              <td>{{ user.name }}</td><td>{{ user.age }}</td>
            </tr>
          </tbody>
        </v-table>
      </v-container>
    </template>
    

    BootstrapVue

    Vue と Bootstrap を組み合わせるためのコンポーネント群です。ただし現時点(2024年10月)時点では Vue.js v2.6, Bootstrap v4.6 にしか対応していません。

    テストフレームワーク(Cypress, Nightwatch, Playwright)

    JavaScript の E2E(End-to-End) テストフレームワークです。Cypress, Nightwatch, Playwright などが利用されます。

    その他

    Vue 2 から Vue 3 への移行

    Vue 2 と Vue 3 は一部互換性が無いものがあります。Vue 2 から Vue 3 への移行ガイドが下記で紹介されています。

    TypeScript を使用する

    create-vue でプロジェクトを作成する際に下記で Yes を回答すると TypeScript を使用することができます。ただし、Vite を使用する場合、TypeScript のトランスパイルは行いますが、型チェックは行いません。型チェックは VS Code などの IDE に任せ、その分、サーバーの起動/再起動時間を高速化しています。

    npm create vue@latest
      :
    Add TypeScript? … No / Yes
    

    SFC の <script>lang="ts" を加えてください。

    <script setup lang="ts">
      :
    

    複数アプリケーション

    Vue アプリケーションを画面内に複数割り当てることも可能です。

    createApp({...}).mount("#app-1")
    createApp({...}).mount("#app-2")
    

    *.vue ファイルから app にアクセスする

    コンポーネントから app にアクセスしたい場合 app.provide()inject() を用います。

    -- main.js --
    app.provide("app", app)
    -- App.vue --
    import { inject } from 'vue'
    const app = inject("app")
    

    フォールスルー属性

    id, class, style, v-on 属性などは、親コンポーネントから子コンポーネントに暗黙的に渡すことができます。

    ---- App.vue ----
    <Child class="big" style="color:red" />
    ---- Child.vue ----
    <template>
      <div>Hello!</div>
    </template>