20201208のvue.jsに関する記事は21件です。

複雑な画面開発に立ち向かう時に捗るVue Composition APIのTips

本記事は CBcloud Advent Calendar 2020 の9日目の記事です。


こんにちは。
現場でNuxtを使ってゴリゴリとフロントを書いているdadayamaです。
VueのComposition APIは少し前から気になっていたのですが、正式版がリリースされたのをいいことにすぐに実務で導入しました。

  • コンポーネントの描画(View)と状態・ロジック(View Model)の分離
  • 関心事の集約
  • ロジックの再利用
  • 型推論の強化(要はTSフレンドリー)

といったメリットはがあるということは公式ドキュメントや紹介記事で理解していましたが、実際に導入した結果、Composition APIは複雑な状態やロジックを持つ画面を開発する際に非常に有効だと改めて納得できました。
今回は表題の通り、そういった複雑な画面を作る時に有効だった手法をTipsとして紹介します。

前置き

  • 実際のコードは載せていません。本記事用に書いたサンプルです
  • 実務ではNuxtを利用していますが、NuxtがVue3.x系に対応しきっていないため、Composition API単体のライブラリを導入しています
  • Composition APIに関して詳細な説明は行いません。優れた紹介記事を書いてくれている方が多いので割愛します

Composition APIとは?

Vue3.x系で導入された新しい機能です。
これまでVueコンポーネントでView Modelを実装するためにはOptions APIという機能を利用する必要がありましたが、それを代替するような形で登場しました。

値を増減させる簡単なカウンターコンポーネントを例にすると以下のような感じです。

Options API ver.

Counter.vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <button type="button" @click="increment">Increment</button>
    <button type="button" @click="decrement">Decrement</button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue';

export default Vue.extend({
  data: (): { count: number } => {
    return {
      count: 0,
    };
  },
  computed: {
    doubled(): number {
      return this.count * 2
    },
  },
  methods: {
    increment(): void {
      this.count++;
    },
    decrement(): void {
      if (this.count > 0) {
        this.count--;
      }
    },
  },
});
</script>

Composition API ver.

Counter.vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <button type="button" @click="increment">Increment</button>
    <button type="button" @click="decrement">Decrement</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, computed } from '@vue/composition-api';

export default defineComponent({
  setup() {
    const count = ref(0);
    const doubled = computed(() => count.value * 2);
    const increment = (): void => {
      count.value++;
    };
    const decrement = (): void => {
      if (count.value > 0) {
        count.value--;
      }
    };

    return {
      count,
      doubled,
      increment,
      decrement,
    };
  },
});
</script>

上記のように関心事をまとめるため、可読性があがります。また基本的にはただの関数なのでテスタビリティも高いです。

詳細は以下記事を参考に。

参考

複雑な画面でComposition APIを利用する時のTips

本題です。
私が主に実装を手掛けた画面は、以下のような仕様になっていました。

  1. 状態変化のパターンが多岐にわたる
  2. 様々な状態ごとに入力バリデーションのロジックが変わる
  3. 複雑な条件が揃うと画面遷移なしにAPIを叩く
  4. ↑のAPIの戻り値を反映し、再度状態を書き換える

このような複雑な仕様を複雑なまま実装しないように取り入れた手法を書き連ねます。

できる限り状態とロジックをComposableに寄せる

コンポーネントから状態とロジックを引き剥がし、関数として別のファイルに寄せるよう徹底しました。
ちなみに切り出した関数はComposableと呼ぶようです。
再利用可能な部品、アプリケーションを構成することができる部品、といった意味で利用していると思うので、例に習います。

先程のカウンターコンポーネントを例にするとこうなります。

composables/useCounter.ts
import { ref, computed, Ref } from '@vue/composition-api';

export const useCounter = (): {
  count: Ref<number>
  doubled: Ref<number>
  increment: () => void
  decrement: () => void
} => {
  const count = ref(0);
  const doubled = computed(() => count.value * 2);
  const increment = (): void => {
    count.value++;
  };
  const decrement = (): void => {
    if (count.value > 0) {
      count.value--;
    }
  };

  return {
    count,
    doubled,
    increment,
    decrement,
  };
};
Counter.vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <button type="button" @click="increment">Increment</button>
    <button type="button" @click="decrement">Decrement</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { useCounter } from '~/composables/useCounter';

export default defineComponent({
  setup() {
    const {
      count,
      doubled,
      increment,
      decrement,
    } = useCounter();

    return {
      count,
      doubled,
      increment,
      decrement,
    };
  },
});
</script>

別ファイルにしただけで何が良いのか?と思う方もいるかもしれませんが、以下の点で優れています。

  1. ロジックを再利用できる
  2. テストが容易
  3. コンポーネントの記述量が減り、可読性が上がる

個人的には2が大きいです。
複雑な状態管理をコンポーネントから引き剥がしてテストすることができるため、テスト実装が容易です。
それこそコンポーネントが無くても実装が可能なので、ロジックだけ先行してTDDで開発することもありだと思います。

できる限り関心事の単位でComposableを分割する

アプリケーションロジックが大きく複雑な場合、Composableが肥大化してしまいます。
そのため関心事単位でComposableを分割して可読性を下げないようにしました。

以下はA・Bの2地点の場所の管理と、作業時間の管理を分けた例です。

composables/useTimes.ts
import { reactive, computed } from '@vue/composition-api'

/**
 * A地点とB地点の時間管理Composable
 */
export type Times = { hour: number }

export type TimeConditions = {
  start: Times // 作業開始時間
  end: Times // 作業終了時間
}

export type TimesState = {
  a: TimeConditions // A地点の作業時間
  b: TimeConditions // B地点の作業時間
}

export const useTimes = (): {
  timesState: TimesState
} => {
  const state: TimesState = reactive({
    a: {
      start: {
        hour: 0,
      },
      end: {
        // A地点の終了時間は開始時間の1時間後
        hour: computed(() => state.a.start.hour + 1),
      },
    },
    b: {
      start: {
        // B地点の開始時間はA地点の開始時間の2時間後
        hour: computed(() => state.a.end.hour + 2),
      },
      end: {
        // B地点の終了時間は開始時間の1時間後
        hour: computed(() => state.b.start.hour + 1),
      },
    },
  });

  return {
    timesState: state,
  };
};
composables/useSpots.ts
import { reactive, toRefs, Ref } from '@vue/composition-api'
import { useTimes, TimesState } from '~/composables/useTimes';

/**
 * A地点とB地点の全体管理Composable
 */
type Spot = {
  name: string // 地点名
  time: TimesState
}

type SpotsState = {
  a: Spot // A地点
  b: Spot // B地点
}

type SpotsRef = Ref<SpotsState>

export const useSpots = (): {
  spotsRef: SpotsRef
} => {
  const { timesState } = useTimes();

  const state: SpotsState = reactive({
    a: {
      name: '',
      // useTimesから取得
      time: timesState.a,
    },
    b: {
      name: '',
      // useTimesから取得
      time: timesState.b,
    }
  });

  return {
    spotsRef: reactive({ spotsRef: state }),
  };
};

分割したことで、地点管理のComposableは時間の内訳を知る必要が無くなりました。
勿論わざわざuseSpots内でuseTimesを呼び出してStateに結合しなくてはいけない、といった訳ではないので、その辺りは適宜調整してもらえればと思います。

異なる関心事(Composable)への入力を監視する

画面全体の状態を監視しエラーを表示するといったケースがあった場合、複数のComposableの状態をエラー管理Composableが知る必要が出てきます。
この対応策として、今回は以下の方法を利用しました。

  1. provideinjectを使う
  2. エラー管理Composableの引数に他のComposableを渡す

実際には以下の通りです。

useInput.ts
/**
 * 入力管理Composable
 */
import { reactive, Ref, InjectionKey } from '@vue/composition-api';

export type InputState = {
  name: string
}

export const useInput = (): {
  inputState: InputState
} => {
  const state: InputState = reactive({
    name: '',
  });

  return { state };
};

// コンポーネント間で共有するためのキー設定
export type InputComposable = ReturnType<typeof useInput>
export const InputComposableKey: InjectionKey<InputComposable> = Symbol();
components/InputName.vue
<template>
  <input type="text" v-model.sync="inputState.name" />
</template>

<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { InputComposable, InputComposableKey } from '~/composables/useInput';

export default defineComponent({
  setup() {
    // injectで親コンポーネントと状態を共有する
    const { inputState } = inject(inputKey) as InputComposable;
    return { inputState };
  },
});
</script>
composables/useErrors.ts
/**
 * エラー管理Composable
 */
import { reactive, computed } from '@vue/composition-api'
import { InputState } from '~/composables/useInput';

type Errors = {
  inputError: string | null
}

export const useErrors = (
  // コンポーネントから他のComposableの状態を引数で受け取る
  inputState: InputState
): {
  errors: Errors
} => {

  const errors: Errors = reactive({
    inputError: computed(() => {
      // 受け取った引数のComposableの値を判定に使う
      return inputState.name === '' ? '未入力です。' : null
    }),
  });

  return { errors };
};
components/Errors.vue
<template>
  <input-name />
  <div class="errors">
    <template v-for="error in errors" :key="error">
      <p v-if="error">{{ error }}</p>
    </template>
  </div>
</template>

<script lang="ts">
import { defineComponent, provide } from '@vue/composition-api';
import { useInput, InputComposableKey } from '~/composables/useInput';
import InputName from '~/components/InputName.vue';

export default defineComponent({
  components: {
    InputName,
  },
  setup() {
    // provideで子コンポーネントと状態を共有する
    provide(InputComposableKey, useInput())

    // provideした状態をinjectで取得する
    const { inputState } = inject(inputKey) as InputComposable;

    // 共有されているComposableの状態を引数として渡す
    const { errors } = useErrors(inputState);

    return { errors };
  },
});
</script>

エラー管理Composableが他のComposableを監視できていることが分かりますでしょうか。
仮に監視項目が増えたとしてもエラー管理Composableの引数を増やせばすぐに監視できるので、拡張も容易です。

こうすることでエラーのことはエラー管理Composableが、入力等はその他のComposableが責任を持っている状態を作れます。
餅は餅屋ってことですね。

