中安拓也のブログ

プログラミングについて書くブログ

【Vue.js入門】Angularのツアー・オブ・ヒーローズチュートリアルをVueで作成する

はじめに

AngularのチュートリアルアプリをあえてVueで作成することによって、Vue.jsに入門してしまおうという試みです。

ツアー・オブ・ヒーローズチュートリアルとは?

Tour of Heroesとは、Angular公式ドキュメントのチュートリアルアプリ(v7.2.0時点)で、たしかv2.0の頃にはすでにあった気がするくらい昔からあるAngularユーザーには馴染深いアプリです(たぶん)。ヒーローの人材派遣会社のアプリという設定らしい、イメージがしづらい。

Angular 日本語ドキュメンテーション

f:id:l08084:20190220204952p:plain
Angular公式サイトから拝借したヒーローズチュートリアルの遷移図

完成後のソースコードはGitHubにアップロードしています。

github.com

バージョン情報

  • 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/を開くとデフォルト設定の画面が出てくるので、環境構築に成功していることがわかります。

f:id:l08084:20190205153631p:plain

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

紹介 | Vue Router

lodashのインストール

JavaScriptで使える便利な関数を揃えているライブラリであるlodashをインストールします。このアプリではヒーロー検索コンポーネントでdebouceを使用するために入れています。

$ npm i lodash

完成後のコード

アプリが完成した後のフォルダ構成のキャプチャーです。

f:id:l08084:20190220210047p:plain
完成後のフォルダー構成

エントリファイル

完成後のコードについてファイル毎に自分なりのポイントなどを説明していきます。

まずVue.jsのエントリファイルであるmain.js、このファイルについてはVue CLIで自動生成されたデフォルトの状態からほとんど変更していません。 ルーティングの設定routerをインポートして追加したくらい...

Vue.jsのエントリファイル

ルートコンポーネント

f:id:l08084:20190223033202p:plain
ルートコンポーネント初期表示

続いて、ルートコンポーネントであるApp.vueファイルです。

dataオプションで定義しているタイトルの表示、 ダッシュボードコンポーネントとヒーローズコンポーネントへのリンクを表示、そして最下部には子コンポーネントであるメッセージコンポーネントの表示をしています。

ルートコンポーネント

<style>タグが2つありますが、scopedのついているほうがルートコンポーネント限定で適用されるCSSで、ついてないほうがアプリ共通に適用されるCSSになります。

シンプルなコンポーネントですが、単一ファイルコンポーネント(.vueファイル)では、dataオプションを関数形式で書かないといけないのを知らなかったのでつまづきました。

ルーティング設定ファイル

ファイル名がわかりづらいですが、各種画面のルーティング(URL)を設定しているファイルです。

path: '/',で設定しているところからわかる通り、初期表示ではダッシュボードコンポーネントが表示されます。そして、ヒーローズリンクをクリック(/heroes)するとヒーローズコンポーネントが表示されるという感じです。

vue-routerの設定ファイル

ヒーロー詳細コンポーネントへの遷移パスで(/detail/:id)、ヒーローIDを使用した動的ルートマッチングを使用していますが、他フレームワークでもよく見るおなじみの書き方だと思うので、特に難しい部分はなかったかなという感じです。

Store

今回作成したアプリの状態管理ですが、Vuexをわざわざ入れるのもどうかな...と思ったので単純なstoreパターンを採用しました。

単一のStoreファイルで、FluxでいうところのStateもActionもStoreも担当する形式になっています。

各種コンポーネントで共有したいデータであるheroes配列とmessages配列をstateとして設定し、そのstateを追加・取得・更新・削除するメソッド(getHeroes, clearなど)を集約しています。

Store

状態管理とstoreパターンについてはVue.jsの公式サイトに説明があります。

ダッシュボードコンポーネント

f:id:l08084:20190223033345p:plain
ダッシュボードコンポーネント

ダッシュボードコンポーネントでは、storeのheroes配列から2 - 5番目のヒーローを取得してボードとして表示するということをやっていて、ボードをクリックするとクリックしたヒーローの詳細画面に遷移します。

ダッシュボードコンポーネント

ダッシュボードコンポーネントの子コンポーネントとして、ヒーロー検索コンポーネントを設定しています。

ヒーローズコンポーネント

f:id:l08084:20190223033510p:plain
ヒーローズコンポーネント

ヒーローズコンポーネントでは、storeで保持している全ヒーローの一覧表示と、ヒーローの追加と削除機能を持っています。ダッシュボードコンポーネントと同様にリストをクリックすると、該当するヒーローの詳細画面に遷移します。

ヒーローズコンポーネント

メッセージコンポーネント

f:id:l08084:20190223033945p:plain
メッセージコンポーネント

storeで保持しているメッセージ配列(messages[])を表示するコンポーネント、メッセージのクリア機能もあります。

メッセージコンポーネント

ヒーロー詳細コンポーネント

f:id:l08084:20190223033553p:plain
ヒーロー詳細コンポーネント

ダッシュボードコンポーネント、ヒーローズコンポーネント、ヒーロー検索コンポーネントから遷移できるヒーローの詳細を表示するコンポーネントになります。ヒーローの名前を変更して保存する機能もついています。

ヒーロー詳細コンポーネント

ヒーロー検索コンポーネント

f:id:l08084:20190223033709p:plain
ヒーロー検索コンポーネント

フォームに入力した文字列からヒーローを検索するコンポーネントとなります。検索結果のリンクをクリックするとヒーロー詳細コンポーネントに遷移します。

ヒーロー検索コンポーネント

_.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を含める必要がありません。

f:id:l08084:20190223052338p:plain
mock-heroes.jsonをHTTPリクエストで取得したい...

修正後のコード
    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)
        }
    }

直し方わからないので直してない....まあ動くからええか....わかる人教えてください

おわりに

最終的に削除したもののmixinaxiosを使ったりしたので、Vue.jsの基本的な機能はあらかた触れたような気がするのが作成してよかった点です。特にmixinにつまづいて、mixinは継承に近い概念なんですけどコンポーネント間でのデータの共有に利用しようとして失敗するなどしました(mixinで共通のデータをコンポーネントに持たせようといても、各コンポーネントごとに同じ名前の別のデータプロパティを持つことになり、データの値自体はコンポーネント間で共有されない)。

Angularと比較するとVue.jsは基礎ガイドのボリュームが圧倒的に少なかったので使い始めるコストは少なかったものの、フルスタックフレームワークであるAngularほどフレームワーク単体でのサポートが少ないと感じたので、個人的にはAngularとVue.jsのどちらが初心者向けかと聞かれると難しい問いになる気がしました...Vue.jsだとすぐにVuexなどの状態管理ライブラリがないと書くのが辛くなってしまう気がする...その点Angularは状態管理ライブラリがなくてもある程度は書けるし...