Open126

Vue.js学習用

Hidden comment
HiroyouHiroyou
HiroyouHiroyou

テンプレートからはVueインスタンスのdataやmethodsのプロパティに直接アクセスできる。一方、Vueインスタンス内でdataやmethodsのプロパティにアクセスする場合はthisが必要になる。

<p>{{ message }}</p>

new Vue({
  data: {
    message: 'Hello World!'
  },
  methods: {
    sayHi: function() {
      this.message;
    }
  }
})
HiroyouHiroyou

一度だけ評価して以降は変更させたくないDOMにはv-onceを付ける。

HiroyouHiroyou

v-htmlにはscriptタグを埋め込むなどのクロスサイトスクリプティングの危険があるので、信頼のできる値にしか使用してはいけない。

HiroyouHiroyou

HTMLタグの属性にはMustache構文が使えないのでv-bindを利用する。v-bindは省力できる。v-bindの引数に変数を使う場合は[]で括る。またv-bindにオブジェクトを渡すこともできる。

<a v-bind:href="url">Google</a>
<a :href="url">Google</a>
<a :[attr]="url">Google</a>
<a v-bind="{ href: url }">Google</a>
<a v-bind="anObject">Google</a>

new Vue({
  data: {
    url: 'https://www.google.com',
    attr: 'href',
    anObject: {
      href: 'https://www.google.com'
    }
  }
})
HiroyouHiroyou

v-onでは何もしなければeventが第一引数として渡される。呼び出すときは関数名だけ書けば良い。

<p v-on:mousemove="changeMousePosition">マウスを乗せてください。</p>

new Vue({
  ...
  methods: {
    changeMousePosition: function(event) {
    }
  }
})

引数とイベントの両方を扱う場合は、呼び出し側で$eventを渡す必要がある。

<p v-on:mousemove="changeMousePosition(10, $event)">マウスを乗せてください。</p>

new Vue({
  ...
  methods: {
    changeMousePosition: function(divideNumber, event) {
    }
  }
})
HiroyouHiroyou

イベント修飾子として、.stopと.preventを学習した。

キー修飾子を使い、例えばv-on:keyup.enterだとEnterを押したときだけ反応するようになる。

v-onでも引数に変数を使う場合は[]で括る。

省略するときはコロンも含めて@で置き換える。例: @click="aMethod"

HiroyouHiroyou

v-modelで双方向データバインディングを実現する。これまではmodelの変更をviewに適用していたが、viewの変更でもmodelが変更されるようになる。

<input type="text" v-model="message">

new Vue({
  data: {
    message: 'こんにちは'
  }
})
HiroyouHiroyou

data内ではthisを使って他のdataにアクセスすることは出来ず、静的な値を定義する場所として用いる。動的な値を使いたい場合はcomputedメソッドを用い、関数を定義し、returnする。呼び出す時は()を付けない。computedメソッドは依存関係に基づいてキャッシュするが、methodsはキャッシュしない。methodsは依存関係にないものが変化しても再計算するので、無駄な計算をしないよう注意して利用する。特別な理由がない限りはcomputedメソッドを使うのが良さそう。

data: {
  counter: 0
},
computed: {
  lessThanThreeComputed: function() {
    return this.counter > 3 ? '3より上' : '3以下'
  },
},
methods: {
  lessThanThreeMethod: function() {
    return this.counter > 3 ? '3より上' : '3以下'
  }  
}
HiroyouHiroyou

watchも値が変化したときに処理をするものだが、(キャッシュがないから?)基本的には使用しない。表示されないものの変化をトリガーにする場合などに使える。

data: {
  counter: 0
},
watch: {
  counter: function() {
    const vm = this; // 非同期関数の中でthisにアクセスできないので。
    setTimeout(function() {
      vm.counter = 0
    }, 3000)
  }
}
HiroyouHiroyou

methodsをv-onディレクティブに渡す時は()を付けずにメソッド名で渡すこともできるし、JavaScript式として()を付けて渡すこともできる。Mustache構文ではメソッド名を渡すだけでは動かないので()を付ける。

HiroyouHiroyou

CSSのclassをバインディングする場合、2通りの方法がある。ひとつはオブジェクトを渡し、それぞれにtrue/falseを適用する。ケバブケースの場合はシングルクォートで括る必要がある。true/falseのところにはdataのbooleanプロパティを渡しても良い。

<h1 :class="{ red: isActive, 'bg-blue': false }">Hello</h1>
data: {
  isActive: true
}

オブジェクトを渡しても良い。なぜComputed?isActiveを使いたいから。

<h1 :class="classObject">Hello</h1>
data: {
  isActive: true
},
computed: {
  classObject: function() {
    return {
      red: isActive,
      'bg-blue': false
    }
  }
}
HiroyouHiroyou

もうひとつはdataで定義しておいた値を配列で渡す方法である。オブジェクトを渡す方法との組み合わせもできる。

<h1 :class="[color, bg]">Hello</h1>
data: {
  color: 'red',
  bg: 'bg-blue'
}
HiroyouHiroyou

styleをバインディングすることもできる。

<h1 :style="{ color: textColor, 'background-color': bgColor }">Hello</h1>
data: {
  textColor: 'red',
  bgColor: 'blue'
}

または

<h1 :style="styleObject">Hello</h1>
data: {
  styleObject: {
    color: 'red',
    'background-color': 'blue'
  }
}

基本のスタイルと固有のスタイルというように複数指定する場合に配列構文を用いる。あとに指定した方で上書き。