ちなみに何で面倒な引数渡しをしているかというと、provideinjectを用いず呼び出されたComposableはスコープが異なってしまうからです。
詳細は「Vue3 Composition APIにおいて、Providerパターン(provide/inject)の使い方と、なぜ重要なのか、理解する。」に素晴らしくよくまとまっているので、見てもらったほうが早いです。

Composableを完全にグローバルに公開してしまえば実装は楽なのですが、そうなるとグローバル変数が大量に発生してしまうので避けました。

Composableをテストする

これは簡単で、Jestなりのテストフレームワークを使ってComposableをテストするだけです。
ただの(リアクティブな)変数と関数のセットでしかないので、入出力のチェックをすれば終わります。
もし1つ前のTipsに挙げたような「引数渡し」があったとしても、テストファイルから呼び出して渡せばいいだけです。
簡単ですね。

ただ(無いとは思いますが)、万が一provideinjectをComposableから呼び出す実装になっていた場合、これは避けたほうが良いと思います。
これら2つの関数はコンポーネントのライフサイクルに依存しているため、コンポーネント抜きでprovideinjectはうまく動きません。
当然Composableのみのテストではコケます。

まとめ

ダラダラと長くなりましたが、私が実務で取り入れたComposition APIの活用方法をTipsとして紹介させてもらいました。
これらの手法のおかげでアプリケーションロジックがあるべきファイルに集約され、可読性やテスタビリティが大きく向上したと思います。
これを読んだ方々のお役に立てれば幸いです。

ただ正式版がリリースしたとはいえ、Composition APIはまだ出て間もない機能です。
そのため使い方に関して改善点や誤りがありましたらツッコんでもらえると嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 6: ActionCableとVue.jsで非同期処理を行うサンプル

環境: Rails 6.0、Vue.js 2.6、ソース: https://github.com/kazubon/cable60

ActionCable、ActiveJob、およびVue.jsを使って、次のような非同期処理を行う画面を作ります。
image.png

必要なライブラリ

GemfileにRedisとSidekiqを追加して、bundle install してください。

gem 'redis', '~> 4.0'
gem 'sidekiq'

Redisをインストールしていない場合は、インストールして起動しておきます。

% brew install redis
% brew services start redis

ActionCableの準備

cable.ymlで、development環境の設定を async から redis に変えます。サンプルでは、ActiveJobのジョブの中でActionCableのブロードキャストを行いますが、async だと効かないからです。

config/cable.yml
development:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: cable60_development

コントローラでは、ActionCable用の接続の識別子となるランダム文字列をクッキーに入れます。このサンプルでは、「1ユーザー - 1識別子 - 1ストリーム」とします。1対多の送信は行いません。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_cable_code

  private
  # Action Cable用ユーザー識別
  def set_cable_code
    cookies.signed[:cable_code] ||= SecureRandom.hex
  end
end

connection.rbではクッキーから識別子 cable_code を取り出します。

app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :cable_code

    def connect
      if cookies.signed[:cable_code]
        self.cable_code = cookies.signed[:cable_code]
      end
    end
  end
end

bin/rails g channel user で user_channel.rb を作っておき、識別子 cable_code を stream_from にそのまま渡します。

app/channels/user_channel.rb
class UserChannel < ApplicationCable::Channel
  def subscribed
    stream_from cable_code if cable_code
  end

  def unsubscribed
  end
end

ActiveJobの準備

config/environments下のdevelopment.rbとproduction.rbを修正し、ActiveJobではSidekiqを使うことを指定します。

config/environments/development.rb
  config.active_job.queue_adapter     = :sidekiq

コントローラでは、「開始」ボタンで呼び出す update アクションを書いておきます。SampleJobというジョブにクッキーの識別子を渡して非同期処理をさせます。

app/controllers/samples_controller.rb
class SamplesController < ApplicationController
  def show
  end

  def update
    SampleJob.perform_later(cookies.signed[:cable_code])
    render json: {}
  end
end

bin/rails g job sample でSampleJobを作っておいて、performメソッドにサンプル用の処理を書きます。20回スリープしながら現在のパーセンテージを進めるだけのものです。

ActionCableのブロードキャストを使い、識別子 cable_code に対してハッシュ(JavaScriptのオブジェクト)を送信します: { type: 処理の種類に付けた名前, progress: パーセンテージ, processing: 処理中かどうか }

app/jobs/sample_job.rb
class SampleJob < ApplicationJob
  queue_as :default

  def perform(cable_code)
    20.times do |idx|
      sleep 0.2
      ActionCable.server.broadcast(cable_code,
        type: 'sample', progress: (idx + 1) * (100 / 20), processing: true)
    end
    ActionCable.server.broadcast(cable_code,
      type: 'sample', progress: 100, processing: false)
  end
end

JavaScriptでデータを受け取り、進行状況を表示する

JavaScript側では、ActionCableからデータを受け取るためのオブジェクトを作ります。Vue.observable を使うことで、sampleプロパティを変更したらVueのテンプレートに反映するようにします。

ほかに非同期処理を扱う画面が増えたら、fooとかbarとかプロパティを増やすことを想定しています。

app/javascript/channels/cable_data.js
import Vue from 'vue';

export default Vue.observable({
  sample: { },
  // foo: { },
  // bar: { },
})

UserChannelに対応するuser_channel.jsを修正します。receivedでデータを受け取ったら、ActionCable用のオブジェクトcableDataのプロパティにそのまま入れます。

オブジェクトのtypeプロパティの値がcableDataの各プロパティの名前に対応していることにします。

app/javascript/channels/user_channel.js
import consumer from "./consumer"
import cableData from "./cable_data";

consumer.subscriptions.create("UserChannel", {
  connected() {
  },

  disconnected() {
  },

  received(data) {
    switch(data.type) {
      case 'sample':
        cableData.sample = data;
        break;
      // case 'foo':
      //   cableData.foo = data;
      //   break;
      // case 'bar':
      //   cableData.bar = data;
      //   break;
    }
  }
});

非同期処理の進行状況を表示するVueコンポーネントです。ActionCable用のオブジェクト cableData.sampleのprogressとprocessingの値を画面に反映させます。

なお、ここではBootstrapのProgressを使っています。

app/javascript/sample.vue
<template>
  <div>
    <div class="form-group row">
      <div class="progress">
        <div class="progress-bar" role="progressbar" :aria-valuenow="progress"
          aria-valuemin="0" aria-valuemax="100" :style="`width: ${progress}%`"></div>
     </div>
    </div>
    <div class="form-group row">
      <button type="button" class="btn btn-primary" @click="startProcess"
        :disabled="processing">開始</button>
    </div>
  </div>
</template>

<script>
import Axios from 'axios'
import cableData from "./channels/cable_data"

export default {
  data() {
    return {
    };
  },
  computed: {
    progress() {
      return cableData.sample.progress || 0;
    },
    processing() {
      return cableData.sample.processing;
    }
  },
  methods: {
    startProcess() {
      cableData.sample = { progress: 0, processing: true };
      Axios.patch('/sample');
    }
  }
}
</script>

<style scoped>
.progress {
  width: 100%;
}
</style>

サンプルのVueコンポーネントをマウントするpacks下のJavaScriptです。require("channels") ががあることを確認しましょう。

app/javascript/packs/application.js
import 'bootstrap';
import '../stylesheets/application';

require("@rails/ujs").start()
require("turbolinks").start()
// require("@rails/activestorage").start()
require("channels")

import Vue from 'vue';
import TurbolinksAdapter from 'vue-turbolinks';

import Sample from '../sample.vue';
import '../axios_config';

Vue.use(TurbolinksAdapter);

document.addEventListener('turbolinks:load', () => {
  if(document.getElementById('sample')) {
    new Vue(Sample).$mount('#sample');
  }
});

bin/rails s でサーバーを起動し、別のターミナルで bundle exec sidekiq を起動すれば、非同期処理の動作を確認できます。

Vue.observable を使わない場合

上記のcable_data.jsで、Vue.observableを使わずにJavaScriptのオブジェクトをそのままエクスポートしても、ActionCableのデータを扱えます。

app/javascript/channels/cable_data.js
export default {
  sample: { }
}

この場合は、Vueコンポーネントでdataを使ってオブジェクトを渡せば、sampleプロパティの変更が反映されます(リアクティブになります)。

app/javascript/sample.vue
<script>
import Axios from 'axios'
import cableData from "./channels/cable_data"

export default {
  data() {
    return {
      cableData: cableData
    };
  },
  computed: {
    progress() {
      return this.cableData.sample.progress || 0;
    },
    processing() {
      return this.cableData.sample.processing;
    }
  },
  methods: {
    startProcess() {
      this.cableData.sample = { progress: 0, processing: true };
      Axios.patch('/sample');
    }
  }
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueCLIでポートフォリオ作った話

概要

自分のポートフォリオを最近学んだVueで作成したのでまとめておきます。
作ったのはこちらです。

開発フロー

Vue create ⇨ build ⇨ Github pagesで公開

始め方

基本的には公式の通りで問題ない
Vueの入門記事はたくさんあるのでざっと読みながら開発していけばいいと思います。

vue roter

ルーティングはvue routerを使用。
公式を読めば大体使い方はわかるはずです。

デザイン

Vuetifyを導入しました。
色々テンプレがあり簡単にリッチなサイトになるのでおすすめ。
カスタマイズはうまくいかず。
公式

build

npm run buildで distというフォルダが作成されそこにビルドされたものが入ります。

Github pages

ビルドで作ったフォルダをGithubにあげて、設定から公開させるだけでOK
詳細はこちら公式

エラーとかとか

vue create VS vue init

プロジェクトを始める際にどちらを使うかの問題。簡単に言えばVersionの違いに関係するようです。
僕はVuetifyの導入のためにvue createにしました。
詳細はこちら(vue init と vue createの違い

buildした時にPATHが通っていない

buildした際に白紙になってページが表示されない問題が生じました。
下の参考のようにHistory Modeの問題のようなのでオフにしました。
参考:
【vue-router】Vue.jsでビルドしたウェブサイトが白紙で表示される場合の対処法 【History Mode】

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueでCSSモジュールを使うためのWebpack設定

cssModules のオプション記述位置が違った

公式に書かれた設定だけだと css modules が実現できなかったので調べたところ、Vue Loader v15 から色々変わった模様。
Vueのルール部分ではなく、CSSのルール部分に書くようです。

参考:$style is undefined when I used <style lang="scss" module>

以前の設定

webpack.config.js の設定から cssModules を使う前のcss部分を抜粋しました。

js
{
  module: {
    rules: [
      {
        test: /\.css/,
        loader: 'style-loader!css-loader',
      },
    ],
  }
}

変更後の設定

js
{
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: 'vue-style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[local]_[hash:base64:8]',
            },
          },
        ],
      },
    ],
  }
}

