- 投稿日:2020-12-08T21:13:00+09:00
複雑な画面開発に立ち向かう時に捗る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
本題です。
私が主に実装を手掛けた画面は、以下のような仕様になっていました。
- 状態変化のパターンが多岐にわたる
- 様々な状態ごとに入力バリデーションのロジックが変わる
- 複雑な条件が揃うと画面遷移なしにAPIを叩く
- ↑のAPIの戻り値を反映し、再度状態を書き換える
このような複雑な仕様を複雑なまま実装しないように取り入れた手法を書き連ねます。
できる限り状態とロジックをComposableに寄せる
コンポーネントから状態とロジックを引き剥がし、関数として別のファイルに寄せるよう徹底しました。
ちなみに切り出した関数はComposableと呼ぶようです。
再利用可能な部品、アプリケーションを構成することができる部品、といった意味で利用していると思うので、例に習います。先程のカウンターコンポーネントを例にするとこうなります。
composables/useCounter.tsimport { 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>別ファイルにしただけで何が良いのか?と思う方もいるかもしれませんが、以下の点で優れています。
- ロジックを再利用できる
- テストが容易
- コンポーネントの記述量が減り、可読性が上がる
個人的には2が大きいです。
複雑な状態管理をコンポーネントから引き剥がしてテストすることができるため、テスト実装が容易です。
それこそコンポーネントが無くても実装が可能なので、ロジックだけ先行してTDDで開発することもありだと思います。できる限り関心事の単位でComposableを分割する
アプリケーションロジックが大きく複雑な場合、Composableが肥大化してしまいます。
そのため関心事単位でComposableを分割して可読性を下げないようにしました。以下はA・Bの2地点の場所の管理と、作業時間の管理を分けた例です。
composables/useTimes.tsimport { 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.tsimport { 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が知る必要が出てきます。
この対応策として、今回は以下の方法を利用しました。
provide
とinject
を使う- エラー管理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が責任を持っている状態を作れます。
餅は餅屋ってことですね。ちなみに何で面倒な引数渡しをしているかというと、
provide
とinject
を用いず呼び出されたComposableはスコープが異なってしまうからです。
詳細は「Vue3 Composition APIにおいて、Providerパターン(provide/inject)の使い方と、なぜ重要なのか、理解する。」に素晴らしくよくまとまっているので、見てもらったほうが早いです。Composableを完全にグローバルに公開してしまえば実装は楽なのですが、そうなるとグローバル変数が大量に発生してしまうので避けました。
Composableをテストする
これは簡単で、Jestなりのテストフレームワークを使ってComposableをテストするだけです。
ただの(リアクティブな)変数と関数のセットでしかないので、入出力のチェックをすれば終わります。
もし1つ前のTipsに挙げたような「引数渡し」があったとしても、テストファイルから呼び出して渡せばいいだけです。
簡単ですね。ただ(無いとは思いますが)、万が一
provide
とinject
をComposableから呼び出す実装になっていた場合、これは避けたほうが良いと思います。
これら2つの関数はコンポーネントのライフサイクルに依存しているため、コンポーネント抜きでprovide
とinject
はうまく動きません。
当然Composableのみのテストではコケます。まとめ
ダラダラと長くなりましたが、私が実務で取り入れたComposition APIの活用方法をTipsとして紹介させてもらいました。
これらの手法のおかげでアプリケーションロジックがあるべきファイルに集約され、可読性やテスタビリティが大きく向上したと思います。
これを読んだ方々のお役に立てれば幸いです。ただ正式版がリリースしたとはいえ、Composition APIはまだ出て間もない機能です。
そのため使い方に関して改善点や誤りがありましたらツッコんでもらえると嬉しいです。
- 投稿日:2020-12-08T20:55:57+09:00
Rails 6: ActionCableとVue.jsで非同期処理を行うサンプル
環境: Rails 6.0、Vue.js 2.6、ソース: https://github.com/kazubon/cable60
ActionCable、ActiveJob、およびVue.jsを使って、次のような非同期処理を行う画面を作ります。
必要なライブラリ
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.ymldevelopment: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: cable60_developmentコントローラでは、ActionCable用の接続の識別子となるランダム文字列をクッキーに入れます。このサンプルでは、「1ユーザー - 1識別子 - 1ストリーム」とします。1対多の送信は行いません。
app/controllers/application_controller.rbclass ApplicationController < ActionController::Base before_action :set_cable_code private # Action Cable用ユーザー識別 def set_cable_code cookies.signed[:cable_code] ||= SecureRandom.hex end endconnection.rbではクッキーから識別子 cable_code を取り出します。
app/channels/application_cable/connection.rbmodule 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.rbclass UserChannel < ApplicationCable::Channel def subscribed stream_from cable_code if cable_code end def unsubscribed end endActiveJobの準備
config/environments下のdevelopment.rbとproduction.rbを修正し、ActiveJobではSidekiqを使うことを指定します。
config/environments/development.rbconfig.active_job.queue_adapter = :sidekiqコントローラでは、「開始」ボタンで呼び出す update アクションを書いておきます。SampleJobというジョブにクッキーの識別子を渡して非同期処理をさせます。
app/controllers/samples_controller.rbclass 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.rbclass 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 endJavaScriptでデータを受け取り、進行状況を表示する
JavaScript側では、ActionCableからデータを受け取るためのオブジェクトを作ります。
Vue.observable
を使うことで、sampleプロパティを変更したらVueのテンプレートに反映するようにします。ほかに非同期処理を扱う画面が増えたら、fooとかbarとかプロパティを増やすことを想定しています。
app/javascript/channels/cable_data.jsimport Vue from 'vue'; export default Vue.observable({ sample: { }, // foo: { }, // bar: { }, })UserChannelに対応するuser_channel.jsを修正します。receivedでデータを受け取ったら、ActionCable用のオブジェクトcableDataのプロパティにそのまま入れます。
オブジェクトのtypeプロパティの値がcableDataの各プロパティの名前に対応していることにします。
app/javascript/channels/user_channel.jsimport 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.jsimport '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.jsexport 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>
- 投稿日:2020-12-08T20:42:22+09:00
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】
- 投稿日:2020-12-08T15:42:16+09:00
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]', }, }, ], }, ], }, }
- 投稿日:2020-12-08T15:42:16+09:00
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()
で確認できます。vuemounted(){ console.log('style', this.$style); }cssModules 以外の class も併用する
cssModules、クラス名の文字列、クラス名の入った変数を併用するなら配列で渡します。
vue<template> <div :class="[$style.foo, 'class-name', myClass]"> hogehoge </div> </template>
- 投稿日:2020-12-08T15:27:55+09:00
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>このようにすることで、以下のように出力されます。
とはいえ、現状のコードだけではこのようには表示されません。
以下の部分の設定が抜けているからです。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
ディレクトリを作ってそこに作るようにしたり、今回私が示したレイヤーが多すぎる・少なすぎるのであれば適宜追加・削除して貰えればと思います。この形が必ずしも正解だとは思っていませんが、何かしらの参考になれば幸いです。
- 投稿日:2020-12-08T14:50:38+09:00
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.jstarget :"static",今回のFile構成
-| pages/ ---| index.vue ---| news/ -----| _slug.vueNuxt 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.jsgenerate: { 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_server2.generateする
npm run generateAPIの情報を取り込みファイルを書き出せました。
-| dist/ ---| index.html ---| news/ -----| title_001/ -------| index.html -----| title_002/ -------| index.html ...略無事APIレスポンスを取り込んで書き出すことができました。
今回はAPIの部分をjson-serverを使ったモックで済ませましたが本来 strapi+nuxt.jsでアドベントカレンダーを作ろうのようにHeadless CMSからのAPIを取得から生成を試して見たかったのですが、それはまたどこかで。
FORK Advent Calendar 2020
15日目 前の日の記事のタイトル @yoh_zzzz
17日目 次の日の記事のタイトル @sy12345
- 投稿日:2020-12-08T14:50:08+09:00
【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
- 投稿日:2020-12-08T14:50:08+09:00
【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
- 投稿日:2020-12-08T14:50:08+09:00
【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
- 投稿日:2020-12-08T14:14:33+09:00
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
- 投稿日:2020-12-08T14:14:33+09:00
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
- 投稿日:2020-12-08T09:44:38+09:00
[解決](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 endoriginsは、サーバーサイド(API),フロントの順で書きます。
↓
Dockerの場合は、再度コンテナを立ち上げて
↓
完了。
- 投稿日:2020-12-08T08:27:01+09:00
Vue vue-router編
はじめに
今回はvue-routerについてまとめる。自分自身まだまだvue-routerにわかなので間違っているところがあれば指摘していただけると嬉しいです。
実行環境
macOS Catalina バージョン 10.15.7
MacBook Pro 13インチ
Vue 2.6.12vue-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.jsimport 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.jsimport 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画像でアプリケーションを動的に示したかったのですが、うまく埋め込むことができませんでした。何か良い方法があればぜひ教えていただきたいです。)
コンポーネント作成
まず、コンポーネントを作成する。srcディレクトリの直下にviewsディレクトリを作成し、その中にページ遷移させるためのコンポーネントを作成する。
ToHome,ToSetting,ToDetailボタンを押下したときにHome,Setting,ToDetailコンポーネントを表示し、さらにHomeコンポーネントの中でProductA,ProductBのリンクを踏むとそれぞれのコンポーネントを表示するという設計にする。これがrouter.jsのコードに関係してくる。
router.jsの編集
router.jsexport 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に遷移する。ここでデベロッパーツールでこの要素を見てみると、と表示されており、働きとしては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でアウトプットしようと思う。
- 投稿日:2020-12-08T03:31:26+09:00
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
作るもの
タイルマップの標準的な入力処理と表示だけができるマップです。
コンポーネントの実装
実装の方針として、テンプレートは
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 : '© <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-lat
やdefault-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>スタイルについて
コントロールや帰属表示のスタイリングに、 mapbox-gl.js では専用の CSS を持っています。
https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css をlink
タグで読み込むか、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 と、初期の中心とズームをプロパティとして外部から与えられるだけの、簡素なものとして実装しました。
- 投稿日:2020-12-08T02:38:31+09:00
親子3世代のバケツリレーをVuexを使わずに状態管理
こんにちは。
株式会社アドベンチャーで、UI/UXデザイナーとフロントエンド周りの開発をしている@f-umebaraと申します。
株式会社アドベンチャーのAdvent Calendar 2020 7日目です。
前回参加したAdventure Advent Calendar 2018が2年前ということに驚愕です。:(;゙゚'ω゚'):ガクブル。さて、最近vue.jsの開発をしておりまして、
内容としてはよくあるものですが自分の忘備録も兼ね、行なったことを書いてみます。Vuexを使わずに状態管理
Vuexを使うまででもない規模(というか使えない)場合にて、コンポーネント間の親子3世代のバケツリレーが辛くなりました。(↓私の中でこんな感じです)
propsとemitが複雑に。。
おばあちゃん辛いよ。孫よ、直接やりとりして欲しい〜。何か良い方法がないかと調べたところ、オブジェクトをリアクティブにする
Vue.observable
を使ってVuexライクにシンプルな状態管理ができるとのことで、やってみる。store.jsimport 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て便利だなぁと改めて感じました。しかしvue3からの変更で書き方は変更した方が良さそうです。
将来の互換性を考えると、Vue.observable に渡したオブジェクトではなく、返されたオブジェクトを使うことを推奨
Vue 2.x では、Vue.observable は渡されたオブジェクトを直接操作するため、ここでデモされる ように戻り値のオブジェクトと等しくなります。Vue 3.x では、代わりにリアクティブプロキシを返し、元のオブジェクトを直接変更してもリアクティブにならないようにします。そのため、将来の互換性を考えると、Vue.observable に渡したオブジェクトではなく、返されたオブジェクトを使うことを推奨します。
store.jsclass Store { // 状態管理部分 } export default Vue.observable(new Store());上記はまだ実装できていないですが、vue3を考えて変更を検討したいと思います。
- 投稿日:2020-12-08T02:38:31+09:00
親子3世代のバケツリレーが辛いのでVuexを使わずに状態管理
こんにちは。
株式会社アドベンチャーで、UI/UXデザイナーとフロントエンド周りの開発をしている@f-umebaraと申します。
株式会社アドベンチャーのAdvent Calendar 2020 7日目です。
前回参加したAdventure Advent Calendar 2018が2年前ということに驚愕です。:(;゙゚'ω゚'):ガクブル。さて、最近vue.jsの開発をしておりまして、
内容としてはよくあるものですが自分の忘備録も兼ね、行なったことを書いてみます。Vuexを使わずに状態管理
Vuexを使うまででもない規模の場合にて、コンポーネント間の親子3世代のバケツリレーが辛くなりました。(↓私の中でこんな感じです)
propsとemitが複雑に。。
おばあちゃん辛いよ。孫よ、直接やりとりして欲しい〜。何か良い方法がないかと調べたところ、オブジェクトをリアクティブにする
Vue.observable
を使ってVuexライクにシンプルな状態管理ができるとのことで、やってみる。store.jsimport 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て便利だなぁと改めて感じました。しかしvue3からの変更で書き方は変更した方が良さそうです。
将来の互換性を考えると、Vue.observable に渡したオブジェクトではなく、返されたオブジェクトを使うことを推奨
Vue 2.x では、Vue.observable は渡されたオブジェクトを直接操作するため、ここでデモされる ように戻り値のオブジェクトと等しくなります。Vue 3.x では、代わりにリアクティブプロキシを返し、元のオブジェクトを直接変更してもリアクティブにならないようにします。そのため、将来の互換性を考えると、Vue.observable に渡したオブジェクトではなく、返されたオブジェクトを使うことを推奨します。
store.jsclass Store { // 状態管理部分 } export default Vue.observable(new Store());上記はまだ実装できていないですが、vue3を考えて変更を検討したいと思います。
- 投稿日:2020-12-08T01:39:01+09:00
【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.tsexport 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.$auth
→context.root.$auth
に変更した所、解決しました。
signIn.vuesetup(_props, context) { ... const onSubmitButtonClick = (e: Event) => { context.root.$auth.loginWith('local', { ...まとめ
今回はcompositionAPIもnuxtも理解が曖昧なまま進めた結果詰まってしまいました、、
勿体ないのでインプットも丁寧にしていきたい。
Twitterやってるのでフォローお願いします!
https://twitter.com/1keiuu
- 投稿日:2020-12-08T01:35:04+09:00
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.vueviewer: { 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属性内の情報や算出プロパティの情報を元にdefault
とguest
を入れ替えてみる。viewer.vueviewer: { query:gql` query { viewer { login } } `, client: this.isLogin ? 'user' : 'default' // ここでエラー }さて、このキーは果たしてリアクティブに書けるのか、答えはノーである。
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.vuethis.$apollo.provider.defaultClient = this.$apollo.provider.clients.user;なんと、これでdefaultClient設定を上書き出来てしまう。この処理を任意の処理で書けば、apolloで使用する全ての処理を異なるclientで実行可能になる。
また、こうすればdefaultClientsに戻すことも可能
hoge.vuethis.$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本当に要るの?みたいなお話とか、雑にやるとあるあるになっちゃうよねーみたいなあるあるネタ満載で面白いのでぜひご覧ください。
- 投稿日:2020-12-08T01:20:47+09:00
【Vue.js】ドラッグアンドドロップでファイルを取得するコンポーネントを作る
はじめに
最近ファイルアップロード画面を作った時に、ドラッグアンドドロップでファイルを取得するコンポーネントを作りました。この時得た知見をまとめてみたいと思います。
開発環境
- VueCLI
- Vue 2.x
- TypeScript
- vue-property-decorator
シンプルな実装
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.vueclass="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.vueonDrop(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
- 投稿日:2020-12-08T00:39:05+09:00
Vue Routerでページを更新or直接アクセス→CannotGETの対処 。Node.js(Express)の例
Vue Routerでページを更新すると
zukan
画面にいるときに、ページをリロードするとみたいになる。
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.jsconst router = new Router({ mode: 'history', base: process.env.BASE_URL, //...略対処
#
を削除しつつ、問題を解決する方法は公式サイトに解説されている。Node.js(Express)の場合
自分が作ったものがこれだったのでもう少し詳しく。
公式にもあるように、connect-history-api-fallbackを使うと良い。
install
npm install connect-history-api-fallbackserver.jsの例
app.use(history())
を追加する位置に注意。
app.use(serveStatic(__dirname + "/docs"))
よりも後に書くと動作しなくなった。server.jsconst 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)