L08084のブログ

技術記事の執筆は祈り

【Angular】NgRx入門

はじめに

AngularでFluxを実現するために採用されるライブラリについては、NgRxがデファクトスタンダードになりつつあるのではないか.....という話を結構前に聞いたのでNgRxの学習をそろそろ始めることにする。

NgRx以外のFluxライブラリを使ったことは何度かあるので、Fluxの概念については理解しているつもり

参考サイト

ngrx.io

バージョン情報

  • angular@7.3.6
  • ngrx/store@7.3.0

環境構築

Angular CLIを利用して、Angularプロジェクトを作成した後に@ngrx/storeライブラリをインストールする。

https://ngrx.io/guide/store/install

Angular CLIのバージョンが6以上の場合は、npmやyarnで@ngrx/storeライブラリをインストールする以外にも、ng addコマンドを使用して(Angular CLI経由で)@ngrx/storeライブラリをインストールすることができる。

$ ng add @ngrx/store

ng addコマンドで@ngrx/storeライブラリをインストールすると、app.module.tsの設定と、src/app/reducers/index.tsの新規作成まで自動でやってくれる。

ただ、今回は自分で設定をしたかったので、npm i @ngrx/storeでインストールを実行した。

開発

f:id:l08084:20190319220128p:plain
完成したカウンターアプリ
公式サイトで@ngrx/storeのチュートリアルアプリとして用意されているカウンターアプリを作成したので、解説する。(なお、公式サイトのものとはディレクトリ構成が異なっている)。

ディレクトリ構成

作成したカウンターアプリのディレクトリ構成を$ tree -I node_modulesコマンドで出力した(一部ファイルは省略している)。

├── src
│   ├── app
│   │   ├── app.component.html
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   └── components
│   │       └── my-counter
│   │           ├── action
│   │           │   └── counter.actions.ts
│   │           ├── reducer
│   │           │   └── counter.reducer.ts
│   │           └── view
│   │               ├── my-counter.component.html
│   │               └── my-counter.component.ts
└── ・・・

コード

まず、FluxのActionを担当するファイルであるcounter.actions.tsから解説する。今仕事で使っているFluxライブラリ(自社製)とはすでに大きく違っている。

カウンターアプリのイベントである"加算"、"減算"、"リセット"をそれぞれアクションとして定義している。

今の仕事で使っているFluxライブラリだと、アクションの定義だけでなくWebAPIの呼び出しや、実際にStateの値を変更するStoraクラスの呼び出しなど(ActionをStoreに渡す)をやっていたため、結構な違いを感じてとまどう。

カウンターアプリのアクションクラス

続いてReducerを担当しているcounter.reducer.tsについて。今仕事で使っているFluxライブラリだとStoreでStateの変更をおこなっているが、NgRxでは渡されたアクションに応じてReducerが新しいStateを返す形式になっているらしい。

export const initialState = 0;の部分でStateの初期化を行っている。

カウンターアプリのReducer

ルートモジュール設定ファイルであるapp.module.tsについて。NgRxに関連している設定として、imports[]でReducerの登録を行っている(StoreModule.forRoot({ count: counterReducer }))のと、Redux Dev Toolの設定をしている(StoreDevtoolsModule.instrument({}),)。

モジュールファイル

カウンターコンポーネントのテンプレートファイル。カウンター値の表示と、加算、減算、リセットボタンを表示を担当している。

stateの値であるcount$Observable型なのでasyncパイプを使用して値の表示を行っている。

カウンターコンポーネントのテンプレートファイル

selectstateの値の読み込みと、加算、減算、リセットのアクションをディスパッチして、Reducerを呼び出すメソッドをそれぞれ定義している。

カウンターコンポーネントのコンポーネントクラス

ルートコンポーネントでは、カウンターコンポーネントを定義しているだけ

ルートコンポーネントのテンプレートファイル

おわりに

少し触ってみた感想としては、NgRxはReduxよりのFluxライブラリなのでは...という印象を受けた。Redux使ったことないので習得に時間がかかりそう

【Python3】特徴的だと思った機能まとめ その4