Vueとcssに関する全体

js
{
  // 省略
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: [
          {
            loader: 'vue-loader',
            options: {
              loaders: {
                js: 'babel-loader',
                scss: 'vue-style-loader!css-loader!sass-loader',
              },
            },
          },
        ],
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'vue-style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[local]_[hash:base64:8]',
            },
          },
        ],
      },
    ],
  },
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueでCSSモジュールを使うためのWebpack設定(SCSS対応)

cssModules のオプション記述位置が違った

公式に書かれた設定だけだと css modules が実現できなかったので調べたところ、Vue Loader v15 から色々変わった模様。
Vueのルール部分ではなく、CSSのルール部分に書くようです。

参考:$style is undefined when I used <style lang="scss" module>

以前の設定

webpack.config.js の設定から cssModules を使う前のcssとscssの部分を抜粋しました。

js
{
  module: {
    rules: [
      {
        test: /\.css/,
        loader: 'style-loader!css-loader',
      },
      {
        test: /\.scss/,
        loader: 'vue-style-loader!css-loader!sass-loader',
      },
    ],
  }
}

変更後の設定

js
{
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: 'vue-style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[local]_[hash:base64:8]',
            },
          },
        ],
      },
      {
        test: /\.scss/,
        use: [
          {
            loader: 'vue-style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[local]_[hash:base64:8]',
            },
          },
          {
            loader: 'sass-loader',
            options: {
              modules: true,
              localIdentName: '[local]_[hash:base64:8]',
            },
          },
        ],
      },
    ],
  }
}

Vueとcss部分の抜粋

js
{
  // 省略
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: [
          {
            loader: 'vue-loader',
            options: {
              loaders: {
                js: 'babel-loader',
                scss: 'vue-style-loader!css-loader!sass-loader',
              },
            },
          },
        ],
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'vue-style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[local]_[hash:base64:8]',
            },
          },
        ],
      },
      {
        test: /\.scss/,
        use: [
          {
            loader: 'vue-style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[local]_[hash:base64:8]',
            },
          },
          {
            loader: 'sass-loader',
            options: {
              modules: true,
              localIdentName: '[local]_[hash:base64:8]',
            },
          },
        ],
      },
    ],
  },
}

webpack.config.js がどんどん長くなりますねw

コンポーネント内での記述

style の記述

こんな感じ。

vue
<template>
  <div :class="$style.foo">hogehoge</div>
</template>

<style module>
.foo {
  background-color: rgba(0, 0, 0, 0.6);
}
</style>

scssならこんな感じ。

vue
<template>
  <div :class="$style.foo">hogehoge</div>
</template>

