AdventCalendar
vue.js
nuxt.js

Nuxtで実案件で開発するときに作ったオレオレプラグイン

ここ数ヶ月はNuxt.jsで開発することが多いのですが、意外と~/pluginsディレクトリにいろいろ貯まってきたので軽めの共有です。

前提

  • mode: 'spa' で開発しています
    • こうするとサーバーサイドレンダリング(SSR)はなくなります
    • SSRの場合は nuxtServerInit というAPIを利用することでストアの初期化などが可能です
  • SPAの場合は初期化処理を ~/plugins で実装することが可能です
    • 名前からしてpluginsで初期化するのが正しいのかどうかあんまり自信ないですが、実装上はこれで可能です。

~/plugins の挙動

  • (build後の状態はわからないですが).nuxt/index.js を見ると、以下のような状態になっています。
~/.nuxt/index.js
// 略
import plugin0 from 'plugin0'
import plugin1 from 'plugin1'
import plugin2 from 'plugin2'
import plugin3 from 'plugin3'

// 略

async function createApp (ssrContext) {

  //略

  if (typeof plugin0 === 'function') await plugin0(ctx, inject)
  if (typeof plugin1 === 'function') await plugin1(ctx, inject)
  if (typeof plugin2 === 'function') await plugin2(ctx, inject)
  if (typeof plugin3 === 'function') await plugin3(ctx, inject)

  //略

  return {
    app,
    router,
     store 
  }
}

export { createApp, NuxtError }

plugin0 とか plugin1 のようなものはおそらく nuxt.config.js の設定によって順番にロードされてるものなのだと思います。

  • これはAppを作成するNuxtの初期化処理なのですが、先頭でpluginがimportされ、createAppの中で、functionがexportされてる場合には(awaitして)その関数をContextとともに実行してくれるという感じになています。
  • つまり、以下の2つのことをpluginsで実現可能です
    • Contextに依存しない初期化処理を行う
    • Contextに依存する初期化処理を行う
  • 繰り返しになりますが、awaitで実行されているので、非同期処理を待ったうえで後続の処理を実行させるようなことも可能です。

オレオレplugins

というわけで、いろんな初期化処理に使えるので、pluginsは重宝しています。
実案件で作ったpluginをいくつか公開したいと思います。

基本的に手探りで開発してるので、「もっとこうしたほうがいいよ!」的なマサカリは大歓迎です。

(TypeScriptで作ってるのですが、たぶんJSしかわからなくても読めるのでご安心ください)

axios

~/plugins/axios.ts
import _ from "lodash";

export default function ({ app, env, isServer }) {
  if (isServer) {
    // SPAモードなのでこの条件に来ることはないけどお作法として。
    return;
  }

  // 未認証エラーになった場合はログインページにリダイレクトする
  app.$axios.interceptors.response.use(response => response, (error) => {
    if (error.statusCode === 401) {
      window.location.href = env.loginUrl;
      return;
    }
    return Promise.reject(error);
  });
}

@nuxtjs/axiosモジュールを利用しているので、Context内でaxiosインスタンスがシングルトンな感じになってるので、そのAxiosInstanceに対してinterceptorを設定しています。

これによって、APIのレスポンスが401だったら、とりあえずログインページにリダイレクトさせる、みたいなことをやっています。
(都合でlocation.hrefしていますが、app.router.pushとかでも動作するはずです)

emoji

~/plugins/emoji.ts
import emoji from "node-emoji";
import Vue from "vue";

Vue.mixin({
  methods: {
    $emojify(text: string) {
      return emoji.emojify(text);
    },
  },
});

Componentの中でemoji使いたいことはきっと多いはずなので :innocent:node-emojiをサクッとComponentから利用できるようにするemojiのpluginsです。

これで {{$emojify(':innocent:')}} をComponentに書くだけでいつでも :innocent: を表示することができます。すばらしい。

i18n

Nuxt.jsでいい感じにi18nを使う
https://qiita.com/takyam/items/4861badeb88dcee67d0a

詳しくは上記参照ください :pray:

vee-validate

~/plugins/vee-validate.ts
import _ from 'lodash';
import VeeValidate, { Validator } from "vee-validate";
import Vue from "vue";

import validations from "./vee-validate/validations";
_.each(validations, (rule, name) => {
  Validator.extend(name, rule);
});

Vue.use(VeeValidate);
~/plugins/vee-validate/validations/index.ts
import _ from "lodash";
import identifier from "./identifier";

export default _.merge({}, identifier);
~/plugins/vee-validate/validations/identifier.ts
import Identifier from "~/domains/Identifier"; // こういうやつがあるもんだと思ってください

export default {
  isId: {
    messages: {
      en: (field) => "Invalid ID format",
      ja: (field) => "有効なIDではありません",
    },
    validate: (value) => Identifier.isValid(value),
  },
};
  • バリデーションにはvee-validateを使ってます。
  • vee-validateはカスタムバリデーションを実装することが可能なのですが、単一ファイルにたくさんのバリデーションを追加していくと肥大化すること間違いなしなので、ある程度のグループ毎にファイルを分割できるようにしています。
  • validationsの定義をplugins以下に置くことにモニョってはいるので、いつかいい感じの場所に移動したいです:innocent:

console

~/plugins/console.ts
import Vue from "vue";

Vue.mixin({
  methods: {
    $console(...args): void {
      console.dir(args);
    },
  },
});

Vueのtemplate内で console.log したいけど、すると怒られたので拡張してます。
テンプレート内で $console(hoge) みたいにできてプチ便利。
(絶対もっといいソリューションがあるんだろうなぁとは思ってます・・・!)

provider

~/plugins/provider.ts
import Vue from "vue";
import _ from "lodash";
import HogeRepository from "~/infra/HogeRepository";

class ServiceProvider {
  private _services;

  constructor(services: { [key: string]: any }) {
    this._services = services;
  }

  get services() {
    return this._services;
  }

  get(serviceName: string): any | null | undefined {
    return _.get(this._services, serviceName);
  }
}

export default function ({ app }) {
  const serviceProvider = new ServiceProvider({
    "HogeRepository": new HogeRepository(app.$axios),
  });

  app.$serviceProvider = serviceProvider;
  Vue.mixin({ provide: serviceProvider.services });
}

苦肉の策というか、何かぜったいもっと素敵なソリューションがあるはずなのですが、VueコンポーネントでDI的なことやるいい感じの方法が見つからず、とりあえずオレオレでやってます。

この例でいうと HogeRepository というRepositoryがaxiosに依存しているので、context生成後にインスタンスを生成してあげて、それをオレオレサービスプロバイダに登録しておきます。

  • app.$serviceProvider として登録
  • Vue.mixin({ provide: serviceProvider.services }) でProvideしておく

という2通りの登録をやっておくことで、

  • asyncData / fetch などのComponentが初期化されていない状態では app.$serviceProvider 経由で取得できる
  • Componentが作られている状態では @Inject("HogeRepository") hogeRepository: HogeRepository のようにしておくことで、 this.hogeRepository にアクセスすることができる

という感じになっています。

(控えめにいって nuxt-class-component / vue-property-decorator / vuex-class は神ってるなと思いました)

まとめ

他にもいくつかあったりするんですが、基本は前述のように、 Contextが生成される前の初期化、Context生成後の初期化の2種類なので、それらうまく組み合わせて必要な初期化処理を実現しています。

他にオススメのpluginであったり、上記に対するマサカリであったり、コメントいただけると嬉しいです :pray: