トップ 最新 追記

2018-04-19 [長年日記]

_ Nuxtでrouteに応じてVuex Storeをmodule分割する方法

まとめ

  • Nuxt は route に応じて自動で Vuex Store を module 分割できるが、あえて手動で registerModule することもできる
  • module 分割の際に大事なのは
    • namespaced 設定
    • state を function に
    • registerModule はすでに存在する module を上書きしてしまう(壊れる)
  • Store は SSR での扱いに注意が必要。不要なら beforeMount() 以降のタイミングで初期化することでサーバサイドでは無視されるようにすることができる。

Vuex Storeのmodule分割の基本

cf. モジュール · Vuex

基本的な Vuex Store の構造は

state: {
},
mutations: {
},
actions: {
}

となるが、これを以下のようにする。

{
  modules: {
    name: {
      state: {
      },
      mutations: {
      },
      actions: {
      }
    },
    name: {
      state: {
      },
      mutations: {
      },
      actions: {
      }
    }
  }
}

Nuxt.js では store/ 以下に page に対応する module 構造を置いてあげれば自動的に module として登録される。便利。

routeに同期させて同一構造のmoduleを手動でregisterする

手動 register

module 構造の Store を作る方法は上の構造の object をそのまま new Vuex.Store() に突っ込んでもよいのだが、動的に組むには store.registerModule(NAME, OBJECT) してあげるとよい。

Nuxt の場合は特に何の設定もしなくても各 component から this.$store で Vuex Store へアクセスできるので、

this.$store.registerModule(NAME, OBJECT)

になる。

同一構造の module を動的 register する際の注意点
  1. namespaced: true を設定
  2. state は function
  3. module を registerModule するタイミングで state が消える
  4. Vue の SSR が beforeCreate(), created() までしか Lifecycle Hook を利用できないので、beforeMount() 以降で初期化すると client side だけに集中できる

created() で registerModule 以外の $store 操作を行なっている場合は module 生成 1 については同一構造の state が複数あるとどれに対してアクセスしているのかを明示しつつそれぞれの影響範囲が閉じている必要がある。そのための namespaced.

2 については Vue component の data と同じ。インスタンスが複数登場する場合は plain object を使ってしまうと state がみんな同じになってしまう。

3 については分かってしまえばその通りなんだけど、route を手動で書かなくても自動的に設定してくれる Nuxt 環境では注意しないといけない点。

実際にどう対処するか、以下のコードをサンプルとして挙げておく。module name を route name と同一にすると仮定した場合はこのように意図せぬ Store の破壊を防ぎつつ構築していくとよい。

if ( typeof this.$store.state[this.$route.name] == 'undefined' ) {
  this.$store.registerModule(
    this.$route.name,
    {
      namespaced: true,
      state() {
        return {
        }
      },
      mutations: {
      },
      actions: {
      }
    }
  )
}

4 はしばらくハマった。Nuxt で Universal アプリを書くことにした場合、Vue の SSR の知識なしに始めると変なところでハマる可能性があるので注意が必要。

Singleton な Store を Universal アプリで利用するには root component が初期化される前に acyncData で行うのがスジらしい。

今回は Nuxt が最初から用意してくれている Page component に hook して registerModule しようという考えだったので、created() の後に registerModule することで SSR 時には無視されるようにした。

cf. データのプリフェッチと状態 · GitBook

※ なお、困ったことに vue-devtool では Store が破壊されたことは分からない。そこまで中の状態を細かく見ていないようだ。少なくとも v4.1.1 の段階では分からなかった。


2018-04-24 [長年日記]

_ Nuxtを利用したVueアプリのユニットテストに関するメモ

今回のゴール

Nuxt.js を利用した Universal な Vue + Vuex アプリのユニットテストをできるようになる。

Nuxt.js についてはユニットテストに関する情報が全然出てこないが、@vue/test-utils のドキュメントを順番に読んでいけば基本的には大丈夫。ただし追加の設定ファイルの作成などは必要。

以下はそのための断片的なメモ。一応完全に動いているサンプルリポジトリはできているので、細かいコードはそっちを参照のこと。

まとめのサンプルリポジトリ

wtnabe/tdd-bed-nuxt-mocha-webpack-power-assert: A minimum base for TDD practice with Node.js, Vue.js, Nuxt.js, mocha-webpack, and PowerAssert

材料

設定ファイルは追加が必要

  • Nuxt 自体は設定を隠蔽するがテスト支援はない
    • Vue.js としてのテストを行おうとする場合、テスト用の各種設定は別途用意する必要がある(ex, webpack.config.js など)
  • Nuxt 公式のサイトではテストについては E2E の記述しかないのでユニットテストについては別途考える必要あり
    • 理由は後述

テストランナーはこれまでの経験からmocha-webpackを選択

テストランナーについては

  • Vue.js 公式は Karma
  • Nuxt 公式は Ava with jsdom

が推されているが、これまで自分は mocha + power-assert の環境を作るようにしてきていたので、今回もこれをベースにすることにした。一回に挑戦は一つまでの法則。

テスト用のwebpack.config.jsを用意

Nuxt 自体は Webpack の設定は動的に生成してさらに nuxt.config.js の内容を加えて動作をするが、Nuxt の外からユニットテストを行う際にこの設定を引っ張ってくるのは難しい。そこで必要最低限の webpack.config.js を用意する。

今回は

test/webpack.config.js

を用意することにした。

aliasが必要

Nuxt ではディレクトリレイアウトにルールがあり、このルールに従ったアプリ開発の際に便利なように @, @@, ~, ~~ に alias が用意されている。

ディレクトリ構造 - Nuxt.js

この alias をテストの際に再現しておかないと component の import すらできないので以下のように設定を用意する。

..
  resolve: {
    alias: {
      '@': path.resolve(__dirname, '../'),
      '~': path.resolve(__dirname, '../')
    }
  },
..
.vueを解釈するloader設定を追加
 module: {
   rules: [
     {
       test: /\.vue$/,
       loader: 'vue-loader'
Babelを通ってないとES2016とか動かないのでそれも追加
{
  test: /\.js$/,
  loader: 'babel-loader'
}

.babelrc

{
  "presets": ["vue-app"]
}

が必要。

※ Nuxt の Babel の設定は vue-app になっている。これは vue + preset-env に相当する。

対応していない import は空 object になる

試しに yaml-loader なしに component の中で YAML を読み込む記述があった場合は

  • ファイルが見つからなければエラーになる
  • ファイルが見つかって対応している loader がない場合 {}
  • ファイルも対応している loader もある場合、通常通り

少なくとも vue-loader, babel-loader 以外を最初からすごく気にする必要はなさそう。

window objectを再現するためにjsdomをglobalに

window object がないと Vue component の renderering の際に正常に動作しないので以下のように必要なパッケージを追加

$ yarn add --dev jsdom jsdom-global

して、これを常に有効にするために以下のようにファイルを用意し、

test/setup.js

require('jsdom-global')()

テスト実行時に読み込む。

$ mocha-webpack --require test/setup.js

もうassertはpower-assertでいいです

global.assert = require('power-assert')

CoffeeScript と組み合わせていた時はどのタイミングで power-assert を解釈できるようにするかいろいろ工夫が必要だったけど、「全部 Babel 通せばええやん」の世界になったおかげか、単に require するだけで利用できるようになっていた。楽だ。

componentをtest-utilsでmountする

Vue アプリも実際に動作させるために mount を行うわけだけど、これを @vue/test-utils で再現する。

実は素の component をテストするだけなら別に直接叩いてもいいんだけど、以下の理由により必ず test-utils で mount した方がいいです。

  • reactive system や render した結果をテストしやすい
  • store など周辺の環境も再現できる

※ この際、本来の利用方法のまま、インスタンス化する前の options オブジェクトを mount する。new Vue() とかしちゃダメ。

test-utilsがなくてもtestableなコードを目指すとよさそう

test-utils 確かに shallow は便利。

また mount / shallow で setProps などで外からデータを与えることができるので、どのような状態もまず再現できる。ただし、option を object で直接組み立てていくのはちょっとイマイチな感じがする。stub out しにくいし、まるっと置き換わってしまうので、同じ component を組み立てているとは限らないし、ちょっと @vue/test-utils ありきすぎる感じがする。

考えたら単に import の処理などを computed などにしてメソッドの stub out するだけでもなんとかなる。この辺は test-utils そのものに詳しくなくても書けるような、標準的なというか一般的なというか、testable code を目指して書いていけばよいような気がする。

Vuex Storeは基本的にただのfunctionなのでTDDで始めやすい

mutation は引数に stateを受け取るので、最も素朴な Store の実装の場合、mutations しか定義できてなくても、Vuex Store を new しなくてもテストは可能である。

export const state = () => {
  return {
    ..
  }
}

export const mutations = {
  method(state) {
    ..
  }
}

こんな感じで個々の mutation には必ず state が渡ってくるので。

もちろんその場合は commit() は使えないし、結果の state を直接覗いてテストすることになる。

(個人的にはこの Vuex Store の構造は TDD でスタートするのにとても向いている気がする。この構造で自信を持てたと言っていい。)

あと気をつけるところは、

  • Vuex インスタンスを生成するには通常の利用と同じく Vue.use が必要
  • テストの安定性、独立性という意味では global な use でいいかどうか迷う
    • 迷う場合は createLocalVue から
ただし、gettersを引き回したりする場合にはVuexインスタンスが必要

getters や mutations には state しか渡ってこない。しかし mutation 内で getters を呼びたい場合もあると思う。その場合は

export const mutations = {
  method(state, {arg, getters}) {
    ..
  }
}

のようなコードが必要になるのだが、ここで getters はちゃんと Store インスタンス生成後の store.getters を渡すようにしてあげないと挙動が変わってしまう。(しばらく悩んだ)

SSRの本当のテストにはE2Eが必要(だけど未検証)

jsdom を前提にして component のテストと store のテストをするだけなら上のやり方で十分なんだけど、Nuxt は Universal アプリを作れるので、実はこれだけでは不十分な場合がある。

例えば生 DOM に依存する component を使っていたり、非同期処理が component mount 前に済んでいなかったりすると SSR には失敗するのだが、これは component 単体のテストではガードできない。

しかしこれはこのエントリの範囲外なので各位頑張れ。


2018-04-26 [長年日記]

_ JavaScriptでRubyのArray#replaceのようなことをしたい

どういうことかというと、新しい Array を生成するのではなく同一オブジェクトのまま Array を破壊的に変更したいということ。

Rubyの場合

a = [1, 2, 3]
b = a.map {|e| e * 2}
a.replace(b)

# => [2, 4, 6]

と Array#replace を利用することで既存の Array を新しい Array で置き換えてしまうことができる。オブジェクトとしては同じものを指したまま内容を書き換えてしまうことができるわけだ。

JavaScriptの場合

古い知識のままだと結構面倒くさいんだけど、

スプレッド構文 - JavaScript | MDN

を使うと簡単にできる。

a = [1, 2, 3]
b = a.map(e => e * 2)
a.splice(0, b.length, ...a)

# => [2, 4, 6]

なるほどな。

Tags: JavaScript