コムセント 技術情報

  1. TOP
  2. コムセント 技術情報
  3. Vue.js 3系の表記ゆれ大全集

Vue.js 3系の表記ゆれ大全集

僕はまだ Vue.js を使用し始めてまだ日が浅いのですが、それでも 2 系→ 3 系の過渡期を体験してきました。
そしてふと、昔に書いた Vue.js のコードを見ると現在のコードと大分見た目が違うことに気が付きます。

人気のフレームワークだけあって、世には Vue.js の解説記事が溢れていますが、同じことを説明していても使用している記法が全然違って混乱したのも懐かしい思い出です。

今回はそんな、解説記事を読んでも方言がキツくて話が通じないとお悩みの皆さんに、表記ゆれケースごとに解説をしてみたいと思います。

html 上のテンプレート ⇔ <div id="app"></div>

Vue.js を学習しようとする際、一番手軽なのは CDN を使って、

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

のような形で Vue.js を読み込んだ後、ロジック部分は後に続く <script></script> の中に書き、テンプレート部分は HTML の上に直接書く方法です。

そのようなチュートリアルを流し見、または写経してみた後に、いざ仕事のコード or 他の解説記事を見てみると、そうなっていないことが多いでしょう。
代わりに、HTML 部分では以下のようなタグがぽつんと存在しているかと思われます。

<div id="app"></div>

テンプレート構文はどこに行ってしまったのか、と言うと .js, または .vue (SFC、単一ファイルコンポーネント)ファイルの中に書いてあります。
ブラウザで直接読み込んでいるのは間違いなく .js ファイルですが、それがそのまま人間の書いたファイルなのか webpack Vite によって生成されたファイルなのかはプロジェクトによります。
元のコードを探したいのであれば、とりあえず「src」や「resources」などの名称のディレクトリが無いか、それがあれば中身を探してみましょう。
なお、あなたがそのサービスをブラウザ越しに利用しているだけで、元のコードが手元に存在しない、またはダウンロードできない場合は元コードを参照できない可能性が高いです。
webpack Vite を用いて開発する場合、人間が読み書きする JavaScript とブラウザに読み込ませる JavaScript は別となり、公開されているのが後者のみ、というケースが多いからです。

対して、元々の html 上に Vue のテンプレート構文を前もって書いておけることが初耳の方もいるかもしれません。
僕は Vue.js のチュートリアルをすっ飛ばして webpack + SFC から始めた開発者なので、 index.html の上にテンプレートが書いてある例を見て驚いたことがあります。

v-bind, v-on, v-slot ⇔ :, @, #

v-bindv-on は Vue.js を作る上では必須のディレクティブですが、実際のコードには一つも見当たらない……と思ったら、

<input
  v-bind:name="nameVar"
  v-on:click="clickHandler"
  type="submit"
  value="送信する"
>

のようなコードではなく、

<input
  :name="nameVar"
  @click="clickHandler"
  type="submit"
  value="送信する"
>

を探してみましょう。

これらはいわゆる短縮記法で、それぞれ

<div v-bind:name="hoge"></div>
<div :name="hoge"></div>

<div v-on:click="hoge"></div>
<div @click="hoge"></div>

が同じ意味となります。

これらについては既に知っている人も多いかと思われますが、最近名前付きスロットを実装する v-slot も Vue.js 2.6 から使用できるようになっています。

これに関しては次の章で詳しく説明しましょう。

slot="hoge" ⇔ v-slot:hoge ⇔ #hoge

コンポーネントをより便利にするスロット機能ですが、一つのコンポーネントで複数のスロットを扱う場合は名前付きスロットを扱う必要があります。

子コンポーネント側は、

<template>
  <article>
    <h1>
      <slot name="title"></slot>
    </h1>
    <div class="content">
      <slot></slot>
    </div>
  </div>
  <aide>
    <slot name="digression"></slot>
  </aside>
</template>

のように name 属性を用いて描画したいスロットを分けています。