引き続き、『入門 Python 3』を読んでいて印象に残った箇所をメモに残していく

前回の記事

【Python3】特徴的だと思った機能まとめ その3 - L08084のブログ

バージョン情報

  • Python 3.7.2

出典元

https://www.oreilly.co.jp/books/9784873117386/

オライリーの『入門 Python 3』

学習メモ

\による長い行の分割

バックスラッシュ(\)で行を分割することができる。

>>> age = 15

# 三項演算子の行を分割して読みやすくしている
>>> name = 'みゃー姉' \
... if age >= 18 \
... else 'アカネ'

>>> name
'アカネ'

内包表記

内包表記とは、イテレータ*1からPythonのデータ構造をコンパクトな構文で作成できる機能を指す。

内包表記で作成できるデータ構造として、リスト、辞書、集合などがあり、それぞれリスト内包表記、辞書内包表記、集合内包表記といった感じに命名されている。

リスト内包表記

リストをシンプルな構文で作成できる。リスト内包表記の文法は下記の通りとなる。

  • [expression for item in iterable]
    • リスト内包表記の最も単純な形式
  • [expression for item in iterable if condition]
    • 条件を追加した形式
# 単純な形式のリスト内包表記
>>> numList = [num for num in range(1, 7)]
>>> numList
[1, 2, 3, 4, 5, 6]

# 0 ~ 7の値を取り出して二乗した後、リスト化している
>>> squaringList = [i * i for i in range(0, 8)]
>>> squaringList
[0, 1, 4, 9, 16, 25, 36, 49]

# 条件を追加したリスト内包表記
>>> evenList = [j for j in range(0, 8) if j % 2 == 0]
>>> evenList 
[0, 2, 4, 6]
辞書内包表記

ディクショナリも内包表記を使用してワンライナーで作成することができる。{key_item : value_item for item in iterable}

# リストを定義
>>> marshalList = ['ランヌ', 'ミュラ', 'ダヴー', 'ベルティエ', 'ネイ']

# 内包表記でdictを作成
>>> marshal_dict = {i : marshalList[i] for i in range(0, 5)}
>>> marshal_dict 
{0: 'ランヌ', 1: 'ミュラ', 2: 'ダヴー', 3: 'ベルティエ', 4: 'ネイ'}

*1:要素を順番に取り出すことのできるオブジェクトのこと。リスト(配列)、辞書、集合、タプルなど

【Python3】特徴的だと思った機能まとめ その3

引き続き、面白いな〜と思った機能をまとめていく(2)

前回の記事

【Python3】特徴的だと思った機能まとめ その2 - L08084のブログ

バージョン情報

  • Python 3.7.2

出典元

https://www.oreilly.co.jp/books/9784873117386/

オライリーの「入門 Python 3」

学習メモ

del文によるリストや辞書の削除

del文を使用することで、指定したオフセットの要素または、配列(リスト)や辞書そのものを削除することができます。

>>> soulBorne = ['DARK SOULS', 'DARK SOULS II', 'DARK SOULS III', 'Bloodborne'] # リストを定義

>>> del soulBorne[1] # リストの2番目の要素を削除

>>> soulBorne # 要素が削除されていることを確認
['DARK SOULS', 'DARK SOULS III', 'Bloodborne']

>>> del soulBorne # 配列soulBorneごと削除

>>> soulBorne #削除されていることを確認
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'soulBorne' is not defined

あんまり使わなそうな機能な気がする。。Pythonの経験0なのでわかりませんが...

inを使った値の有無のテスト

リストや文字列、辞書や集合などに指定した値が含まれているかを確認する

>>> fromsoftware = ['DARK SOULS', 'DARK SOULS II', 'DARK SOULS III', 'Bloodborne', 'SEKIRO'] # リストを定義

>>> 'SEKIRO' in fromsoftware # 'SEKIRO'はリストに含まれている
True
>>> 'splatoon' in fromsoftware # 'splatoon'はリストに含まれていない
False

>>> 'hedoro' in 'Dorohedoro' # リストだけではなく、文字列にも使える
True
>>> 'hayashida' in 'Dorohedoro'
False

タプル