<h1 :style="[baseStyles, styleObject]">Hello</h1>
data: {
  styleObject: {
    color: 'red',
    'background-color': 'blue'
  },
  baseStyles: {
    fontSize: '60px'
  }
}
HiroyouHiroyou

条件付きレンダリング

v-elseはv-ifかv-else-ifの直後に書く必要がある。間に何か挟まると表示されない。

<p v-if="ok">OK!</p>
<p v-else-if="maybeOk">maybe OK!</p>
<p v-else>Not OK...</p>

data: {
  ok: false,
  maybeOk: true
}
HiroyouHiroyou

templateタグはHTML5で導入されたもので、v-if等を複数タグにまとめて適用したいときに使える。templateタグは実際には描画されない。

HiroyouHiroyou

v-showもv-ifと同じように動作するがdisplay: none;によって描画されなくなるという違いがある。v-ifは要素そのものを描画しない。display: none;に出来ないtemplateタグは使えないので注意する。v-ifは繰り返しに弱くチラつきの原因になり、v-showは初期レンダリングが遅くなるデメリットがある。

HiroyouHiroyou

リストレンダリング

v-forを使って配列を繰り返し処理できる。inの代わりにofを使うこともできる(違いはなし)

<ul>
  <li v-for="(fruit, index) in fruits">({{ index }}) {{ fruit }}</li>
</ul>
data: {
  fruits: ['りんご', 'バナナ', 'ぶどう']
}

v-forの対象はオブジェクトでも良い。繰り返しで取得できるのは、第一引数がvalueで、第二引数がkeyであり、第三引数としてindexも取得できる。順番は保証されないので注意する。

<ul>
  <li v-for="(value, key, index) in object">({{ index }}) {{ key }} - {{ value }}</li>
</ul>
data: {
  firstName: '太郎',
  lastName: '山田',
  age: 21
}
HiroyouHiroyou

要素を複数含めたい場合はtemplateタグを用いる。

<ul>
  <template v-for="fruit in fruits">
    <li>{{ fruit }}</li>
    <hr>
  </template>
</ul>
HiroyouHiroyou

v-forには整数値を渡すこともできる。

<ul>
  <li v-for="n in 10">{{ n }}</li>
</ul>
HiroyouHiroyou

以下の例では、fruitsから要素を削除しても、<div>ごとにまとめて削除されるのではなく、レンダリング効率の良い方法で処理される。そのため<div>の中身が以前と一致しないことがある。

<ul>
  <div v-for="fruit in fruits">
    <p>{{ fruit }}</p>
    <input type="text">
  </div>
</ul>
<button @click="remove">先頭を削除</button>

