Vue.jsに入門する
いい加減観念してVueをやる。
この本に沿って勉強していきます。
【chapter1-02 インストール方法】
CDNで読み込む。書籍のバージョンは3.0.0だが、せっかくなので詰まるまでは最新バージョンでやっていこうかな。
もちろん今回は開発者モードで。
<script src="https://unpkg.com/vue@next"></script>
console.log(Vue.version);
>>3.2.36
【chapter1-03 devtoolsのインストール】
拡張機能をインストール。特に使い方の説明はないので入れるだけ
【chapter2 Todoアプリを作る①】
インスタンスの作り方
マウントすることでそのセレクタ内にインスタンス内のコードが適応される。
Vue.createApp({
//code
}).mount('#app')
マウント先が複数ある場合は最初の一つのみ適応。
dataプロパティ
具体的な使い道が今のところイメージできない。
data: function(){
return {
message: 'ToDoアプリケーション',
}
},
省略した書き方もできるらしい
data() {
return {
message: 'ToDoアプリケーション',
}
},
テンプレート構文
dataのmessageキーをHTML側に表示する。
<div id="app">{{message}}</div>
bladeと同じ書き方。Mustache記法というらしい(名前あったのか…)
つまりdataプロパティはフロント側に埋め込むものを入れておく感じか?
テキストボックスの操作
テキストボックスにv-modelディレクティブでmessageキーを指定すると、value値はmesseageキーの値になる(バインディング)
さらにテキストボックスの入力でmessageキーの値を変更できる(双方向性バインディング)
<input type="text" v-model="message"/>
<textarea v-model="message"></textarea>
<p>{{message}}</p>
チェックボックスの操作
こちらもv-modelを使って制御する。
チェックボックスが単体の場合dataプロパティ内のキーはtrue/falseになるが、チェックボックスが複数ある場合は配列にチェックが入っているチェックボックスのvalueが入っていく
セレクトボックスの操作
dataプロパティのキーは選択した項目(option)のvalueになる。multipul属性で複数選択できる場合は、チェックボックスのように配列になる。
v-modelの修飾子
v-model.○○のようにつけると、v-modelのふるまいを変えることができる。
例えばv-model.number="num"のようにnumber修飾子を付けると、入力値が文字列型ではなく数値型になる。
v-modelの修飾子一覧:
- .lazy:入力された内容がdataプロパティ側に反映されるのがinputイベント発火時ではなくchangeイベント発火時になる
- .number:入力値が数値型になる
- .trim:入力値の両端の空白を削除する
Computedプロパティ
文字列結合など算出用のプロパティ。
todoCategoriesがチェックボックスのv-modelの場合、チェックした項目のvalueがtodoCategoriesに配列として入る。
その場合、computedプロパティを使えばthis.todoCategories.join(' / ')とすることで/区切りで文字列結合ができる(forなどでループ処理するより簡潔)
data: function () {
return {
todoCategories: [],
}
},
computed: {
joinedToDoCategories: function () {
return this.todoCategories.join(' / ')
},
categoryText: function () {
return '選択されているカテゴリー: ' + this.joinedToDoCategories
},
},
書籍の説明だけでは具体的な使い道が思いつかなかったが、インクメンタルサーチを簡単に実装できるらしい。ユーザーの入力内容を動的に加工して何かするときにはいいのかも?
なお、先ほどのサンプルコード内のthisはvueインスタンス自身を指している。
例外はありそうだが、プロパティ内のthisはvueインスタンス自身を指すと思っていていいのかな
Computedにはキャッシュがある。Computed内で参照しているdataプロパティの値や他のComputedプロパティに変化がない場合、算出結果は必ず同じになる。
computed: {
randomNum: function () {
console.log('実行');
return Math.random() //ランダムな数字を表示
}
},
<p>{{randomNum}}</p>
<p>{{randomNum}}</p>
上記のようなプログラムの場合、randomNumは二行とも同じ数字が表示される。
またconsole.logが表示されるのも一回だけ。
methodsだとキャッシュされないらしい
methodsプロパティ
急に解説が薄くてよくわからない……調べた感じcomputedとほぼ同じ用途で使う?
ただ前述した通りmethodsだとキャッシュされないので、そこで使い分けるようだ
v-onディレクティブ
対象要素に指定したイベントが発生した場合任意の処理を実行できる。
書式は「v-on:監視イベント名="メソッド名"」もしくは「v-on:監視イベント名="式"」
例:クリックしたらカウントアップする
<p>{{count}}</p>
<button v-on:click="onClickCountUp">Count Up</button>
data: function () {
return {
count: 0,
}
},
methods: {
onClickCountUp: function (event) {
this.count += 1
}
},
ここでmethodsプロパティが出てきたが、確かにcomputedプロパティだとdata内に変化がないのでキャッシュが残って実行されない。こういう使い方をするのか。
v-on:click="onClickCountUp(arg)"
とすれば引数も渡せる。イベントオブジェクトを渡したい場合はv-on:click="onClickCountUp($event)"
とする。素のJSより明示的でわかりやすいね。
逆に引数が不要の場合は()をつける必要はない。
またv-onの部分を@にする省略記法もある。@click="onClickCountUp"
ということだ。
v-onにも修飾子がある。
.preventはevent.preventDefault()を引き起こすとのことだが、preventDefault()を知らなかったのでメモ。
例えば@submit.preventとすれば、submitさせないで別の処理を挟んでからsubmitするようなことができるのだろうか。バリデーション処理で使えそう。
watchプロパティ
computedやmethodsのように何かを監視して実行するプロパティ。違う点は下記。
- プロパティ名を監視対象のプロパティ名にする
- 監視対象が変化した場合、第一引数に監視対象の変化後が、第二引数に変化前が格納される。
- computed同様キャッシュが残る
- 配列内の変更検知にはdeepプロパティ(true)とhandllerプロパティ(実行するfunction)を指定する
v-bindディレクティブ
属性の値を動的に変更できる。disabledなどの付いてるだけで意味のある属性の場合はtrue/falseで付与の制御ができる。
classの場合は配列を使うと複数指定ができる。配列の値にはcomputedやmethodsプロパティの値となっているfunctionも指定できる。
<!-- 実際の記述 -->
<div v-bind:class="[className, 'selected', classNameComputed, classNameMethod()]"></div>
<!-- 出力結果 -->
<div class="from-data-class-name selected from-computed-class-name from-methods-class-name"></div>
オブジェクトでも指定ができる。
下記の場合isActiveはtrueなのでclass="is-active"
になるが、falseに変えるとclass="is-inactive"
になる。disabled同様falseの場合は出力されず、またis-inactiveの条件が「!isActive」なためだ。
<div v-bind:class="{'is-active': isActive, 'is-inactive': !isActive}"></div>
Vue.createApp({
data: function () {
return {
isActive: true,
}
},
}).mount('#app')
style属性にv-bindを使う場合はオブジェクトで指定する。
<div v-bind:style="{ fontSize: '14px', 'background-color': defaultColor }"></div>
v-bindにも省略記法があり、:class="propatyName"
のように書くこともできる。
v-showとv-if
どちらも表示非表示を制御するもの。v-showをfalsyな値にするとdisplay:noneが付与され要素が非表示になる。v-ifの場合はdisplayで制御するのではなく要素そのものを削除する。
v-ifの方が描画負荷が高いため、頻繁に表示非表示を切り替えるものはv-showでそうでないものはv-ifを使うと良いらしい。
v-forディレクティブ
配列の中身を一つずつ取り出して処理をする。要はfor in文。
<!-- 実際の記述 -->
<div v-for="item in ['item-1', 'item-2']">{{ item }}</div>
<!-- 出力結果 -->
<div>item-1</div>
<div>item-2</div>
下記のようにすればインデックスも取り出せる
<!-- 実際の記述 -->
<div v-for="(item, index) in ['item-1', 'item-2']">{{ index }}:{{ item }}</div>
<!-- 出力結果 -->
<div>0:item-1</div><div>1:item-2</div>
オブジェクトでも同様に使える。またオブジェクトの場合はv-for="(value, key, index) in object"
とすれば、値、キー、インデックスを取り出せる。
key属性を指定することで、値とキーを紐づけることができる。
こうすることで、値が変化したかどうかキーごとに判定ができるらしく、値の一つが変わるたびに都度ループする必要がないらしい?
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
data: function () {
return {
items: [
{
id: 1,
name: 'item-1',
},
{
id: 2,
name: 'item-2',
},
],
}
},
key属性はindexも指定できるので、v-forディレクティブとはセットで使うのが良いらしい。
またこういう懸念点もあるそうな
【Chapter5】コンポーネント
コンポーネント=部品。
今まではhtmlに記述された要素にVueの機能をあてていたが、Vueのファイル内にhtml要素と機能をセットで記述し、それをパーツとしていろいろなファイルから呼び出し使いまわす。
グローバルコンポーネントとローカルコンポーネントの二種類あり、ローカルの場合はコンポーネントを定義したインスタンス内でのみ使える。グローバルだと他のコンポーネントからも呼び出せる?
グローバルだとどこからでも呼び出せる以上、意図せず使ってしまったりトラブルの原因になる。なので基本的にはローカルを使った方がいいのかな?
ただ現状インスタンスは特定要素にマウントする使い方しか知らないので、グローバルでも使える場所は限られるからローカルでも支障はなさそう(逆にグローバルの活用方法がわからない)
グローバルコンポーネント
Vue.createApp({}).component('global', {
template: '<h1>global component</h1>'
}).mount('#app')
ローカルコンポーネント
Vue.createApp({
components: {
'local': {
template: '<h1>local component</h1>'
}
}
}).mount('#app')
呼び出し方はグローバルもローカルも同じ
<div id="app">
<global></global>
<local></local>
</div>
出力結果
<div id="app">
<h1>global component</h1>
<h1>local component</h1>
</div>
コンポーネントのtemplateに文字列でHTMLを指定していたが、htmlファイルにテンプレートを記述することもできる
<script type="text/x-template" id="title-template">
<h1>global component</h1>
</script>
Vue.createApp({
components: {
'my-title': {
template: '#title-template',
},
},
}).mount('#app')
scriptタグにtype="text/x-template"
とid="title-template"
を指定し、idをコンポーネントのtemplateに指定するとテンプレートとして表示ができる。
親コンポーネントから子コンポーネントにデータを渡す
データの受け渡しの前に何が親子なのか(何がインスタンスなのか)について躓いたので整理。
例として以下のようなプログラムが掲載されていた
const myTitle = {
~省略~
}
Vue.createApp({
data: function () {
return {
authorName: 'yamada',
}
},
components: {
'my-title': myTitle,
},
}).mount('#app')
このプログラムでいうと、dataプロパティは親コンポーネントのdataプロパティで、componentsプロパティ以下が子コンポーネントになる。コンポーネントはそれ自体がインスタンスになるためだ。
なので例ではauthorNameプロパティを子コンポーネントのmy-titleに渡そうとしていることになる。
下記のようにインスタンスが二つあって、どちらかが親で子になるイメージだったので戸惑った
Vue.createApp({
components: {
//処理
},
}).mount('#app')
Vue.createApp({
components: {
//処理
},
}).mount('#app')
親から子へデータを渡すことはできるが、子から親へはできない。
受け渡しを行う際は子側にpropプロパティを設定する。下記の例であれば、受け取った値はpropsプロパティのnameに格納される。
props: {
name: {
type: String, //受け取るデータ型
default: '', //受け取れなかった時の初期値
validator: function (value) { //受け取った値のバリデーション
return value.length > 0
},
required: true, //受け取りを必須とするか
},
},
子から親へデータを渡したり、親のデータを子が直接書き換えることはできない。
だが$emitを使えば、子が親のイベントを発火させ親側にデータを変更してもらうことはできる。
下記はボタンをクリックするとカウントアップするコード。
Vue.createApp({
data: function () {
return {
count: 0,
}
},
components: {
'count-up-button': {
template: '#btn-template',
methods: {
onClick: function () {
this.$emit('count-up')
},
},
}
},
methods: {
countUp: function () {
this.count += 1
},
},
}).mount('#app')
<div id="app">
<count-up-button @count-up="countUp"></count-up-button>
<div>{{ count }}</div>
</div>
<script type="text/x-template" id="btn-template">
<button type="button" @click="onClick">COUNT UP</button>
</script>
ボタンをクリックしたあとの処理の流れは、
- count-up-button要素の中身は#btn-templateのbuttonタグなので、子コンポーネントのonClickメソッドが発火
- onClickメソッドでは
$emit('count-up')
としているので、count-upイベントが発火 - count-up-button要素にはount-upイベントが発火した時、親コンポーネントのcountUpメソッドが発火するよう設定しているのでカウントアップされる
ということだと思う。
$emitの引数などフローを理解するのが難しかった。
参考:
親からpropsで受け取りつつ$emitで親側でイベントを発火させることもできる。
v-modelでpropsに親の現在のカウントを渡し、子ではそれをカウントアップして親に返している。
<div id="app">
<count-up-button :count="count" @count-up="countUp"></count-up-button>
<div>{{ count }}</div>
</div>
<script type="text/x-template" id="btn-template">
<button type="button" @click="onClick">COUNT UP | {{ count }} + 1</button>
</script>
Vue.createApp({
data: function () {
return {
count: 0,
}
},
components: {
'count-up-button': {
template: '#btn-template',
props: {
count: {
type: Number,
required: true,
},
},
methods: {
onClick: function () {
this.$emit('count-up', this.count + 1)
},
},
},
},
methods: {
countUp: function (nextCount) {
this.count = nextCount
},
},
}).mount('#app')
transitionを使ったアニメーション
<transition>タグで囲った要素のv-ifやv-show属性の変化を検知すると、変化のタイミングに合わせて任意のクラスを要素に付与する。
そのクラスに対しCSSを指定することでアニメーションをさせることができる。
またCSSだけでなくJSも変化に合わせて発火させることができる。
その場合はtransitionタグにv-onディレクトリを使って紐づけをする。
下記の場合は、before-enterは要素が表示される前に実行されるので、before-enterが発火したらmethodsプロパティに指定したbeforeEnterメソッドを実行する。JSだけを起動しCSSを変更したくない場合はv-showにはfalseを渡す。
<transition>
<div v-show="false" @before-enter="beforeEnter">トランジション</div>
</transition>
v-forを使う場合など、複数の要素をtransitonタグで変化させたい場合はtransition-groupタグで囲う必要がある。
transitionタグと違う点は、
- transition-groupタグ自体も描画される。デフォルトはspanタグだが、tag属性で任意のタグに変更できる
- トランジション中に付与されるクラスはtransition-group内の各要素に付与される
- 中の要素にはkey属性が必須
モーダルを作成する
書籍を写経しつつ内容を理解していく。以下メモ
書籍ではIEでarray.prototype.findを使うためにLodashというライブラリを使用していたが、IEで動作確認はしないので、使わない書き方に置き換え。
v-onディレクティブの修飾子は複数同時に使える。(例:@click.prevent.stop)
preventとstopの使い方は下記がわかりやすかった。
cssの疑似要素befreについて。
v-onのself修飾子は子要素にイベントを伝搬させない。stop修飾子は親にイベントを伝搬させない。
サンプルコードと解説には@after-leaveでJSフックさせていたが、フックするメソッドは書いてないし完成品のコードからは消えてるので多分不要。
transition-groupではなくtransitionなのにtag属性が付与されていたが、何か意味があるのだろうか?外しても問題なさそうだったが。
カルーセルパネルを作る
【Image()を使った画像の高さの取得方法】
イメージタグのsrcを指定することで画像読み込みの監視ができる。読み込まれたら高さを取得して画像サイズに応じて調整するsetTargetHeight: function (index) {
const img = new Image()
const self = this
img.src = this.items[index].src
img.onload = function (event) {
// 画像が読み込まれたら画像の高さを親要素へセット
self.currentHeight = event.currentTarget.height
}
}
なぜわざわざthisをselfに入れているのかわからなかったが、img.onloadのコールバック関数内ではスコープの問題かthisが参照できないようだ。
【mountプロパティ】
突然出てきてなんの説明もないし調べてもよくわからなかったが、実際の挙動と名前から推測。
mounted: function () {
this.setTargetHeight(this.selectedIndex)
}
先ほどの画像の高さを調整するメソッドが呼び出されている。これをコメントアウトしているとページ読み込み直後は画像の高さが調整されなかった(次の画像を呼び出せば大丈夫)
VueのライフサイクルにMountedというDOM要素にマウントされるときに呼び出されるフェーズがあるそう。
なのでこのプロパティはそのMountedのフェーズで呼び出されるので、ページ読み込み時に画像の高さを調節してくれるのではないだろうか。
アコーディオンを作る
【slotタグとは】
【requestAnimationFrame】
アニメーション描画時に引数のコールバック関数が実行される。
setIntevalやsetTimeoutと比べてアニメーションの描画時に実行なのでメモリに優しい