しかし、親コンポーネント側では Vue.js 2.5 までは

<HogeContainer>
  <template slot="title">
    <span class="titleSub">【なにも分からない】</span>
    <span class="titleMain">Vue.js 完全に理解した</span>
  </template>
  <p>ごめんなさい全然分かりません。</p>
  <p>三日調べても Hello World が動きません。</p>
  <template slot="digression">
    <p>※React は一週間かけても駄目でした。</p>
  </template>
</HogeContainer>

といった記法だったのに対し、 Vue.js 2.6 からは

<HogeContainer>
  <template v-slot:title>
    <span class="titleSub">【なにも分からない】</span>
    <span class="titleMain">Vue.js 完全に理解した</span>
  </template>
  <p>ごめんなさい全然分かりません。</p>
  <p>三日調べても Hello World が動きません。</p>
  <template v-slot:digression>
    <p>※React は一週間かけても駄目でした。</p>
  </template>
</HogeContainer>

という記法となっています。更に、

<HogeContainer>
  <template #title>
    <span class="titleSub">【なにも分からない】</span>
    <span class="titleMain">Vue.js 完全に理解した</span>
  </template>
  <p>ごめんなさい全然分かりません。</p>
  <p>三日調べても Hello World が動きません。</p>
  <template #digression>
    <p>※React は一週間かけても駄目でした。</p>
  </template>
</HogeContainer>

のように # を付けると v-slot: の短縮記法として使用できます。

先述の :, @ より見かける頻度は低いと思われるので、もしかしたら戸惑う記法かもしれないですね。

new Vue() ⇔ createApp() ⇔ Vue.createApp()

2 系であろうと 3 系であろうと、 Vue.js が起動する最初のステップはインスタンスを作成することです。

それぞれ、2 系では Vue インスタンス、 3 系ではアプリケーションのインスタンスと表現されていますが、ロジックを定義したオブジェクト・またはコンポーネント自体を初期値として実 DOM にマウントする流れは変わりません。

ただし、インスタンスの作り方は 2 系が

new Vue({
  el: '#app',
  data() {
    return { hoge: 'fuga' };
  }
});

のようなものだったのに対し、 3 系では

createApp({
  data() {
    return { hoge: 'fuga' };
 }
}).mount('#app');

と、Vue そのものではなく、createApp というグローバル API を使用する形になりました。

.mount() の部分は 2 系でも .$mount() が使用できていましたが、 3 系では第一引数に指定するオプションオブジェクトから el が無くなり、.mount() に一本化されました。

3 系からは公式のグローバルビルドの使用で説明されているように、下手すると Vue は createApp などの API が含まれるオブジェクトを指しているケースがありますので、余計に注意が必要です。
つまり、 Vue を CDN などから直接読み込む場合は、

Vue.createApp({
  data() {
    return { hoge: 'fuga' };
  }
}).mount('#app');

のような記法も使用できます。

対してES モジュール ビルドの使用でも紹介されているように、ほとんどのモダンブラウザが ES モジュールに対応している現在においては、

import {createApp} from "Vue.js の URL、またはパス";

という表記も当たり前になってきました。

なのでこれから 3 系に触れる開発者は、createApp などの API が Vue.js を読み込めば使用できるグローバルなモノではなく、あくまで Vue から選択的に import して使用するモノだと、頭の片隅に置いておくと混乱しないと思います。