methods: {
  remove: function() {
    this.fruits.shift()
  }

上の問題を解決するためにkeyをv-bindし、必ず使うこと。divの代わりにtemplateを使うことはできない。また、indexだと要素を削除したときにズレてしまうため使用してはいけない。keyが重複しないようにも注意すること。

<div v-for="fruit in fruits" :key="fruit">
HiroyouHiroyou

セクション4: Vueインスタンスとその内部構造はこうなっている

Vueインスタンスは複数作ることが出来る。

変数に入れておけば、外からアクセスできる。プロパティを追加することは出来ない(厳密には、追加は出来ているが、リアクティブになっていない(getter, setterは初期化時にしか用意されない。))

dataそのものにアクセスしたい場合はvm.$dataでアクセスできる。

HiroyouHiroyou

elプロパティを使わず、$mountメソッドで後から接続することも可能である。

vm.$mount('#app')

new Vue({
  ...
}).$mount('#app') // とも書ける
HiroyouHiroyou

templateプロパティを使って、Vueインスタンス内に文字列でDOMを書くこともできなくはない。

HiroyouHiroyou

render関数プロパティを使って、DOMを書くこともできる。厳密には、render関数の引数でVNode(仮想Node)を返すメソッドを受け取り、VNodeを返す。実は、テンプレートを書いた場合でも、templateプロパティを使った場合でも、内部ではrender関数に変換されている。

render: function(h) { // 仮想DOMを扱うときによくhと表現されるらしい
  return h('h1', 'こんにちは、' + this.name);
}

document.createElement('div')は直接DOMを作成する。

HiroyouHiroyou

this.$destroyでVueインスタンスを破壊することができる。レンダリングされたDOMは削除されないが、以降の変更が反映されなくなる。

HiroyouHiroyou

elにクラスを指定して、そのクラスを持つDOMが複数あっても、適用されるのは1度だけである。使い回したい場合はコンポーネントを作成する。

Vue.component('hello', {
  template: '<p>Hello</p>'
})
HiroyouHiroyou

セクション5: Vue CLIを使った実践的な開発をはじめる方法

以前はrender関数にタグ名とその値を渡したが、コンポーネントオブジェクトを渡すこともできる。

.vueファイルのことをシングル(単一)ファイルコンポーネントと呼び、template, script, styleの3つのタグから成る(不要であれば省略できる)

HiroyouHiroyou

セクション6: ゼロから始めるコンポーネント

Vueインスタンスのdataはオブジェクトで良かったが、コンポーネントではオブジェクトを返す関数でなければならない。さもないとdataを共有することになり干渉してしまうため。

Vueインスタンスにcomponentsプロパティを登録することで、そのインスタンス内でしか使えないコンポーネントになる。

var component = {
  data: function() { return { ... } },
  template: '<p>いいね( {{ number }} )</p>'
}
components: {
  'my-component': component;
}
HiroyouHiroyou

.vueファイルはimportして使うもので、オブジェクトにして使えるようにしてくれる。

HiroyouHiroyou

data: function() { ... }と書いていたところはES6ではdata() { ... }と書ける。

HiroyouHiroyou

.vueファイルの<template>の中では、ルート要素を1つにしなければならない。

componentsに登録するときはパスカルケースLikeHeaderで登録すれば、ケバブケースlike-headerでも利用できるが、ケバブケースで登録するとパスカルケースでは利用できない。パスカルケースの方が自動補完が効くし、HTML要素との見分けがしやすい。

コンパイルを通さないDOMテンプレートで書くときは、ブラウザは大文字と小文字を区別せず、先にHTMLを読み込むため、必ずケバブケースを使用しなければならない。

キャメルケースlikeHeaderでも動くが、Vue.jsがパスカルケースを使うように言っている。

HiroyouHiroyou

単純にstyleタグに書くとグローバルスコープになるが、scopedと書くことでローカルスコープになる。<style scoped>

HiroyouHiroyou

セクション7: コンポーネント間でデータを受け渡す方法

propsで親から子にデータを渡す。受け取り側のコンポーネントに、受け取りたい名前をキーとし、型を値とするオブジェクトで定義する。配列でも定義できるが、バリデーションが利くオブジェクトの方が良さそう。値を型ではなくオブジェクトにして、より複雑なバリデーションにすることも可能である。

受け渡し側はコンポーネントを呼び出す際の属性として渡す。動的な値を渡す場合はv-bindを使用する。

propsでは、キャメルケースかケバブケースを使う。受け渡し側ではケバブケースを使うのが公式のオススメである。

// 親コンポーネント
<LikeNumber :totalNumber="number"></LikeNumber>
data() {
  return {
    number: 10
  }
}

// 子コンポーネント
props: {
  totalNumber: Number
}
// or
props: {
  totalNumber: {
    type: Number,
    required: true // or default: 10
  }
}

初期値(default)に配列やオブジェクトを直接指定すると参照渡しになってしまい子から親の値を書き換えることが出来てしまうので、関数にして渡すこと。

HiroyouHiroyou

子から親にデータを渡す時は$emitを使う。受け取り側はv-onディレクティブにカスタムイベント名と$eventを指定し、受け渡し側では$emit関数で、第一引数にカスタムイベント名を、第二引数に渡したい値を指定する。

// 親コンポーネント
<LikeNumber :totalNumber="number", @my-click="number = $event"></LikeNumber>

// 子コンポーネント
methods: {
  increment() {
    this.$emit("my-click", this.totalNumber + 1)
  }
}

v-onディレクティブにはメソッドだけを指定することもでき、その場合は第一引数で値を受け取れる。

// 親コンポーネント
<LikeNumber :totalNumber="number", @my-click="incrementNumber"></LikeNumber>
methods: {
  incrementNumber(value) {
    this.number = value
  }
}

// 子コンポーネント
methods: {
  increment() {
    this.$emit("my-click", this.totalNumber + 1)
  }
}

本質的には、値を渡すための仕組みではなく、子のタイミングで親のメソッドを発火するための仕組みである。

カスタムイベント名にはケバブケースを使う。

HiroyouHiroyou

セクション8: コンポーネントの高度な機能はこう書く

slotを使うと、子コンポーネントにHTMLを送れる。親コンポーネントに書いたものから、子コンポーネントで定義した値にアクセスは出来ないが、styleに書いたCSSは適用される。

// 親コンポーネント
<LikeHeader>
  <h1>トータルのいいね数</h1>
  <h2>{{ number }}</h2>
</LikeHeader>

// 子コンポーネント
<template>
  <div>
    <slot></slot>
  </div>
</template>
HiroyouHiroyou

<slot></slot>の中に書いたものはフォールバックコンテンツとして、親コンポーネントで何も渡さなかった場合に使われる。

HiroyouHiroyou

slotを複数持つこともでき、親コンポーネントで書いたものが繰り返し適用される。名前を付けることもでき、区別することもできる。templateタグ(divタグ等ではダメ)で括ってv-slotディレクティブで名前を指定する。

// 親コンポーネント
<template v-slot:title>
  <h2>こんにちは</h2>
</template>
<template v-slot:number>
  <p>{{ number }}</p>
</template>

// 子コンポーネント
<slot name="title"></slot>
<slot name="number"></slot>

親コンポーネントでtemplateタグで括られなかったものは、名前のないデフォルトslotに適用される。内部的にはdefaultという名のslotとして扱われる。

HiroyouHiroyou

スロットプロパティを使うことで、子コンポーネントのデータにアクセスすることも出来なくはない。

// 親コンポーネント
<template v-slot:title="slotProps">
  <h2>{{ slotProps.user.firstName }}</h2>
</template>

// 子コンポーネント
<slot name="title" :user="user"></slot>
data() {
  return {
    user: { firstName: "Jack", lastName: "Donald" }
  }
}

defaultのslotしかない場合はtemplateタグを書く必要がなく、slotPropsは子コンポーネントを呼び出すところに書くことができる。defaultも不要である。

<LikeHeader v-slot="slotProps">
  <p>{{ slotProps }}</p>
</LikeHeader>
HiroyouHiroyou

[]を使えばスロット名を動的にすることも出来る。

v-slot:#で置き換えることが出来る。#defaultは使える。

HiroyouHiroyou

componentタグとis属性で動的に切り替えることができる。v-ifで書く必要はない。

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

毎回コンポーネントを破壊して再生成してを繰り返すため、データを保持したいような場合に都合が悪い。keep-aliveタグの中に入れることでデータが保持される。

<keep-alive>
  <component :is="currentComponent"></component>
</keep-alive>

createdとdestroyedが呼ばれなくなる代わりに、activatedとdeactivatedが呼ばれる。

HiroyouHiroyou

セクション9: こうすればVue.jsでフォームが簡単に作れる

v-modelに指定するのはオブジェクトでも良い。

<input v-model="eventData.title">
data() {
  return {
    eventData: { title: "タイトル" }
  }
}

v-modelの中身は、以下と同じである。

:value="eventData.title"
@input="eventData.title = $event.target.value"
HiroyouHiroyou

.lazy修飾子で、inputイベントからchangeイベントの同期に変わる。

<input v-model.lazy="...">

.number修飾子は、ユーザ入力をNumber型に変換する。input要素はtype="number"にしてもユーザ入力をデフォルトでStringとして受け取る。

.trim修飾子は、前後の空白を削除する(参考:<pre>タグは空白を含めてそのまま表示する。<p style="white-space: pre;">と同じ。)

HiroyouHiroyou

複数行テキスト

<textarea v-model="eventData.detail">

チェックボックスは、単体と複数でv-modelの扱いが異なる。

// 単体
<input type="checkbox" id="isPrivate" v-model="eventData.isPrivate">
<label for="isPrivate">非公開</label>
isPrivate: false // Boolean

// 複数
<input type="checkbox" id="10s" value="10代" v-model="eventData.target">
<label for="10s">10代</label>
<input type="checkbox" id="20s" value="20代" v-model="eventData.target">
<label for="20s">20代</label>
target: [] // Array

ラジオボタン

<input type="radio" id="free" value="無料" v-model="eventData.price">
<label for="free">無料</label>
<input type="radio" id="paid" value="有料" v-model="eventData.price">
<label for="paid">有料</label>
price: "無料"

チェックボックスもラジオボタンも要素をまとめるタグ等があるわけではないが、v-modelを指定することで適切に処理される。

セレクトボックスにはv-forを使うと良い。複数選択はselectにmultiple属性を付けるだけ。その際、v-modelが配列に変わる。

<select v-model="eventData.location">
  <option v-for="location in locations" :key="location">{{ location }}</option>
</select>
locations: ["東京", "大阪", "名古屋"],
eventData: { location: "東京" }
HiroyouHiroyou

カスタムコンポーネントでv-modelを使う場合は、v-modelが裏で何をしているか意識して、必要な要素を子コンポーネントに用意してあげる。

// 親コンポーネント
<EventTitle v-model="eventData.title"></EventTitle>
<EventTitle :value="eventData.title" @input="eventData.title = $event></EventTitle> // 上のタグの実態

// 子コンポーネント
<input id="title" type="text" :value="value" @input="$emit('input', $event.target.value)">
props: ["value"]
HiroyouHiroyou

セクション10: カスタムディレクティブで自由にディレクティブを作る方法

グローバルに登録する場合は、Vue.directiveで登録する。第一引数が名前(v-は付けない)、第二引数が登録したいフック関数(5種類)のオブジェクトである。

Vue.directive("border", {
  bind() { /* ディレクティブが初めて対象の要素に紐づいた時 */ },
  inserted() { /* 親Nodeに挿入された時 */ },
  update() { /* コンポーネントが更新される度、子コンポーネントが更新される前 */ },
  componentUpdated() { /* コンポーネントが更新される度、子コンポーネントが更新された後 */ },
  unbind() { /* ディレクティブが紐づいている要素から取り除かれた時 */ }
})

各フック関数は引数としてel, binding, vnodeの3つと、update()とcomponentUpdated()だけoldVnodeも含めた4つを取ることができる。

よく使うのはbind()とupdate()の2つで、同じ内容を持つことが多いので、省略記法が存在し、第二引数に無名関数を直接書くことができる。elにはカスタムディレクティブを書いた要素が渡される。

Vue.directive("border", function(el, binding) {
  el.style.border = "solid black 2px"
})

binding.valueで受け取ったデータを使うことが出来る。渡すときはJavaScript式として評価されるので文字列ならクォートで括ること。binding.argで引数を受け取ることが出来る。引数はひとつしか取れないことに注意する。binding.modifiers.xxxでその修飾子が存在するかBooleanを返す。

Vue.directive("border", function(el, binding) {
  el.style.borderWidth = binding.value.width
  el.style.borderColor = binding.value.color
  el.style.borderStyle = binding.arg
  if (binding.modifiers.round) {
    el.style.borderRadius = "0.5rem"
  }
  if (binding.modifiers.shadow) {
    el.style.boxShadow = "0 2px 5px rgba(0, 0, 0, 0.26)"
  }
})

<p v-border:solid.round.shadow="{width: '5px', color: 'red'}">Home</p>
HiroyouHiroyou

ローカルに登録する場合はdirectivesに書く。

<script>
export default {
  directives: {
    border(el, binding) {
      el.style.xxx = yyy
    }
  }
}
</script>

カスタムディレクティブではthisが使えないので注意する。

HiroyouHiroyou

セクション11: フィルターとミックスイン

Vue.filterでグローバルフィルターを登録する。第一引数に名前、第二引数に引数を持った関数を取る。適用する際はパイプで繋ぐ。

Vue.filter("upperCase", function(value) {
  return value.toUpperCase()
})
<p>{{ title | upperCase }}</p>

ローカルに登録する場合は、filtersに登録する。

filters: {
  lowerCase(value) {
    return value.toLowerCase()
  }
}

フィルターはパイプを重ねて連結することができる。フィルターもthisは使えない。フィルターもcomputedのようにキャッシュをしないので、頻繁に描画をするコンポーネントで使う際は注意する。

HiroyouHiroyou

mixinをローカルで使う場合は、共通化したい部分をオブジェクトにしてexportし、importしてmixinsの配列の要素にして使う。import fromで指定する@はsrcを示す。

// components/CountNumber.vue
<script>
import { tokyoNumber } from "@/tokyoNumber"

export default {
  mixins: [tokyoNumber]
}
</script>

// src/tokyoNumber.js
export const tokyoNumber = {
  data() { ... },
  filters: { ... }
}

mixinに定義されている要素と重複する場合は、基本的にコンポーネントのもので上書きされる。ただし、ライフサイクルフックの場合は、mixinのものが先に実行され、コンポーネントのものが後で実行され、上書きはされない。

HiroyouHiroyou

mixinをグローバルで使う場合は、すべてのVueインスタンスに自動的に適用されてしまうので注意する。故に、第一引数に名前を指定することもない。グローバルmixinが一番最初に呼ばれる。

Vue.mixin({
  created() {
    console.log("global mixin")
  }
})
HiroyouHiroyou

セクション12: トランジションとアニメーション

<transition>と<transition-group>がある。<transition>には要素を1つしか入れられない(最終的に1つになれば良い。)<transition-group>の中の要素にv-forを使い、滑らかに要素を追加・削除するなどの使い方ができる。

nameを付けなかった場合はdefaultのname="v"になる。transitionさせるときは裏で、まずv-enter/leave, v-enter/leave-activeを付与する。1フレーム後、v-enter/leaveを削除し、v-enter/leave-toを付与する。transitionが終わると、v-enter/leave-to, v-enter/leave-activeを削除する。

<transition name="fade">
  <p v-if="show">hello</p>
</transition>

<style scoped>
.fade-enter { /* 現れる時の最初の状態 */
  opacity: 0; }
.fade-enter-active { /* 現れる時のトランジションの状態 */
  transition: opacity 5s; }
.fade-enter-to { /* 現れる時の最後の状態 */
  opacity: 1; }
.fade-leave { /* 消える時の最初の状態 */
  opacity: 1; }
.fade-leave-active { /* 消える時のトランジションの状態 */
  transition: opacity 5s; }
.fade-leave-to { /* 消える時の最後の状態 */
  opacity: 0; }
</style>

nameにv-bindを使って動的に変更することもできる。

HiroyouHiroyou

163. CSSアニメーションを使ってslideするトランジション効果を実際に作成する

activeにanimationプロパティを書く。leaveではreverseが使える。active以外は書かなくて良い。@keyframesの詳細な説明はなし。

.v-enter-active {
  animation: slide-in 0.5s
}
.v-leave-active {
  animation: slide-in 0.5s reverse
}

@keyframes slide-in {
  from {
    transform: translateX(100px)
  }
  to {
    transform: translateX(0)
  }
}
HiroyouHiroyou

transitionとanimationを併用することができる。時間が異なる場合は、デフォルトだと長い方に合わせられるが、<transition>タグにtype="xxx"を指定することで短い方に合わせることもできる。

HiroyouHiroyou

<transition>タグにappearを付けることで、初期描画時にもtransitionさせることができる。

HiroyouHiroyou

カスタムトランジションクラスを使うことで、Animate.cssを使うことができる。animate__animatedとアニメーションクラスを付ける。関係のないクラスが付いて気になる場合は空文字を指定することで、そのクラスが付かなくすることはできる。

<transition
  enter-active-class="animate__animated animate__bounce"
  leave-active-class="animate__animated animate__shakeX"
>
HiroyouHiroyou

<transition>でv-if等を使って中身の要素を切り替える際、HTMLタグが同じだと中身だけ変えようとしてtransitionが起きないことがある。こういう問題を避けるためにkeyを使う習慣を付けると良い。

<transition name="fade">
  <p v-if="show" key="bye">さよなら</p>
  <p v-else key="hello">こんにちは</p>
</transition>
HiroyouHiroyou

要素が切り替わるようなtransitionでは、何もしないと要素が現れるのと消えるのが同じタイミングになり重なってしまうことがある。それを防ぐためのmode属性が存在し、out-in(消えてから現れる)かin-out(現れてから消える)を指定する。

<transition name="fade" mode="out-in">...</transition>
HiroyouHiroyou

JavaScriptアニメーションでtransitionさせる。アニメーションが終わったことを伝えるためにdone()を使う。CSSアニメーションと一緒に使うときはdone()をしなくても良い(なぜ?)leave-cancelledはv-showのときだけ発火される。

<transition
  @before-enter="beforeEnter"
  @enter="enter"
  @after-enter="afterEnter"
  @enter-cancelled="enterCancelled"
  @before-leave="beforeLeave"
  @leave="leave"
  @after-leave="afterLeave"
  @leave-cancelled="leaveCancelled"
methods: {
  beforeEnter(el) { /* 現れる前 */ },
  enter(el, done) { /* 現れるとき */ },
  afterEnter(el) { /* 現れた後 */ },
  enterCancelled(el) { /* 現れるアニメーションがキャンセルされたとき */ },
  leaveEnter(el) { /* 消える前 */ },
  leave(el, done) {/* 消えるとき */},
  afterLeave(el) {/* 消えた後 */},
  leaveCancelled(el) {/* 消えるアニメーションがキャンセルされたとき */}
}
HiroyouHiroyou

beforeEnter() → v-enter, v-enter-activeを追加 → enter() → v-enterを削除、v-enter-toを追加 → アニメーション → v-enter-to, v-enter-activeを削除 → afterEnter() という順番

beforeLeave() → v-leave, v-leave-activeを追加 → leave() → v-leaveを削除、v-leave-toを追加 → アニメーション → v-leave-to, v-leave-activeを削除 → afterLeave() という順番

HiroyouHiroyou

CSSアニメーションを適用しないことをv-bindでfalseを渡すことで明示できる。

<transition :css="false"
HiroyouHiroyou

JavaScriptでのアニメーションの例

beforeEnter(el) {
  el.style.transform = 'scale(0)'
},
enter(el, done) {
  let scale = 0
  const interval = setInterval(() => {
    el.style.transform = `scale(${scale})`
    scale += 0.1
    if ( scale > 1) {
      clearInterval(interval)
      done()
    }
  }, 200)
},
leave(el, done) {
  let scale = 1
  const interval = setInterval(() => {
    el.style.transform = `scale(${scale})`
    scale -= 0.1
    if ( scale < 0) {
      clearInterval(interval)
      done()
    }
  }, 200)
}
HiroyouHiroyou

<transition-group>と<transition>の違いは5つ。

  • 複数の要素を入れられる
  • 必ずkey属性が必要
  • transition-groupはspanタグになる。tag属性で変更は可能
  • mode属性がない
  • v-moveクラスが存在する
.v-move { /* 要素を追加したときに移動する要素に適用される */
  transition: transform 1s
}
.v-leave-active { /* 要素を削除したときはv-moveが適用されないのでposition: absoluteで対応する */
  position: absolute
}
HiroyouHiroyou

transitionを再利用するときはコンポーネントを作ってslotで中身を渡せるようにする。

HiroyouHiroyou

セクション13: Vue Routerでルーティングできる仕組み

Vue Routerはプラグインとして提供されている。Vue.useでプラグインを適用する。pathとcomponentのオブジェクトを持つroutes配列を指定した、Routerインスタンスをexportする(export defaultすると使う側で自由に名前を付けることができる。)

// router.js
import Router from 'vue-router'
import Home from './views/Home.vue'
Vue.use(Router)
export default new Router({
  routes: [
    { path: '/', component: 'Home' }
  ]
})

// App.vue
<template><div><router-view></router-view></div></template>
HiroyouHiroyou

リンクをaタグで書くとサーバーにアクセスが発生してしまうが、router-linkを使うことでSPAに出来る。router-linkのtoはv-bindで動的に書くことも出来る。

<router-link to="/">Home</router-link>
HiroyouHiroyou

リンクがアクティブなときに適用されるクラスを指定するactive-class属性がある。ただしデフォルトではtoに指定したpathが完全一致しなくとも含まれるだけで適用されてしまい、exact属性を付けることで完全一致になる。

<router-link active-class="link--active" exact></router-link>
HiroyouHiroyou

JavaScriptで遷移させたいときは、$routerにpushする。

methods: {
  toUsers() {
    this.$router.push('users') // push({ path: 'users' })でも可
  }
}
HiroyouHiroyou

routesにpathを書く際、コロンを付けた任意の変数名でパラメータを取得できる。使うときは$route.params.xxxでアクセスできる。

routes: [
  { path: '/users/:id', component: Users }
]

$router$routeの違いに注意する。

HiroyouHiroyou

パラメータが変わっただけではライフサイクルフックが呼ばれないことに注意する。watchを使うことで監視することが可能である。

watch: {
  $route(to, from) { /* toが変更後の値、fromが変更前の値 */ }
}
HiroyouHiroyou

routesのパラメータにprops: trueとすることで、idがpropsとして渡されるようになる。

// router.js
routes: [
  { path: '/users/:id', component: Users, props: true }
]

// Users.vue
<p>{{ id }}</p>
props: ['id']
HiroyouHiroyou

通常ではrouter-viewが置き換わったコンポーネント内でrouter-viewを呼ぶことは出来ない。routesのパラメータにchildrenを持つことで可能になる。

import UsersPosts from './views/UsersPosts.vue'
routes: [
  { path: 'users/:id', component: Users, props: true, children: [
    { path: 'posts', component: UsersPosts }
  ]}
]
HiroyouHiroyou

routesには任意のnameを付けることができ、router-linkで使用することが出来る。パラメータを渡すときはparamsで渡す。pathではparamsは渡せない。

routes: [
  { path: 'users/:id', ..., childred: [
    { path: 'profiles' component: UsersProfile, name: 'users-id-profile' }
  ]}
]
<router-link :to="{ name: 'users-id-profile', params: { id: Number(id) + 1 } }"
HiroyouHiroyou

queryを渡すこともできる。

<router-link :to="{ name: xxx, params: xxx, query: { lang: 'ja' } }"
HiroyouHiroyou

router-viewに名前を付けることもできる。そのとき、routesのcomponentをcomponentsにし、オブジェクトを渡す。propsも名前ごとにtrue/falseを指定しなければならない。

routes: [
  { path: '/users/:id', components: {
    default: Users,
    header: HeaderUsers
  }, props: {
    default: true,
    header: false
  }, ... }
]
HiroyouHiroyou

リダイレクトを設定することもでき、*で全てをキャッチするルートを作ることが出来る。

routes: [
  { path: '*', redirect: '/' }
]
HiroyouHiroyou

router-linkにhashを渡すこともできるが、#は自分で書かなければならないのと、Vue Routerを経由すると自動でidまでスクロールしない(ブラウザ経由であればスクロールする。)

スクロールさせるにはnew Router()にscrollBehavior()関数を定義する。最初のレンダリングでは呼ばれないが、それ以降は毎回呼ばれるので気をつける。

また、transitionをさせているとtransition中にそのidがまだ存在しない可能性があるので気をつける。

scrollBehavior()は3つの引数を取り、to/fromは移動先/前の情報を、savedPositionは「戻る」をしたときの位置、を持っている。

<router-link :to="{ name: xxx, params: xxx, hash: '#next-user' }"

scrollBehavior(to, from, savedPosition) {
  if (savedPosition) { return savedPosition }
  if (to.hash) {
    return {
      selector: to.hash,
      offset: { x: 0, y: 100 } // 上に100px余白を取る
    } // { x: 0, y: 0 }で指定することもできる。
  }
}
HiroyouHiroyou

transitionをさせつつ、hashでスクロールもさせたいときは、@before-enterを利用する。その中で$rootに対しイベントを発火させて、new Routerのインスタンスで受け取る。this.appはrouterを登録したAppである。

<transition @before-enter="beforeEnter">

methods: {
  beforeEnter() {
    this.$root.$emit('triggerScroll')
  }
}

// router.js
scrollBehavior(to, from, savedPosition) {
  return new Promise(resolve => {
    this.app.$root.$on('triggerScroll', () => {
      let position = { x: 0, y: 0 }
      if (savedPosition) { position = savedPosition }
      if (to.hash) { position = { selector: to.hash } }
      resolve(position)
    })
  })
}
HiroyouHiroyou

あらゆるページ遷移の前に特定の処理をしたい場合はbeforeEachガードを使用する。処理が終わったら必ずnext()を呼ばなければならない。next()を書かなかったり、next(false)を呼ぶと次のページに遷移しない。nextの引数にはパスを指定することもできるが無限ループに注意する。

// main.js
router.beforeEach((to, from, next) => {
  next()
})
HiroyouHiroyou

特定のページへの遷移の前に処理をしたい場合はbeforeEnterガードをroutesに書く。

// router.js
routes: [
  { path: '/', beforeEnter(to, from, next) { next() }}
]
HiroyouHiroyou

ルートコンポーネントに指定できるガードが3つある。beforeEnterとの大きな違いはUpdate/Leaveでも指定できることにある。Enterではまだthisを使えないが、Update/Leaveではthisが使える。またnextの引数でvmを受け取った処理を書くことができる。Updateは同じコンポーネントのページに遷移するときに呼ばれる。

Leaveは例えば「本当にこのページを離れますか?」のようなときに使える。

beforeRouteEnter(to, from, next) { next() },
beforeRouteUpdate(to, from, next) { next() },
beforeRouteLeave(to, from, next) { next() },
HiroyouHiroyou

コンポーネントを遅延ローディングすることができる。アノテーションで名前を指定することもできる。指定しない場合はランダムな数値。名前を同じにすることでひとつのChunkにまとめることも出来る。vue-cli@3はすべてのrouteをprefetchする。prefetchでは空いた時間にデータをダウンロードする。

// import Home from './views/Home.vue'
const Home = () => import(/* webpackChunkName: 'Home' */ './views/Home.vue')
HiroyouHiroyou

Vuexを使う場合は一般的にstore.jsを用意し、main.jsでVueインスタンスにstoreを設定し、computedで$store.stateを経由して値にアクセスする。

// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
  state: { count: 2 }
})