TypeScriptにもタプル型はあり、関数から複数の値を返す時などに使用することができます。
Pythonのタプルについても同様に一度に複数の変数に代入するなどの使い方もできますが、イミュータブルで値の追加・削除・変更ができない、定数リストとして使われることの方が多いそうです。

# ()を使って空のタプルを作成することができる
>>> empty_tuple = ()
>>> empty_tuple
()

# 2個の値を持ったタプルの作成
>>> brother = ('虎杖', '藤堂')
>>> brother
('虎杖', '藤堂')

# ()を省略してもカンマがあればタプルを作成できる
>>> freshman = '虎杖', '伏黒', '釘崎' 
>>> freshman
('虎杖', '伏黒', '釘崎')

# 一度に複数の変数に値を代入することができる(タプルのアンパックと呼ばれる)
>>> omimura, mikuriya = 'hinata', 'hikage'
>>> omimura
'hinata'
>>> mikuriya
'hikage'

次回の記事

【Python3】特徴的だと思った機能まとめ その4 - L08084のブログ

【Python3】特徴的だと思った機能まとめ その2

引き続き、面白いな〜と思った機能をまとめていく

前回の記事

【Python3】特徴的だと思った機能まとめ その1 - L08084のブログ

バージョン情報

  • Python 3.7.2

出典元

https://www.oreilly.co.jp/books/9784873117386/

オライリーの「入門 Python 3」

学習メモ

「入門 Python 3」を読んでいて、印象に残った関数・機能をまとめています。

文字列操作関数の種類が豊富(isalnum(), title(), capitalize(), center(), ljust(), rjust())

文字列をあれこれする関数が多い。正直、これいる?みたいなのもある。

文字列が英数字のみか判定するisalnum()

文字列が英数字のみだと、Trueを返す関数

>>> old_commic = 'koroshiya1'
>>> yondenai = 'ホムンクルス1巻'
>>> new_commic = 'HIKARI-MAN'

>>> old_commic.isalnum() # 英数字しか含まれていないため、True
True
>>> yondenai.isalnum() # 英数字以外が含まれているのにTrue(えっ????)
True
>>> new_commic.isalnum() # 英数字以外(記号)が含まれているため、False
False

実際にサンプルコードを書いてみて初めて気づいたんですが、全角文字はTrueと判定されるらしい。まじで何の役に立つんだ??英語圏の人間が憎い...

>>> 'アイウエオ'.isalnum() # 全角文字はTrueと判定される
True
文字列の単語の頭文字を大文字にするtitle()

一生使わない気がする

>>> 'fight club'.title() # 英字単語の頭文字が大文字になる
'Fight Club'
先頭の一文字を大文字にするcapitalize()

capitalize には「〜を大文字で書く(始める)」という意味があるそうな

>>> 'django unchained'.capitalize() # 先頭の一文字を大文字にする
'Django unchained'
文字列の左寄せ、中央寄せ、右寄せ(ljust(), center(), rjust())

ljust(), center(), rjust()は、空白を追加することによって、指定した文字列を左寄せ、中央寄せ、右寄せしてくれる関数です。
第一引数に生成する文字列の長さを指定してあげる必要があります。

>>> 'serpent'.center(15) # 中央寄せ
'    serpent    '
>>> 'serpent'.ljust(15) # 左寄せ
'serpent        '
>>> 'serpent'.rjust(15) # 右寄せ
'        serpent'

三項演算子の文法が独特

JavaとかJavaScriptでもおなじみの条件(三項)演算子ですが、Pythonだと文法がだいぶ異なります。

  • JavaScriptの場合

JavaScriptの条件演算子の文法は次のようになっています。

[条件] ? [真の場合に返される値] : [偽の場合に返される値]

// 年齢が20才以上だったら、statusに'adult'の文字列が代入される
const status = age >= 20 ? 'adult' : 'child';
  • Pythonの場合

Pythonの場合はこうです。

[真の場合に返される値] if [条件] else [偽の場合に返される値]

status = 'adult' if age >= 20 else 'child'

Pythonの三項演算子、JavaScriptのそれと比べると書き方がちょっとくどいなって感じたんですが、文法が冗長なぶん読みやすいので、好みの問題かもしれないです。