(new Vue(App) ⇔ new Vue({ render: h => h(App) })

章タイトルでは 2 系で書いてありますが、 3 系では以下のような記述になります。

import {createApp} from "vue";
import App from "App.vue のパス";
createApp(App).mount('#app');

import {createApp} from "vue";
import App from "App.vue のパス";
createApp({ render: h => h(App) }).mount('#app');

この書きかたを見かける理由ですが、以下の 2 パターンに大別されると考えられます。

・JavaScript の力を使ってテンプレートでは解決し辛いレンダリングを行いたかった
・流行っていたので採用された

前者のケースでは、App コンポーネントの内に Vue のテンプレート構文が存在せず、代わりに JavaScript で HTML 文字列を作成しようとしている部分が見つかると思います。
肝になる render 部分ですが、これは描画関数(3 系ではレンダー関数)という機能で、普段テンプレート構文を使用して書く HTML 部分を JavaScript によって書く機能です。
2 系の公式ドキュメントでは、render を使用するとどのようなメリットがあるのか分かりやすく紹介されています。

対して後者は、確証は持てませんが恐らく webpack-simple という Vue.js 2.x + webpack 3.x で構築された環境の雛形ファイルが初出の書き方です。
このリポジトリの Issue 「Explanation for render: h => h(App) please #29」でも尋ねられているのですが、公式が

import Vue from 'vue'
import App from './App.vue'
new Vue({
  el: '#app',
  render: h => h(App)
})

といったコードを書いており、多くの人が「render とはなんだ?」とは思いつつ、これを使用し始めたのではないかと思われます。
なぜなら、render を使わず、素直に Vue インスタンスを作成しようとすると以下になるため、render を使用したほうがコードが短くなるからでしょう。

import Vue from 'vue'
import App from './App.vue'
new Vue({
  el: '#app',
  components: {App},
  template: '<App />'
})

ただし、Vue.js の 2 系でテストしてみたところ、el ではなく $mounted() を使用すれば、わざわざ render を使用せずとも以下で動作しました。

import Vue from 'vue'
import App from './App.vue'
new Vue(App).$mounted('#app')

render を使用するインスタンス作成方法は、厳密には効率やメリットと言うよりは、単によく知られた定型文として広まった記法なのかもしれません。

Vue.component('HogeContainer', {}) ⇔ import HogeContainer from "…"

どちらもコンポーネントを読み込むための方法ですが、二つの点で使用目的が異なっています。

・.vue (SFC、単一ファイルコンポーネント)ファイルを用いてコンポーネントを作るか、 JavaScript に直書きするか。
・グローバル登録するか、ローカル登録するか

やや古い公式ドキュメントにおいて、コンポーネントはまず「JavaScript に直書き」 × 「グローバル登録」で説明されていました。
ややこしい準備や概念の学習無しに始められるので、最初に Vue.js を学習するときは以下のように書くことが多いでしょう。

<div id="app"><test-component /></div>

import { createApp } from 'vue';
const app = createApp({}).mount('#app');
app.component('TestComponent', {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
 });

ただし、開発に慣れて SFC を使用するようになると、以下のような書き方で開発を進めるケースが多くなると思います。

<div id="app"></div>
import { createApp } from 'vue';
import TestComponent from '/path/to/TestComponent.vue';
const app = createApp(TestComponent).mount('#app');

まず、

import TestComponent from '/path/to/TestComponent.vue';

の有無は分かりやすいと思いますが、よく見ると app.component() も省かれているのが分かります。
app.component() を使用しない場合、コンポーネントはローカルに登録されます。

上記では createApp() にいきなりコンポーネントを使用していますが、サブコンポーネントをローカル登録する場合は components に登録します。

TestComponent.vue

<script>
import SubTestComponent from '/path/to/SubTestComponent.vue'
export default {
  components: {
    SubTestComponent
  },
  data() {
    return {
      //…
    }
  }
}
</script>
<template>
  <SubTestComponent />
</template>

しかし、CompositionAPI + <script setup> を使用すると components すら不要になり、よりネイティブ JavaScript に近い見た目になります。

<script setup>
import SubTestComponent from '/path/to/SubTestComponent.vue'
</script>
<template>
  <SubTestComponent />
</template>

<hoge-container> ⇔ <HogeContainer>

Vue を扱っていると主にパスカルケース(単語ごとに先頭を大文字にする)とケバブケース(単語ごとに-で区切る)の二つを扱うことになります。

コンポーネントを命名するときはほぼ大抵複数単語のコンポーネント名を使用しますが、実際にテンプレート構文上で使用する場合はパスカルケースを使うケースとケバブケースを使うケースが混在しています。

テンプレート内でのコンポーネント名の形式

公式ドキュメントによれば、JavaScript の中ではパスカルケース、 HTML ではケバブケースが使われているので、Vue でもそれを推奨しているとのこと。

ぶっちゃけどちらも動くと思いますが、仕事の場合はプロジェクトの方針に合わせて開発を進めましょう。

{data() {}, methods: {}, mounted() {}, …} ⇔ { setup(props, emits) {} }

2 系の頃からプラグインで提供されていましたが、従来の OptionsAPI に対して CompositionAPI という記法が登場しました。

2 系で開発をしたことがある人が、「テンプレート部分は分かるが、script 部分は本当に Vue なのか……?」という感想を抱いたらほぼ間違いなく CompositionAPI が採用されているコードでしょう。

CompositionAPI の紹介は公式を参照して貰うとして、追加で紹介したいこととして、同じ CompositionAPI でも更に <script setup> という記法を使用するか否かでコードが変わってくる点があります。

従来の CompositionAPI では

<script>
import { ref } from 'vue';
export default {
  setup() {
    const count = ref(0);
    return {
      count 
    };
  }
}:
</script>
 のように書いていた部分が、
<script setup>
import { ref } from 'vue';
const count = ref(0)
</script>

と、まるで <script></script> 自体が setup: function() {} のスコープのように振舞います。

defineProps({}), defineEmits([])、 ⇔ defineProps<{}>(), defineEmits<{}>()

<script setup> では export default {} が取っ払われるので、props や emits も一緒にどこかへ行ってしまいます。

その代わりに使用するのが defineProps() と defineEmits() です。

<script setup>
const props = defineProps({
  title: {
    require: true,
    type: String
  },
  important: {
    require: false,
    type: Boolean
  }
});
const emits = defineEmits(['hoverTitle']);
</script>
<template>
  <h1
    :class="important ? 'isImportant' : ''"
    @hover="hoverTitle"
  >{{title}}</h1>
</template>

世の解説記事では TypeScript を前提にした記事も多くありますが、この二つのマクロは JavaScript か TypeScript かで使用方法が異なります。

上記を TypeScript で書き直すと以下になります。

<script setup lang="ts">
interface Props {
  title: string,
  important?: boolean
}
const props = defineProps<Props>();
const emits = defineEmits<{ (e: 'hoverTitle', value: Event): void }>();
</script>
<template>
  <h1
    :class="important ? 'isImportant' : ''"
    @hover="hoverTitle"
  >{{title}}</h1>
</template>

このように第一引数ではなく、TypeScript の型引数を上手く使うことによって、より強力に型の恩恵を受けられるようになります。

(余談) https://ja.vuejs.org/ ⇔ https://v3.ja.vuejs.org/ ⇔ https://jp.vuejs.org/

Vue.js の日本語版公式ドキュメントは確認した限り 3 種類あります。それぞれ、

基本的には一番上を参照するのが良いいと思いますが、世の記事からのリンクは上記 3 種類が混在しているので注意しましょう。

https://github.com/vuejs/jp.vuejs.org/issues/1534
何故分かれているのかの理由を探していたらこんな issue を見つけました。やはり水面下で色々苦労されているようで……

まとめ

歴史の長いフレームワークは時代によって求められるもの、対応すべきものが変化していくので、どうしても変わっていくものです。

それはポジティブにとらえるのであれば「進化」ですが、追従するには我々開発者も常に知識をアップデートしていかなければなりません。

これら表記ゆれは単なる方言ではなく、それぞれに存在理由があります。
学び始めは資料探しを邪魔するものになりがちですが、余裕があれば複数の書き分けができる理由を調べてみると、意外な知識が得られるかもしれません。

プログラマー/N.Go

このメンバーの記事一覧へ

おすすめ記事