Vue.jsを100時間ほど勉強して分かったことを整理します。
勉強時間の内訳は、
- Udemyの Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex) をだいたい全て完了(85時間)
- 実際に自分でコードを書いてみた(15時間)
です。
学習開始時のレベルは、JavaScript・jQueryはそれなりに扱うことができ、過去に少しだけReactを勉強したことがある感じでした(専門は Ruby on Rails)。
Vue.js 自体の構文
まず、Vue.js 自体の基本的な構文を整理します。
Vue インスタンス
Vue インスタンスの書き方は次のような感じです。
new Vue({
el: "#app",
data: {
name: "Kei",
age: "30",
counter: 0
},
methods: {
increase: function() {
this.counter += 1;
}
}
})
この app.js
を Vue.js と共にインポートします。
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
<script src="app.js"></script>
{{ }}
{{ }} でVueインスタンスの要素を呼び出すことができます。
<p>{{ name }}</p>
v-bind
HTML要素をVue.jsで動的に指定したい場合は、v-bind
を使います。
<a v-bind:href="link">Google</a>
v-once
v-once
をHTMLタグに含めることで1度描画された内容を変更不可にできます。
<h1 v-once>{{ title }}</h1>
v-html
v-html
はHTMLタグのまま出力するときに用います。
<p v-html="finishedLink"></p>
v-on
v-on
でイベントリスニングします。
<button v-on:click="increase(2, $event)">Click me</button>
new Vue({
methods: {
increase: function(step, event) {
this.counter += step;
}
}
})
v-on:click=“counter++”
のように、if や loop を含まないJSの場合はインラインに直書きできます。
また、v-on:keyup.enter.space
のようにつなげられます。
v-model
v-model
を使えば、Vueインスタンスの data
をHTML要素にバインドできます。変更された場合は、その値を参照しているDOM全てに反映されます。
<input type="text" v-model="name">
<p>{{ name }}</p>
<!-- 入力値によって動的に name が変わる -->
new Vue ({
el: "#app",
data: {
name: "Kei"
}
})
@
v-on:
の省略記法で、@click="link"
のように書けます。
また、v-bind
の代わりに :
も使えます。
:class
:class="{YOUR_CLASS: boolean}"
で動的にCSSを適用できます。
<div
class="demo"
@click="attachedRed = !attachedRed"
:class="{red: attachedRed}"></div>
:class
を複数定義する場合は array で書きます。
:style
:class
と同様に、:style
で動的にスタイルを適用できます。
<div :style="{backgroundColor: color}"></div>
スタイルのプロパティ名はキャメルケース(もしくは"")で書きます。
computed
Vueインスタンスの computed
オブジェクトは、 methods
に似ていますが、data プロパティのように処理結果を保持させる時に使います。methods
と違い、呼び出す時に ()
は不要です。
new Vue ({
computed: {
divClassses: function() {
return {
red: this.attachedRed,
blue: !this.attachedRed
}
}
}
})
watch
Vueインスタンスの watch
オブジェクトは、特定のプロパティの変化をトリガーに処理を走らせる時に使います。data
だけでなく computed
のプロパティも watch
できます。
new Vue ({
watch: {
// counter を watch する
counter: function(value) {
var vm = this;
setTimeout(function() {
vm.counter = 0;
}, 2000);
})
}
})
v-if, v-else
v-if
が false
の時は、そのHTML要素とその子要素をDOMから取り除きます。
v-else
は直前の v-if
が false
の時に、そのHTML要素とその子要素をDOMに表示させます。
<p v-if="boolean">Hello!</p>
<p v-else>Hello again!</p>
v-else-if
という書き方もあります:
https://vuejs.org/v2/guide/conditional.html#v-else-if
v-show
v-show
は v-if
と同じ振る舞いをしますが、false
の場合 display: none;
となって、HTMLコード自体は残ります。
v-for
v-for
で配列などプロパティに対するイテレーションを書きます。
<!-- ingredients: ["meat", "fruit", "cookies"] -->
<ul>
<li v-for="ingredient in ingredients">{{ ingredient }}</li>
</ul>
index を取りたい時は次のように第2引数を定義してあげます。
<li v-for="(ingredient, i) in ingredients">{{ ingredient }}</li>
上は配列の例ですが、Objectにも同様に v-for
を使えます。Objetctの場合、第2引数が key, 第3引数が index になります。
<li v-for="person in persons">
<div v-for="(value, key, index) in person">
{{ key }}: {{ value }} ({{ index }})
</div>
</li>
また、Rubyでいう 10.times {}
は v-for="n in 10"
と書きます。
v-for
や v-if
の中で変更するデータを Vue.js に認識させたい場合は、意図した挙動になるように v-key=“ユニークな値”
を付けます。
<li v-for="ingredient in ingredients" :key="ingredient"></li>
ライフサイクル
Vueインスタンスのライフサイクルは次の通りです。それぞれのタイミングでフックして処理を書くことができます。
- beforeCreate
- Created
- beforeMount
- mounted
- (DOM描画)
- (DOMの変更を検知)
- beforeUpdate
- updated
- ( this.$destroy(); )
- beforeDestroy
- Destroyed
Vue.js でのアプリ開発
上記で見てきたように、.js
に書いたVueインスタンスでもHTMLで使えますが、実際のアプリ開発では .vue
の形式でコードを書いていきます。
そして最終的には、複数の .vue
ファイル等から単一ファイルを生成し、本番環境にデプロイします。
Vue CLI
Vue CLI は Vue.js でのアプリ開発で利用するテンプレートを提供してくれるツールです。
インストール
以下のコマンドでインストールできます。
$ sudo npm install -g vue-cli
webpack-simple
というテンプレートで新規アプリを作る場合は、次のコマンドを実行します。
$ vue init webpack-simple [YOUR_APP_NAME]
開発環境でアプリを立ち上げます。
$ npm run dev
エラー 1
Error: Cannot find module 'webpack-cli/bin/config-yargs'
このエラーがでたら、次のコマンドを実行してみてください:
https://github.com/rails/webpacker/issues/1295
$ yarn add webpack-cli -D
$ npm run dev
エラー 2
TypeError: Cannot destructure property `compile` of 'undefined' or 'null'.
このエラーがでたら、次のコマンドを実行してみてください:
https://github.com/vuejs/vue-cli/issues/714
$ yarn remove webpack-dev-server
$ yarn add webpack-dev-server@2.9.1 --dev
$ yarn run dev
webpack-simple テンプレート
webpack-simple で作られるテンプレートは次の通りです。
project_name
├── src
│ ├── assets
│ ├── App.vue
│ └── main.js
├── .babelrc
├── .gitignore
├── index.html
├── package.json
└── webpack.config.js
- src(メインでいじってくフォルダ。Vue.jsのコードを書いていく。)
- App.vue(トップのVueコンポーネント)
- main.js(App.vueをコンパイルして、HTML Viewに描画する。)
- .babelrc(
babel
のコンフィグファイル。EC6 などを JSコードにコンパイルしてくれる。) - index.html(View テンプレート。ここに src で書いたコードが
webpack
によりビルドされて描画される。) - package.json(ソースの依存関係を記載する。)
ビルド
以下のコマンドで、本番で利用できる dist ファイルが生成されます。
$ npm run build
デプロイするのは index.html
と build.js
ファイルだけで、build.js
は dist
フォルダに格納します。
Vueコンポーネント
Vueコンポーネントでは、template
script
style
を単一ファイル内で書きます。
例えば、次のような感じです。
<template>
<div>
<p>Server Status: {{ status }}</p>
<hr>
<button @click="changeStatus">Change Status</button>
</div>
</template>
<script>
export default {
data () {
return {
status: 'Critical'
}
},
methods: {
changeStatus() {
this.status = 'Normal';
}
}
}
</script>
<style>
</style>
通常のVueインスタンスと異なり、Vueコンポーネント内の data
は data(){}
で(関数的に)定義しないといけません。Vueコンポーネントは、Vueインスタンスの拡張なので、data
をオブジェクト型で定義すると、元の data
と干渉してしまい、意図してるように動かなくなってしまうからです。
また <template>
内にキーエレメントがないと(div
など1つのタグでラップしていないと)エラーが出ます。
グローバルにインポート
この Home.vue
を main.js
に読み込ませる場合は次のように書きます。
import Vue from 'vue'
import App from './App.vue'
import Home from './Home.vue'
Vue.component('app-server-status', Home)
new Vue({
el: '#app',
render: h => h(App)
})
これで <template>
内の <app-server-status></app-server-status>
に Homeコンポーネントを描画できるようになります。
ローカルにインポート
.vue ファイル内でローカルにインポートする場合は、次のように書きます。
<template>
<div>
<app-server-status></app-server-status>
</div>
</template>
<script>
import Home from './Home'
export default {
components: {
'app-server-status': Home // テンプレート名を上書き
}
}
</script>
コンポーネントの定義にキーだけ書いた場合は、次のように認識されます。
components: {
Servers // Servers: Servers と同義
}
コンポーネントの設計
Vueコンポーネントの保存場所は、components
ディレクトリを作って、そこに Shared
や ServerStatus
などのサブディレクトリを作って格納していく設計が一般的です。
project_name
├── src
│ ├── assets
│ ├── components
│ │ ├── Shared
│ │ └── ServerStatus
│ ├── App.vue
│ └── main.js
ローカルスタイル
style scoped
と書くことで、スタイルをローカルに適用できます。
<style scoped>
div {
border: 1px solid red;
}
</style>
filters
filters
はVueインスタンスの出力結果を調整するのに使います。|
で区切ることで呼び出せます。
<p>{{ text | toUppercase | toLowercase }}</p>
<!-- チェーンでつなげる -->
new Vue ({
filters: {
toUppercase(value) { // いつも value を引数にとる
value.toUpperCase();
}
}
})
グローバルに定義することもできます。
Vue.filter("name", function(value){
// 処理
});
不必要に filters
が呼ばれないように computed
の中で定義する方が良い実務例のようです。
computed: {
filteredFruits() {
return this.fruits.filter((element) => {
return element.match(this.filterText);
});
}
}
mixins
mixins
は重複するコードスニペットをまとめるのに使います。
まず hogeMixin.js
ファイルを新規作成し、export default { // 処理 }
を書きます。
それをインポートし、mixins: []
とすれば、どのコンポーネントでも hogeMixin.js
で定義した処理を使うことができます。
import { hogeMixin } from './hogeMixin';
export default {
mixins: [hogeMixin]
}
インポート先のコンポーネントに重複する名前のメソッドがあれば、それぞれが呼び出され、Mixin のメソッドから先に実行されます。
また、グローバルに定義することもできます。
Vue.mixin({
created() {
// 処理
}
});
ただこれは、全ての Vue インスタンスで呼び出されるので、利用シーンはすごく限定されます。
transition
<transition>
で囲った要素にアニメーションを適用できます。
<template>
<transition name="hoge"></transition>
</template>
アニメーションを適用する要素は、<template>
内に書いておかないといけないので、例えば、JSで append した要素などには適用されません。
デフォルトのクラス名
<transition name="hoge">
の場合、デフォルトのクラス名は次の通りです。
<style>
.hoge-enter {} /* アニメーション開始時のスタイル */
.hoge-enter-active {} /* アニメーション進行中のスタイル */
.hoge-leave {} /* アニメーション終了時のスタイル */
.hoge-leave-active {} /* アニメーション終了中のスタイル */
</style>
デフォルト以外のクラス名
デフォルト以外のクラス名を使いたい場合は、<transation name="">
を指定せずに、次のように直書きします。
<transition
enter-active-class="animated shake"
leave-active-class></transation>
typeで優先を明示
基本的にいい感じにエフェクトの時間を適用してくれますが、2つ以上の時間が存在する場合は、<transition type="">
で優先するプロパティを明示してあげます。
On-loadでエフェクト
また、On-load でエフェクトを適用させたい時は、<transition appear>
と書きます。
動的な定義
name
type
を動的に定義することも可能です。
<transition :name="hoge" :type="hogehoge">
v-if・v-show を使う時
<transition>
内で v-if
v-show
をつかって、2つ以上のアニメーションを適用させる時は、それぞれの div
に type
を指定し、Vueがちゃんと認識できるようにしてあげます。また、 <transtion mode="in-out">
or <transtion mode="out-in">
と書くことで、2つのアニメーションのつなぎがスムーズにいくようにします。
JSフック
transition で使える JSフックは次の通りです。
- before-enter
- enter(done 必要)
- after-enter
- after-enter-cancelled
- before-leave
- leave(done 必要)
- after-leave
- after-leave-cancelled
これらのフックを使って JSのアニメーションを適用できます。
<template>
<transition @enter="enter"></transition>
</template>
<script>
export default {
methods: {
enter(el, done) {
done(); // Vueにアニメーションの終了を知らせる(enter, leave のみ)
}
}
</script>
props
props
は別コンポーネントからの data を受け取るために使います。
親 → 子
親から子への受け渡しはシンプルです。
親コンポーネントのHTMLタグにデータを v-bind:
して子に渡します。
<template>
<div>
<app-user-deital :name="name"></app-user-detail>
</div>
</template>
<script>
import UserDetail from "./UserDetail.vue"
export default {
data: function(){
return {
name: "Kei"
}
}
}
</script>
子コンポーネントでは props
でデータを受けとります。
<template>
<div>
<p>{{ name }}</p>
</div>
</template>
<script>
export default {
props: ["name"]
}
</script>
受け取った props
は、子コンポーネント内で data
オブジェクトと同様に操作できます。
子 → 親
子から親への受け渡しの主なやり方は、次の2つです。
1. $emit
変更した props
を $emit
で親に伝達します。
$emit
の第1引数には「イベント名」、第2引数には「イベントデータ」を渡します。
<template>
<div>
<p>{{ name }}</p>
</div>
</template>
<script>
export default {
props: ["name’",
methods: {
resetName() {
this.myName = "Kei"
this.$emit("nameWasReset", this.myName)
}
}
}
</script>
親では $emit
されるイベントを @
でリッスンし、それに応じた処理を書きます。
<template>
<div>
<app-user-deital :name="name" @nameWasReset="name = $event"></app-user-detail>
</div>
</template>
<script>
import UserDetail from "./UserDetail.vue"
export default {
data: function(){
return {
name: "Kei"
}
}
}
</script>
2. コールバック
props
で親の関数を子に渡すこともできます。
<template>
<div>
<app-user-deital
:name="name"
:resetFn="resetName()"></app-user-detail>
</div>
</template>
<script>
import UserDetail from "./UserDetail.vue"
export default {
data: function(){
return {
name: "Kei"
}
},
methods: {
resetName() {
this.myName = "Kei";
}
}
}
</script>
受け取った関数(resetFn
)を実行することで、親の data
を更新します。
<template>
<div>
<p>{{ name }}</p>
</div>
</template>
<script>
import UserDetail from "./UserDetail.vue"
export default {
props: {
resetFn: Function
}
}
</script>
子 → 子
子・子間の受け渡しは複雑になるので、次のような eventBus
という設計を用います。
export const eventBus = new Vue();
new Vue({
el: "#app",
render: h => h(App)
})
<script>
import { eventBus } from "../main";
export default {
methods: {
editAge() {
this.age = 30;
eventBus.$emit("ageWasEditted", this.userAge);
}
}
}
</script>
<script>
import { eventBus } from "../main";
export default {
created() {
eventBus.$on("ageWasEditted", (data) => {
// 処理
})
}
</script>
$emit
のロジックは、eventBus
の中に書くこともできます。
また、eventBus
でも複雑になる場合は、Vuex
(後述)を使ってステート管理するのが一般的です。
slot
slot
は子コンポーネント内で使いたいHTMLコードを、親から子に受け渡すために使います。
親の子HTMLエレメント内で slot
を渡します。
<template>
<div>
<app-child>
<h2 slot="title">{{ title }}</h2>
<p slot="content">{{ content }}</p>
</app-child>
</div>
</template>
子では <slot name="NAME"></slot>
で受け取ります。
<template>
<div>
<div class="title">
<slot name="title"></slot>
</div>
<div class="content">
<slot name="content"></slot>
</div>
</div>
</template>
Vue 2.6.0より v-slot
が推奨(追記: 2019-05-18)
コメントでご指摘いただいた通り、Vue 2.6.0 から v-slot
が導入され、上の slot
および slot-scope
は非推奨になっています。(非推奨の構文 スロット - Vue.js)
さらなる詳細についてはこちら。
動的なコンポーネント
<component>
タグの :is=
で動的なコンポーネントを描画することができます。
<template>
<div>
<component :is="selectedComponent"></component>
</div>
</template>
<script>
// selectedComponent = "コンポーネント名"
</script>
<component>
は切り替えられる毎に destroy
され、データはリセットされます。
destroy
したくない時は、<keep-alive>
タグで <component>
をラップします。この場合、activated
と deactivated
の2つのライフサイクルで動きます。
よく使われるライブラリ
Vue CLI のテンプレートでWebアプリを作る場合によく使われるライブラリは次の通りです。
Axios
Axios はHTTPリクエスト用のJavaScriptライブラリです。
これを使ってCRUD処理を書いていきます。
Vue.js用の Vue Resource というライブラリ(昔は推奨されていた)もありますが、JavaScriptで使える Axios の方が汎用性が高いです。
セットアップ
次のコマンドでインストールします。
$ npm install --save axios
グローバルな設定としてインポートします。
import axios from "axios";
axios.defaults.baseURL = "base_url";
axios.defaults.headers.common["Authorization"] = "hogehoge"
axios.defaults.headers.get["Accepts"] = "application/json"
GETリクエスト
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
POSTリクエスト
axios.post('/users', data)
.then(res => console.log(res))
.catch(error => console.log(error))
interceptors
リクエスト・レスポンス前の処理実行には、interceptors
を使います。
axios.interceptors.request.use(config => {
console.log(config);
return config;
})
axios.interceptors.response.use(res => {
console.log(res);
return res;
})
Vue Router
Vue.jsでのルーティングでは Vue Router を使います。
セットアップ
次のコマンドでインストールします。
$ npm install --save vue-router
ルーティングは次のように書きます。
import User from "./components/user/User.vue";
export const routes = [
{ path: "", component: Home }, // Root path
{ path: "/user/:id", component: User } // :id は動的なURL
]
このルーティングをグローバルな設定としてインポートします。
import VueRouter from "vue-router";
import { routes } from "./routes";
Vue.use(VueRouter);
const router = new VueRouter({
routes, // Same as "routes: routes”
mode: "history" // No hash tag style in URL
});
new Vue({
el: "#app",
router // Same as "router: router"
render: h => h(App)
})
router-view
URLごとのコンポーネントを描画する場所を <router-view>
で指定します。
<template>
<div>
<router-view></router-view> // 描画の場所を指定
</div>
</template>
router-link
リンクには <router-link>
を使います。リンクと同じページにいるときは、クリックしても再描画されません。
<router-link to="/" tag="li" active-class="active" exact>Home</router-link>
<!-- "exact" をつけないと、"/" で開始するURL全てがアクティブになる。 -->
<router-link to="/user" tag="li" active-class="active">User</router-link>
動的なパスを指定する場合は {{}}
を使って書きます。
<router-link
tag="button"
:to="{ name: 'UserEdit', params: { id: $route.params.id }, query: { locale: 'en' } }"
class="btn btn-primary">Edit</router-link>
$route.params
$route.params
でVueインスタンスからURLパラメータにアクセスできます。
<script>
export default {
data() {
return {
id: this.$route.params.id
}
}
}
</script>
children (routes.js)
ルーティングをネストする場合は、children
を使って次のように書きます。
import User from "./components/user/User.vue";
export const routes = [
{ path: "", component: Home },
{ path: "/user/", component: User, children: [
{ path: "", component: UserStart },
{ path: ":id", component: UserDetail },
{ path: ":id/edit", component: UserEdit }
]}
]
beforeEnter
beforeEnter
を使えば、router-view
を描画する直前に処理を走らせられます。
書ける場所は次の3つです。
1. グローバル
router.beforeEach((to, from, next) => {
// グローバルに適用される
next();
});
2. ローカル
import User from "./components/user/User.vue";
export const routes = [
{ path: "", component: Home },
{ path: "/user/", component: User, children: [
{ path: "", component: UserStart },
{
path: ":id", component: UserDetail, beforeEnter: (to, from, next) => {
// ローカルに適用される
next();
}
},
{ path: ":id/edit", component: UserEdit }
]}
]
3. コンポーネント
beforeRouteEnter(to, from, next) {
next():
}
beforeRouteLeave
同様に beforeRouteLeave
でURLの離脱前に処理を走らせられます。
Vuex
前述の通り、Event Bus
やコールバックを使えば、コンポーネント間の props
更新を実現できますが、大規模なアプリになると管理が煩雑になります。
そのような場合は Vuex というステートマネジメントフレームワークを使うのが一般的です。
設計
設計は Redux
と似ているらしく、全体像は次の通りです。
Udemy Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex) より引用
store
にステートを保管し、各コンポーネントは getters
でその値にアクセスします。値を変更する時は mutations
から store
のステートを更新します。mutations
は同期処理になってしまうので、非同期処理 を実行する場合は間に actions
をかませます。
セットアップ
次のコマンドでインストールします。
$ npm install --save vuex
store/store.js
を作成します。
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
counter: 0
}
});
main.js
に次のコードを追加します。
import { store } from "./store/store";
new Vue({
store
})
これでコンポーネントから $store.state
で必要なデータにアクセスできます。
this.$store.state.counter // これでアクセスできる。更新をemitする必要なし。
getters
state
の要素に対するメソッドを getters
で定義すると、各コンポーネントから使うことができます。
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
counter: 0
},
getters: {
doubleCounter: state => {
return state.counter * 2;
}
}
});
コンポーネントでの呼び出しには $store.getters
を使います。
export default {
computed: {
count() {
return this.$store.getters.doubleCounter
}
}
}
mapGetters
getters
が増えると、各コンポーネントでの呼び出しが冗長になってきます。その場合 mapGetters
を使えば、より効率的に書くことができます。
<template>
<div>{{ doubleCounter }}</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
computed: {
...mapGetters([
"doubleCounter"
]),
ownMethod();
}
}
</script>
なお、...
を使うには babel-preset-stage-2
のセットアップが必要です。
babel-preset-stage-2
次のコマンドでインストールします。
$ npm install --save-dev babel-preset-stage-2
作成される .babelrc
を次のように記載します。
{
"presets": [
["ec2015", {"modules": false}],
["stage-2"]
]
}
mutations
state
の値の変更には mutations
を使います。使い方は getters
と同じです。
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
counter: 0
},
getters: {
doubleCounter: state => {
return state.counter * 2;
}
},
mutations: {
decrement: state => {
state.counter--;
}
}
});
コンポーネントでの呼び出しには $store.commit
を使います。
export default {
methods: {
decrement() {
this.$store.commit("decrement");
}
}
}
mutations は非同期処理に対応していないので、必ず同期させる必要があります。
mapMutations
mapGetters
と同様に、mapMutations
として効率的に記載できます。
actions
非同期処理を実現するために、コンポーネントと mutations
の間に actions
をかませます。
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
counter: 0
},
getters: {
doubleCounter: state => {
return state.counter * 2;
}
},
mutations: {
decrement: state => {
state.counter--;
}
},
actions: {
decrement: context => {
context.commit("decrement");
},
decrement: ({commit}) => {
commit("decrement");
}
// 上記、2つのアクションは同義
}
});
コンポーネントでの呼び出しには $store.dispatch
を使います。
export default {
methods: {
decrement() {
this.$store.dispatch("decrement");
}
}
}