Vue.js + Vuex + axios

某案件で Vue.js + Vuex + axios を採用しました。備忘録としてまとめます。

前提

それぞれのバージョンは以下の通り。

"dependencies": {
  "axios": "^0.17.1",
  "es6-promise": "^4.1.1",
  "vue": "^2.5.2",
  "vue-router": "^3.0.1",
  "vuex": "^3.0.1"
}

開発には Visual Studio Code を利用。以下のプラグインを入れると便利。

  • HTML Snippets
  • vetur
  • VueHelper

vue-cli

vue-cli の webpack テンプレートを利用。当初、イチからプロジェクトを準備しようと思っていたが、vue-cli で作成した方がいろいろと確実と思われたので。ハマりどころも多いかもしれないが...。

webpack / source maps

source maps については、webpack の 公式ドキュメント と以下が参考になりました。

t-hiroyoshi.github.io

devtool の指定によってマッピングファイルの生成有無や中身、js ファイルのサイズが変わってくる模様。試したところ以下のような感じ。

デバッグ△は変数名が minify される程度、js サイズ△は kByte 程度。(分かり辛い...)

devtool デバッグ js サイズ source maps
source-map あり
eval-source-map なし
inline-source-map なし
cheap-eval-source-map なし
eval なし
minify なし

結局、開発環境向けには eval-source-map を指定、プロダクション環境向けには source-map を指定。

その他、webpack のちょっとした小細工は以下を参照。

vue-cli の webpack テンプレートでテスト環境向けにビルドしたい - kntmr-blog

プロジェクト構成

├─build
├─config         ... 設定ファイル
├─dist
├─node_modules
├─src            ... エントリーポイントの js を配置
│  ├─api         ... API アクセス
│  ├─components  ... 共通コンポーネント / 基底コンポーネント
│  ├─pages       ... ページコンポーネント
│  ├─router      ... ルーティング (今回は利用しない予定)
│  └─store
│      └─modules ... 状態管理
├─static
└─test           ... テストコード

このあたりは Vuex の examples などを参考にしました。shopping-cart とか。

github.com

ページコンポーネントはアプリケーション内で単一となるコンポーネント。共通コンポーネントや基底コンポーネントはアプリケーション内で使い回されるコンポーネント。共通コンポーネントは複数の基底コンポーネントで構成される。

共通コンポーネントや基底コンポーネントに id 属性を付けると、ページ内で id が重複する可能性があるため注意。ページコンポーネントには id 属性、共通コンポーネントと基底コンポーネントには class 属性を指定するといいかもしれない。

スタイルガイド

Vue.js の スタイルガイド は必読。

store の namespace

複数の store に分割するときは、namespace を指定してモジュールを管理する。

モジュール - Vuex

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}

モジュール間でデータを受け渡す場合は dispatch する。(この方法がベストかどうかはちょっとあやしい...)

API

axios に依存するコードをまとめて抽象化する。ここには業務的なロジックは実装しない。ここで export した関数を store の actions から呼び出す。コンポーネント内には API 呼び出しの処理は書かない。

gist.github.com

mixins / filters

mixins は Vue コンポーネントの実装を共通化する用途で使う。今回はローディングやエラー処理など、各画面共通で使う computedmethods の実装を mixins にまとめた。filters は日付やテキストをフォーマットする用途で使う。

反省点など

コンポーネント命名規則がイマイチ

このコンポーネントがどういう役割を持っているのか名前から判断できるとよい。そのためには単一責務のコンポーネントとして切り出すようにするのが理想と思われる。

再掲: スタイルガイド

actions / mutations / state

今回、1つの action の中では基本的に1つの mutation を呼び出すような形で mutations を書いたが、ある action の中でどの state が書き換わるのか分かり辛い感じになってしまった。ある程度、state の粒度に合わせて mutations を書いた方がいいのかもしれない。

APIコンポーネントの分離

API の項にも書いたが、API の変更をコンポーネント側に波及させたくなかったので、コンポーネント内に API 呼び出しの処理は書かないことにした。が、結局、API のレスポンスオブジェクトがコンポーネントのあちこちで参照されていたため、このあたりはきれいに分離できなかった。

APIコンポーネントを分離するために、store 内で変換するような仕組みを持たせてもよかったかもしれない。ただ、store の責務がどんどん膨らんでしまうのが懸念。API と store の間にもう1レイヤあるとよかったのだろうか。

デザインとコンポーネントの設計

当たり前だが、デザインとコンポーネントは密接に関係する。きれいなコンポーネント設計にするためには、当然、それを意識したデザインを作らないといけない。

今回、開発体制的な理由で基本的にデザインはこちらでコントロールできるものではなかったため、一部のコンポーネントに重複や無駄が入り込んでしまった。が、これはしょうがないのかもしれない。

IE 対応

babel や es6-promise は使っていたものの、Edge では動くが IE10 では動かないということがあった。が、npm update してパッケージをアップデートしたら動くようになった...。詳しくは追い切れていないが、babel あたりの更新が要因か。

