Edited at

Vue.js の主要な機能をざっくりとつかってみたときのメモ

久しぶりにWEB開発しようとしたら、2-3年前にやってた AngularJS + Grunt + Bower + Yeoman な構成はいまや古い部類の構成になってるようで、、いま時点(2018/12)で、似た感じかつシンプルなフレームワークを探してて Vue.js にたどり着きました。

ってことで、ちょっとさわってみたときの作業メモ。

WEB系のフレームワークで必ずでてくる、TODOリストの構築を行ってみましょう。

ちなみに2019/01/28時点:

これだけQiita化しています。


前提環境

今回作業している環境です。

$ sw_vers

ProductName: Mac OS X
ProductVersion: 10.14.2
BuildVersion: 18C54
$ node --version
v10.15.0
$ npm --version
6.4.1
$


インストール

vue-cli というVue.jsのテンプレを生成してくれるコマンドラインツールをインストールします。

$ npm install -g vue-cli

$ source ~/.bash_profile
$ vue --version
2.9.6

よさそうですね。


やってみる


プロジェクト作成

$ vue init webpack todo-examples

↓とりあえずすべて Yes

? Project name todo-examples
? Project description A Vue.js project
? Author Masatomi KINO <kino@example.com>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner jest
? Setup e2e tests with Nightwatch? Yes
? Should we run `npm install` for you after the project has been created? (recom
mended) npm

vue-cli · Generated "todo-examples".

# Installing project dependencies ...
# ========================
...

# Project initialization finished!
# ========================

To get started:

cd todo-examples
npm run dev

Documentation can be found at https://vuejs-templates.github.io/webpack

完了しました。


Hello World

最後にcd todo-examples,npm run dev とでてましたので、その通りに。。

$ cd todo-examples

$ npm run dev
...
DONE Compiled successfully in 7650ms 15:42:24

I Your application is running here: http://localhost:8080

さて、メッセージ通りにブラウザで http://localhost:8080 をひらいてみます。

image.png

よさそうですね。。


TIPS


v-model でデータバインディング

まずはデータバインディングから。 HelloWorld.vue を以下のようにします。


HelloWorld.vue

<template>

<div class="hello">
<p><input type='text' v-model='msg'></p> <!-- このへん -->
<p>{{msg}}</p>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Hello World.'
}
}
}
</script>


ブラウザをリロードしてみると(たぶん勝手にリロードされてます)テキストボックスが表示されると思いますが、 その v-modelが設定されたテキストボックスに値を入力すると、{{msg}}部分も値が更新されると思います。

image.png


配列データを v-forで繰り返し,submit ボタンでメソッド呼び出し

つづいて配列をHTML上の v-forタグで走査したり、v-on:submitもしくはその省略形の@submitでイベントハンドラを登録したりしてみます。

イベントハンドラとして登録する、画面で使用するメソッドは、methods に定義していきます。

また、Vue.jsはその画面上で使用する変数をdata()で定義するようになっていて、todos配列には初期値をセットしてあります。

結果、HelloWorld.vue を以下の通りにしましょう。


HelloWorld.vue

<template>

<div class="hello">
<ul>
<li v-for='todo in todos' >{{todo}}</li> <!-- 繰り返し処理 -->
</ul>
<form @submit.prevent='addItem'> <!-- submitされたらaddItemメソッドを呼ぶ -->
<input type='text' v-model='newTask'>
<input type='submit' value='追加' >
</form>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data () { // 画面で使用する変数を定義する場所
return {
newTask:'',
todos : [
{ name: "task 1", isDone: false },
{ name: "task 2", isDone: false },
{ name: "task 3", isDone: false },
{ name: "task 4", isDone: false }
]
}
},
methods:{ // 画面やscriptから呼び出すメソッド
addItem: function(){
this.todos.push({
name: this.newTask,
isDone: false
})
this.newTask=''
}
}
}
</script>


data() で、newTasktodos という変数を定義しました。またmethodsでは、追加ボタンをクリックしたときに呼び出す addItemメソッドを定義しました。

ちなみにそのメソッド内では、this.newTaskなどと「this」をつけることで、data()で定義した変数にアクセスできます。

以上で、追加ボタンをクリックするとformのsubmitが行われて、addItemメソッドが呼び出された結果、タスクが追加されるようになりました。

image.png


v-bind:classでクラスの制御、computedで計算処理

さてつぎは、v-bindです。下記のように li をv-forしている箇所に