// main.js
import store from './store'
new Vue({
  ..., store, ...
})
HiroyouHiroyou

共通のメソッドはgettersとしてStoreに定義し、$store.gettersを経由して呼び出す。

// store.js
new Vuex.Store({
  getters: {
    doubleCount: state => state.count * 2
  }
})

// Home.vue
computed: {
  doubleCount() {
    return this.$store.getters.doubleCount
  }
}
HiroyouHiroyou

mapGettersヘルパーを使うと簡単に書ける。オブジェクトにすることで名前を変えて使うことも出来るが普通はやらない。

import { mapGetters } from 'vuex'
computed: {
  ...,
  ...mapGetters([ 'doubleCount', 'tripleCount' ])
}
HiroyouHiroyou

mutationsを使ってデータの変更を追いやすくする。中に定義する関数は第一引数にstateを取り、値を受け取りたい場合は第二引数を使う。呼び出すときは$store.commitを使い、第一引数に呼び出したい関数名を書き、第二引数に渡したい値を書く。複数の値を受け渡しする場合はオブジェクトを使う。

// store.js
new Vuex.Store({
  mutations: {
    increment(state, number) {
      state.count += number
    }
  }
})

// HeaderHome.vue
methods: {
  increment() {
    this.$store.commit('increment', 1)
  }
}
HiroyouHiroyou