<style lang="scss" module>
.foo {
  background-color: rgba(#000, 0.6);
}
</style>

動作確認

this.$style に格納されているので console.log() で確認できます。

vue
mounted(){
  console.log('style', this.$style);
}

cssModules 以外の class も併用する

cssModules、クラス名の文字列、クラス名の入った変数を併用するなら配列で渡します。

vue
<template>
  <div :class="[$style.foo, 'class-name', myClass]">
    hogehoge
  </div>
</template>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.js + TypeScriptのアーキテクチャ一例

こんにちは。
Web・iOSエンジニアの三浦です。

今回は、TypeScriptでセットアップしたNuxt.jsにおけるアーキテクチャについて、実際に私が使っているものを一例として紹介します。

はじめに

Vue.jsはViewでのバインディングに非常に長けているJavaScriptのフレームワークであり、コンポーネントにて適切にデータを取得することができれば、Vue.jsの作法に従いきれいにコードを書くことが可能です。
一方で、コンポーネントにデータを渡すまで、すなわち例えばAPIからのデータの取得や整形などの、MVVMで言うModel部分については特にVue.js側でフレームは用意されておらず、自ら構造を考える必要があります。
シンプルなアプリケーションであれば問題ありませんが、複雑性が増すほどきちんとModel部分の構造を考える必要が出てくるでしょう。

ここでは、実際に私がVue.jsのフレームワークであるNuxt.jsを使う上で、Model部分の構造をどのように設定しているかを紹介していきます。

Vue.jsとNuxt.jsの紹介

具体的な紹介に入る前に、まずVue.jsとNuxt.jsについて説明します。

Vue.jsとは

Vue.jsはJavaScriptのフレームワークの一つであり、Viewへの変数等のバインディングに優れたフレームワークです。
各ページやそのパーツをコンポーネントという単位に分け、それらを組み合わせてアプリケーションを作成します。

Nuxt.jsとは

Nuxt.jsはVue.jsのフレームワークであり、Vue.jsが本来持つ機能を活かしつつ、ルーティングやレンダリングなど様々な追加機能を提供してくれます。
セットアップ時にコードフォーマッターやユニットテストの設定等も同時に行うことができ、Nuxt.jsをインストールすれば開発に必要な一通りの準備が整うと言って良いでしょう。

使用するアーキテクチャ

Vue.jsを使う以上必然的に全体のアーキテクチャはMVVMになるわけですが、Model部分に関してはクリーンアーキテクチャを意識して作りました。
クリーンアーキテクチャには、

  • 役割ごとの機能の分離
  • DIによる依存性逆転

などの特徴があり、コードの可読性の向上やユニットテストのしやすさの向上に寄与します。

ディレクトリ構造

結果からいうと、ディレクトリ構造は以下のようになりました。

.
├── model
│   ├── persistence
│   │   ├── persistence1.ts
│   │   └── persistence2.ts
│   ├── repository
│   │   ├── repository1.ts
│   │   └── repository2.ts
│   └── service
│       └── service1.ts
├── pages
│   └── sample.vue
├── plugins
│   └── service.ts
├── types
│   └── index.d.ts.ts
└── test
    └── model
        ├── repository
        │   ├── repository1.spec.ts
        │   └── repository2.spec.ts
        └── service
            └── service1.spec.ts

順に説明していきます。

MVVMのModel

modelディレクトリ配下にて、MVVMでいうModel部分を担当します。

persistence

persistenceは、外部のストレージやAPIと直接やり取りする役割を持ちます。
クリーンアーキテクチャで言うとrepositoryがそれを担当する場合もありますが、あえてpersistenceとして分離することで、以下の利点を得ることができます。

  • repositoryが直接ストレージやAPIとのやり取りをする場合、受け取ったデータをエンティティとして変換したりバリデーションしたりする役割も兼務することになるが、それを分離することができる
  • ユニットテスト実行時、persistenceを擬似的にストレージやAPI本体と捉えることで、ストレージやAPI自体のモックを用意しなくてもpersistenceのモックを用意するだけでrepositoryのユニットテストを実行できる

ファイルの中身は以下のようになっています。

persistence1

export interface Persistence1 {
  get(): string
}

export class Persistence1Impl implements Persistence1 {
  get(): string {
    return 'Persistence1のデータを取得'
  }
}

persistence2

export interface Persistence2 {
  get(): string
}

export class Persistence2Impl implements Persistence2 {
  get(): string {
    return 'Persistence2のデータを取得'
  }
}

repository

repositoryは、persistenceが外部から取得したデータを受け取って整形やバリデーション等を行い、アプリケーション内で使用できる形に変換します。
多くの場合、 persistence : repository = 1 : 1 になるでしょう。
変換だけならTranslaterのようなものを作ってもいいですが、その他バリデーション処理等もここで行う想定なのでrepositoryとして切り分けています。

ファイルの中身は以下のようになっています。

repository1

import { Persistence1 } from '~/model/persistence/persistence1'

export interface Repository1 {
  get(): string
}

export class Repository1Impl implements Repository1 {
  private readonly persistence1: Persistence1

  constructor(persistence1: Persistence1) {
    this.persistence1 = persistence1
  }

  get(): string {
    return 'Repository1経由で' + this.persistence1.get()
  }
}

repository2

import { Persistence2 } from '~/model/persistence/persistence2'

export interface Repository2 {
  get(): string
}

export class Repository2Impl implements Repository2 {
  private readonly persistence2: Persistence2

  constructor(persistence2: Persistence2) {
    this.persistence2 = persistence2
  }

  get(): string {
    return 'Repository2経由で' + this.persistence2.get()
  }
}

service

serviceは、1~複数のrepositoryを使用して各ページに必要なデータを取得・必要に応じて整形し、Vueコンポーネントにそのデータを渡します。
そのため、基本的に ページ : service = 1 : 1 になるイメージです。

ファイルの中身は以下のようになっています。

service1

import { Repository1 } from '~/model/repository/repository1'
import { Repository2 } from '~/model/repository/repository2'

export interface Service1 {
  get1(): string
  get2(): string
}

export class Service1Impl implements Service1 {
  private readonly repository1: Repository1
  private readonly repository2: Repository2

  constructor(repository1: Repository1, repository2: Repository2) {
    this.repository1 = repository1
    this.repository2 = repository2
  }

  get1(): string {
    return 'Service1から' + this.repository1.get()
  }

  get2(): string {
    return 'Service1から' + this.repository2.get()
  }
}

MVVMのV/VM

ここまで見てくださった方は、どこでこれらをDIするのか疑問に思われているかと思いますが、先にMVVMにおけるV/VMを担当するpagesディレクトリ配下を見ていきます。
pagesはNuxt.jsにもとからあるディレクトリで、ここに各ページのViewとViewModelの処理を書いていきます。
今回のmodel配下からデータを取得しているsample.vueファイルは、以下のようになっています。

<template>
  <div>
    <p>
      {{ data1 }}
    </p>
    <p>
      {{ data2 }}
    </p>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

interface DataType {
  data1: string
  data2: string
}

export default Vue.extend({
  name: 'Index',
  data(): DataType {
    return {
      data1: this.$service.service1.get1(),
      data2: this.$service.service1.get2(),
    }
  },
})
</script>

このようにすることで、以下のように出力されます。

スクリーンショット 2020-12-08 14.21.19.png

とはいえ、現状のコードだけではこのようには表示されません。
以下の部分の設定が抜けているからです。

data1: this.$service.service1.get1(),
data2: this.$service.service1.get2(),

そしてこれは、今回のDI方法にも関連する部分となっています。
上記の設定やDIは、plugins配下で実現しています。

DI設定

Nuxt.jsでは inject() というものが用意されており、これを使用することで変数などをグローバルに登録することができます。
この機能をpluginsディレクトリ配下で使うことで、pages配下で行ったような記法やDIを実現します。

plugins/service.tsは、以下のような記述となっています。

import { Context } from '@nuxt/types'
import { Inject } from '@nuxt/types/app'
import { Persistence1Impl } from '~/model/persistence/persistence1'
import { Persistence2Impl } from '~/model/persistence/persistence2'
import { Repository1Impl } from '~/model/repository/repository1'
import { Repository2Impl } from '~/model/repository/repository2'
import { Service1, Service1Impl } from '~/model/service/service1'

export interface Service {
  service1: Service1
}

export default function ({ $axios }: Context, inject: Inject): void {
  const service: Service = {
    service1: getService1(),
  }
  inject('service', service)
}

function getService1(): Service1 {
  const persistence1 = new Persistence1Impl()
  const persistence2 = new Persistence2Impl()
  const repository1 = new Repository1Impl(persistence1)
  const repository2 = new Repository2Impl(persistence2)

  return new Service1Impl(repository1, repository2)
}

このようにここでmodel配下のコードを予め組み立てて inject するようにしておき、Nuxt.js標準の nuxt.config.js

  plugins: ['@/plugins/service'],

のように設定することで、Vue.jsのアプリケーション初期化前にこの処理が実行されるので、グローバルにmodel配下の処理が登録されます。
inject した変数等は $ + {引数で渡した文字列名} で登録されるので、今回の場合実行時は

this.$service.*

の形で参照できるようになります。

ちなみにTypescriptの場合、これだけでは型が判別できないので、typesディレクトリ配下で型定義を行います。
types/index.d.tsにて、

import { Service } from '~/plugins/service'

declare module 'vue/types/vue' {
  interface Vue {
    readonly $service: Service
  }
}

declare module 'vuex' {
  interface Store<S> {
    readonly $service: Service
  }
}

のように記述しておき、こちらもNuxt.js標準の tsconfig.json

{
    *,
    "types": [
      *,
      "types/index.d"
    ],
    *
}

のように記述することで、型を判別してくれるようになります。

pluginsでDI行うその他の利点

上記で示した点以外にも、pluginsでDIを行うことの利点があります。
それは、model配下にNuxt.jsのContext情報を持っていくことができることです。
例えば今回の例だと、

import { Context } from '@nuxt/types'
import { Inject } from '@nuxt/types/app'
import { NuxtAxiosInstance } from '@nuxtjs/axios'
import { Persistence1Impl } from '~/model/persistence/persistence1'
import { Persistence2Impl } from '~/model/persistence/persistence2'
import { Repository1Impl } from '~/model/repository/repository1'
import { Repository2Impl } from '~/model/repository/repository2'
import { Service1, Service1Impl } from '~/model/service/service1'

export interface Service {
  service1: Service1
}

export default function ({ $axios }: Context, inject: Inject): void {
  const service: Service = {
    service1: getService1($axios),
  }
  inject('service', service)
}

function getService1($axios: NuxtAxiosInstance): Service1 {
  // persistenceのコンストラクタで $axios: NuxtAxiosInstance を受け取る処理が必要
  const persistence1 = new Persistence1Impl($axios)
  const persistence2 = new Persistence2Impl($axios)
  const repository1 = new Repository1Impl(persistence1)
  const repository2 = new Repository2Impl(persistence2)

  return new Service1Impl(repository1, repository2)
}

このようにすることで、NuxtAxiosをmodel配下でも使用できるようになります。

ユニットテスト

ここまでで一通り処理ができるようになりましたが、せっかくなのでユニットテストをどうやるかまで紹介していきます。
ユニットテスト用のファイルは、こちらもNuxt.js標準のtestディレクトリ配下に作成します。

Vue.jsでユニットテストを行う場合、View部分からテストを行おうとすると、ユニットテスト上でViewをマウントしたり操作する必要があり少し面倒です。
もちろん必要性があればやるべきだとは思いますが、とりあえず最低限のユニットテストで良いのであれば、今回の構成の場合model配下をテストすれば最低限と言えると思います。

ファイルを見てもらえば分かる通り、今回はすべてInterfaceを経由してアクセスしていく形にしているので、ユニットテストも容易です。
以下のようになっています。

service/service1.spec

import { Repository1 } from '~/model/repository/repository1'
import { Repository2 } from '~/model/repository/repository2'
import { Service1, Service1Impl } from '~/model/service/service1'

describe('Service1', () => {
  let service1: Service1

  class Repository1Mock implements Repository1 {
    get(): string {
      return 'テスト1'
    }
  }

  class Repository2Mock implements Repository2 {
    get(): string {
      return 'テスト2'
    }
  }

  beforeAll(() => {
    const repository1Mock = new Repository1Mock()
    const repository2Mock = new Repository2Mock()
    service1 = new Service1Impl(repository1Mock, repository2Mock)
  })

  it('get string from repository1', () => {
    const actualResult = service1.get1()
    const expectedResult = 'Service1からテスト1'

    expect(actualResult).toEqual(expectedResult)
  })

  it('get string from repository2', () => {
    const actualResult = service1.get2()
    const expectedResult = 'Service1からテスト2'

    expect(actualResult).toEqual(expectedResult)
  })
})

repository/repository1.spec

import { Persistence1 } from '~/model/persistence/persistence1'
import { Repository1Impl } from '~/model/repository/repository1'

describe('Repository1', () => {
  class Persistence1Mock implements Persistence1 {
    get(): string {
      return 'テスト'
    }
  }

  it('get string from persistence1', () => {
    const persistence1Mock = new Persistence1Mock()
    const repository1 = new Repository1Impl(persistence1Mock)
    const actualResult = repository1.get()
    const expectedResult = 'Repository1経由でテスト'

    expect(actualResult).toEqual(expectedResult)
  })
})

repository/repository2.spec

import { Persistence2 } from '~/model/persistence/persistence2'
import { Repository2Impl } from '~/model/repository/repository2'

describe('Repository2', () => {
  class Persistence2Mock implements Persistence2 {
    get(): string {
      return 'テスト'
    }
  }

  it('get string from persistence1', () => {
    const persistence2Mock = new Persistence2Mock()
    const repository2 = new Repository2Impl(persistence2Mock)
    const actualResult = repository2.get()
    const expectedResult = 'Repository2経由でテスト'

    expect(actualResult).toEqual(expectedResult)
  })
})

さいごに

以上が、私が現在使っているアーキテクチャになります。
もちろんこれらはかなり簡略化していますので、例えば必要なエンティティがあればmodel配下に entity ディレクトリを作ってそこに作るようにしたり、今回私が示したレイヤーが多すぎる・少なすぎるのであれば適宜追加・削除して貰えればと思います。

この形が必ずしも正解だとは思っていませんが、何かしらの参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Full Static Generationを試す

Nuxt.jsの Full Static Generationとは Nuxt 2.13で導入された
APIレスポンスも合わせてすべて静的化するための機能です。

今回はnpm packageのjson-serverをつかってモックapiを作成して、
APIの値を取得して静的書き出しをするところまでやってみます。
(Nuxt @ v2.14.9で試しています)

API側の準備

jsonを用意

db.json
{
  "news": [
    {
      "id": "title_001",
      "body": "Hello World"
    },
    {
      "id": "title_002",
      "body": "おはようございます"
    },
    {
      "id": "title_003",
      "body": "こんにちは"
    },
    {
      "id": "title_004",
      "body": "おやすみ"
    }
  ]
}

コマンドの追加

package.json に
npm run api_serverというコマンドを追加します。

package.json
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "api_server" : "node_modules/.bin/json-server --watch db.json --port 3333"
  }

Nuxtの準備

nuxt.config.jsの設定

はじめにnuxt.config.jsを
generateモードに設定するためにtargetをstaticにします。

nuxt.config.js
  target :"static",

今回のFile構成

-| pages/
---| index.vue
---| news/
-----| _slug.vue

Nuxt v2.13から generate時に内部的にクローリング処理が行われ、
リンク先を自動的に検出してページ生成を行われるようになりました。

たとえばnews/index.vueを作成して

news/index.vue
<template>
  <div class="container">
    <ul>
      <li v-for="item in news">
        <nuxt-link :to="`/news/${item.id}`">
          {{item.body}}
        </nuxt-link>
      </li>
    </ul>
    </div>
</template>

<script>
export default {
  async asyncData ({ params}) {
    return axios.get('http://localhost:3333/news').then((res) => {
      return {news : res.data}
    }).catch((error) => {
      return { error: error }
    })
  }
}
</script>

このようなファイルを用意してnpm run generateをたたくと
APIから取得したデータと<nuxt-link>から動的に静的ファイルが生成されます。
(Nuxt v2.13以降)

今回はリンクされていないページを想定し
_slug.vueにAPIにあるidの値を元に動的にファイルを生成させることにします。

nuxt.config.jsにAPIを取得するコードを追加

nuxt.config.js
  generate: {
    routes () {
      return axios.get('http://localhost:3333/news')
        .then((res) => {
          return res.data.map((news) => {
            return {
              route: '/news/' + news.id,
              payload: news
            }
          })
        })
    }
  }

generate routesで使われるのがAPIのレスポンスデータのnews.idを使ってのものだけだと
_sulg.vue側で都度取得することになってしまい生成時間の増加につながってしまうらしく、
そういう場合はpayloadを設定して(今回でいうとAPIのデータ "id","body"をpaylodで渡せる)、動的ルーティング生成の高速化をおこなうようです。

_sulg.vue側のコード