HelloWorld.vue(抜粋)

    <ul>

<li v-for='(todo,index) in todos' >
<input type='checkbox' v-model='todo.isDone'>
<span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
<span @click='deleteTask(index)' class='xButton'>[x]</span>
</li>
</ul>

って<span v-bind:class='{done: todo.isDone}'>...</span> と書くことで、todo.isDoneのときだけ(タスクが終わってるときだけ) class属性に doneがセットされるようになります。そしてcssで、そのクラスは完了線を引くようにしています。

チェックボックスは、v-modelをつかって todo.isDoneに連動させています。

また

<span @click='deleteTask(index)' class='xButton'>[x]</span>

として、クリックしたときのメソッド deleteTask を追加しています。引数の「index」は、v-for を <li v-for='(todo,index) in todos' >と記述することで、index番号を使用可能にしています。

さいごにdata()methodsのならびに、computed部を追加し、そこにメソッドremainingTask を定義しています。computedに定義したメソッドは、画面上から{{remainingTask}}とやって呼び出すことができます。

まとめると、全体はこんな感じ。


HelloWorld.vue

<template>

<div class="hello">
<h1>My Todo Task<span class='info'>({{remainingTask.length}}/{{todos.length}})</span></h1>
<ul>
<li v-for='(todo,index) in todos' >
<input type='checkbox' v-model='todo.isDone'>
<span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
<span @click='deleteTask(index)' class='xButton'>[x]</span>
</li>
</ul>
<form @submit.prevent='addTask'>
<input type='text' v-model='newTask'>
<input type='submit' value='追加' >
</form>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data () {
return {
newTask:'',
todos : [
{ name: "task 1", isDone: true },
{ name: "task 2", isDone: false },
{ name: "task 3", isDone: false },
{ name: "task 4", isDone: false }
]
}
},
methods:{
addTask: function(){
this.todos.push({
name: this.newTask,
isDone: false
})
this.newTask=''
},
deleteTask: function(index){
this.todos.splice(index,1)
}
},
computed:{
remainingTask: function(){
return this.todos.filter(function(todo){
return !todo.isDone
})
}
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
font-size: 16px;
border-bottom: 1px solid #ddd;
padding: 16px 0;
}

.xButton{
cursor: pointer;
font-size: 12px;
color: red
}

li > span.done{
text-decoration: line-through;
color:#bbb
}

.info {
color: #bbb;
font-size: 12px;
}
</style>


ブラウザを見てみると、、、

image.png

チェックボックスと削除ボタンが追加され、チェックを入れるとv-modelでバインドしたtodo.isDoneがtrueとなり、完了線がつきます。

削除ボタンをクリックすると、deleteTaskが呼ばれて該当タスクが削除されます。

よさそうです。


ボタンで全削除

つぎに「完了タスク全削除」って機能を追加します。すぐうえでcomputed

remainingTask メソッドを追加ましたが、内容は「いま未完了のタスクを返す」ようにしてあるので、todosをそれに置き換えるだけですね。


HelloWorld.vue

<template>

<div class="hello">
<h1>
<button @click="deleteEndTask">完了タスクの削除</button>My Todo Task<span class='info'>({{remainingTask.length}}/{{todos.length}})</span></h1>
... 省略
</template>