以上

次回の記事

【Python3】特徴的だと思った機能まとめ その3 - L08084のブログ

【Python3】特徴的だと思った機能まとめ その1

はじめに

仕事では主にTypeScript(JavaScript)とJavaしか使用していないんですが、使える言語の幅を広げたいな〜という思いからPythonの勉強を始めました。

この記事は、Pythonを学んでいる中でなじみが薄いな〜とか今までに経験した言語(少ないですが)にはなかった機能だな!と感じたものをまとめることによって、Pythonの特徴を理解し、実施中のPython学習の成果を高めようというものです。

バージョン情報

  • Python 3.7.2

出典元

https://www.oreilly.co.jp/books/9784873117386/

オライリーの「入門 Python 3」(ISBN978-4-87311-738-6)

学習メモ

JavaとかTypeScript(JavaScript)になくて、筆者にとってなじみが薄いと思ったPythonの機能をまとめています(筆者の経験が浅いために、普通にJava/TypeScriptにある機能が含まれている可能性もありますがご容赦ください)。

算術演算子(//)による切り捨て除算

サンプルコードはREPLで実行しています。おなじみの/演算子による割り算や%演算子による割り算の余りの計算だけでなく、Pythonだと演算子(//)で割り算の結果を整数(小数点以下切り捨て)で出力することができます(だからなんだという感じではある)

>>> 13 / 5 # 除算
2.6

>>> 13 % 5 # 除算の余り
3

>>> 13 // 5 # 切り捨て除算
2

intの制限の最大値が任意の数

int型(Python3の整数はint型のみです)の最大値が設定されておらずメモリが許す限り設定できるとのこと(なぜ...?)

スライス([start:end:step])による部分文字列の取得

スライスとは、文字列から部分文字列を取り出すことができる機能です。
先頭オフセット*1(start)、末尾オフセット(end)、ステップ(step)で定義されています。なお、各種値は省略可能です。

>>> japanese = 'あいうえおかきくけこさしすせそ' # 文字列を作成

>>> len(japanese) # 作成した文字列の長さをチェックする
15

>>> japanese[:] # [:]は、先頭から末尾までの文字列を抽出する
'あいうえおかきくけこさしすせそ'

>>> japanese[5:] # 先頭6文字目から末尾まで
'かきくけこさしすせそ'

>>> japanese[5:8] # 先頭6文字目から8文字目まで
'かきく'

>>> japanese[-3:] # 最後の3文字を取り出す
'すせそ'

>>> japanese[::2] # 先頭から末尾までを2文字ごとに取り出す
'あうおきけさすそ'

>>> japanese[1:10:2] # 2文字目から10文字目までを2文字ごとに取り出す
'いえかくこ'

次回の記事

【Python3】特徴的だと思った機能まとめ その2 - L08084のブログ

*1:基準点からの距離

【Docker入門】とりあえずDockerでWebサーバ(Nginx)を動かしてみる

はじめに

職場の開発環境構築を自動化したいのでDockerの勉強を始めました。今回はDocker for MacのインストールからDockerを使用したNginxの起動までをやります。

バージョン情報

  • macOS High Sierra:@10.13.6
  • docker@18.09.2

インストール

Install Docker Desktop for Mac | Docker Documentation

まず、上記URLからDocker for Macをインストールします。

Nginxの起動と停止

続いてHello world的な目的で、インストールしたDockerからWebサーバーのNginxを起動してみましょう。

https://hub.docker.com/_/nginx

f:id:l08084:20190303155019p:plain
Docker HubのNginxのページ

Docker HubのNginx公式Dockerイメージ画面に遷移して、「Copy and paste to pull this image」の項目に記載されているコマンド(docker pull nginx)をコピーします

f:id:l08084:20190303155153p:plain
"Docker Desktop is running"

メニューバーのDockerアイコンをクリックしてDockerが起動状態になっているのを確認した後、ターミナルを開いてDocker Hubからコピーしたコマンドを実行します

$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
6ae821421a7d: Pull complete 
58702d4af197: Pull complete 
b165f42e8fd4: Pull complete 
Digest: sha256:18c0755594af107923baa2e65fcef35aea4ab0cea7862d19c27aa127bacb458e
Status: Downloaded newer image for nginx:latest

docker image lsコマンドでNginxのDockerイメージのダウンロードに成功したことを確認する

$ docker image ls nginx
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              8c9ca4d17702        3 days ago          109MB

コンテナを作成するdocker container runコマンドを実行して、Nginxを起動します。下記のコマンドでは80番ポートを使用してDockerイメージの「nginx」から「tutorialserver」という名前のコンテナを起動しています

MacBook-Pro-2:~ takuya$ docker container run --name tutorialserver -d -p 80:80 nginx

54569771cfef520e2cf74b3b818d5177f6fbdc74bc6a2407adfd0656a79d89b7
docker: Error response from daemon: driver failed programming external connectivity on endpoint  tutorialserver (994db4b2efc0aac1059feb64515cfe2ae346db83c7f85dbe04d2ad8cf784fa13): Error starting userland proxy: Bind for 0.0.0.0:80: unexpected error (Failure EADDRINUSE).

エラーが出力されてdocker container runコマンドが失敗しました。80番ポートをすでに占有しているアプリケーションがないか確認します。

$ sudo lsof -i -P | grep "LISTEN"
Password:
httpd       91           root    4u  IPv6 0x5f4e4fd2058c6d59      0t0  TCP *:80 (LISTEN)
xartstora  209           root    3u  IPv4 0x5f4e4fd2058faa01      0t0  TCP *:61500 (LISTEN)
xartstora  209           root    4u  IPv6 0x5f4e4fd2058c7319      0t0  TCP *:61500 (LISTEN)
httpd     2214           _www    4u  IPv6 0x5f4e4fd2058c6d59      0t0  TCP *:80 (LISTEN)
httpd     2220           _www    4u  IPv6 0x5f4e4fd2058c6d59      0t0  TCP *:80 (LISTEN)
httpd     2221           _www    4u  IPv6 0x5f4e4fd2058c6d59      0t0  TCP *:80 (LISTEN)

上記コマンド(sudo lsof -i -P | grep "LISTEN")の結果、80番ポートを占有しているアプリケーションがApache httpdであることがわかります。

Nginx起動のために、Apacheの方は停止してしまいましょう。

$ sudo apachectl stop

Apacheを停止して80番ポートの競合を解決したところで再度、docker container runコマンドを実行します。

$ docker container run --name tutorialserver -d -p 80:80 nginx
docker: Error response from daemon: Conflict. The container name "/tutorialserver" is already in use by container "f93a6126da0db6d74fdf8b812fa7814ef02ad691acdb46e262abbdbb53a6b240". You have to remove (or rename) that container to be able to reuse that name

またエラーが発生しました。エラーメッセージからコンテナの名前(tutorialserver)が重複しているのが原因であることがわかります。

邪魔なコンテナを削除するか、作成するコンテナ名を変更しましょう。

コンテナ名をtutorialserver2に変更して再度コンテナの作成を実行します。

$ docker container run --name tutorialserver2 -d -p 80:80 nginx
786a573afe09b43fd880e9f1fc8153b646ac254f5aeed9d974918e9680fafcbb

今度こそ成功しました。http://localhost:80/をwebブラウザを開いてNginxサーバーが稼働していることを確認しましょう。

<figure class="figure-image figure-image-fotolife" title="Nginxの起動が成功している時の画面">[f:id:l08084:20190303172133p:plain]<figcaption>Nginxの起動が成功している時の画面</figcaption></figure>

起動したコンテナを停止したいときは、docker stop [コンテナ名]コマンドを使用します。

$ docker stop tutorialserver2 
tutorialserver2

http://localhost:80/を再びwebブラウザで開くと、コンテナ「tutorialserver2」が停止して、Nginxに繋がらなくなっていることがわかります。

f:id:l08084:20190303172723p:plain
コンテナを停止した時の画像

参考記事

Dockerで80番へポートマッピングした際に起きたエラーについて | Hodalog

【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は状態管理ライブラリがなくてもある程度は書けるし...