動的に表示する_sulg.vueは下記の様に設定します。

_sulg.vue
<template>
  <div class="container">
    <div>
        <p>title : {{id}}</p>
        <p>content : {{body}}</p>
    </div>
    </div>
</template>


<script>
import axios from "axios";

export default {
  data () {
    return {
      id:"",
      body:""
    }
  },
  async asyncData ({ params, error, payload }) {
    if (payload) {
      return {
        id: payload.id,
        body: payload.body,
      }
    } else {
      return axios.get('http://localhost:3333/news').then((res) => {
        return res.data.find((post) => post.id === params.slug);
      }).catch((error) => {
        return { error: error }
      })
    }
  }
}
</script>

npm run devで開発できるように
payloadの条件分岐をわけています。

generate

この状態で
1.json-server を立ち上げ

npm run api_server

2.generateする

npm run generate

APIの情報を取り込みファイルを書き出せました。

-| dist/
---| index.html
---| news/
-----| title_001/
-------| index.html
-----| title_002/
-------| index.html
...略

news/title_001/index.html
image.png

無事APIレスポンスを取り込んで書き出すことができました。
今回はAPIの部分をjson-serverを使ったモックで済ませましたが本来 strapi+nuxt.jsでアドベントカレンダーを作ろうのようにHeadless CMSからのAPIを取得から生成を試して見たかったのですが、それはまたどこかで。


:christmas_tree: FORK Advent Calendar 2020
:arrow_left: 15日目 前の日の記事のタイトル @yoh_zzzz
:arrow_right: 17日目 次の日の記事のタイトル @sy12345

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】コンポーネントの再レンダリング if編

はじめに

Vueを使っていて、ローカルストレージ(indexedDB)に値の保存したとき、
コンポーネントの中の要素を更新したい(再レンダリング)状況になりました。
その時、躓いたため記録します。

例えばCRADをし、コンポーネントの一部分だけ更新したいときに役に立ちます。

環境

@vue/cli 4.4.6

解説

v-ifを使ったら、コンポーネントが再計算されます。

以下をすることで、
子コンポーネントで処理が終わったあとに、
ページ更新などをしなくても再描画されます。

前提知識
①emit → 子から親をいじれる奴
②$nextTick → DOMの更新サイクル後に、子コンポーネントを再計算させる。
(単純にtrue→false→trueとやっても再描画はうまく行かない。)

Parent.vue
<template>
  <div>
    <child v-if="showChild" @add="toggle"></child>
  </div>
</template>

<script>
import Child from "./Child.vue";

export default {
  components: {
    Child
  },
  data() {
    return {
      showChild: true
    };
  },
  methods: {
    toggle() {
      this.showChild = false;
      this.$nextTick(() => (this.showChild = true));
    }
  }
};
</script>
Child.vue
<template>
    <button class="button" @click="add">追加する</button>
</template>

<script>
    export default {
        methods: {
            add(){
                //何かしらCRAD等の処理の後に↓
                this.$emit('add');
            }
        }
    }
</script>

ご参考にさせていただいた記事

https://tomatoaiu.hatenablog.com/entry/2019/09/28/133319
https://qiita.com/shosho/items/b9b24a52dc0cc0fc33f5

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】コンポーネントの更新・再レンダリング if編

はじめに

Vueを使っていて、ローカルストレージ(indexedDB)に値の保存したとき、
コンポーネントの中の要素を更新したい(再レンダリング)状況になりました。
その時、躓いたため記録します。

例えばCRADをし、コンポーネントの一部分だけ更新したいときに役に立ちます。

環境

@vue/cli 4.4.6

解説

v-ifを使ったら、コンポーネントが再計算されます。

以下をすることで、
子コンポーネントで処理が終わったあとに、
ページ更新などをしなくても再描画されます。

前提知識
①emit → 子から親のメソッドをいじれたりする
②$nextTick → DOMの更新サイクル後に、子コンポーネントを再計算させる。
(単純にtrue→false→trueとやっても再描画はうまく行かない。)

Parent.vue
<template>
  <div>
    <child v-if="showChild" @add="toggle"></child>
  </div>
</template>

<script>
import Child from "./Child.vue";

export default {
  components: {
    Child
  },
  data() {
    return {
      showChild: true
    };
  },
  methods: {
    toggle() {
      this.showChild = false;
      this.$nextTick(() => (this.showChild = true));
    }
  }
};
</script>
Child.vue
<template>
    <button @click="add">追加する</button>
</template>

<script>
    export default {
        methods: {
            add(){
                //何かしらCRAD等の処理の後に↓
                this.$emit('add');
            }
        }
    }
</script>

ご参考にさせていただいた記事

https://tomatoaiu.hatenablog.com/entry/2019/09/28/133319
https://qiita.com/shosho/items/b9b24a52dc0cc0fc33f5

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】メソッド実行した際のコンポーネントの更新・再レンダリング if編

はじめに

例えばCRADメソッドを実行、
その後コンポーネントの一部分だけ更新したいときありませんか。

環境

@vue/cli 4.4.6

解説

v-ifを使ったら、コンポーネントが再計算されます。

以下をすることで、
子コンポーネントで処理が終わったあとに、
ページ更新などをしなくても再描画されます。

前提知識
①emit → 子から親のメソッドをいじれたりする
②$nextTick → DOMの更新サイクル後に、子コンポーネントを再計算させる。
(単純にtrue→false→trueとやっても再描画はうまく行かない。)

Parent.vue
<template>
  <div>
    <child v-if="showChild" @add="toggle"></child>
  </div>
</template>

<script>
import Child from "./Child.vue";

export default {
  components: {
    Child
  },
  data() {
    return {
      showChild: true
    };
  },
  methods: {
    toggle() {
      this.showChild = false;
      this.$nextTick(() => (this.showChild = true));
    }
  }
};
</script>
Child.vue
<template>
    <button @click="add">追加する</button>
</template>

<script>
    export default {
        methods: {
            add(){
                //何かしらCRAD等の処理の後に↓
                this.$emit('add');
            }
        }
    }
</script>

ご参考にさせていただいた記事

https://tomatoaiu.hatenablog.com/entry/2019/09/28/133319
https://qiita.com/shosho/items/b9b24a52dc0cc0fc33f5

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vueのstyleの上書き(子コンポーネントに対しての指定)

子コンポーネントに対してのstyleの上書き

ディープセレクタ>>>で繋ぐ。

<style scoped>
.親コンポーネントクラス >>> .子コンポーネントクラス {
  background-color: #fff000 !important
}
</style>

情報元こちらです?
https://blog.piyo.tech/posts/2018-10-23-vuejs-override-child-component-style/
https://vue-loader-v14.vuejs.org/ja/features/scoped-css.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vueのstyleの上書き(親コンポーネントから子コンポーネントに対してオーバーライド)

子コンポーネントに対してのstyleの上書き

ディープセレクタ>>>で繋ぐ。

<style scoped>
.親コンポーネントクラス >>> .子コンポーネントクラス {
  background-color: #fff000 !important
}
</style>

情報元こちらです?
https://blog.piyo.tech/posts/2018-10-23-vuejs-override-child-component-style/
https://vue-loader-v14.vuejs.org/ja/features/scoped-css.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[解決](Rails, Vue)CORSエラーAccess to XMLHttpRequest at 'http://localhost:3000' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

なぜこのエラーが出るのか。

https://qiita.com/att55/items/2154a8aad8bf1409db2b

解決方法(Rails, Vue)

railsのgem

gemfile
#gem rack-coes

のコメントアウトを外し、

$bundle install


config/initializers/cors.rbにある記述も下のようにコメントアウト。

config/initializers/cors.rb
# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:3000', 'https://localhost:8080/'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

originsは、サーバーサイド(API),フロントの順で書きます。

Dockerの場合は、再度コンテナを立ち上げて

完了。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue vue-router編

はじめに

今回はvue-routerについてまとめる。自分自身まだまだvue-routerにわかなので間違っているところがあれば指摘していただけると嬉しいです。

実行環境

macOS Catalina バージョン 10.15.7
MacBook Pro 13インチ
Vue 2.6.12

vue-routerとは?

vue-routerを学ぶまではページ遷移をすることがなく、そのページで完結するウェブアプリケーションしか知らなかったが、ページ遷移を必要とするウェブアプリケーションを作ろうとしたとき、vue-routerが必要になった。vue-routerとはurlとコンポーネントを関連づけ、ページ遷移を可能にする機能のことである。

vue-routerを導入する

インストール

vue-routerをインストールするには以下のコマンドを叩けばインストールすることができる。

npm install vue-router

これでvue-routerを使うことができる状態が整った。

router.jsに記述

router.jsというファイルを新しく作成し、以下のように記述する。別にrouter.jsを作成せず、直接main.jsに記述することもできるが、初心者なので習ったように真似する。

router.js
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
  routes: [{},{},{}]
})

上のコードをみるとまず、Routerをインポートしている。その後、Vue.use(Router)としてvueのプラグイン(機能拡張用のソフトウェア)を使用する宣言をしている。最後に新しくRouterを定義し、他のファイルでインポートできるようにしている。routesのオブジェクトの部分には第一引数にパス、第二引数にコンポーネントを指定することでパスを指定するとそれに紐づいたコンポーネントが表示されるようになる。
続いてmain.jsを編集する。

main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

三行目でrouterをrouter.jsからインポートしている。そしてvueインスタンスを定義しているところでrouter,と記述することでvue-routerを使用することができるようになる。

簡単なルーティングアプリケーションを作ってみる

vue-routerを使って簡単にアプリケーションを作ってみる。こんな感じのアプリケーションを想定している。(gif画像でアプリケーションを動的に示したかったのですが、うまく埋め込むことができませんでした。何か良い方法があればぜひ教えていただきたいです。)

スクリーンショット 2020-12-07 23.36.51.png

コンポーネント作成

まず、コンポーネントを作成する。srcディレクトリの直下にviewsディレクトリを作成し、その中にページ遷移させるためのコンポーネントを作成する。

スクリーンショット 2020-12-08 7.47.56.png

