Nuxt.jsアプリケーションに外部パッケージのVuex StoreをModuleとして登録する方法

いろいろな事情があって、Nuxt.js アプリケーションからデカ目の機能を切り出して Vuex Store と Component を独立させたパッケージにすることになった。 要件はこんな感じ:

  • 外部 module は @@/packages/<package名>/... 以下に配置
  • Vuex Store は Module mode

@/store/export * from ... する js ファイル置けば終わりでしょ?とか思ってたけど、ダメ。一捻り必要だった。

Modules mode

Nuxt.js の Vuex Store は store フォルダ内にある js ファイルごとに自動で 名前空間の独立したVuex Moduleを生成してくれる便利機能がある。これを Modules mode という。

  • store/hoge.jshoge
  • store/deep/something.jsdeep/something

という namespace を持つ。なので、いわゆる 非 Nuxt 環境の Vue アプリケーションのように自分で 子 module を import する必要がない。

Classic mode(deprecated) について

new Vuex.Store() を返す関数を store/index.jsexport default することで、
従来の Vuex のように自分で store を設定可能な Classic モードになる。
作った store をそのまま使ってくれるので、当然ながらネストした modules も普通に取り込める。Module mode では フォルダ構造が module 構造と一致するのでこれは出来ない。

しかし、現時点で deprecated (Nuxt 3 で廃止予定)で 、非推奨機能なので避けたほうが良い。規約ファーストな Nuxt.js というフレームワークの設計思想的にも使う理由がない。なので実質 Modules mode 一択。

どうやって外部化した Vuex module を Module mode に組み込むか

子 module を持たない store なら単純に import してやればいい:

@@/packages/shallowModule/index.js
const state = () => ({
  is: 'shallow module',
});

const getters = {
  greet(state) {
    return `this is ${state.is}`;
  },
};

export default {
  state,
  getters,
};

上記のように Module mode 非互換の export default { ... } 形式の export についても、Module mode の module としてグルーコードを書けば問題ない:

@/store/shallow.js
import shallow from '@@/packages/shallowModule';

export const state = shallow.state;
export const getters = shallow.getters;

ところが、modules プロパティに関してはうまくいかない。そもそも Nuxt.js が module 登録する過程で ['state', 'getters', 'actions', 'mutations'] 以外は無視している ので次のコードは意図通り動かない:

@/store/family.js
import parent from '@@/packages/family';

export const state = parent.state;
export const getters = parent.getters;
export const modules = parent.modules; // No children allowed 😭

modules を一旦無視して、modules 相当の構造を Nuxt.js アプリケーション上で自力用意すれば一応動かせる。要は Vuex modules を Nuxt.js Vuex Modules mode で再現することになる。外部パッケージに隠蔽されるべき内部的な module 構成と実装が Nuxt.js アプリケーション側と密結合するので流石に無理がある。

では、そのような状態で外部パッケージの store を module としてどう登録すればいいのか。
ここから本題。

Nuxt.js Plugins で Dynamic (Vuex) module registration

結論から言えば、Vuex store に対して動的に module 登録する Nuxt.js の Plugin を作れば良い。

注意すべきなのは、これは Vuex Plugins ではなく、Nuxt.js Plugins であること。
(Modules も同じく、Nuxt.js と Vuex と名前かぶりしていて紛らわしい。)

Nuxt.js Plugins は、Nuxt.js の中で生成される Vue.js アプリケーションのインスタンス化前の処理を追加する。 ここでは js ファイル (たとえば Vue の Plugin を import して Vue.use(...) するようなコード) を静的に評価する以外に、関数を export default することで、 引数経由で context をとれる。

Vuex store が初期化されていれば context.store から RootStore を参照できるので store.registerModule で動的に module 登録ができる。

つまり、ネストした外部 Vuex Module を、Vuex Modules mode と共存させるにはこうすれば良い:

@@/packages/someModule/plugin.js
import rootModule from './store.js';

export default ({ store }) => {
  store.registerModule('someModule', rootModule);
};

外部 module なので当然だけど Nuxt.js Modules mode の外にいるので、自動名前空間設定が行われず、module ごとにnamespaced: trueは明示的に設定する必要がある。 登録する module の namespace (上記例で someModule ) は既存 module とぶつからないように注意。

Nuxt.js の Plugin は今の所 nuxt.config.js から 設定値が渡せないので、もし namespace を決め打ちではなく利用側に設定させたくなってきたら Nuxt.js Modules 化を検討したほうがよい。そちらのやり方は割愛。

Example

せっかくなので全体像を。まずは適当な子モジュール:

@@/packages/someModule/modules/hoge.js
const state = () => ({
  is: 'hoge',
  bornIn: 1990,
});

const getters = {
  profile: state => {
    return `${state.is} was born in ${state.bornIn}.`;
  },
};

export default {
  state,
  getters,
  namespaced: true,
};
@@/packages/someModule/modules/fuga.js
// 略

(要らないなら別に良いけど) namespaced: true を忘れずに。 そして、これらを読み込む親(root):

@@/packages/someModule/store.js
import hoge from './modules/hoge';
import fuga from './modules/fuga';

export default {
  modules: {
    hoge,
    fuga,
  },
  namespaced: true,
};

これも namespaced: true を忘れずに。
名前空間取れたほうが良い気がするので定数にして export してある:

@@/packages/someModule/index.js
export const NAMESPACE = 'ryonkmr::someModule';

// UI を export したり
export { default as HogeProfile } from './components/HogeProfile.vue';
export { default as FugaPforile } from './components/FugaProfile.vue';

最後に肝心の Nuxt.js Plugin:

@@/packages/someModule/plugin.js
import { NAMESPACE } from './index.js'; // === 'ryonkmr::someModule'
import rootModule from './store.js';

export default ({ store }) => {
  store.registerModule(NAMESPACE, rootModule);
};

後は読み込むだけ。

@@/nuxt.config.js
plugins: ['@@/packages/someModule/plugin.js'];

initialized store

おしまい。