mapMutationsヘルパーを使うと簡単に書ける。methodsに書いていた引数はmutationを呼び出すところで渡す。

// HeaderHome.vue
<button @click="increment(1)">+1</button>

import { mapMutations } from 'vuex'

methods: {
  ...mapMutations(['increment', 'decrement'])
}
HiroyouHiroyou

mutationsは非同期処理をサポートしない。非同期処理をするためのactionsが用意されている。定義する関数は第一引数にcontextを取り、値を受け取りたい場合は第二引数を使う。呼び出すときは$store.dispatchを使い、第一引数に呼び出したい関数名を書き、第二引数に渡したい値を書く。複数の値を受け渡しする場合はオブジェクトを使う。

// store.js
new Vuex.Store({
  actions: {
//    increment(context, number) {
//      context.commit('increment', number)
  increment({ commit }, number) { // ここをオブジェクトにする省略記法がある
    commit('increment', number)
  }
})

// HeaderHome.vue
methods: {
  increment() {
    this.$store.dispatch('increment', 1)
  }
}
HiroyouHiroyou

mapMutationsヘルパーを使うと簡単に書ける。

// HeaderHome.vue
<button @click="increment(1)">+1</button>

import { mapActions } from 'vuex'

methods: {
  ...mapActions(['increment', 'decrement'])
}
HiroyouHiroyou

v-modelを使いたい場合はcomputedにgetとsetを書く。v-modelを分解して設定する方法もある(ここでは省略する。)

<input type="text" v-model="message">

computed: {
  ...mapGetters(...),
  message: {
    get() { return this.$store.getters.message },
    set(value) { this.$store.dispatch('updateMessage', value) }
  }
}
HiroyouHiroyou

Vuex.Storeは肥大化しがちなのでファイルを分割する。src > store > modulesとフォルダを作成し、storeにはindex.jsを置く。modulesの中にcount.jsを置く。

// count.js
const state = { count: 2 }
const getters = { ... }
const mutations = { ... }
const actions = { ... }
export default { state, getters, mutations, actions }

// index.js
import { count } from './modules/count'
new Vuex.Store({
  ...,
  modules: { count }
})
HiroyouHiroyou

store配下に、getters.js, mutations.js, actions.jsを用意する。モジュール化できなかったものをまとめると良い。

// getters.js
export default { ... }

// index.js
import getters from './getters'
new Vuex.Store({
  state: { ... },
  getters,
  ...,
  modules: { count }
})
HiroyouHiroyou

モジュール間で同じ名前を使うと問題になるので、名前空間が用意されている。名前が変わってしまうので、mapGettersするときは第一引数に名前空間を指定する。

// count.js
export default {
  namespaced: true,
  state, getters, mutations, actions
}

// Home.vue
computed: {
  ...mapGetters('count', ['doubleCount', 'tripleCount']),
  // doubleCount() { return this.$store.getters['count/doubleCount'] } と同じ
}
HiroyouHiroyou

セクション15: 世界中に自分のアプリを公開する

npm run buildで作成された本番用のビルドは、distディレクトリに格納される。

例えばNetlifyでアカウントを作成し、distフォルダをドラッグ&ドロップするだけで公開できる。サブドメインも選べる。また、historyモードの場合は、どんなURLでもindex.htmlを返す設定を忘れずに。

// public/_redirects
/* /index.html 200
HiroyouHiroyou

セクション17: ボーナス:axiosを使ってサーバーにhttp通信をする方法

ここではFirebaseが提供する機能のうち、Cloud Firestore (Firestore Database)を使う。

axios.postでPOSTリクエストを送る。引数は、URL、データ、オプションの3つである。URLは「cloud firestore rest api」で検索したときに出てくる公式ドキュメントのものを使う。YOUR_PROJECT_IDや/cities/LAのところは適当に置き換える。axios.postは非同期処理でPromiseを返すのでthen/catchで受ける。

axios.post(
  URL, DATA
).then(response => {
  console.log(response)
}).catch(error => {
  console.log(error)
})
this.name = ''
this.comment = ''
HiroyouHiroyou

axios.getでGETリクエストを送る。引数は、URL、オプションの2つである。axios.getも非同期処理でPromiseを返す。

<div v-for="post in posts" :key="post.name">
  <br>
  <div>名前:{{ post.fields.name.stringValue }}</div>
  <div>コメント:{{ post.fields.comment.stringValue }}</div>
</div>
data() {
  return {
    posts: []
  }
}
created() {
  axios.get(...).then(response => {
    this.posts = response.data.documents
  })
}
HiroyouHiroyou

main.jsでaxiosをインポートしてaxios.defaults.xxxを設定すれば、baseURL等の初期化が可能である。

リクエストヘッダーを初期化する場合は、axios.defaults.headers.common['xxx']を設定する。Authorizationなど。commonはGETやPOSTで共通の設定。特定のリクエストのみに設定する場合は、commonの代わりにgetやpostなどを指定する。

HiroyouHiroyou

main.jsでaxios.interceptors.requestやaxios.interceptors.responseを使うことで割り込み処理を書ける。正常系はrequestならconfigを、responseならresponseを、異常系はPromise.reject(error)を返すのが一般的である。useはIDを返し、それをejectすることで特定の割り込みをスキップさせることもできる。

const interceptorsRequest = axios.interceptors.request.use(
  config => { return config },
  error => { return Promise.reject(error) }
)
axios.interceptors.request.eject(interceptorsRequest)
HiroyouHiroyou

他のbaseURLにもリクエストを送りたい場合はaxios.createを使う。

// axios-auth.js
import axios from 'axios'
const instance = axios.create({
  baseURL: 'xxx'
})
export default instance

// App.vue
import axios from './axios-auth.js'