ToHome,ToSetting,ToDetailボタンを押下したときにHome,Setting,ToDetailコンポーネントを表示し、さらにHomeコンポーネントの中でProductA,ProductBのリンクを踏むとそれぞれのコンポーネントを表示するという設計にする。これがrouter.jsのコードに関係してくる。

router.jsの編集

router.js
export default new Router({
  routes: [
    {
      path: '/home',
      component: Home,
      children: [
        {
          path: '/ProductHome/productA',
          component: ProductA   
        },
        {
          path: '/ProductHome/productB',
          component: ProductB
        },
      ]
    },
    {
      path: '/detail',
      component: Detail
    },
    {
      path: '/setting',
      component: Setting
    },
  ]
})

HomeコンポーネントにあたるProductA,ProductBコンポーネントはHomeコンポーネントのパスが設定してあるところでchildrenオプションを付与し、その中で定義することができる。

各コンポーネントの編集

App.vue
<template>
  <div>
    <h1>This is my homepage.</h1>
    <router-link to="/home" class="btn btn-primary">ToHome</router-link>
    <router-link to="/setting" class="btn btn-danger">ToSetting</router-link>
    <router-link to="/detail" class="btn btn-info">ToDetail</router-link>
    <router-view></router-view>
  </div>
</template>

router-viewコンポーネントはrouter.jsで定義されたパスに応じて変化する動的なコンポーネントである。
router-linkコンポーネントはクリックすると、to属性に指定しているurlに遷移する。ここでデベロッパーツールでこの要素を見てみると、

スクリーンショット 2020-12-08 8.07.59.png

と表示されており、働きとしてはaタグと同じであるということがわかる。router-linkで表示するコンポーネントを決定し、router-viewコンポーネントでそのコンポーネントを表示するというはたらきをしている。

Home.vue
<template>
    <div>
        <h1>Home</h1>
        <router-link to="/ProductHome/productA">ProductA</router-link>
        <router-link to="/ProductHome/productB">ProductB</router-link>
        <router-view></router-view>
    </div>
</template>
Detail.vue
<template>
    <div>
        <h1>Detail</h1>
    </div>
</template>
Setting.vue
<template>
    <div>
        <h1>Setting</h1>
    </div>
</template>
ProductA.vue
<template>
    <div>
        <h3>This is my productA.</h3>
    </div>
</template>
ProductB.vue
<template>
    <div>
        <h3>This is my productB.</h3>
    </div>
</template>

これで目的のアプリケーションを作ることができた。このアプリケーションを作っていて学んだことで重要だと思ったのはrouter.jsのパスの指定の仕方である。Homeコンポーネントの直下でさらにProductA,ProductBコンポーネントをurlによって切り替えたいときにrouter.jsでのこれら二つのコンポーネントのパスの指定の仕方はHome,Setting,Detailコンポーネントと同列に指定するのではなく、Homeコンポーネントのパスの指定の中のchildrenオプション中でパスを指定している。これは親コンポーネントと小コンポーネントの位置関係と対応しているということがわかる。

さいごに

これまでの記事でvue.jsのcomponent,slot,form,vue-routerなどについてまとめてきた。残りのきじでvuexについてまとめた後、それの集大成としてなんらかのアプリケーションを作成し、qiitaでアウトプットしようと思う。

参考文献 https://www.ritolab.com/entry/181

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mapbox-gl.js + Vue.js で OpenStreetMap タイルの地図を表示する

この記事は CivicTechテック好き Advent Calendar 2020 の 8 日目の記事です。

この記事について

この記事は Vue.js と mapbox-gl.js を組み合わせて OpenStreetMap (OSM) タイルの地図を表示する方法についての記事です。

地域課題と結びつくことが多いシビックテックのプロジェクトでは、地図をアプリに組み込む機会が結構あります。
以前 (例えば初期版の紙マップ) は Leaflet も選択肢でしたが、バイナリベクトルタイルの表示や TypeScript との相性の良さなどから、現行版の紙マップ をはじめとして、mapbox-gl.js を使ったプロジェクトも増えています。東京都新型コロナウイルス感染症対策サイトで一時的に表示されていた人口推移(参考値)のマップ(現在は非表示)の実装を通して私も触る機会 があり、使いやすかったため新しいプロジェクトでも採用したりしています。
本記事では mapbox-gl.js を Vue.js のプロジェクトで使う際に、最初に作る最低限の地図の表示までの実装を紹介します。

Vue.js との組合せは VueMapbox など、ライブラリ化されたコンポーネントもありますが、今回は mapbox-gl.js を直接使います。地図を出すコンテナ要素の下に作られる DOM 要素たちのことを特に気にしなければ、それほど相性の悪いライブラリでもないため、素直に組み合わせることができます。

最小限のコードで動く完成品が CodePen に置いてあります。

環境

  • Vue.js 2.6.11
  • mapbox-gl.js 1.13.0

作るもの

image.png

タイルマップの標準的な入力処理と表示だけができるマップです。

コンポーネントの実装

実装の方針として、テンプレートは id 付きの div 要素を一つ持ち、 mouted フックの中で mapboxgl.Map オブジェクトの初期化を行います。mapboxgl.Map オブジェクトへのアクセスは頻繁に行いたくなるので、このコンポーネントのデータオブジェクトとして保持しておき、また、マップの初期位置、初期ズームは外から与えたい場合が多いため、コンポーネントのプロパティに持たせます。最後に、mapbox-gl.js では地図を出すコンテナ要素に ID を使うため、1 ページに複数の地図を出したりできるように、この ID もプロパティとして外から与えられるようにしておきます。

これらのことをコードで表現すると以下のようになります。

const mapComponent = {
  template: `<div :id="mapId"/>`,
  props: {
    defaultCenterLat: { type: Number, default: 35.6811574199219 },
    defaultCenterLng: { type: Number, default: 139.76702113084735 },
    defaultZoom: { type: Number, default: 14 },
    mapId: { type: String },
  },
  data() {
    return { map: null }
  },
  mounted() {
    this.createMap()
  },
  methods: {
    createMap() {
      this.map = new mapboxgl.Map({
        container: this.mapId,
        zoom: this.defaultZoom,
        style: {
          version: 8,
          sources: {
            OSM: {
              type: 'raster',
              tiles: [
                'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png',
                'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png',
              ],
              tileSize: 256,
              attribution : '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
            },
          },
          layers: [{ id: 'OSM', type: 'raster', source: 'OSM' }],
        },
        center: [this.defaultCenterLng, this.defaultCenterLat],
      })
    },
  },
}

呼び出し方について

あとは Vue コンポーネントとして登録し、map-component として map-id 指定と共に呼び出せば完成です。

new Vue({
  el: '#app',
  components: {
    mapComponent,
  }
})
<div id="app">
  <map-component :map-id="'map'"></map-component>
</div>

複数出す場合、例えば 2 枚の地図を出して、1 枚目と 2 枚目で初期位置を変えるなどする場合は、map-id として異なる文字列を与え、 default-center-latdefault-center-lng にそれぞれ緯度、経度を与えます。

<div id="app">
  <map-component :map-id="'map1'"> </map-component>
  <map-component :map-id="'map2'" :default-center-lat="35.688515514961521" :default-center-lng="139.70118255576324"> </map-component>
</div>

image.png

スタイルについて

コントロールや帰属表示のスタイリングに、 mapbox-gl.js では専用の CSS を持っています。
https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.csslink タグで読み込むか、mapbox-gl モジュールにバンドルされているものを mapbox-gl/dist/mapbox-gl.css からインポートする必要があります。

また、地図を出すコンテナの大きさが 0 だと、全く地図が描画されないように見えるのですが、JavaScript 側のバグを疑って時間を浪費してしまうことが多かったため、開発中は CSS の ID 指定でコンテナ要素の大きさを指定するなどしています。

#map {
  height: 500px;
  width: 960px;
}

まとめ

Vue.js と mapbox-gl.js を使って OSM タイルの地図を表示する方法の一つを紹介しました。ここから機能拡張していく前提で、mapboxgl.Map オブジェクトをデータオブジェクトに持ち、コンテナの ID と、初期の中心とズームをプロパティとして外部から与えられるだけの、簡素なものとして実装しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

親子3世代のバケツリレーをVuexを使わずに状態管理

こんにちは。
株式会社アドベンチャーで、UI/UXデザイナーとフロントエンド周りの開発をしている@f-umebaraと申します。
株式会社アドベンチャーのAdvent Calendar 2020 7日目です。
前回参加したAdventure Advent Calendar 2018が2年前ということに驚愕です。:(;゙゚'ω゚'):ガクブル。

さて、最近vue.jsの開発をしておりまして、
内容としてはよくあるものですが自分の忘備録も兼ね、行なったことを書いてみます。

Vuexを使わずに状態管理

Vuexを使うまででもない規模(というか使えない)場合にて、コンポーネント間の親子3世代のバケツリレーが辛くなりました。(↓私の中でこんな感じです)
store_1.png
propsとemitが複雑に。。
おばあちゃん辛いよ。孫よ、直接やりとりして欲しい〜。

何か良い方法がないかと調べたところ、オブジェクトをリアクティブにするVue.observableを使ってVuexライクにシンプルな状態管理ができるとのことで、やってみる。

store.js
import Vue from "vue";
import axios from "axios";

// state 状態
export const state = Vue.observable({
  userNumber: 0,
});
// getters これで呼び出す
export const getters = {
  getUserNumber() {
    return state.userNumber;
  }
}
// mutations 状態を変更
export const mutations = {
  updateUserNumber(userNumber) {
    state.userNumber = userNumber;
  }
}
// actions api呼び出して状態を変更するmutationsを呼ぶ
export const actions = {
  async fetchUserNumber(){
    const res = await axios.post(apiUrl,{
      "userNumber": '1'
    })
    const userNumber = res.data.userNumber;
    mutations.updateUserNumber(userNumber);
  }
}

上記をvueから呼び出して使用します。

component.vue
<template>
 <p>状態管理した値は{{ getUserNumber }}です</p>
</template>

<script>
import { getters, actions } from "../store";

export default {
 methods: {
   fetchUserNumber() {
     actions.fetchUserNumber();
   }
 },
 computed: {
   getUserNumber() {
     return getters.getUserNumber();
   }
 }
}
</script>

store_2.png
こうなったイメージ。
storeて便利だなぁと改めて感じました。