<script>
export default {
... 省略
methods:{
... 省略
deleteEndTask:function(){
this.todos = this.remainingTask;
}
},
... 省略
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
...省略
li > span.done{
text-decoration: line-through;
color:#bbb
}
</style>



LocalStorageからの読み書き、watchによる変数の監視。

つづいて、リロードしても消えないように画面初期化時にlocalStorageから読み込んでみたり、画面から追加や削除などtodosに対しての操作を監視し、todosに変更があったときにlocalStorageへ保存したり、してみます。

変数の監視は、watch をつかって実装します。watch部に


HelloWorld.vue(抜粋)

watch:{

todos: {
handler: function() {
localStorage.setItem('todos',JSON.stringify(this.todos))
},
deep: true
}
}

と記述することで該当の変数の変更を検知することが出来ます。

したがって、todos変数を監視し、変更を検知したらStorageに書き込むようにしました。

また、created部は、起動時に呼び出される関数なので、そこで


HelloWorld.vue(抜粋)

created: function() {

this.todos = JSON.parse(localStorage.getItem("todos")) || [];
}

として、Storageから読み込むか、読めければカラの配列をセットします。

変更した差分は以下の通りです。


HelloWorld.vue(抜粋)

<script>

export default {
name: 'HelloWorld',
data () {
return {
newTask:'',
- todos : [
- { name: "task 1", isDone: true },
- { name: "task 2", isDone: false },
- { name: "task 3", isDone: false },
- { name: "task 4", isDone: false }
- ]
+ todos : []
}
},
+ watch: {
+ todos: {
+ handler: function() {
+ localStorage.setItem('todos',JSON.stringify(this.todos))
+ },
+ deep: true
+ }
+ },
+ created: function() {
+ this.todos = JSON.parse(localStorage.getItem("todos")) || [];
+ },
methods:{
addTask: function(){
this.todos.push({

全体はこんな感じになりました


HelloWorld.vue

<template>

<div class="hello">
<h1>
<button @click="deleteEndTask">完了タスクの削除</button>My Todo Task<span class='info'>({{remainingTask.length}}/{{todos.length}})</span></h1>
<ul>
<li v-for='(todo,index) in todos' >
<input type='checkbox' v-model='todo.isDone'>
<span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
<span @click='deleteTask(index)' class='xButton'>[x]</span>
</li>
</ul>
<form @submit.prevent='addTask'>
<input type='text' v-model='newTask'>
<input type='submit' value='追加' >
</form>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data () {
return {
newTask:'',
todos : []
}
},
watch: {
todos: {
handler: function() {
localStorage.setItem('todos',JSON.stringify(this.todos))
},
deep: true
}
},
created: function() {
this.todos = JSON.parse(localStorage.getItem("todos")) || [];
},
methods:{
addTask: function(){
this.todos.push({
name: this.newTask,
isDone: false
})
this.newTask=''
},
deleteTask: function(index){
this.todos.splice(index,1)
},
deleteEndTask:function(){
this.todos = this.remainingTask;
}
},
computed:{
remainingTask: function(){
return this.todos.filter(function(todo){
return !todo.isDone
})
}
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
font-size: 16px;
border-bottom: 1px solid #ddd;
padding: 16px 0;
}

h1 > button {
float: right;
}

.xButton{
cursor: pointer;
font-size: 12px;
color: red
}

li > span.done{
text-decoration: line-through;
color:#bbb
}

.info {
color: #bbb;
font-size: 12px;
}
</style>


ブラウザで開いたのち、追加・完了・削除などを行ってからリロードしてみたり、ブラウザを閉じて開いたりしてみて、タスクが正しく保存されていることを確認してみてください。


コンポーネントをつくる

いまのところtodoの一画面があるだけですが、たくさん画面を作ったら、共通のヘッダやフッタをつけたくなりますよね。というわけで、ヘッダのコンポーネント(Header.vue) を作成し、App.vue から参照するようにします。


components/Header.vue(新規)

<template>

<header>{{message}}</header>
</template>

<script>
export default {
name: 'Header',
data () {
return {
message:'共通ヘッダ'
}
},
computed:{
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>



App.vue(修正)

<template>

<div id="app">
<Header /> <!-- 追加 -->
<router-view/>
</div>
</template>

<script>
import Header from '@/components/Header' // コンポーネントをimportして

export default {
name: 'App',
components:{
Header // つかうことを宣言する
}
}
</script>

<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* text-align: center; */
color: #2c3e50;
margin-top: 60px;
}
</style>


ブラウザを開いてみると、

image.png

よさそうですね。このヘッダはApp.vueに設置されているので、すべての画面に表示されるようになります。


いったんまとめ

いやあ、Vue.js よいですね。AngularJSやってた身からすると、作者がおなじだからか今のところほぼおなじといえる使い勝手です。逆にいうと数年前のAngularJSと違いがまだ体感できてない状態です、、orz。

いまのところドットインストールのVue.js入門とほぼおなじ仕組みになっちゃったので、ひきつづきFirebaseやVuexなどとの連携を整理してみようと思います。


ソースコード

https://github.com/masatomix/todo-examples

上記はマスタブランチで、この時点でのソースはこちらです。

https://github.com/masatomix/todo-examples/tree/for_qiita_init_001

下記手順でビルドできます。

$ git clone --branch for_qiita_init_001 https://github.com/masatomix/todo-examples.git

$ cd todo-examples/
$ npm install
$ npm run dev

これで、http://localhost:8080/ にブラウザでアクセスできるとおもいます。

おつかれさまでした。。


関連リンク