はじめに

Vue.jsの基本的な構文をいくつか紹介させていただきます。私自身Vueを勉強中なので、説明に不十分な点もあるかと思いますが、ぜひ最後までご覧いただけますと幸いです。

以前のバージョンVue2ではOptions APIと呼ばれる記法でしたが、現在Vue2のサポートは終了しています。その為、Vue3で主流となっているComposition APIの記法で書いたコードを、この記事では使用しています。

Composition API (.vueファイルの構成)

この記法ではリアクティブな値の定義付けを行うref(),reactive()などのリアクティビティーAPI、トリガーを設定できるライフサイクルフックなどを使用することが出来ます。

<template>
  <div>
    <div>Vue.js</div>
    <div>{{ item }}</div>
    <button @click="onChangeItem()">CLICK</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'App',
  setup() {
    const item = ref<string>("red");

    const onChangeItem = () => {
      item.value = "blue";
    };

    return {
      item,
      onChangeItem
    };
  },
});
</script>

<style scoped>
.button {
  padding: 2px;
  border: 1px solid brown;
  border-radius: 4px;
  color:brown;
}
</style>

vueファイルは基本的に、

  1. <template> で囲まれたHTML記述部分
  2. <script> で囲まれたJavascript記述部分
  3. <style> で囲まれたCSS記述部分

の3つで構成されています。

  1. <template> で囲まれたHTML記述部分
<template>
  <div>
    <div>Vue.js</div>
    <div>{{ item }}</div>
    <button @click="onChangeItem()">CLICK</button>
  </div>
</template>

ここではwebページに表示するDOM要素を書きます。コンポーネントとして扱う為、全体を<template> で囲む必要があります。基本的に普段のHTMLの書き方と変わりませんが、いくつかVue独自の記法が存在します。

<div>{{ item }}</div>

マスタッシュ構文と呼ばれる{{ }}を使用することで、Javascriptで書いた変数や関数をHTMLに直接記述することが出来ます。バニラJSでは毎回DOM要素単位で渡す必要がありましたが、Vueではその必要がありません。

  1. <script> で囲まれたJavascript記述部分
<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
 name: 'App',
 setup() {
   const item = ref<string>("red");

   const onChangeItem = () => {
     item.value = "blue";
   };

   return {
     item,
     onChangeItem
   };
 },
});
</script>

script内のコードは、Typescriptで書くこともできます。その場合<script>内にlang="ts" を記入します。import 文で他ファイルから関数やコンポーネントを呼び出し、export文で他ファイルでの呼び出しを可能にします。他コンポーネントで利用しない変数や関数であればexportする必要はありません。Typescriptでコンポーネントを定義するにはdefineComponent() オブジェクトを使用します。このオブジェクトではいくつかのオプションを設定できます。

  • name

コンポーネント名を定義します。

  • components

importしたコンポーネントを定義します。これによりコンパイルエラーを防ぎ、保守性が上がります。

  • props

親コンポーネント(呼び出し側のコンポーネント)から渡された値を定義します。初期値や、必須かどうかの設定が出来ます。

  • emits

コンポーネント内で使用するemitメソッドを定義します。

Javascriptコードはsetup() 関数内に記述することで、コンパイルされます。

setup() {
  const item = ref<string>("red");

  const onChangeItem = () => {
    item.value = "blue";
  };

  return {
    item,
    onChangeItem
  };
},

受け取ったpropsやemitを使用したい場合は第一引数にprops、第二引数にSetup Contextオブジェクトが渡されます。そのオブジェクトのプロパティにemitがあります。<template>内で使用したい場合はreturn{}で返す必要があります。

setup(props, {emit}) {
  const item = ref<string>(props.item);
   
  return {
    item
  }
}
  1. <style> で囲まれたCSS記述部分
<style scoped>
.button {
  padding: 2px;
  border: 1px solid brown;
  border-radius: 4px;
  color:brown;
}
</style>

cssのクラスを定義できます。複数箇所でコンポーネントを呼び出すことを考慮し、クラス名が重複して上書きされないよう、Vue.jsではscoped 属性を付与するのが一般的です。これによりこのコンポーネント内でしか適用されません。(子・孫…コンポーネントには適用される)

ディレクティブ (Vue独自のDOM操作 )

HTMLタグに対しVue独自の属性をつけることで、様々なDOM操作を行うことが出来ます。それがディレクティブです。ここでは使用頻度の高いものをいくつか紹介します。

  • v-bind

HTMLに動的な値を渡すことが出来る属性です。使い方としてはv-bind:class="xxx"で変動するスタイルを渡したり、変動するprops(親→子への値)を渡す際に使います。具体的なコード説明は次の章で行います。v-bind:で省略して書くことが出来ます。

<Children :item="item" />
  • v-model

v-bind は親→子へ単方向にのみ値を渡すのに対し、v-modelは子→親にも動的に渡せる、双方向での値渡しを可能にする仕組みです。詳しくは公式ドキュメントを参照してください。

