はじめに
AngularのチュートリアルアプリをあえてVueで作成することによって、Vue.jsに入門してしまおうという試みです。
ツアー・オブ・ヒーローズチュートリアルとは?
Tour of Heroesとは、Angular公式ドキュメントのチュートリアルアプリ(v7.2.0時点)で、たしかv2.0の頃にはすでにあった気がするくらい昔からあるAngularユーザーには馴染深いアプリです(たぶん)。ヒーローの人材派遣会社のアプリという設定らしい、イメージがしづらい。
完成後のソースコードはGitHubにアップロードしています。
バージョン情報
- Vue CLI@3.4.0
- vue-router@3.0.2
開発
環境構築
VueのCLIツールのインストールと初期アプリケーションプロジェクトの作成、プロジェクトのサーブまでをやっています。
$ npm install -g @vue/cli $ vue create vue-tour-of-heros-tutorial $ cd vue-tour-of-heros-tutorial $ npm run serve
上記のコマンドをターミナルで実行した後、ブラウザでhttp://localhost:8080/
を開くとデフォルト設定の画面が出てくるので、環境構築に成功していることがわかります。
axiosをインストールする
HTTPリクエストによるデータの取得を行う予定のため、HTTPクライアントのaxiosをインストールします。
$ npm i axios
https://jp.vuejs.org/v2/cookbook/using-axios-to-consume-apis.html
インストールしておいてなんですが、axios(というかHTTPリクエスト)なくても作成できるなって思ったので、axiosは途中でアンインストールしました
vue-routerをインストールする
画面のルーティング処理も必要なので、vue-routerをインストールします。
$ npm i vue-router
lodashのインストール
JavaScriptで使える便利な関数を揃えているライブラリであるlodashをインストールします。このアプリではヒーロー検索コンポーネントでdebouce
を使用するために入れています。
$ npm i lodash
完成後のコード
アプリが完成した後のフォルダ構成のキャプチャーです。
エントリファイル
完成後のコードについてファイル毎に自分なりのポイントなどを説明していきます。
まずVue.jsのエントリファイルであるmain.js
、このファイルについてはVue CLIで自動生成されたデフォルトの状態からほとんど変更していません。
ルーティングの設定router
をインポートして追加したくらい...
ルートコンポーネント
続いて、ルートコンポーネントであるApp.vue
ファイルです。
data
オプションで定義しているタイトルの表示、
ダッシュボードコンポーネントとヒーローズコンポーネントへのリンクを表示、そして最下部には子コンポーネントであるメッセージコンポーネントの表示をしています。
<style>
タグが2つありますが、scoped
のついているほうがルートコンポーネント限定で適用されるCSSで、ついてないほうがアプリ共通に適用されるCSSになります。
シンプルなコンポーネントですが、単一ファイルコンポーネント(.vue
ファイル)では、data
オプションを関数形式で書かないといけないのを知らなかったのでつまづきました。
ルーティング設定ファイル
ファイル名がわかりづらいですが、各種画面のルーティング(URL)を設定しているファイルです。
path: '/',
で設定しているところからわかる通り、初期表示ではダッシュボードコンポーネントが表示されます。そして、ヒーローズリンクをクリック(/heroes
)するとヒーローズコンポーネントが表示されるという感じです。
ヒーロー詳細コンポーネントへの遷移パスで(/detail/:id
)、ヒーローIDを使用した動的ルートマッチングを使用していますが、他フレームワークでもよく見るおなじみの書き方だと思うので、特に難しい部分はなかったかなという感じです。
Store
今回作成したアプリの状態管理ですが、Vuexをわざわざ入れるのもどうかな...と思ったので単純なstoreパターンを採用しました。
単一のStoreファイルで、FluxでいうところのStateもActionもStoreも担当する形式になっています。
各種コンポーネントで共有したいデータであるheroes
配列とmessages
配列をstateとして設定し、そのstateを追加・取得・更新・削除するメソッド(getHeroes
, clear
など)を集約しています。
状態管理とstoreパターンについてはVue.jsの公式サイトに説明があります。
ダッシュボードコンポーネント
ダッシュボードコンポーネントでは、storeのheroes
配列から2 - 5番目のヒーローを取得してボードとして表示するということをやっていて、ボードをクリックするとクリックしたヒーローの詳細画面に遷移します。
ダッシュボードコンポーネントの子コンポーネントとして、ヒーロー検索コンポーネントを設定しています。
ヒーローズコンポーネント
ヒーローズコンポーネントでは、storeで保持している全ヒーローの一覧表示と、ヒーローの追加と削除機能を持っています。ダッシュボードコンポーネントと同様にリストをクリックすると、該当するヒーローの詳細画面に遷移します。
メッセージコンポーネント
storeで保持しているメッセージ配列(messages[]
)を表示するコンポーネント、メッセージのクリア機能もあります。
ヒーロー詳細コンポーネント
ダッシュボードコンポーネント、ヒーローズコンポーネント、ヒーロー検索コンポーネントから遷移できるヒーローの詳細を表示するコンポーネントになります。ヒーローの名前を変更して保存する機能もついています。
ヒーロー検索コンポーネント
フォームに入力した文字列からヒーローを検索するコンポーネントとなります。検索結果のリンクをクリックするとヒーロー詳細コンポーネントに遷移します。
_.debounce(this.search(this.searchName), 500)
の部分では、lodashのdebouceメソッドを呼び出していて、内容としては検索フォームの入力が終わってから500ミリ秒後に検索メソッドを呼び出すという処理になっています。
発生したバグ
本アプリを作成する過程で発生したエラーの一覧です。Vue.jsを初めて使った人がどういうエラーでつまづくのかのサンプルとしてご利用ください(?)。
data
オプションの定義に失敗する
エラーメッセージ
[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.
Vue.jsのdata
オプションでtitle
プロパティを定義したタイミングで発生しました。
単一ファイル型コンポーネントでdata
オプションを定義する場合は、function()
をreturn
する形式にしないといけない、というエラーのようです。
https://jp.vuejs.org/v2/guide/components.html#data-は関数でなければなりません
修正前(エラー発生時)のコード
<script> export default { name: 'app', data: { title: 'Tour of Heroes', } }; </script>
修正後のコード
<script> export default { name: 'app', data: function() { return { title: 'Tour of Heroes', } } }; </script>
親コンポーネントから子コンポーネントに値を渡すのに失敗する
エラーメッセージ
[Vue warn]: Error in render: "TypeError: Cannot read property XXX of undefined"
おなじみのundefinedエラーですね。親コンポーネントから子コンポーネントにオブジェクトを渡すタイミングで発生しました。
親コンポーネントから子コンポーネントに動的な値やオブジェクトを渡す場合は、v-bind
が必要になりますが、それをつけ忘れていることが原因で発生しました。
https://jp.vuejs.org/v2/guide/components-props.html#オブジェクトの受け渡し
修正前(エラー発生時)のコード
<HeroDetail hero="selectedHero"></HeroDetail>
エラーが発生したコードです。静的な値を渡す場合はこのテンプレートの書き方でも親から子へ値を渡すことができます。
修正後のコード
<HeroDetail v-bind:hero="selectedHero"></HeroDetail>
今回は、親から子へオブジェクトを渡しているので、v-bind
が必要になります。なお、v-bind
は省略できるので、<HeroDetail v-bind:hero="selectedHero">
ではなく、<HeroDetail :hero="selectedHero">
という書き方でもOKです
JSONをaxiosのGETで取得できない
axiosは途中で削除したから、最終的なコードには含まれていないんですが、404が返ってくるというエラーに遭遇したりしました。
エラーメッセージ
GET http://localhost:8080/public/mock-heroes.json 404 (Not Found)
https://forum.vuejs.org/t/vue-cli-3-0-reading-json-with-axios/30053
修正前のコード
methods: { getHeroes: function () { this.messages.push('HeroService: fetched heroes') return axios.get('/public/mock-heroes.json') } }
単純にJSONファイルへのパスが間違っているのが原因でした。public/
ディレクトリ配下に配置している資源に対してパスを設定する場合、パスにはpublic
を含める必要がありません。
修正後のコード
methods: { getHeroes: function () { this.messages.push('HeroService: fetched heroes') return axios.get('/mock-heroes.json') } }
ウオッチャでエラーが発生する
[Vue warn]: Error in callback for watcher "searchName": "TypeError: Expected a function" TypeError: Expected a function at Function.debounce (lodash.js?2ef0:10319) at VueComponent.debouncedGetHeroes (HeroSearch.vue?fa11:39) at VueComponent.searchName (HeroSearch.vue?fa11:31) at Watcher.run (vue.runtime.esm.js?2b0e:3418) at flushSchedulerQueue (vue.runtime.esm.js?2b0e:3160) at Array.eval (vue.runtime.esm.js?2b0e:1965) at flushCallbacks (vue.runtime.esm.js?2b0e:1891)
lodashのdebounceメソッドを使用していて発生したエラーです。現時点でも発生しています...
修正前のコード
watch: { searchName: function () { this.debouncedGetHeroes() } }, methods: { search: function (name) { this.heroes = store.search(name) }, debouncedGetHeroes: function () { _.debounce(this.search(this.searchName), 500) } }
直し方わからないので直してない....まあ動くからええか....わかる人教えてください
おわりに
最終的に削除したもののmixin
やaxios
を使ったりしたので、Vue.jsの基本的な機能はあらかた触れたような気がするのが作成してよかった点です。特にmixin
につまづいて、mixinは継承に近い概念なんですけどコンポーネント間でのデータの共有に利用しようとして失敗するなどしました(mixinで共通のデータをコンポーネントに持たせようといても、各コンポーネントごとに同じ名前の別のデータプロパティを持つことになり、データの値自体はコンポーネント間で共有されない)。
Angularと比較するとVue.jsは基礎ガイドのボリュームが圧倒的に少なかったので使い始めるコストは少なかったものの、フルスタックフレームワークであるAngularほどフレームワーク単体でのサポートが少ないと感じたので、個人的にはAngularとVue.jsのどちらが初心者向けかと聞かれると難しい問いになる気がしました...Vue.jsだとすぐにVuexなどの状態管理ライブラリがないと書くのが辛くなってしまう気がする...その点Angularは状態管理ライブラリがなくてもある程度は書けるし...