フロントエンドエンジニアの仲間に入りたくて&流行りに乗りたくてVue.jsで遊んでみました。
遊んだ結果、localeを元に時差を計算して各地の時間を表示して、比較できる実に実用的になアプリができたので使って見てください。
(実際はPWAにしたかったのですが、想像以上に時間を取ってしまったので、PCのみ対応というクソなアプリが仕上がりました)
アプリはこちら。隣画面のTODOリストはウォーミングアップとして作成しました。
↓↓↓
https://location-timer.herokuapp.com/myList
ソースみせろよって方はこちら
↓↓↓
https://github.com/hatamasa/location-timer
こんな感じのアプリがスタイルあわせて250行程度で書くことができたのでやはりすげーぞVue.js。バズってるだけあるじゃん。
ってのが感想です。
その感想と、作ったアプリをサンプルにVue.jsの要点を少しですがまとめたいと思います。
Location-timerについて
作成時間
30-35h程度(cssがあまり得意でないので実際はもっと早くできるかも)
使用言語
- Vue.js 2.9.6 (フロント)
- Laravel 5.6.8(バックエンド)
- heroku
- PostgreSQL(herokuなので)
Laravelは特にfindしか使っていないので詳しい説明は割愛。
herokuについても割愛してしまいます。
Vue.jsについて
- フロントエンドのReactiveな動きを簡潔に記述できるようにするためのフロントエンドフレームワーク
- コンポーネント思考(細々使えるため、既存のアプリなどに少しずつ導入できる。共存できる。)
- 基本的にデータバインディングで画面に動きをつける(後ほど解説)
- コンパイルというかビルドを行なって仮想DOMをレンダリングするための関数に変換する
- バズってる(Laravelに標準搭載されて流行った。今更感あるか?)
サンプル解説
インストール
ググればたくさん出てくるので最初の方はここでは割愛
Vue.js初期設定
必要な部分のみ記載します。
app.jsがVueのコンポーネントを操作する基点となる設定ファイルとなります。
ここではメインの動作を記載したMyList.vueを読み込んでMyListコンポーネントとして定義しています。
その定義したMyListコンポーネントをid="myList"のエレメントで動作するようにします!ということが書いてあります。
- /resources/asset/js/app.js
import Vue from 'vue';
import MyList from './components/MyList.vue';
require('./bootstrap');
const myList = new Vue({
el: '#myList',
components: {
MyList
}
});
この後に記載するMyList.vueのtemplateタグ内に記載されているhtmlをMyListコンポーネントとして
id="myList"のエレメントのmy-listタグにブチ込みます!
他にもコンポーネントを定義すればタグとしてブチ込むことが可能です。
<div id="myList">
<my-list></my-list>
</div>
メインのMyList.jsですが、実際のアプリはごちゃごちゃ書いてあるので削って載せます。
- /resources/asset/js/components/MyList.vue
<template>
<div>
<h1>Location MyList</h1>
<select v-model="selected">
<option v-for="country in countries" v-bind:value="country.id">
{{ country.country_name }}
</option>
</select>
<button type="button" v-on:click="addCountry()">MyListに追加</button>
<!-- 中略 -->
</div>
</template>
<!-- cssかけるよ -->
<style>
/** 中略 */
</style>
<script>
export default {
data () {
return {
selected: "",
countries: [],
myList: [],
date: ""
}
},
mounted: function () {
this.initData();
this.fetchCountry();
this.loadMyList();
},
updated: function () {
this.changeForcusHeight();
this.changeForcusOffset();
},
methods: {
initData () {
this.date = new Date();
},
fetchCountry () {
this.$http({
url: '/api/country',
method: 'GET'
}).then(res => {
this.countries = res.data
});
},
loadMyList () {
this.myList = JSON.parse(localStorage.getItem('myList'));
if (! this.myList) {
this.myList = [];
// 日本を取得
this.$http({
url: '/api/country/81',
method: 'GET'
}).then(res => {
this.myList.push({
id: res.data['id'],
country_name: res.data['country_name'],
utc_diff: res.data['utc_diff'],
});
});
}
},
addCountry () {
if (! this.selected) return;
this.$http({
url: '/api/country/'+this.selected,
method: 'GET'
}).then(res => {
this.myList.push({
id: res.data['id'],
country_name: res.data['country_name'],
utc_diff: res.data['utc_diff'],
});
this.saveMyList();
})
this.changeForcusHeight();
},
// 中略
},
// 中略
}
</script>
このようにhtmlとcss, jsを合わせて記述してコンポーネントして部品化します。
それをいろんなところでタグにして使い回すということがVue.jsの使い方ですね。
Vueのデータバインディング
気づいたと思いますが、見慣れないv-modelやv-forなどhtmlタグ内部に記述してありますが、こいつらがデータバイディングの正体と思えばわかりやすいかもしれません。
わかりやすい部分を解説するとMyListコンポーネントにはcountriesというデータが定義してあります。
<script>
data () {
return {
selected: "",
countries: [],
myList: [],
date: "",
}
},
下記でcountriesをjsで操作します。
ちなみに、mountedはこのコンポーネントがエレメントに展開された時の初期動作で実行するメソッドを記載する部分で、
methodsでこのコンポーネントで実行可能なメソッドを記載しておきます。
他にも色々な段階ごとのライフサイクルフックメソッドと呼ばれるものがありますが、割愛。
下記を参考に使いたいフックメソッドを使いましょう。
https://jp.vuejs.org/v2/guide/instance.html
下記では
- initDate()で現在のDateオブジェクトを作成。
- fetchCountry()でLaravelで作成したAPIを実行しcountries(locale)をDBから取得してcountriesに設定。
- loadMyList()でローカルストレージから前回の情報を取得。ついでに日本のロケールを取得(何もない状態になってもリロードすれば日本がリストに入るように)。
などを初期動作として定義しています。
mounted: function () {
this.initData();
this.fetchCountry();
this.loadMyList();
},
~~ 中略 ~~
methods: {
initData () {
this.date = new Date();
},
fetchCountry () {
this.$http({
url: '/api/country',
method: 'GET'
}).then(res => {
this.countries = res.data
});
},
loadMyList () {
this.myList = JSON.parse(localStorage.getItem('myList'));
if (! this.myList) {
this.myList = [];
// 日本を取得
this.$http({
url: '/api/country/81',
method: 'GET'
}).then(res => {
this.myList.push({
id: res.data['id'],
country_name: res.data['country_name'],
utc_diff: res.data['utc_diff'],
});
});
}
},
mountedでcountriesを操作するだけで下記のselectボックスの初期データを投入することができます。
通訳すると、vueを使ったselectボックスを作成します。optionはcountriesをforeachします。valueはcountryのidを使うよー。
っという事が書いてあります。
(内側の波括弧の部分はLaravel搭載のbladeというテンプレートエンジンの記法です)
<select v-model="selected">
<option v-for="country in countries" v-bind:value="country.id">
{{ country.country_name }}
</option>
</select>
これがVue.jsのちょー簡単なデータバインディングです。
だけじゃなくてonClickとかもしたい
jsのアクションもいろいろサポートしているみたいです。
methodsに定義しておいてv-on:click="addCountry()"みたいに書きます。
(@click="deleteCountry(row.id)"は省略記法なので動作は同じonClickです)
他のイベントも@mouseoverとかでメソッドを指定すれば動作してくれます。
その後のDOM操作的な部分はvなんちゃらでデータと関連づけされているDOMは全てデータをいじるだけでよしなにやってくれます。
<button type="button" v-on:click="addCountry()">MyListに追加</button>
<tr v-for="row in myList">
<td scope="row" class="handle">{{ row.country_name }}</td>
<td>{{ row.utc_diff | getTimeFromDiff(1) }}</td>
<td scope="row" class="time-bar">
<div class="bar-inline">
<div v-for="n in 24" class="timeCol" v-bind:class="row.utc_diff | getTimeFromDiff | calcTime(n, addHour) | getTimeClass">
{{ row.utc_diff | getTimeFromDiff | calcTime(n, addHour) }}
</div>
</div>
</td>
<td><button type="button" @click="deleteCountry(row.id)" class="deleteCountry">削除</button></td>
</tr>
下記はクリックで呼び出されるメソッドたちで、リストにセレクトボックスから追加した国を保存しています。
その後にフォーカスのエレメントの高さの調整を行なっています。
ここではmyListの値を弄ることで表示されている国のリストを変更しています。
そして、なんと一緒にjQueryが書けちゃうんですね。
実際に書くかどうかはさておき、書けちゃいます。
Vueで全てやるのがいいのでしょうかね。。。何方か教えていただきたい。。。
addCountry () {
if (! this.selected) return;
this.$http({
url: '/api/country/'+this.selected,
method: 'GET'
}).then(res => {
this.myList.push({
id: res.data['id'],
country_name: res.data['country_name'],
utc_diff: res.data['utc_diff'],
});
this.saveMyList();
})
this.changeForcusHeight();
},
saveMyList () {
localStorage.setItem('myList', JSON.stringify(this.myList));
},
changeForcusHeight () {
var header_height = $(".time-bar-header").outerHeight();
var table_height = $(".table").innerHeight();
$(".forcus").css("height", table_height - header_height);
},
次に気になるこの部分。フィルターです。
前に記載された値を引数にして値をいじるメソッドを定義することができます。
<div v-for="n in 24" class="timeCol" v-bind:class="row.utc_diff | getTimeFromDiff | calcTime(n, addHour) | getTimeClass">
{{ row.utc_diff | getTimeFromDiff | calcTime(n, addHour) }}
</div>
フックメソッドと並列でfiltersの部分にメソッドを定義していきます。
もっとうまいことできますが、今回はUTC+09:00などをDBに入れてしまっているので、かなり力技で現在時刻を計算しています。
getTimeFromDiff() UTCとの時差から時刻を算出
calcTime() 時刻から表示のための時刻を作成
filters: {
getTimeFromDiff (diff, min_flg) {
var dt = new Date();
var offset = dt.getTimezoneOffset() / 60;
var hours = dt.getHours();
// UTC標準時間に設定
dt.setHours(hours + offset);
var sign = diff.substr(3, 1);
var hour_diff = diff.substr(4, 2);
var minutes_diff = diff.substr(7, 2);
if (sign == "+") {
var now = dt.getTime() + ((parseInt(hour_diff)*60 + parseInt(minutes_diff)) * 60000);
} else if (sign == "-") {
var now = dt.getTime() - ((parseInt(hour_diff)*60 + parseInt(minutes_diff)) * 60000);
}
var jikan = new Date(now);
var month = ('00' + (jikan.getMonth()+1)).slice(-2);
var day = ('00' + jikan.getDate()).slice(-2);
var hour = ('00' + jikan.getHours()).slice(-2);
var minutes = ('00' + jikan.getMinutes()).slice(-2);
if (min_flg) {
// min_flgがある時は現在時刻を返却する
return month+'/'+day+' '+hour+':'+minutes;
} else {
// min_flgがない時はUTC時差から分を連結する
return month+'/'+day+' '+hour+':'+minutes_diff;
}
},
calcTime (time, n, addHour) {
var dt = new Date();
var newDate = new Date(dt.getFullYear(), time.substr(0, 2), time.substr(3, 2), time.substr(6, 2), time.substr(9, 2));
newDate.setHours(newDate.getHours()+(n-1+addHour));
var month = ('00' + newDate.getMonth()).slice(-2);
var day = ('00' + newDate.getDate()).slice(-2);
var hour = ('00' + newDate.getHours()).slice(-2);
var minutes = ('00' + newDate.getMinutes()).slice(-2);
if (minutes == '00') {
// 分が00の場合は分表示なし
return month+'/'+day+' '+hour;
} else {
// 分が00以外の場合は分を表示
return month+'/'+day+' '+hour+':'+minutes;
}
},
クラスを動的に設定したい
一番苦労したところです。
ここのv-forの部分のクラスを時間によって(時間は表示のため1~24を順に足して表示)可変につけたくなってしまったのですが。。。
結果クラスの部分にもfilterを適用することができるようでこのように、v-bindとしてv-forごとに可変のクラスをつけることにしました。
<div v-for="n in 24" class="timeCol" v-bind:class="row.utc_diff | getTimeFromDiff | calcTime(n, addHour) | getTimeClass">
{{ row.utc_diff | getTimeFromDiff | calcTime(n, addHour) }}
</div>
こんなへんちくりんのクラス返却のためのメソッドを作成。
ここも他にいいやり方あればご教授いただければ幸いです。。。
getTimeClass (time) {
var hour = parseInt(time.substr(6, 2));
switch (true) {
case (hour >= 6 && hour <= 8):
return "morning";
case (hour >= 9 && hour <= 18):
return "day";
case (hour >= 19 && hour <= 24):
return "night";
case hour == 0:
return "mid-night twelve-mid-night";
case (hour >= 1 && hour <= 5):
return "mid-night";
}
},
以上です!
主に自分の備忘録なので最悪自分が読めればという感じですが、
他の方にも何かの参考になれば!