<input v-model="item" />
  • v-on

ユーザーが入力・ボタン押下など、イベントに応じて実行する処理を渡せます。ボタン押下を検知するv-on:click="xxx" やフォームの入力を検知するv-on:change="xxx"などがあります。v-on@で省略して書くことが出来ます。

<button @click="onChangeItem()">CLICK</button>
  • v-for

DOM用を反復して表示させることが出来るディレクティブです。コードで説明します。

 const fruits = [
   {name: 'apple', price: '200'},
   {name: 'grape', price: '300'}, 
   {name: 'banana', price: '100'}
 ];
<template v-for="fruit in fruits">
 <p>{{ fruit.name }}</p>
 <p>{{ fruit.price }}</p>
</template>

このように配列要素を順に展開します。複数のHTMLタグをまとめて展開したい場合、<template>v-forディレクティブを付与することで実装できます。(複数要素の反復)

  • v-if

DOM要素の表示・非表示を切り替えることが出来ます。条件が真であれば表示します。
同じく表示・非表示の切り替えを行えるv-show ディレクティブがあります。v-if はDOM要素の追加と削除を行い、v-show はCSSプロパティのdisplay: none を付け替えることで画面上での非表示を実現しています。細かい違いですが、切り替え頻度が多い様であれば切り替えコストの低いv-show を使用することを推奨します。(v-if vs v-show)

props・emit (コンポーネント間のデータ移動)

propsとemitを使用することで、親子間のコンポーネントで値を受け渡すことが出来ます。

  • props (親→子)

親コンポーネントではv-bindディレクティブを使い、子コンポーネントへ値を動的に渡します。

const item = ref<string>("blue");
<Children 
  :localItem="item" 
/>

子コンポーネントではdefineComponent()propsに定義します。

  • type 型定義を行います。
  • require 親からの受け渡しを必須にするかどうか設定します。
  • default 親から渡されていない場合の初期値を指定します。

propsで宣言したものは<template>内で直接呼び出せます。

<template>
  <div class="sub">
    {{ localItem }}
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'Children',
  props: {
    localItem: {
      type: String,
      require: true,
      default: 'black'
    }
  }
});
</script>

<style scoped>
.sub {
  display: flex;
  column-gap: 10px;
  padding: 10px;
  border: 2px solid blue;
}
</style>  
  • emit (子→親)

emitイベントは親コンポーネントのカスタムイベントを発火させるメソッドです。

emit(カスタムイベント名, 親に渡す値)

第一引数にイベント名を定義します。これにより親コンポーネントで名前が一致するイベントを発火させます。また任意で第二引数に値を指定できます。

以下のコードを用いて解説します。

<template>
  <div class="main">
    <p>COLOR</p>
    <p v-show="isDisplayFood">DISPLAY!!</p>
    <Children 
      :localItem="item" 
      @update:flag="onUpdateFlag"  
   />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import Children from './Children.vue';

export default defineComponent({
  name: 'App',
  components: { Children },
  setup() {
    const item = ref<string>("blue");
    const isDisplayFood = ref<boolean>(false);

    const onUpdateFlag = () => {
      isDisplayFood.value = !isDisplayFood.value;
    };
    
    return {
      item,
      isDisplayFood,
      onUpdateFlag
    };
  },
});
</script>
<template>
  <div class="sub">
    <div>
      {{ localItem }}
    </div>
    <button
      class="button"
      @click="$emit('update:flag')"
    >
      CLICK
    </button>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue';

export default defineComponent({
  name: 'Children',
  props: {
    localItem: {
      type: String as PropType<string>,
      require: true,
      default: 'black'
    }
  },
  emits: ['update:flag']
});
</script>

親コンポーネントApp.vue が赤枠の領域、子コンポーネントChildren.vueが青枠の領域です。CLICKボタンを押すと”DISPLAY!!”を表示・非表示させる事が出来ます。この実装手順と合わせてemitメソッドを紹介します。

子コンポーネントではdefineComponent()emitsに定義します。イベントの発火のみをさせたい場合は、v-onディレクティブに直接emitメソッドを記述すればよいです。ボタンが押されるとemitメソッドの引数に指定した、親コンポーネント側のイベントが発火します。

@click="$emit('update:flag')"

親コンポーネント側では、v-onディレクティブに渡したイベントが発火します。このサンプルコードではボタン押下時、onUpdateFlagイベントが発火します。イベント内ではisDisplayFoodの真偽値を変更しています。

@update:flag="onUpdateFlag"  
const onUpdateFlag = () => {
  isDisplayFood.value = !isDisplayFood.value;
};

この値がv-showディレクティブに渡されている為、”DISPLAY!!”を表示・非表示させることが出来ます。

<p v-show="isDisplayFood">DISPLAY!!</p>

computed・watch (複雑なロジック処理)

  • computed

propsで受け取った値を、一部加工をして表示させたい場面では、computed()オプションを使います。computed() オプションで定義した値はデフォルトで上書きが出来ません。厳密には代入操作も可能なのですが、DOMには反映されなかったりとref()と同じ感覚で使用するとややこしくなります。そのため表示のみを目的とした、定数的な使用をする場合に有効です。