しかしvue3からの変更で書き方は変更した方が良さそうです。

将来の互換性を考えると、Vue.observable に渡したオブジェクトではなく、返されたオブジェクトを使うことを推奨

TypeScriptでVuexを使わないという選択肢

Vue.js公式より

Vue 2.x では、Vue.observable は渡されたオブジェクトを直接操作するため、ここでデモされる ように戻り値のオブジェクトと等しくなります。Vue 3.x では、代わりにリアクティブプロキシを返し、元のオブジェクトを直接変更してもリアクティブにならないようにします。そのため、将来の互換性を考えると、Vue.observable に渡したオブジェクトではなく、返されたオブジェクトを使うことを推奨します。

store.js
class Store {
 // 状態管理部分
}
export default Vue.observable(new Store());

上記はまだ実装できていないですが、vue3を考えて変更を検討したいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

親子3世代のバケツリレーが辛いのでVuexを使わずに状態管理

こんにちは。
株式会社アドベンチャーで、UI/UXデザイナーとフロントエンド周りの開発をしている@f-umebaraと申します。
株式会社アドベンチャーのAdvent Calendar 2020 7日目です。
前回参加したAdventure Advent Calendar 2018が2年前ということに驚愕です。:(;゙゚'ω゚'):ガクブル。

さて、最近vue.jsの開発をしておりまして、
内容としてはよくあるものですが自分の忘備録も兼ね、行なったことを書いてみます。

Vuexを使わずに状態管理

Vuexを使うまででもない規模の場合にて、コンポーネント間の親子3世代のバケツリレーが辛くなりました。(↓私の中でこんな感じです)
store_1.png
propsとemitが複雑に。。
おばあちゃん辛いよ。孫よ、直接やりとりして欲しい〜。

何か良い方法がないかと調べたところ、オブジェクトをリアクティブにするVue.observableを使ってVuexライクにシンプルな状態管理ができるとのことで、やってみる。

store.js
import Vue from "vue";
import axios from "axios";

// state 状態
export const state = Vue.observable({
  userNumber: 0,
});
// getters これで呼び出す
export const getters = {
  getUserNumber() {
    return state.userNumber;
  }
}
// mutations 状態を変更
export const mutations = {
  updateUserNumber(userNumber) {
    state.userNumber = userNumber;
  }
}
// actions api呼び出して状態を変更するmutationsを呼ぶ
export const actions = {
  async fetchUserNumber(){
    const res = await axios.post(apiUrl,{
      "userNumber": '1'
    })
    const userNumber = res.data.userNumber;
    mutations.updateUserNumber(userNumber);
  }
}

上記をvueから呼び出して使用します。

component.vue
<template>
 <p>状態管理した値は{{ getUserNumber }}です</p>
</template>

<script>
import { getters, actions } from "../store";

export default {
 methods: {
   fetchUserNumber() {
     actions.fetchUserNumber();
   }
 },
 computed: {
   getUserNumber() {
     return getters.getUserNumber();
   }
 }
}
</script>

store_2.png
こうなったイメージ。
storeて便利だなぁと改めて感じました。

しかしvue3からの変更で書き方は変更した方が良さそうです。

将来の互換性を考えると、Vue.observable に渡したオブジェクトではなく、返されたオブジェクトを使うことを推奨

TypeScriptでVuexを使わないという選択肢

Vue.js公式より

Vue 2.x では、Vue.observable は渡されたオブジェクトを直接操作するため、ここでデモされる ように戻り値のオブジェクトと等しくなります。Vue 3.x では、代わりにリアクティブプロキシを返し、元のオブジェクトを直接変更してもリアクティブにならないようにします。そのため、将来の互換性を考えると、Vue.observable に渡したオブジェクトではなく、返されたオブジェクトを使うことを推奨します。

store.js
class Store {
 // 状態管理部分
}
export default Vue.observable(new Store());

上記はまだ実装できていないですが、vue3を考えて変更を検討したいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt/Composition API】Cannot read property '$auth' of undefined の解決法

はじめに

@nuxt/authを使ってログイン認証をしようとしていたがエラーが出て詰まったのでメモ。
※ めちゃくちゃ初歩的なミスです。

環境

"nuxt": "^2.14.6",
"@vue/composition-api": "^1.0.0-beta.20",
"@nuxtjs/auth": "^4.9.1"

問題のコード

onSubmitButtonClickでリクエストを送りたい

signIn.vue
<script lang="ts">
import { defineComponent, reactive, ref } from '@vue/composition-api'
export default defineComponent({
  setup() {
    const email = ref('')
    const password = ref('')
    const onSubmitButtonClick = (e: Event) => {
      this.$auth
        .loginWith('local', {
          // emailとpasswordの情報を送信
          data: {
            email: email,
            password: password,
          },
        })
        .then(
          (res:any) => {
            // 認証成功後に実行したい処理
          },
          (e: Error) => {
            // 失敗時の処理
          }
        )
    }
    return { email, password, onSubmitButtonClick }
  },
})
</script>
nuxt.config.ts
export default {
  ...
  modules: [
    '@nuxtjs/axios',
    '@nuxtjs/auth'
  ],
  ...
}
tsconfig.json
{
  ...
  "types": [
    "@types/node",
    "@nuxt/types",
    "@nuxtjs/axios",
    "@nuxtjs/auth",
  ],
  ...
}

エラーメッセージ

Cannot read property '$auth' of undefined

nuxt.config.tsの設定もしてるのになぜ、、、

解決法

composition APIの理解の甘さでした。
Vue 2.xまでthis.$~で取得していたものは、compositionAPIではsetup関数の第二引数であるcontextから取得する様変更された様です。

公式ドキュメントにもありました。

The second argument provides a context object which exposes a selective list of properties that were previously exposed on this in 2.x APIs:

https://composition-api.vuejs.org/api.html#setup

const MyComponent = {
  setup(props, context) {
    context.attrs
    context.slots
    context.emit
  }
}

今回の場合は

this.$authcontext.root.$auth

に変更した所、解決しました。