割と短期間で挙動が変わるような更新があるのは、ちょっと腑に落ちないところがある。このあたりもきちんと追えるようにしたい。

テストコード

今回、Avoriaz や Sinon.JS でコンポーネントのテストコードを作成する予定だったが、メンバーの教育に時間がかかりそうだったため諦めることにした。(その代わり社内で使われている画面系のテスト仕様書でテストした)

まとめ

Vue.js の特長として、シンプルな構成で、かつ拡張性が高いということが挙げられる。また、ミニマムにスタートできるメリットがある。ただ、この ミニマム にフォーカスされるためか、世間からは「小規模向け」というイメージを持たれているように思える。(主観ですが...)

決してそんなことはなく、小規模なアプリケーションはもちろん、大規模なアプリケーションにも十分適用できると思われる。

vue-cli の webpack テンプレートでテスト環境向けにビルドしたい

やりたいこととしては、dev.env.jsprod.env.js 以外に、テスト環境向けの config が欲しい。ちなみに、ここで言う「テスト環境」とは、ローカル以上、プロダクション未満のような環境を指します。

で、stg.env.js のような config を新しく用意しようかと思ったのですが、webpack.prod.conf.js を見ると、普通に webpack を使ってる分には test.env.js が使われてなさそう...?

というわけで、次の方法でビルドできるようにしてみました。

前提

  • vue-cli@2.9.1

コマンド

ローカル環境向け (dev.env.js)

> npm run dev

テスト環境向け (test.env.js)

> npm run build

プロダクション環境向け (prod.env.js)

> npm run build -- production

変更点

build/build.js

const targetEnv = process.argv[2] === 'production' ? 'production' : 'testing'
process.env.NODE_ENV = targetEnv

npm run build-- に続けてパラメータを渡すと、process.argv で受け取ることができる。

あとは、コンソールに表示するスピナーとか完了メッセージをいい感じにする。

...
// スピナー
const spinner = ora(`building for ${targetEnv}...`)
...
// 完了メッセージ
console.log(chalk.cyan(`  Build complete. (build for ${targetEnv})\n`))

現場からは以上です。

アロー関数で空のオブジェクト返そうとしてハマる

普通に考えれば当たり前なんですが、最初なにが起きてるのか分からなくてハマった...。

const func = () => {}
func() // => undefined

つまり、{} がオブジェクトのリテラルではなく、関数のブロックとして認識されている模様。で、return 文がないので undefined が返る。

こうする。

const func = () => { return {} }
func() // => {}

もしくはこう。

const func = () => new Object()
func() // => {}

あまり需要はないかもしれないですが。

その他

どうやら ES6 より ES2015 が推奨っぽいので、今日から ES2015 と呼ぶことにしよう。

追記 (2018/01/11)

括弧で囲めば式として扱われる模様。

const func = () => ({})
func() // => {}

2017年のふりかえりと2018年のこと

賀正。

2017年の行動指針 - kntmr-blog

2017年の初めにこんなエントリを書いていたので、これのふりかえりと2018年のことでも書いてみようかと。

『基本的に毎日勉強することが目標。』

これはさすがに無理でしたね。全然だめでした。圧倒的にだめでした。就寝前に勉強しようと思ってもついつい YouTube とか観ちゃうんですよね。まぁ、それがストレス発散になってるところもあるんですけど。

『平日の通勤時間を利用して英語を勉強する。』

一時期、mikan という英単語アプリで勉強してたんですが、1ヶ月程度しか続けられず...。2018年はもっとちゃんと頑張りたい。

Oracle Certified Java Programmer, Gold SE 8 認定資格』

取得しました。2017年はコードレビューする機会が多かったんですが、資格勉強の成果か、以前よりコードを読む力が向上したように感じます。たぶん。

Oracle Certified Java Programmer, Gold SE 8 認定資格 - kntmr-blog

JavaScript フレームワーク

2017年末頃から Vue.js を実案件で使っています。Vue + Vuex + axios の組み合わせ。最近は avoriaz を頑張っています。ついでに ES6 も学べていいですね。ところで ES6 と ES2015 はどっちで呼べばいいの。

というわけで、基本的に何か目標がないとだめだめマンみたいなので、今年は英語勉強の目標として TOEIC で600点あたりを目指してみようかな。あと、どこかで Java 9 と Spring 5 は腰を据えて学ばなければ。現場からは以上です。

Avoriaz で filters や methods の中身を差し替える

前回の続編。用途があるか不明ですが、こんな感じでできそうという話。

kntmr.hatenablog.com

filters を差し替える

例えば、こういうコンポーネントがあって、期待する値が出力されるかテストするパターン。ここでは、グローバルフィルターが設定されているものとする。

<template>
  <div>
    <span>{{ username | toUpper }}</span>
  </div>
</template>

<script>
export default {
  name: 'AvoriazTest',
  props: [ 'username' ]
}
</script>

この場合は、単純に Vue.filters で差し替える。というか、普通にテストを実行するとグローバルフィルターは参照不可のようで、何かしらのスタブは用意する必要がある模様...。ちなみにフィルター自体のテストは別途する前提。

import Vue from 'vue'
import { shallow } from 'avoriaz'
import AvoriazTest from '@/components/AvoriazTest'

Vue.filter('toUpper', (v) => 'TEST') // あらかじめフィルターにスタブをセットしておいて

describe('AvoriazTest.vue', () => {
  it('should render correct contents', () => {
    const wrapper = shallow(AvoriazTest, { propsData: { username: 'abc' } })
    expect(wrapper.find('span')[0].text()).to.equal('TEST') // 期待値が出力されることを確認する
  })
})
methods のメソッドを差し替える

例えば、レンダリング時に data で値を返す前に methods に食わせるパターン。

<template>
  <div>
    <span>{{ changedText }}</span>
  </div>
</template>

<script>
export default {
  name: 'AvoriazTest',
  props: [ 'username' ],
  data () {
    return {
      changedText: this.toUpper(this.username)
    }
  },
  methods: {
    toUpper (v) {
      return v.toUpperCase()
    }
  }
}
</script>

最初、前回のような wrapper.setMethods({ 'toUpper': stub }) でできるかと思ったんですが、これでは差し替えられず...。おそらくレンダリング時にはすでに data の内容が計算済みのためと思われる。

というわけで、今回は shallow または mount でラッパーオブジェクトを生成する際に propsData と同じように methods を指定してスタブを差し替える。

import { shallow } from 'avoriaz'
import AvoriazTest from '@/components/AvoriazTest'

describe('AvoriazTest.vue', () => {
  it('should render correct contents', () => {
    const stub = (v) => 'TEST' // スタブを用意して
    const wrapper = shallow(AvoriazTest, { propsData: { username: 'abc' }, methods: { toUpper: stub } }) // ここで差し替える
    expect(wrapper.find('span')[0].text()).to.equal('TEST')
  })
})

冒頭に書いた通り、これらの用途があるのか分かりませんが、いろいろいじくりまわしていると勉強になりますね。

Avoriaz と Sinon.JS で methods のメソッドが呼ばれたことをテストする

このあたりを調べたときのメモ。

  • avoriaz@6.3.0
  • sinon@4.0.0

例えば、こういうコンポーネントがあって、ボタンクリック時のイベントハンドラmethods のメソッドにバインドしているものとする。

<template>
  <div>
    <button @click="onClick">Submit</button>
  </div>
</template>

<script>
export default {
  name: 'AvoriazTest',
  methods: {
    onClick () {
      // ...
    }
  }
}
</script>

で、以下の方法でボタンクリックでメソッドが呼ばれたことをテストする。

import { shallow } from 'avoriaz'
import sinon from 'sinon'
import AvoriazTest from '@/components/AvoriazTest'

describe('AvoriazTest.vue', () => {
  it('calls methods when the button clicked', () => {
    const wrapper = shallow(AvoriazTest)

    const stub = sinon.stub() // スタブを作って
    wrapper.setMethods({ 'onClick': stub }) // setMethods で差し替えて

    wrapper.find('button')[0].trigger('click')
    expect(stub.calledOnce).to.equal(true) // 呼ばれたことを確認する
  })
})
その他

using with vuex · Avoriaz

Vuex の mapActionsmapGetters を使っていて、それらのメソッドが呼ばれたことをテストする方法は Avoriaz の公式ドキュメントに書いてあります。参考になり過ぎる。

コンポーネントの props で required を付けるときは default を定義した方がよさそう

小ネタですみませんが、Vue.js #4 Advent Calendar 2017 の15日目が空いてたので書きます。最近、Vue.js を実案件で使い始めました。Vue.js 歴は浅いです。

タイトルの通りなんですが、誤りや本来はこうあるべきというのがあればご指摘いただけますと幸いです。


Vue.js のスタイルガイドの「プロパティの定義」というセクションに「プロパティの定義はできる限り詳細とするべきです。」と書かれています。

プロパティの定義 - Vue.js

というわけで、

props: [ 'count' ]

と書いていたところをスタイルガイドの例に倣って次のように変更してみました。

props: {
  count: {
    type: Number,
    required: true
  }
}

が、コンソールに警告が出るようになりました。

[Vue warn]: Invalid prop: type check failed for prop "count". Expected Number, got Undefined.

で、このメッセージでググってみたら、この issue を発見。

github.com

この issue 自体は単純な定義ミスが原因のようなんですが、なんとなく 初期値をセットした方がよさそう なのかなと思い付きました。というわけで、次のように default を定義してみたところ、先述の警告は出なくなりました。

props: {
  count: {
    type: Number,
    required: true,
    default: 0 // これを追加
  }
}
補足

ちなみに、required を外すと default が未定義でも先述の警告は出ないようです。

なんとなく、requireddefault は一緒に定義するものなのかな?と思ったんですが、ドキュメントを探しても特にこれといったものは見つからず...。もしくは、コンポーネントの呼び出し方が原因なんだろうか。

このあたりは仕組みをきちんと理解しないとだめですね...。中途半端ですみませんが、現場からは以上です。