実際にどのように使用できるか以下の実装例で紹介します。

「xx/yy」という文字列を受け取り「xx時yy分」と変換して表示させる(Date型・dayjsは使用していません)。

<template>
  <div class="sub">
    {{ item }}
  </div>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue';

const onFormatClockTime = (value: string) => {
  const timeArr = value.split('/');
  return `${timeArr[0]}時${timeArr[1]}分`
};

export default defineComponent({
  name: 'Children',
  props: {
    // '14/25'
    item: {
      type: String,
      require: true,
      default: ''
    }
  },
  setup(props) {
    const item = computed(() => onFormatClockTime(props.item));

    return {
      item
    };
  },
});
</script>

computed() オプション内で関数を呼び出しpropsを渡しています。props.itemが親コンポーネントで更新された場合、自動的にcomputed() 内の処理も実行されます。実はこの実装はref() でも可能です。

<template>
  <div class="sub">
    {{ onFormatClockTime(item) }}
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

const onFormatClockTime = (value: string) => {
  const timeArr = value.split('/');
  return `${timeArr[0]}時${timeArr[1]}分`
};

export default defineComponent({
  name: 'Children',
  props: {
    // '14/25'
    item: {
      type: String,
      require: true,
      default: ''
    }
  },
  setup(props) {
    const item = ref<string>(props.item);

    return {
      item,
      onFormatClockTime
    };
  },
});
</script>

ref() を使用した場合、リアクティブ変数item にはpropsをそのまま代入します。そしてDOM要素内で関数を呼び出し、描画を実現させています。

一見どちらで実装しても変わらない様に感じますが、パフォーマンスの観点で差があります。
ref() はそのコンポーネントが呼びだされる度に再評価(更新)されます。例えば、一度別の画面に遷移し、再びこの実装画面に戻ってきた時、propsの値が同じであっても更新の処理が走ります。そしてDOMも更新される為、onFormatClockTime() 関数も毎回実行されます。
一方computed() は内部で受け取る値にしか依存しません。つまりコンポーネントの呼び出しに関わらず、propsの値が変更された場合のみ再評価されます。
実務ではパフォーマンスを意識した開発が必要になると思うので、こういった違いを念頭に置いておくと役立ちます。

  • watch

状態の変更に応じて副作用(他処理)を行いたい場合、watch() オプションを使用します。computed()と混同しやすいですが、はっきりとした違いがあります。

  1. computed()は内部で計算が行える
  2. watch()は特定の条件下で実行されるトリガー

computed() は値です。内部では関数を用いた計算処理なども行えますが、最終的に戻り値として1つの値を返す必要があります。watch() はトリガーです。特定の値を監視させ、それに変更が加わる事で、関数や何らかの処理をさせることが出来ます。APIの呼び出しや、同じタイミングで複数の値に代入させる時に使われます。

<template>
  <div class="sub">
    <div class="detail">
      <p>{{ name }}</p>
      <p>{{ `${price}円` }}</p>
    </div>
    <div>
      {{ message }}
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, watch, PropType } from 'vue';

export type Item = Readonly<{
  name: string,
  price: number,
  isSoldOut: boolean
}>

export default defineComponent({
  name: 'Children',
  props: {
    items: {
      type: Object as PropType<Item>,
      require: true,
      default: {}
    }
  },
  setup(props) {
    const name = ref<string>('');
    const price = ref<number>(0);
    const message = ref<string>('');

    const validateMessage = (value: boolean) => {
      message.value = value 
        ? '現在、売り切れています'
        : '現在、販売中です'
    };

    watch(
      props.items, (newValue) => {
        name.value = newValue.name;
        price.value = newValue.price;
        validateMessage(newValue.isSoldOut);
      },
      {immediate: true}
    )

    return {
      name,
      price,
      message
    };
  },
});
</script> 
const items = ref<Item>({
  name: 'apple',
  price: 300,
  isSoldOut: true
});

watch()を使った実装例を紹介します。冗長な処理も一部ありますがお許しください。
今回の例ではprops.itemを監視対象とし、変更が加わった際に後ろのアロー関数が実行されます。関数内では、複数の値への代入や、関数の呼び出しが行われています。

watch(
  props.items, (newValue) => {
    name.value = newValue.name;
    price.value = newValue.price;
    validateMessage(newValue.isSoldOut);
  },

またwatch() にはカスタムオプションが用意されています。immediate オプションは即時ウォッチャーと呼ばれるもので、初期描画時もwatch() を実行させることが出来ます。

{immediate: true}

おわりに

リアクティブな値の仕組みを理解することで、Vueでの開発をスムーズに行うことが出来ます。今回紹介したディレクティブ、props・emit、computed・watchについては主要機能のみ紹介しており、他にも種類やオプションが存在します。興味のある方はVue.js 公式ドキュメントを参照してください。