signIn.vue
setup(_props, context) {
...
  const onSubmitButtonClick = (e: Event) => {
    context.root.$auth.loginWith('local', {
...

まとめ

今回はcompositionAPIもnuxtも理解が曖昧なまま進めた結果詰まってしまいました、、
勿体ないのでインプットも丁寧にしていきたい。


Twitterやってるのでフォローお願いします!
https://twitter.com/1keiuu

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue-apolloでdefaultClientを無理矢理リアクティブ(動的)に切り替える

この記事は?

DMM advent calendar 2020 5日目(3日遅れ)の記事です。遅れてごめんなさい・・・

なぜ意地でも5日目にしたかったかというと、12/5が誕生日だったからです?

概要

vue-apolloにはmultiple clientと呼ばれる機構が存在する。

例えば、GraphQLサーバーが2つに別れており、ユーザー系の情報はAに、商品系の情報はBに格納されているといった場合に、エンドポイントや認証情報を各クエリ毎に切り替えるといった用途に使える。

しかし、今回やりたいのは、同じクエリに対して認証状態に応じてエンドポイント≒クライアントを切り替えたいのである。

その手法として、認証状態を管理しているVuexStore上でvue-apolloのprovider情報を書き換える事で動的にクライアントを切り替える方法を探していた。

ここに記載している方法を使えば、例に上げているエンドポイントだけでなくトークンや認証情報等の定義可能な情報をまるごと切り替えできる。

結論が先に見たい方はこちら → まとめ

切り替える手段を探る

vue-apolloのSmart Apolloで切り替えできないか

まず、nuxt.config.js内でこんな感じでエンドポイントだけ切り替えたいが、それ以外は共通みたいな感じで定義しておく

nuxt.config.js
{

/* ~以上略= */

  apollo: {
    clientConfigs: {
      default: {
        httpEndpoint: 'https://example.com/guestGateway'
      },
      user: {
        httpEndpoint: 'https://example.com/gateway'
      }
    }
  },

/* ~以下略~ */

}

で、このclient設定を元にSmartQueryでclientを分ける場合、 guest のクライアント設定を使用するコンポーネント上でこのように書くと思う

viewer.vue
viewer: {
  query:gql`
    query { 
      viewer { 
        login
      }
    }
  `,
  client: 'user'
}    

が、この辺りのドキュメントがびっくりするほど見当たらない。何ならAPI ReferenceのsmartQueryの項にmultiple Clientに関するキーが全く書かれていない。Advanced TopicのMultiple Clientの項目に書かれてる内容がドキュメントの全て。

https://apollo.vuejs.org/guide/multiple-clients.html

Vue界隈のこういう応用ドキュメントが雑なとこが嫌いなんだ そんなわけで、ここにコードを足して、data属性内の情報や算出プロパティの情報を元に defaultguest を入れ替えてみる。

viewer.vue
viewer: {
  query:gql`
    query { 
      viewer { 
        login
      }
    }
  `,
  client: this.isLogin ? 'user' : 'default' // ここでエラー
}    

さて、このキーは果たしてリアクティブに書けるのか、答えはノーである。

スクリーンショット 2020-12-08 1.02.04.png

VScodeのVutrが見事にエラーにするし、実行しても、もちろんErrorになる。

今回やりたいのはログイン状態に応じてエンドポイントを切り替えるという動作。既にこの時点で出来ない。

もちろん、算出プロパティ等で書くと、多分created前に呼ばれるのでundefinedになって動かない。

Apollo Providerにインジェクションしてみる

じゃあこれをどうするか。

vue-apolloには3種類ほどGraphQLへアクセスする手段を提供している。

最初に述べたdata属性のようにapollo自信が振る舞う「SmartQuery」、

v-slotを使ってクエリの結果をインジェクションする「Apollo components」、

そして、もっともオーソドックスな、react向けに使われるApollo clientと同じような使い方ができる「Apollo Provider」「Dollar Apollo」がある。

※vue-apollo v4からはVue3のCompositionAPIに対応した apollo-composable が使用できますが、この話はVue2×vue-apollo v3での悩みを解決したときの話なので割愛します。

「Dollar Apollo」のプロパティには provider と呼ばれるApollo Providerが挿入されたキーを持っている。

https://vue-apollo.netlify.app/api/dollar-apollo.html

そして、そのApollo Providerはnuxt.config.jsのapolloオプションの中身と同じく、 defaultClient を持っている。

https://vue-apollo.netlify.app/api/apollo-provider.html

では、このApollo Providerが持つ defaultClient を上書きすればどうなるか。

hoge.vue
this.$apollo.provider.defaultClient = this.$apollo.provider.clients.user;

なんと、これでdefaultClient設定を上書き出来てしまう。この処理を任意の処理で書けば、apolloで使用する全ての処理を異なるclientで実行可能になる。

また、こうすればdefaultClientsに戻すことも可能

hoge.vue
this.$apollo.provider.defaultClient = this.$apollo.provider.clients.defaultClient;

まとめ

実装方法

nuxt.config.jsにapolloのオプションにclientconfigを設定して複数のclientsを定義しておく。

nuxt.config.js
{

/* ~以上略= */

  apollo: {
    clientConfigs: {
      default: {
        httpEndpoint: 'https://example.com/guestGateway'
      },
      user: {
        httpEndpoint: 'https://example.com/gateway'
      }
    }
  },

/* ~以下略~ */

}

そして、任意のmethodやvuex storeのaction等で下記処理を定義する。

hoge.vue
// user client を使う場合
this.$apollo.provider.defaultClient = this.$apollo.provider.clients.user;

// 元のdefault client を使う場合
this.$apollo.provider.defaultClient = this.$apollo.provider.clients.defaultClient;

完走した感想

  • ログインメンバーかゲストかでエンドポイント違うのもどうなんだろうとは思うけど、トークンが異なるとかはよくある話なので、この手法は使いみちありそう
  • てかこんな設定の仕方でええんかな
  • 公式ドキュメントもっとちゃんと書いてくれ〜

VueとかNuxtをただ単に使うみたいな記事はやたら多いけど、こういうvue-apolloを使うとかちょっと込み入ったライブラリを使うみたいな応用的な話はなかなか日本語記事に上がってこないので、役に立てたら幸いです。

明日の投稿は〜?

6日目は @naka_kyon さんです。

ちょうどフロントの話題から比較的バックエンドサイドなGraphQLのお話です。この記事の問題にぶち当たったときに考えた、このGraphQL本当に要るの?みたいなお話とか、雑にやるとあるあるになっちゃうよねーみたいなあるあるネタ満載で面白いのでぜひご覧ください。

GraphQL あるある(ないない含む) 15選

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】ドラッグアンドドロップでファイルを取得するコンポーネントを作る

はじめに

最近ファイルアップロード画面を作った時に、ドラッグアンドドロップでファイルを取得するコンポーネントを作りました。この時得た知見をまとめてみたいと思います。

開発環境

  • VueCLI
  • Vue 2.x
  • TypeScript
  • vue-property-decorator

シンプルな実装

file-upload-1.gif

FileUploadCard.vue
<template>
  <div
    class="file-upload-card"
    @dragover.prevent="drag = true"
    @dragleave.prevent="drag = false"
    @drop.prevent="onDrop"
  >
    <div v-if="!drag">
      ドラッグアンドドロップでファイルを追加
    </div>
    <div v-else>
      ドラッグ中
    </div>

    <div v-if="file">
      ファイル名: {{ file.name }}
      <button @click="file = null">
        クリア
      </button>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator"

@Component
export default class FileUploadCard extends Vue {
  drag = false

  file: File | null = null

  onDrop(event: DragEvent): void {
    this.drag = false

    if (!event) {
      return
    }

    if (!event.dataTransfer) {
      return
    }

    if (event.dataTransfer.files.length === 0) {
      return
    }

    this.file = event.dataTransfer.files[0]
  }
}
</script>
<style lang="scss" scoped>
.file-upload-card {
  display: flex;
  flex-direction: column;
  border: solid 1px;
  padding: 1rem;
  width: 20rem;
}
button {
  color: white;
  background: gray;
  padding: 0.2rem;
}
</style>

ポイント

ドラッグ状態の取得

dragover は要素上でドラッグ操作をしているとき、dragleave は要素上からドラッグしているカーソルが離れたときに発行されるイベントです。これらのイベントで drag の true/false を切り替えることでドラッグ状態を取得できます。

ドロップ時の処理

drop がドロップイベントです。この時 dragleave は発行されないので、onDrop でも drag を false にする処理を入れてあります。

必ず prevent を付ける

@dragover.prevent のように .prevent 修飾子を付けていますがこれはこのイベント時に行われるブラウザのデフォルトの動作を無効化するものです。
この修飾子を付けなかった場合、ブラウザ上でそのファイルが展開されてしまいます。

イベントの型は DragEvent

TypeScript を使っていると悩みがちなイベント型ですが、ドラッグ時のイベントは DragEvent を指定しておくといい感じにファイル取得の処理を書くことができます。

抽象化してみる

あなたのプロジェクトで、ドラッグアンドドロップ機能を持たせたいコンポーネントは一つとは限りません。複数ある場合に、毎回 @dragover.prevent="drag = true" や、ファイルをイベントから取り出す処理を書くのは面倒です。先ほどの FileUploadCard も下記のように書けると便利そうです。

FileUploadCard.vue
<template>
  <FileDropArea
    class="file-upload-card"
    :drag.sync="drag"
    @drop="file = $event"
  >
    <div v-if="!drag">
      ドラッグアンドドロップでファイルを追加
    </div>
    <div v-else>
      ドラッグ中
    </div>

    <div v-if="file">
      ファイル名: {{ file.name }}
      <button @click="file = null">
        クリア
      </button>
    </div>
  </FileDropArea>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator"
import FileDropArea from "./FileDropArea.vue"

@Component({
  components: {
    FileDropArea,
  },
})
export default class FileUploadCard extends Vue {
  drag = false

  file: File | null = null
}
</script>
// スタイルは省略
FileDropArea.vue
<template>
  <div
    @dragover.prevent="drag = true"
    @dragleave.prevent="drag = false"
    @drop.prevent="onDrop"
  >
    <slot />
  </div>
</template>

<script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator"

@Component
export default class FileDropArea extends Vue {
  drag = false

  @Watch("drag")
  syncOnDragChanged(): void {
    this.$emit("update:drag", this.drag)
  }

  onDrop(event: DragEvent): void {
    this.drag = false

    if (!event) {
      return
    }

    if (!event.dataTransfer) {
      return
    }

    if (event.dataTransfer.files.length === 0) {
      return
    }

    this.$emit("drop", event.dataTransfer.files[0])
  }
}
</script>

ポイント

FileDropArea

ドラッグ/ドロップイベントの取得とファイルのオブジェクトの取り出しだけを行う抽象的な FileDropArea を定義します。
このコンポーネントは slot を持っているため、

<template>
  <FileDropArea
    class="file-upload-card"
    :drag.sync="drag"
    @drop="file = $event"
  >
    <!-- ファイルのドロップでの取得機能を持たせたい要素 -->
  </FileDropArea>
</template>

このように任意の要素を挟むだけでファイルのドロップでの取得機能を持たせることができる。

:drag.sync でドラッグ状態を取得する

Vue.js では v-model 以外にも .sync 修飾子を使った双方向バインディングができます。
このように

FileDropArea.vue
  @Watch("drag")
  syncOnDragChanged(): void {
    this.$emit("update:drag", this.drag)
  }

子コンポーネント側で update:[event] というイベント名で値を emit すると、

FileUploadCard.vue
    class="file-upload-card"
    :drag.sync="drag"
    @drop="file = $event"
  >

親コンポーネント側で :[event].sync="value" という形式でその値を同期することができます。
ちなみに、.sync を使った書き方は糖衣構文なので、下記と等価です。

FileUploadCard.vue
  <FileDropArea
    class="file-upload-card"
    @update:drag="drag = $event"
    @drop="file = $event"
  >

この方法は、FileDropArea余計なイベントを定義しなくて良いという点、drag が変更された時だけイベントが発行され親要素に伝わるという点で優れていると思います。

drop イベントでファイルオブジェクトを emit

FileDropArea 側で

FileDropArea.vue
  onDrop(event: DragEvent): void {
    this.drag = false

    if (!event) {
      return
    }

    if (!event.dataTransfer) {
      return
    }

    if (event.dataTransfer.files.length === 0) {
      return
    }

    this.$emit("drop", event.dataTransfer.files[0])
  }

この処理をしているため、親コンポーネント側では

FileUploadCard.vue
    @drop="file = $event"

この1行のみでファイルを取得することが可能となります。

おわりに

.prevent やイベントの型は知らないとハマりがちだと思います。同じ轍を踏まない人が少しでも増えると幸いです。

今回実際に使ったソースコードは以下です。
https://github.com/punkshiraishi/file-drop-sample

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue Routerでページを更新or直接アクセス→CannotGETの対処 。Node.js(Express)の例

Vue Routerでページを更新すると

zukan画面にいるときに、ページをリロードすると

画面
スクリーンショット 2020-12-08 0.14.26.png

Console
スクリーンショット 2020-12-08 0.25.15.png

みたいになる。

SPAは常にindex.html一枚で処理をしている。

にもかかわらず、URLが見かけ上のzukanのPATHにアクセスしようとするため、エラーになる。

環境

version
Node.js v11.15.0
OS macOS Catalina v10.15.7
プロセッサ Intel Core i5

原因

historyを設定してURLから#を削除していると起こる。
ちなみにhistoryとは下で設定したやつ。

router.js
const router = new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  //...略

対処

#を削除しつつ、問題を解決する方法は公式サイトに解説されている。

Node.js(Express)の場合

自分が作ったものがこれだったのでもう少し詳しく。

公式にもあるように、connect-history-api-fallbackを使うと良い。

install

npm install connect-history-api-fallback

server.jsの例

app.use(history())を追加する位置に注意。
app.use(serveStatic(__dirname + "/docs"))よりも後に書くと動作しなくなった。

server.js
const express = require('express')
const serveStatic = require('serve-static')
const history = require('connect-history-api-fallback') // 追加
const port = process.env.PORT || 5000

app = express()
app.use(history()) // 追加
app.use(serveStatic(__dirname + "/docs"))
app.listen(port)
console.log('server started '+ port)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む