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 インスタンスの書き方は次のような感じです。
app.js
new Vue({ el: "#app", data: { name: "Kei", age: "30", counter: 0 }, methods: { increase: function() { this.counter += 1; } } })
この app.js を Vue.js と共にインポートします。
html
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script> <script src="app.js"></script>
{{ }}
{{ }} でVueインスタンスの要素を呼び出すことができます。
html
<p>{{ name }}</p>
v-bind
HTML要素をVue.jsで動的に指定したい場合は、v-bind を使います。
html
<a v-bind:href="link">Google</a>
v-once
v-once をHTMLタグに含めることで1度描画された内容を変更不可にできます。
html
<h1 v-once>{{ title }}</h1>
v-html
v-html はHTMLタグのまま出力するときに用います。
html
<p v-html="finishedLink"></p>
v-on
v-on でイベントリスニングします。
html
<button v-on:click="increase(2, $event)">Click me</button>
app.js
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全てに反映されます。
html
<input type="text" v-model="name"> <p>{{ name }}</p> <!-- 入力値によって動的に name が変わる -->
app.js
new Vue ({ el: "#app", data: { name: "Kei" } })
@
v-on: の省略記法で、@click="link" のように書けます。
また、v-bind の代わりに : も使えます。
:class
:class="{YOUR_CLASS: boolean}" で動的にCSSを適用できます。
html
<div class="demo" @click="attachedRed = !attachedRed" :class="{red: attachedRed}"></div>
:class を複数定義する場合は array で書きます。
:style
:class と同様に、:style で動的にスタイルを適用できます。
html
<div :style="{backgroundColor: color}"></div>
スタイルのプロパティ名はキャメルケース(もしくは"")で書きます。
computed
Vueインスタンスの computed オブジェクトは、 methods に似ていますが、data プロパティのように処理結果を保持させる時に使います。methodsと違い、呼び出す時に () は不要です。
js.app.js
new Vue ({ computed: { divClassses: function() { return { red: this.attachedRed, blue: !this.attachedRed } } } })
watch
Vueインスタンスの watch オブジェクトは、特定のプロパティの変化をトリガーに処理を走らせる時に使います。data だけでなく computed のプロパティも watch できます。
js.app.js
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に表示させます。
html
<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 で配列などプロパティに対するイテレーションを書きます。
html
<!-- ingredients: ["meat", "fruit", "cookies"] --> <ul> <li v-for="ingredient in ingredients">{{ ingredient }}</li> </ul>
index を取りたい時は次のように第2引数を定義してあげます。
html
<li v-for="(ingredient, i) in ingredients">{{ ingredient }}</li>
上は配列の例ですが、Objectにも同様に v-for を使えます。Objetctの場合、第2引数が key, 第3引数が index になります。
html
<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=“ユニークな値” を付けます。
html
<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 を単一ファイル内で書きます。
例えば、次のような感じです。
scr/Home.vue
<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 に読み込ませる場合は次のように書きます。
src/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インスタンスの出力結果を調整するのに使います。| で区切ることで呼び出せます。
html
<p>{{ text | toUppercase | toLowercase }}</p> <!-- チェーンでつなげる -->
app.js
new Vue ({ filters: { toUppercase(value) { // いつも value を引数にとる value.toUpperCase(); } } })
グローバルに定義することもできます。
Vue.filter("name", function(value){ // 処理 });
不必要に filters が呼ばれないように computed の中で定義する方が良い実務例のようです。
app.js
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 という設計を用います。
main.js
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
グローバルな設定としてインポートします。
main.js
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
ルーティングは次のように書きます。
routes.js
import User from "./components/user/User.vue"; export const routes = [ { path: "", component: Home }, // Root path { path: "/user/:id", component: User } // :id は動的なURL ]
このルーティングをグローバルな設定としてインポートします。
main.js
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> で指定します。
App.vue
<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 を使って次のように書きます。
routes.js
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. グローバル
main.js
router.beforeEach((to, from, next) => { // グローバルに適用される next(); });
2. ローカル
routes.js
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 を作成します。
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 に次のコードを追加します。
main.js
import { store } from "./store/store"; new Vue({ store })
これでコンポーネントから $store.state で必要なデータにアクセスできます。
コンポーネント
this.$store.state.counter // これでアクセスできる。更新をemitする必要なし。
getters
state の要素に対するメソッドを getters で定義すると、各コンポーネントから使うことができます。
store/store.js
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 を次のように記載します。
.babelrc
{ "presets": [ ["ec2015", {"modules": false}], ["stage-2"] ] }
mutations
state の値の変更には mutations を使います。使い方は getters と同じです。
store/store.js
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 をかませます。
store/store.js
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"); } } }
あわせて読みたい
'JLPT' 카테고리의 다른 글
本当に若者の間でコロナの人数が急激に増えているのか検証した話 (0) | 2021.06.27 |
---|---|
ありがちだけどPythonでチャットボット作ってみた (0) | 2021.06.27 |
【PHP8.0】PHPに名前付き引数が実装される (0) | 2021.06.27 |
RoRやLaravelなどのフレームワークを使ってきた人がScalaを導入した時に引っかかる点とその解決策 (1) | 2021.06.27 |
ニューラルネットワーク研究開発職の募集 (0) | 2021.06.26 |
댓글