この記事の目的
この記事は、新人研修で使われていた「STEP1〜STEP13の手順に沿って
ToDoリストを作る教材」を、より分かりやすく解説し直したものです。
研修では、手順通りに進めればアプリ自体は完成しますが、
「なぜこのコードを書くのか」「何が起きているのか」が分からないまま
作業だけが進んでしまうことが多く、理解が追いつかないという声がありました。
そこでこの記事では、各STEPで何をしているのかを丁寧に補足しながら、
初心者でも流れを追いやすいように再構成しています。
説明は詳しめですが、そのぶん文章量も多めです。ご了承ください。
「とりあえず完成したけど、結局どういう仕組みなのか分からない」
という状態を解消することが目的です。
使用するもの
- Vue.js 2
- Visual Studio Code(VS Code)
- HTML
- CSS
- JavaScript
参考サイト
STEP0 はじめに
ここでは、このチュートリアルで使う前提知識について解説します。
まずは ToDo アプリとは何か、そして Web ページを構成する基本要素を確認します。
【ToDoアプリ】
やることを管理するアプリ。
【HTML】
ページの骨組み(何を表示するか)。
【CSS】
見た目のデザイン(色・配置・大きさ)。
【JavaScript】
動きをつける(追加・削除・状態変更など)。
【ブラウザ(Web ブラウザ)】
インターネットのページを見るためのアプリ。
スマホやパソコンでニュースを読んだり、動画を見たり、検索したりするときに使う「いつものアプリ」。
代表例:Microsoft Edge、Google Chrome、Safari など。
ブラウザは Web ページを表示するときに次の処理を行う:
- HTML を読み取り、ページの「骨組み」を作る
- CSS を読み取り、色や文字の大きさ、配置などの「見た目」を整える
- JavaScript を読み取り、ボタンを押したときの動きなどを実行する
つまりブラウザは、
「Web ページを読み取り、人が見て操作できる形にしてくれるアプリ」
という役割を持っている。
今回の ToDo アプリも、ブラウザが HTML・CSS・JavaScript(Vue.js)を読み込み、
画面に表示し、ボタンを押したときに動く。
【タグ】
HTML で “ここに何を置くか” を指定するための記号。
HTML はタグを使って Web ページの構造を作る。
今回登場するタグの一部を、必要に応じて確認できるよう一覧にまとめました。
見なくても読み進められますが、気になる方は開いてみてください。
今回登場する HTML のタグ一覧(クリックで開く)
【<!DOCTYPE html>】
HTML5 で書かれた文書であることをブラウザに伝える宣言。ページの最初に必ず書く。
【<html>】
HTML 文書全体を囲むタグ。この中に
<head>と<body>が入る。
【<head>】
ページの設定情報(タイトル、文字コード、CSS など)を書く部分。
【<meta>】
ページのメタ情報(ブラウザに伝える設定)を指定するタグ。
今回は文字コードを UTF-8 にする。
文字コードが正しく指定されていないと日本語が文字化けするため重要。
【<title>】
ブラウザのタブに表示されるページタイトル。
【<link>】
外部ファイル(CSS など)を読み込むためのタグ。
【<body>】
実際に画面に表示される内容を書く部分。
【<div>】
レイアウトを作るための箱。Vue アプリ全体を囲むために使用。
【<h1>】
ページの中で一番大きく、重要な見出しを作るタグ。
【<h2>】
<h1>より少し小さい見出し。
見出しタグは h1 → h2 → h3… のように数字が大きくなるほど重要度が下がる。
【<p>】
段落(文章)を表すタグ。説明文や件数表示に使用。
【<label>】
フォーム部品に説明文をつけるタグ。
<label>の中に<input>がある場合、文字をクリックしてもボタンが選択される。
【<input>】
ユーザーが文字を入力したり、選択したりするためのタグ。
テキスト入力やラジオボタンに使用。
【<form>】
ユーザーが入力した内容をまとめるための「箱」。
入力欄やボタンをひとまとめにできる。
【<button>】
クリックできるボタン。追加・削除・状態変更に使用。
【<script>】
JavaScript を読み込むタグ。今回は Vue.js を CDN から読み込んでいる。
CDN は「インターネット上に置かれた共有のファイル置き場」のようなもの。
有名なライブラリ(Vue.js など)が置かれていて、自分のパソコンに保存しなくても使える。
STEP1 インスタンスの作成
STEP1の本文(クリックで開く)
(本文より引用)
まずは、アプリケーションを紐付ける要素 #app を作成します。
本文中の言葉について説明していきます
【要素】
HTMLにある部品のこと
<div>, <p>, <table>など、
タグで囲まれたものを指します。
#はidを表すので<div id="app">の部分が該当します
(本文より引用)
コンストラクタ関数 Vue を使ってルートインスタンスを作成します。
アプリケーションで使用したいデータは data オプションへ登録していきます。
順々に解説していきます
【コンストラクタ関数】
一言で言うと同じ形のものをたくさん作るための関数
Vueではnew Vue({...})と書く時Vueの部分がコンストラクタ関数にあたります。
また、el:'#app'の部分が紐づける要素を指定する部分です。
【関数】
コンストラクタ関数と区別するために、
「普通の関数」についても簡単に説明しておきます。普通の関数は、ある処理をまとめておき、
必要なときに呼び出して使うための仕組みです。例えば(aとbを足し算したものを返す関数):
function add(a, b) { return a + b }このように、何かを計算したり、値を返したり、
一連の処理をひとまとめにして再利用できるのが普通の関数です。コンストラクタ関数も見た目は同じ「function」ですが、
役割が少し違います。普通の関数は「処理を実行するためのもの」ですが、
コンストラクタ関数は
「決まった形を持つ“まとまり”を新しく作るためのもの」です。例えば Vue の場合、
new Vue({...})と書くと、Vue というコンストラクタ関数を使って
“Vue アプリの本体”を新しく作っています。このように、
-普通の関数 → 何かの処理を実行するためのもの
-コンストラクタ関数 → 決まった形を持つ“まとまり”を作るためのもの
という違いがあります。
【ルートインスタンス】
Vue が動き始めるスタート時点のことです。
new Vue({ ... }) の中に書いた内容をもとに Vue が処理し、
その結果として出来上がるものがルートインスタンスです。(例えるなら、el:..., data:... の部分が設計図で、
Vue がその設計図をもとに家(ルートインスタンス)を建てるイメージ)※「インスタンス」という言葉の詳しい説明は STEP9 で行います。
ここでは「Vue が作るアプリの本体」くらいのイメージで大丈夫です。
【data オプション】
オプションとは、Vue に渡す設定項目のことです。
dataオプションとは、簡単に言うと、アプリの中で使いたい「変わる情報」を入れておく場所です。
画面に表示する文字や、後で変わる数字などをここに書きます。data は Vue で決められた特別な名前なので変更できません。
(ほかにも el, methods, computed, watch などは変更できません)data オプションへ登録したデータは、すべてリアクティブデータに変換されます。
【リアクティブデータ】
リアクティブ(reactive)は「反応する」という意味で、
データが変わると自動で画面も変わる仕組みを持ったデータのことです。
Vue の data の中に書いた値が、このリアクティブデータにあたります。
STEP2 ローカルストレージ API の使用
STEP2の本文(クリックで開く)
(本文より引用)
データはサーバーではなく 「ローカルストレージ」へ保存することにします。
ストレージ周りの実装は Vue.js 公式サンプル「TodoMVC の例」
のコードを使用させていただきます。
【サーバー】
サーバーとは、インターネット上にある “データを保存したり処理したりするコンピュータ” のことです。
ユーザーの情報、アプリの設定、画像や文章など、Web サイトを動かすための情報を保存しておく場所です。特徴として:
- インターネットを通してアクセスできる
- 複数のユーザーが同じデータを共有できる
- どの端末からでも同じ情報を見られる
たとえるなら みんなで使える倉庫 のようなイメージです。
【ローカルストレージ】
ストレージとは「データを保存しておく場所」のことです。
ローカルストレージは、ブラウザ(Chrome や Safari など)の中にある
保存場所で、データを端末に保存できます。特徴として:
- インターネット不要
- その端末だけにデータが残る
- 最大 5MB 程度まで保存できる
- 簡単に使える
たとえるなら 自分だけが使える机の引き出し のようなものです。
今回の ToDo アプリでは、
- 他の人と共有する必要がない
- 設定が簡単
という理由からローカルストレージが採用されていると考えられます。
(本文より引用)
公式のコードの内容については詳しく説明しませんが、
これは Storage API を使ったデータの取得・保存の処理だけを抜き出したものです。
小さなライブラリだと思ってください。
【API】
API(エーピーアイ)とは、アプリやプログラムが “決められた方法で機能を使えるようにする仕組み” のことです。
「この方法で呼び出せば、この機能が使えますよ」という 道具の説明書 のようなものです。例:
- Google Maps の地図をアプリに表示する → Google Maps API
- ブラウザにデータを保存する → Storage API(今回使用)
- カメラを使う → Camera API
API は 機能を安全に・簡単に使うための窓口 です。
【ライブラリ】
ライブラリとは、よく使う処理をまとめて必要な時に呼び出して使える “部品セット” のことです。
例:
- 保存する処理
- データを読み込む処理
- 日付を扱う処理
- 画面を操作する処理
何度も使う処理をまとめた便利な道具箱です。
(本文より引用)
実際にストレージに保存されるデータのフォーマットは、次のような JSON です。
【フォーマット】
フォーマットとは、データをどのような形で保存するかを決めた “書式” のことです。
ローカルストレージは 文字列しか保存できません。
そのため:
- 保存するとき → 元のデータを JSON 文字列に変換して保存
- 読み込むとき → JSON 文字列を元のデータに戻す
という処理が必要になります。
【JSON】
JSON(ジェイソン)とは、データを表現するための “書き方のルール(フォーマット)” のことです。
一言で言うと、データを保存したり、やり取りしたりするために使われます。特徴:
- 人間にも読みやすい
- どの言語でも扱える
- 軽くてシンプル
- ローカルストレージと相性が良い
JSON は「名前: 値」の組み合わせを
{ }で書き、
複数のデータを扱うときは[ ]でまとめます。(具体例は STEP3 の ToDo データで説明します)
STEP3 データの構想
STEP3の本文(クリックで開く)
(本文より引用)
さあ、ここから実際に作るコードです!
どんなデータが必要になりそうかを、ざっくりと考えておきましょう。
- ToDo のリストデータ
- 要素の固有ID
- コメント
- 今の状態
- 作業中・完了・すべて などオプションラベルで使用する名称リスト
- 現在絞り込みしている作業状態
アプリケーションに付けたい機能から考えると、こんなところでしょうか。
【ToDo のリストデータ】
下記のような JSON のデータです。
[ { "id": 1, "comment": "買い物に行く", "state": "作業中" }, { "id": 2, "comment": "Vue の勉強", "state": "完了" } ]
- 固有ID:それぞれの ToDo を区別するための番号で、
同じ ID にならないように管理します。- コメント:ToDo の内容(画面に表示するテキスト)
- 状態:作業中 / 完了 のどちらか
画面で「作業中だけ表示」「完了だけ表示」などの絞り込みにも使います。
STEP4 リスト用テーブル
STEP4の本文(クリックで開く)
(本文より引用)
まずは、ToDo リストデータを表示するテーブルの枠組みを作成します。
【テーブル】
STEP3 で考えた ToDo データを、
画面に表形式で表示するためにテーブルを使います。
ここではテーブルに使うタグを紹介します。
<table>表全体を作るタグ。ToDo リストを表形式で表示するために使用。
<thead>表のヘッダー部分(列名)をまとめるタグ。
<tbody>表の本体部分(データ行)をまとめるタグ。
<tr>表の行(row)を表すタグ。
<th>表の見出し用のセル(1マス)。太字・中央寄せになる。
また、表では
- 縦の列のことを「カラム(column)」
- 横の行のことを「ロウ(row)」
と呼びます。
STEP5 リストレンダリング
STEP5の本文(クリックで開く)
(本文より引用)
ToDo リストデータ用の空の配列を data オプションへ登録します。
これは、データが何もない時でも 配列として認識されるようにするためと、
もともと data オプション直下のデータは 後から追加ができないため
初期値で宣言しておく必要があるためです。
【リストレンダリング】
リストレンダリングとは、
配列のデータ(ToDo リストデータ)を画面に繰り返し表示する仕組みです。(HTML には
<tr>...</tr>を 1 つだけ書いておき、
Vue が ToDo リストデータの件数分<tr>...</tr>を自動で増やし、
その中にデータを入れてくれる)ToDo リストのように複数の項目を一覧で表示する場合に必ず使います。
ここでは Vue の v-for(後ほど解説)を使用します。
【配列】
配列は「番号付きの箱が横に並んでいる」イメージです。
[ データ1, データ2, データ3, ... ] 0 1 2左から順番に 0番、1番、2番… と番号がつきます(0番から始まるのが一般的)。
この番号を使ってデータを取り出せます。ToDo リストデータは STEP3 で説明した JSON 配列で管理されています。
例:
{ id: 1, comment: "買い物に行く", state: "作業中" }{ id: 2, comment: "Vue の勉強", state: "完了" }ToDo リストは 1件だけでなく、2件、3件、10件と増えていきます。
この複数のデータをまとめて扱うために配列を使用します。
【初期値】【宣言】
初期値とは「最初に入れておく値」のことです。
Vue の data に登録したデータは、最初に存在していないと後から追加できない
(データを追加しても画面に反映されない)というルールがあります。
これはVue は最初に data の中身を読み取って、リアクティブデータに変換する仕組みのためです。
その結果、最初に存在しないデータはリアクティブにならず、画面に反映されません。そこで ToDo リストを入れるための todos は、
最初に空の配列[]を初期値として宣言しておく必要があります。宣言とは「この名前のデータを使います」とプログラムに教えることです。
data: { todos: [] // ← これが「宣言」 }
(本文より引用)
テーブルタグの [1] で
配列要素の数だけ繰り返し表示させるには、
対象となるタグ(ここでは <tr> タグ)に
v-for ディレクティブを使用します。
【v-for ディレクティブ】
v-for とは Vue で「繰り返し表示」を行うためのディレクティブ(特別な命令)です。
配列の中に複数のデータが入っているときに:
- 配列から 1 つ取り出す
<tr>を 1 つ作る- 次のデータを取り出す
- また
<tr>を作るという処理を 自動で繰り返してくれます。
書き方:
v-for="一時的な名前 in 繰り返したい配列"具体例:
<tr v-for="item in todos">
(本文より引用)
ディレクティブの値は JavaScript の式になっており次のように書きます。
v-for="各要素の一時的な名前 in 繰り返したい配列やオブジェクト"
【オブジェクト】
オブジェクトとは、JavaScript のデータの形のひとつで、
「名前(キー)と値(バリュー)のセット」をまとめたものです。例:
{ id: 1, title: "買い物に行く", state: "作業中" }v-for は配列だけでなくオブジェクトも繰り返し処理できます。
v-for="item in items"のように書くと、配列の各要素やオブジェクトの各値を順番に取り出して使えます。
(本文より引用)
v-for を記述したタグとその内側で
todos データの各要素のプロパティが使用できるようになります。
<tr> タグの内側に
「ID」「コメント」「状態変更ボタン」「削除ボタン」のカラムを追加していきましょう。
【プロパティ】
プロパティとは、STEP2 で説明した JSON と同じ構造で、
「名前(プロパティ名): 値」の組み合わせで情報を持っています。例:
{ id: 1, comment: "買い物に行く", state: "作業中" }JSON と JavaScript では書き方に少し違いがあります。
JSON(キーにダブルクオーテーション必須)
{ "id": 1, "comment": "買い物に行く" }JavaScript(記号やスペースがなければ省略可)
{ id: 1, comment: "買い物に行く" }
(本文より引用)
このボタンはまだなにも機能しないモックのため、
機能はこれから実装していきます。
【モック】
モックとは「見た目だけ用意した仮の部品」のことです。
- ボタンは画面に表示される
- クリックもできるように見える
- でも中身の処理(動作)はまだ入っていない
という “形だけの状態” を指します。
STEP6 フォーム入力値の取得
STEP6の本文(クリックで開く)
(本文より引用)
新しい ToDo をリストへ追加するための入力フォームを作成します。
ref 属性を使って参照するための名前をタグに付けておくと、
その DOM に直接アクセスできます。
【ref 属性】
ref(レフ)は、HTML のタグに「あとで呼び出すための名前」を付ける仕組みです。
たとえるなら、
学校で先生が「前から3番目の赤いカバンの子」と言うのではなく、
「田中さん」と名前で呼ぶようなものです。例:
<input type="text" ref="newComment">このように ref を付けておくと、
Vue の中から「newComment」という名前でこの入力欄を呼び出せます。すると、こんなことができます:
- 入力欄の中に書かれた文字を取り出す
- 入力欄にカーソルを合わせる(自動で入力できる状態にする)
- 特定の場所をスクロールさせる
- 動画や絵を表示する特別なタグを直接さわる
今回は、新しい ToDo を追加するときに
入力欄の中の文字を取り出すために ref を使います。
【DOM(ドム)】
DOM とは「ブラウザが HTML を読み取って作る、画面の部品の一覧表」
のようなものです。たとえば、Web ページには
- 見出し
- 文字
- ボタン
- 入力欄
- 画像
など、いろいろな部品があります。ブラウザは、これらの部品を「木の枝のように、親と子の関係で並べたもの」
として覚えています。その“画面の部品の並び”のことを DOM と呼びます。
たとえるなら、
家の中の家具を「リビング → テーブル → イス」のように
どこに何があるか整理してメモしているイメージです。Vue では、この DOM の中にある部品を
ref で名前を付けて呼び出したり、
画面の内容を変えたりできます。今回は、入力欄(input)が DOM の中のどこにあるかを
ref を使って名前で呼び出し、
その中の文字を取り出します。
(本文より引用)
ref 属性で名前を付けたタグは、メソッド内から次のように使用できます。
this.$refs.名前
テンプレートでは変数名(プロパティ名)だけでデータを使用できましたが、
メソッド内でデータやメソッドを使用するときは this を付ける必要があります。
たとえば、comment の場合なら次のように使用します。
this.$refs.comment.value
実際は、次の STEP7 で使用します。
【メソッド】
メソッドとは、「ボタンを押したときに実行したい処理」をまとめて書いておく場所です。
たとえるなら、
「このボタンを押したら、こう動いてね」という動きの説明書 のようなものです。例:
- 新しい ToDo を追加する
- ToDo を削除する
- 状態(作業中/完了)を切り替える
Vue では、メソッドの中でデータや ref を使うときは
this を付けて呼び出します。this.todos.push(新しいデータ) this.$refs.comment.valueこのように書くことで、
「Vue が持っているデータ」や「ref で名前を付けた入力欄」
をメソッドの中から使えるようになります。
【テンプレート】
テンプレートとは、「画面にどんな見た目で表示するか」を書く場所です。
たとえるなら、
家を建てるときの間取り図 のようなものです。Vue では HTML の中にテンプレートを書き、
その中で Vue のデータを使って画面を作ります。例:
{{ message }}→ message の中身が表示されるv-for→ 配列の数だけ繰り返し表示v-if→ 条件で表示・非表示を切り替えテンプレート内では、Vue のデータを
そのまま名前を書くことで使用できます。
(例:{{ todos.length }})
(本文より引用)
v-model ディレクティブを使えばデータとフォーム入力を同期することもできますが、
今回は入力したデータを画面に表示させないのと
常にデータとして持っている必要がないため、
この $refs を使って入力値を取得することにします。
【v-model】
v-model は、入力欄と Vue のデータを 自動で同期する仕組み です。
(同期とは「入力欄とデータが常に同じ状態になる」という意味です。)
入力欄に書いた文字がそのままデータにも反映されるため、
入力欄とデータが常に同じ状態になります。今回は「入力値を保持する必要がない」ため、
v-model ではなく $refs で必要なときだけ取得 します。
(本文より引用)
テーブルの下あたりに追加しておきます。
v-on:submit.prevent="doAdd"
この v-on ディレクティブによって、
ボタンをクリックしたり入力フォームでエンターを押してフォームのサブミットが行われると、
それをハンドリングして doAdd メソッドが呼び出されるようになります。
【v-on】
v-on は、「〇〇されたら、この動きをしてね」と
ボタンや入力欄に“合図”をつけるための仕組みです。たとえるなら、
「チャイムが鳴ったら席に着く」
「ボールを投げられたらキャッチする」
といった “合図と動き” のセットを決めるイメージです。今回の例では、
v-on:submit.prevent="doAdd"
と書いています。これは、
- フォームが送信されたら(ボタンを押す or Enter を押す)
- ページを再読み込みしないようにして(prevent)
- doAdd メソッドを実行する
という合図になります。
つまり v-on は、
「どんな操作が行われたときに、どんなメソッドを動かすか」
を決めるための仕組みです。
【サブミット(submit)】
サブミットとは、「フォームを送信する」という意味です。
入力フォームには、名前やコメントなどを入力して、
最後に「送信」ボタンを押す仕組みがあります。Web では、この “送信する” という動きを
submit(サブミット) と呼びます。たとえば、
- ボタンをクリックしたとき
- 入力欄で Enter キーを押したとき
にフォームが送信されると、それが「サブミットされた」状態です。
Vue では、このサブミットのタイミングを
v-on:submitで受信して、好きなメソッドを実行できます。
【ハンドリング(handling)】
ハンドリングとは、「起きた出来事に対して、どう動くかを決めて実行すること」です。
たとえるなら、
- チャイムが鳴ったら席に着く
- ボールが飛んできたらキャッチする
といった “合図に対して行動する” イメージです。
Web では、クリックされた、送信された、入力された、といった出来事(イベント)が起きたときに、
それに合わせて処理を実行することを
イベントをハンドリングする と言います。Vue では、v-on を使って
「このイベントが起きたら、このメソッドを実行する」
というハンドリングを設定できます。
STEP7 リストへの追加
STEP7の本文(クリックで開く)
(本文より引用)
つづいて doAdd メソッドを定義しましょう。
このメソッドは、フォームの入力値を取得して新しい ToDo の追加処理をします。
ルートコンストラクタの methods オプションに、メソッドを登録します。
【ルートコンストラクタ】
ルートコンストラクタとは、Vue アプリ全体の設定を書いておく場所です。
STEP1 で説明したように、Vue という名前そのものがコンストラクタ関数で、
new Vue({ ... })の{ ... }に書く設定が
「ルートコンストラクタ」にあたり、
elやdata、methodsなどアプリ全体の設計図が入っています。
(家を建てるための “設計図” のようなもの)Vue はこのルートコンストラクタ(設計図)を読み取り、実際にアプリを作り上げます。
その結果できあがるのが 「ルートインスタンス」 です。
【methods オプション】
methods オプションは、Vue の中で使う 処理(メソッド) をまとめて書く場所です。
ボタンを押したときなど、ユーザーの操作に応じて実行したい動きをここに定義します。
methods に登録したメソッドは、
@clickや@submitなどのイベントから呼び出せます。また、methods の中で
dataや$refsを使うときは
this を付けてアクセスします。例:
this.todos.push(...) // データの追加 this.$refs.comment.value // 入力欄の値を取得今回の doAdd メソッドも、この methods の中に定義します。
(本文より引用)
コメントと一緒に1行づつ読んでみてください。
通常の配列メソッド push を使うだけで、リストデータへ追加できます。
【配列メソッド push】
push とは?
JavaScript の配列に 新しい要素を一番うしろに追加する メソッドです。例:
var list = [1, 2, 3] list.push(15) // → [1, 2, 3, 15]Vue の
dataにあるtodosもただの配列なので、this.todos.push({ id: todoStorage.uid++, comment: comment.value, state: 0 })と書くだけで、新しい ToDo をリストに追加できます。
todoStorage.uid++ とは?
todoStorage.uid は「次に使う ID の番号」を覚えておくための箱(変数)です。
uid++ は
「今の番号を使ってから、1つ増やしておく」という意味になります。たとえば最初の値が 1 なら:
新しい ToDo に id: 1 を使う
そのあと uid の値が 2 に増える
という動きになります。
こうすることで、ToDo を追加するたびに
1, 2, 3, 4… と重複しない ID を自動で割り振れる
仕組みになっています。Vue は配列の変化を自動で監視しているため、
push でデータを追加すると画面のリストも自動的に更新されます。
STEP8 ストレージへの保存の自動化
STEP8の本文(クリックで開く)
(本文より引用)
さて、JavaScript 内ではデータは追加されましたが、
これではまだローカルストレージに保存されていません。
ブラウザをリロードしたら消えてしまいます。
doAdd メソッドの最後に todoStorage.save メソッドを使って保存してもよいのですが、
追加・削除・作業状態の変更すべて同じ処理をしなければいけません。
todos データの内容が変わると、自動的にストレージへ保存してくれたら素敵ですね。
これは watch オプションの「ウォッチャ」機能を使うことで可能です。
ウォッチャはデータの変化に反応して、あらかじめ登録しておいた処理を自動的に行います。
これで、todos データに何か変化があれば自動的にストレージへ保存されるようになりました。
【リロード】するとデータが消える理由
リロードとは、ブラウザで表示しているページを「もう一度読み直す」ことです。
ページを読み直すと、そのページで動いていた JavaScript も
いったんすべて初期状態に戻ります。そのため、画面上で ToDo を追加しても、
それは JavaScript のメモリの中にあるだけで、
ページをリロードするとデータは消えてしまいます。これを防ぐには、ページを閉じても残る場所に
データを保存する必要があります。その保存場所が ローカルストレージ です。
【todoStorage.save メソッド】【watch オプション】
todoStorage.save とは?
現在の todos データをブラウザのローカルストレージに保存するメソッドです。save メソッドでは、まず todos(配列)を
JSON.stringifyを使って 文字列に変換 します。
ローカルストレージは文字列しか保存できないためです。そして:
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))とすることで、STORAGE_KEY という名前の箱に
現在の ToDo リストを保存します。save メソッドはつまり:
- todos を文字列に変換する
- ローカルストレージに書き込む
という 2 つの処理をまとめたものです。
save を呼び出すだけで、その時点の ToDo リストが保存されます。
watch(ウォッチ)オプションとは?
watch は「特定のデータに変化があったとき、自動で処理を実行する仕組み」です。
今回は todos を監視します。たとえば:
- todos に新しい ToDo が追加された
- ToDo が削除された
- 状態(作業中/完了)が切り替わった
こうした todos の変化を見張って(ウォッチして)、
変化が起きた瞬間に自動で todoStorage.save を呼び出します。つまり watch を使うことで、
「todos が変わったら必ず保存する」という動きを自動化できます。
【なぜ doAdd の中で save を使わないのか?】
ToDo アプリでは、保存が必要になるタイミングが複数あります:
- やることを追加したとき
- やることを削除したとき
- 状態(作業中/完了)を切り替えたとき
これらすべての処理の中に save を書くと、
同じコードを何度も書くことになり、管理が大変 になります。また、どこかで書き忘れると保存されず、バグの原因にもなります。
そこで Vue の watch(ウォッチャ) を使うことで、
todos データに変化があった瞬間に
自動的に todoStorage.save を呼び出す ようにできます。つまり:
- 「追加・削除・状態変更のたびに save を書く必要がなくなる」
- 「書き忘れやミスを防げる」
- 「コードがすっきりする」
というメリットがあるため、
doAdd の中で save を直接使わないようにしているのです。
STEP9 保存されたリストを取得しよう
STEP9の本文(クリックで開く)
(本文より引用)
ストレージへの保存ができたので、次はストレージからの取得です。
このアプリケーションの「インスタンス作成時」に、
ローカルストレージに保存されているデータを「自動的」に取得して、
Vue.js のデータとして読み込みましょう。
特定のタイミングに何か処理をはさみたいときは
「ライフサイクルフック」のメソッドを使用します。
タイミングがいくつか用意されていますが、
今回の「インスタンス作成時」には created メソッドを使うとよいでしょう。
【インスタンス】
インスタンスとは、STEP1 で作った
new Vue({ ... })によって作られるもののことです。Vue.js のデータとして読み込む とは、
ローカルストレージに保存されていた ToDo の一覧を
Vue インスタンスのdataにセットすることです。具体的には:
this.todos = todoStorage.fetch()の部分で読み込んでいます。
【ライフサイクルフック】【created メソッド】
Vue インスタンス(
new Vue(...)で作られるもの)は、
生成されてから画面に表示され、破棄されるまでの間に
いくつかの 決まったタイミング を通過します。そのそれぞれのタイミングで処理を差し込める仕組みが
ライフサイクルフック です。例:
- インスタンスが作られた直後(created)
- 画面に表示される直前(beforeMount)
- 画面に表示された直後(mounted)
- インスタンスが破棄される直前(beforeDestroy)
今回の「インスタンス作成時」には created を使います。
created は、Vue がインスタンスを作り、
「dataが使える状態になった直後」に呼ばれるメソッドのため、
ローカルストレージからデータを読み込むのに最適です。
またcreatedはmethodsの中ではなく、dataと同じ階層に書きます。
(本文より引用)
データの取得には、先に作っておいた todoStorage オブジェクトの
fetch メソッドを使用します。
ライフサイクルメソッドの定義は「methods の中ではない」ことに注意してください。
ローカルストレージは Ajax と違い同期的に結果を取得できるため、
返り値を代入すればいいだけなので簡単です!
【fetch メソッド】
fetch メソッドは、ローカルストレージに保存されている
ToDo リストの 文字列データを取り出し、
JSON.parseという 関数 を使って 文字列をJavaScript の配列に戻して返す メソッドです。そのため created の中で:
this.todos = todoStorage.fetch()と書くだけで、保存されていた ToDo リストを
Vue のdataにセットできます。
【Ajax(エイジャックス)との違い】
Ajax は、Web ページを 再読み込みせずに サーバーと通信して
データの送受信を行う仕組みです。例:
- ボタンを押したらサーバーから最新データを取得
- ページをリロードせずに検索結果を表示
Ajax の特徴は 非同期処理 であることです。
非同期とは、データが返ってくるまで待たずに
次の処理がどんどん進む動きのことです。そのため Ajax を使う場合は:
- コールバック関数
- Promise(then)
- async / await
などを使って「データが返ってきた後の処理」を書く必要があります。
一方、ローカルストレージは Ajax と違い
同期的にすぐ結果が返ってくる ため、this.todos = todoStorage.fetch()と書くだけで完了します。
【返り値】
返り値とは、
「関数やメソッドが実行されたあとに返してくる結果」 のことです。例:
let x = 1 + 2この場合、計算結果の 3 が返り値で、x に入ります。
関数でも同じです:
let result = myFunction()
myFunction()が返した値が result に入ります。
STEP10 状態の変更と削除の処理
STEP10の本文(クリックで開く)
(本文より引用)
つづいて「状態の変更」と「削除」機能を実装しましょう。 methods オプションにそれぞれのメソッドを作成します。
doChangeState メソッド(状態変更)item.state の値を反転します。
【doChangeStateメソッド】
doChangeState: function (item) { item.state = !item.state ? 1 : 0 }コードの解説:
- item.state が 1(完了) の場合
→!item.stateは false になるので 0(作業中) が代入される- item.state が 0(作業中) の場合
→!item.stateは true になるので 1(完了) が代入される三項演算子を使って、状態を 0 と 1 で切り替える仕組みになっています。
意味としては:
「今の状態が 1 なら 0 に、0 なら 1 にする」三項演算子とは?
JavaScript の「短く書ける if 文」のようなものです。条件式 ? 条件が true のときの値 : 条件が false のときの値if 文で書くと次のようになります:
if (item.state) { item.state = 0 } else { item.state = 1 }単純な条件分岐の場合は、三項演算子を使うほうが短く書けます。
(本文より引用)
doRemove メソッド(削除)
インデックスを取得して配列メソッドの splice を使って削除します。
【doRemove メソッド】
doRemove: function (item) { var index = this.todos.indexOf(item) this.todos.splice(index, 1) }このメソッドは、クリックされた ToDo(=item)を
todos 配列から削除する処理 です。削除した後は、画面の一覧からも自動的に消えます。
テンプレート側では:
@click="doRemove(item)"のように呼び出され、削除したい ToDo オブジェクトが item として渡されます。
コードの流れ:
1. 配列の中で item が何番目か調べる
var index = this.todos.indexOf(item)
this.todosは ToDo の一覧が入った配列indexOf(item)は、配列の中で item が最初に見つかった位置(index)を返す例:
todos = [A, B, C] todos.indexOf(B) // → 結果は1になる(配列は 0 番目から数えるため)2. splice でその位置の要素を削除する
this.todos.splice(index, 1)
splice(開始位置, 削除する数)の形で使う- index の位置から 1 個だけ削除 するという意味
Vue の ToDo アプリでは、
「削除したい ToDo(item)」はわかっていても、
その item が配列の何番目にあるかはわからない ため、
indexOf(item)で位置を調べspliceでその位置を削除するという処理になります。
(本文より引用)
どちらも引数として要素の参照を渡しています。
【要素の参照を引数として渡している理由】
doChangeState(item) や doRemove(item) のように、
どちらのメソッドも item を引数として受け取っています。ここで渡されている item は、単なる値ではなく、
todos 配列の中に入っている ToDo オブジェクトそのもの(=参照) です。引数(ひきすう)とは?
「関数(メソッド)に渡す値のこと」です。テンプレート側で次のように書くと:
@click="doRemove(item)"item が引数として doRemove に渡される、という意味になります。
参照を渡すとは?
(難しい場合は飛ばしても問題ありません)
JavaScript のオブジェクトや配列は「箱そのもの」ではなく、
“箱への矢印(住所)” を変数に入れている と考えると理解しやすいです。図でイメージすると:
- todos →
[ itemA, itemB, itemC ]- item → itemB を指す矢印
item は itemB のコピーではなく、
itemB そのものを指している(=参照している) ということです。item を変更すると todos の中身も変わる理由
item.state = 1これは:
- item が指している itemB の state を変える
- todos の中に入っている itemB も同じもの
よって todos の中身も変わる という動きになります。
"値渡し"(プリミティブ型)との違い
JavaScript では、数値や文字列などは 値渡し になります。
これらは 値そのものが変数に入る という特徴があります。例:
let a = 10 let b = a b = 20 console.log(a) // 10(変わらない)b は a のコピーなので、b を変えても a は変わりません。
"参照型"(オブジェクト・配列)の場合
オブジェクトや配列は 参照(矢印) が渡されるため、
コピーではなく 同じものを共有 します。例:
let a = { value: 10 } let b = a b.value = 20 console.log(a.value) // 20(変わる)a と b は 同じオブジェクトを指している からです。
参照を渡すことで:
- 正しい ToDo を直接変更・削除できる
- コピーではなく “同じもの” を共有しているため、
item を変更すると todos の中身も変わるというメリットがあります。
(本文より引用)
まだモックの状態だった、状態変更ボタンのイベントをハンドルします。
これまで動いていなかった状態変更ボタンに、実際の処理をつけますという意味です。
(本文より引用)
つづいて削除ボタンもハンドルします。
「削除」は注意するべき操作のため、
キー修飾子 .ctrl を使って
「コントロールキーを押しながらクリック」しなければ呼び出されないようにします。
<button v-on:click.ctrl="doRemove(item)">
削除
</button>
削除ボタンも実際の処理を付けます
削除する際は注意が必要な操作なので、誤って押してしまわないように、
コントロールキー(ctrl)を押しながらクリックしたときだけ
削除が実行されるようにします。v-on:click.ctrlでその設定をします
STEP11 選択用フォームの作成
STEP11の本文(クリックで開く)
(本文より引用)
特定の作業状態のリストのみを表示させる「絞り込み機能」を追加しましょう。
スローガンテキストの下にラジオボタンをリストで表示します。
ToDo リストと同じように動的に作成するため、選択肢の options リストを作成しました。
【スローガンテキスト】
スローガンテキストとは、画面上部に表示されている
『Hello Vue.js World!!』 のようなアプリのタイトル部分のことです。
【動的に作成】
動的に作成とは、配列などのデータをもとに、
必要な数だけ HTML 要素を自動で生成する仕組みのことです。
今回のラジオボタンは、options 配列を v-for でループすることで、
3つの選択肢が自動的に画面に表示されます。
(本文より引用)
options リストを タグで繰り返し描画して、
内側の タグの value 属性には、データ側の label.value データをバインドします。
v-model ディレクティブを使って、ラジオボタンの選択値と current データを同期させます。
ラジオボタンが変更されると、その要素の label.value が
current プロパティへ代入される仕組みです。
【実際の処理の流れ】
※元のコードでは v-for の変数名が label でしたが、
以下の説明では読みやすさのために option に変更して解説します。
(label.value → option.value と読み替えて理解してください)HTML のこの部分で、ラジオボタンが自動的に生成されています:
(元のファイルは<label v-for="label in options">だったが説明用に変更)<label v-for="option in options"> <input type="radio" v-model="current" v-bind:value="option.value"> {{ option.label }} </label>実際の処理の流れ
v-for="option in options"
options 配列をループし、配列の要素数ぶん<label>ブロックを自動で生成します。
1回のループでは、options 配列の1つのオブジェクトが option に入ります。
<input type="radio">
ループのたびにラジオボタンが1つ作られます。v-model="current"
選択されたラジオボタンの値と、Vue 側の current データを常に同期させます。
初期値として current に -1 が入っているため、最初は「すべて」が選択された状態になります。v-bind:value="option.value"
options 配列の各要素(option オブジェクト)の value を
<input>の value 属性にバインドします。
→ ラジオボタンの値がデータから自動で設定されます。
(バインド=データと HTML を結びつけること。){{ option.label }}
option オブジェクトの label プロパティの値
("すべて" / "作業中" / "完了")を画面に表示します。1回目のループ:
option は次のオブジェクト:
{ value: -1, label: "すべて" }
label プロパティの値は "すべて"2回目のループ:
option は次のオブジェクト:
{ value: 0, label: "作業中" }
label プロパティの値は "作業中"3回目のループ:
option は次のオブジェクト:
{ value: 1, label: "完了" }
label プロパティの値は "完了"
STEP12 リストの絞り込み機能
STEP12の本文(クリックで開く)
(本文より引用)
current データの選択値によって表示させる
ToDo リストの内容を振り分けるため「算出プロパティ」という機能を使用します。
算出プロパティは、データから別の新しいデータを作成する関数型のデータです。
定義方法は、computed オプションに加工したデータを返すメソッドを登録します。
算出プロパティは、元になったデータに変更があるまで、結果をキャッシュするという性質を持っています。
【算出プロパティ】
算出プロパティとは、Vue が提供している
「データを元に新しい値を計算して返す仕組み」です。
見た目はメソッド(関数)のようですが、
実際には「データから派生した別のデータ」を
自動的に作り出すための特別なプロパティです。算出プロパティは、computed オプションの中に
メソッドとして定義します。
そのメソッドの return で返した値が、
新しいデータとして扱われます。また、算出プロパティには
「キャッシュされる」 という特徴があります。"キャッシュ"されるとは?
一度計算した結果を Vue が覚えておき、
元データが変わらない限り、再計算しないことです。元になっているデータ(今回でいう current や todos)が変わらない限り、
算出プロパティの結果は再計算されず、
前回の結果がそのまま使われます。そのため、無駄な処理が減り、
アプリが効率よく動作します。methods でも同じような処理はできますが、
methods は「呼ばれるたびに毎回計算される」ため、
画面が再描画されるたびに関数が実行され、
処理が重くなる可能性があります。
(methods はキャッシュされないためです)今回の絞り込み機能では、
current の値(-1, 0, 1)に応じて表示する ToDo リストを切り替えるために、
算出プロパティを使って
「表示用の ToDo リスト」 を作成しています。
(本文より引用)
定義方法が違うだけで使い方はデータと一緒です。
一覧表示テーブルの v-for ディレクティブで使用している todos の部分を
computedTodos に置き換えましょう。
たとえば「◯件見つかりました」という結果の要素数を表示したいとき、
単純にその配列の computedTodos.length を見れば欲しい数字が得られます。
{{ computedTodos.length }} 件を表示中
キャッシュ機能があるおかげで、
メソッドと違い何度使用しても処理は 1 度しか行われません。
【todos を computedTodos に置き換える理由】
「絞り込み後のリスト」を画面に表示したいからです。
todos … 元の全データ
computedTodos … current の値に応じて絞り込まれたデータ画面に表示したいのは 「絞り込まれた結果」 なので、
todos のままでは目的を達成できません。computedTodos.length
computedTodos の中にあるオブジェクトの数(=絞り込み後の件数)を表します。todos は「元データ」であり、ユーザーの選択に応じて内容が変わることはありません。
一方、computedTodos は current の値に応じて内容が変わる 表示用データ です。computedTodos を使うことで、ユーザーがラジオボタンを切り替えた瞬間に
表示内容が自動的に更新されるようになります。
STEP13 文字列の変換処理
STEP13の本文(クリックで開く)
(本文より引用)
最後の仕上げとして「状態変更ボタン」のラベルが数字になっているのを修正しましょう。
状態変更ボタンで使っている状態の item.state データは、
文字列そのものではなく「キー」になる数字を保存しています。
一般的にもカテゴリーなどのデータでは、こういった数字や短い英数字のキーの状態で保存されます。
しかし、このままでは作業中なら「0」完了なら「1」と表示され、まったく意味がわかりません。
絞り込みのセレクトボックス用の options データをもとに、
value から label へ変換するための labels 算出プロパティを作成します。
Mustache で labels オブジェクトを通すように変更します。
<button v-on:click="doChangeState(item)"> {{ labels[item.state] }} </button>
これで人が理解できる文字で表示されるようになりました。
このような文字の処理は、フィルタ機能を使っても同じように変換できます。
【なぜ labels(算出プロパティ)が必要なのか】
ToDo の状態(item.state)は、データとして扱いやすいように
数字(0 / 1)で保存されています。
しかし、この数字をそのまま画面に表示すると「0」「1」と表示され、
ユーザーには意味が伝わりません。そこで、絞り込み用に使っている options データを利用して、
value(数字)→ label(文字)に変換するための
labels(算出プロパティ) を作成します。labels() のコード
labels() { return this.options.reduce(function (a, b) { return Object.assign(a, { [b.value]: b.label }) }, {}) }labels() の処理を一行ずつ説明
1.
labels() {
labels という算出プロパティの定義を開始します。2.
return this.options.reduce(function (a, b) {
options 配列を reduce で 1 つずつ処理し、
最終的に 1 つのオブジェクトにまとめます。
- a … これまでに作られたオブジェクト
- b … options の現在の要素
reduce の初期値は{}(空のオブジェクト)です。3.
return Object.assign(a, { [b.value]: b.label })
Object.assign を使って、a に新しいプロパティを追加します。
- b.value(数字)をキーにして
- b.label(文字)を値として
a に追加する処理です。例:
b が{ value: 0, label: "作業中" }の場合、
{ 0: "作業中" }というプロパティが追加されます。4.
{ [b.value]: b.label }の意味
[b.value]は「変数をキーとして使う」ための書き方です。
- b.value が 0 →
{ 0: "作業中" }- b.value が 1 →
{ 1: "完了" }
のように、数字をキーにしたオブジェクトを作れます。5.
}, {})
reduce の初期値として{}を渡しています。
空のオブジェクトからスタートし、1 つずつプロパティを追加していきます。6.
}
labels() の処理が終了します。labels() の最終的な結果
options が次のような配列なら:[ { value: -1, label: "すべて" }, { value: 0, label: "作業中" }, { value: 1, label: "完了" } ]labels() の返すオブジェクトは次のようになります:
{ -1: "すべて", 0: "作業中", 1: "完了" }これにより、数字をキーにして文字を簡単に取り出せます。
- labels[0] → "作業中"
- labels[1] → "完了"
- labels[-1] → "すべて"
Mustache(マスタッシュ)で変換結果を表示する
Vue の Mustache({{ }})は、データを画面に表示するための構文です。{{ labels[item.state] }}と書くことで、数字の状態(0 / 1)を labels を通して
文字に変換して表示できます。例:
<button v-on:click="doChangeState(item)"> {{ labels[item.state] }} </button>item.state が 0 の場合 → "作業中"
item.state が 1 の場合 → "完了"このようにして、数字で保存された状態を
人が読める文字に変換して表示 できるようになります。