20200625のvue.jsに関する記事は10件です。

Nuxt.js + Vuex + TypeScriptでがっつりインテリセンスを効かせる

はじめに

以前は、Vue.jsアプリケーションにおいてTypeScriptを導入する最大の障壁となるのがVuexでした。

TypeScriptとVuexの相性は良くなく、コンポーネントからstoreを呼び出したときに型安全が守られない、インテリセンスが効かないといった問題がありました。

Vuexの型課題を解決するために様々な方法が考案されており、Treeと称される型定義を使用したりそもそもVuexを利用しないで独自の状態管理を行うなど様々です。

今回はその中でも、Nuxt.js公式で推奨されているvuex-module-decoratorsを使用します。

セットアップ

Nuxt.jsでVuexを通常利用する場合には、storeディレクトリにモジュールと対応するファイルを設置します。
例えば、myModule.jsというファイルをstoreディレクトリに設置すれば、myModuleといモジュールで自動的に作成され、コンポーネントからアクセスすることができます。

ただし、今回のようにvuex-module-decoratorsを使用する場合には、下準備が必要です。

vuex-module-decoratorsをインストール

ます初めに、vuex-module-decoratorsを使用するためにインストールをします。

yarn add dev vuex-module-decorators
# OR
npm install -D vuex-module-decorators

store/index.ts

ここからはNuxt.jsで使用するために必要な手順です。公式のREAD MEの手順に従って実装していきます。

まずは、~/store/index.tsファイルを作成し、以下のコードを記述します。

~/store/index.ts
import { Store } from 'vuex'
import { initialiseStores } from '~/utils/store-accessor'
const initializer = (store: Store<any>) => initialiseStores(store)
export const plugins = [initializer]
export * from '~/utils/store-accessor'

このファイルは一度作成したら、基本編集しません。
コンポーネントからimport { todoStore } from '~/storeのようにできるようにするためここで初期化します。

utils/store-accsessor

次に、store/index.tsの中で利用されている~/utils/store-accsessor.tsです。

~/utils/store-accsessor.ts
/* eslint-disable import/no-mutable-exports */
import { Store } from 'vuex'
import { getModule } from 'vuex-module-decorators'
import Todo from '~/store/todo'

let TodoStore: Todo
function initialiseStores(store: Store<any>): void {
  TodoModule = getModule(Todo, store)
}

export { initialiseStores, TodoModule }

ここでは、作成したモジュールをインポートしてstoreに登録します。
新たにモジュールを作成するたびに、作成したモジュールをこのファイルに追加していきます。

これでVuexのセットアップは完了です。実際にモジュールを作成して使用してみましょう。

モジュールの作成

今回はみんな大好きTODOリストを作成して、vuex-module-decoratorsを体感します。

まずは、storeの作成です。todoというモジュールで作成するので、~/store/todo.tsという構造でファイルを作成します。

~/store/todo.ts
import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'
import { $axios } from '~/utils/api'

type Todo = {
  id?: Number
  title: String
  description: String
  done: Boolean
}

@Module({
  name: 'todo',
  stateFactory: true,
  namespaced: true
})
export default class Todos extends VuexModule {
  private todos: Todo[] = []

  public get getTodos() {
    return this.todos
  }

  public get getTodo() {
    return (id: Number) => this.todos.find((todo) => todo.id === id)
  }

  public get getTodoCount() {
    return this.todos.length
  }

  @Mutation
  private add(todo: Todo) {
    this.todos.push(todo)
  }

  @Mutation
  private remove(id: Number) {
    this.todos = this.todos.filter((todo) => todo.id !== id)
  }

  @Mutation set(todos: Todo[]) {
    this.todos = todos
  }

  @Action({ rowError: true })
  public async fetchTodos() {
    const { data } = await $axios.get<Todo[]>('/api/todos')
    this.set(data)
  }

  @Action({ rowError: true })
  public async createTodo(payload: Todo) {
    const { data } = await $axios.post<Todo>('/api/todo', payload)
    this.add(data)
  }

  @Action({ rowError: true })
  async deleteTodo(id: Number) {
    await $axios.delete(`/api/todo/${id}`)
    this.remove(id)
  }
}

こんな感じで作成してみました。
Vuexをクラスベースで作成するのが特徴です。
さらに、デコレータを使用してモジュールであることや、MutationActionメソッドであることを伝えます。
クラス内でなら、他のプロパティの要素にはthisでアクセスすることができます。

モジュールについて、一つづつ詳しく見てみましょう。

デコレータ、Nuxt アプリケーションインスタンスインポート

まずはファイルの先頭で必要なものをインストールします。

import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'
import { $axios } from '~/utils/api'

クラスの作成に必要なものをvuex-module-decoratorsからインポートします。
また、VuexのモジュールからはNuxtアプリケーションインスタンスにアクセスできないので、axiosなどを使用したいときには一手間必要です。

Vuexで$axiosを使用する方法

まずは、プラグインを作成します。plugins/axios-accessor.tsファイルを作成します。

import { Plugin } from '@nuxt/types'
import { initializeAxios } from '~/utils/api'

const accessor: Plugin = ({ $axios }) => {
  initializeAxios($axios)
}

export default accessor

nuxt.config.jspluginsに忘れずに追加します。

plugins: [
    '~/plugins/axios-accessor',
  ]

utils/api.tsファイルを作成して、そこからインポートする必要があります。

/* eslint-disable import/no-mutable-exports */
import { NuxtAxiosInstance } from '@nuxtjs/axios'

let $axios: NuxtAxiosInstance

export function initializeAxios(axiosInstance: NuxtAxiosInstance) {
  $axios = axiosInstance
}

export { $axios }

型宣言

モジュールで使用する独自の型を宣言しています。
typesフォルダを作成して、そこから型定義をインポートするのでもよいでしょう。

type Todo = {
  id: number
  title: string
  description: string
  done: boolean
}

クラス作成

モジュールクラスを作成します。
クラス宣言の前に@moduleデコレータを付与する必要があります。
また、stateFactory: trueを渡すことで、Nuxt.jsのモジュールであることを宣言します。
クラスはVuexModuleを継承して作成されます。

@Module({
  name: 'todo',
  stateFactory: true,
  namespaced: true
})
export default class Todos extends VuexModule {

state

stateは、クラスのプロパティとして作成します。

private todos: Todo[] = []

アクセス修飾子は必須ではありませんが、Vuexの流儀の従うのなら、外部からstateにアクセスさせたくないのでprivateで宣言しておくのがよいでしょう。
stateクラス内部でのみ扱うようにします。

getters

gettersはそのままクラスのget構文として作成します。
get構文には引数を渡すことができないので、関数をreturnすることで渡してあげることができます。

public get getTodos() {
    return this.todos
  }

  public get getTodo() {
    return (id: number) => this.todos.find((todo) => todo.id === id)
  }

  public get getTodoCount() {
    return this.todos.length
  }

mutasions

mutationsには、@Mutationsデコレータを付与します。

@Mutation
  private add(todo: Todo) {
    this.todos.push(todo)
  }

  @Mutation
  private remove(id: number) {
    this.todos = this.todos.filter((todo) => todo.id !== id)
  }

  @Mutation
  private set(todos: Todo[]) {
    this.todos = todos
  }

mutationsには本来外部から直接アクセスしても構わないですが、非同期の有無にかかわらず、actions経由での更新に統一するというルールにしたがってアクセス修飾子はprivateとしています。

actions

最後に、actionsです。@Actionデコレータを付与して作成します。

@Action({ rowError: true })
public async fetchTodos() {
  const { data } = await $axios.get<Todo[]>('/api/todos')
  this.set(data)
}

@Action({ rowError: true })
public async createTodo(payload: Todo) {
  const { data } = await $axios.post<Todo>('/api/todo', payload)
  this.add(data)
}

@Action({ rowError: true })
public async deleteTodo(id: number) {
  await $axios.delete(`/api/todo/${id}`)
  this.remove(id)
}

mutationsのアクセスにthisが使えるので、インテリセンスが使えるのでいい感じです。

コンポーネントから呼び出す

それでは、実際に作成したモジュールをコンポーネントから呼び出してみます。
従来のようなインテリセンスの効かないmapActionsmapGettersは使用せずに、methodscomputedに定義して使用します。

~/pages/todo.vue
<template>
  <div>
    <h1>TODOリスト</h1>
    <table>
      <tr>
        <th>ID</th>
        <th>TITLE</th>
        <th>DONE</th>
      </tr>
      <tr v-for="todo in todos" :key="todo.id">
        <td>{{ todo.id }}</td>
        <td>{{ todo.title }}</td>
        <td v-if="todo.done"></td>
        <td v-else></td>
      </tr>
    </table>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { TodoStore } from '~/store'

export default Vue.extend({
  async asyncData({ error }) {
    try {
      await TodoStore.fetchTodos()
    } catch (e) {
      console.log(e)
      error({
        statusCode: e.response.status,
        message: e.response.message
      })
    }
  },
  computed: {
    todos() {
      return TodoStore.getTodos
    }
  }
})
</script>

import { TodoStore } from '~/store'でモジュールをインポートして使用します。
下記の通り、インテリセンスがよく効いています。

スクリーンショット 20200624 22.49.42.png

computedプロパティにも型が効いています。

スクリーンショット 20200624 22.51.20.png

実際に正しく動作させるよう、/api/todosエンドポイントを作成する必要があります。
試しに適当にリストを返すものを作成しました。

router.use('/todos', (_req, res) => {
  res.json([
    {
      id: 1,
      title: 'リスト1',
      description: 'lorem ipsum',
      done: true
    },
    {
      id: 2,
      title: 'リスト2',
      description: 'lorem ipsum',
      done: false
    },
    {
      id: 3,
      title: 'リスト3',
      description: 'lorem ipsum',
      done: true
    }
  ])
})

ページを表示すると、全てが正しく動作していることがわかります。

スクリーンショット 20200624 22.54.42.png

注意する点

@Actionの{ rowError: true }を忘れると正しいエラーが得られない

Actionsメソッド内では、エラーを非同期処理などエラーを捕捉したい場面が多いかと思います。

例えば、次のようなこコードはAxiosのエラーを捕捉することを期待しています。

@Action
  public async createTodo(payload: Todo) {
    const { data } = await $axios.post<Todo>('/api/todo', payload)
    this.add(data)
  }
async asyncData({ error }) {
    try {
      await TodoStore.fetchTodos()
    } catch (e) {
      console.log(e)
      error({
        statusCode: e.response.status,
        message: e.response.message
      })
    }
  },

しかし、このままだと実際に補足するエラーは次のようになってしまいます。

ERR_ACTION_ACCESS_UNDEFINED: Are you trying to access this.someMutation() or this.someGetter inside an @action?
That works only in dynamic modules.

ERR_ACTION_ACCESS_UNDEFINEDと全く身に覚えがないエラーが補足されていますが、これは一体何のエラーなのでしょうか?

実は、@Actionの{ rowError: true }を指定しないと、デフォルトですべてのエラーはライブラリ内部で定義されている固定文言がthrowされます。

デフォルトでエラーを握りつぶしてしまう動作は予期しづらく、かつエラーメッセージも分かりづらいものになっているのでハマりどころだと思います。

他のモジュールがVuexを使用すると競合が発生する

ERR_STORE_NOT_PROVIDEDというエラーに悩まされていたのですが、原因はAuth-Moduleというモジュールを追加したことでした。

このモジュールに限らず、Vuexを使用しているモジュールを使用すると同様のエラーが発生すると思われます。

解決策は、nuxt.config.jsのモジュールの設定でvuex:falseを追加します。

  auth: {
    redirect: {
      login: '/login',
      logout: '/',
      callback: '/login',
      home: '/'
    },
    strategies: { 
       // 省略
    },
    vuex: false // これを追加
  },

このエラーのたちの悪いところは@Actionの{ rowError: true }を指定しないとさらにわけがわからなくなるところですね。

おわりに

はじめは、今までのVuexの記法と大きく違うクラス記法で慣れない部分もありましたが、いざ使ってみるとインテリセンス効きまくりで完全に虜になりました。普段からtypoしまくってる私にとってもうTypeScriptは手放せない存在になりつつあります。

Vuex + TypeScriptはまだ発展途上で、情報もあまり多くなくもしかしたら1年もしないうちにベストプラクティスが変わってしまう可能性はありますが、それを差し置いても導入するメリットはあると感じられました。

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

Nuxt.js + TypeScript + Vuexをvuex-module-decoratorsでがっちりインテリセンスを効かせる

はじめに

以前は、Vue.jsアプリケーションにおいてTypeScriptを導入する最大の障壁となるのがVuexでした。

TypeScriptとVuexの相性は良くなく、コンポーネントからstoreを呼び出したときに型安全が守られない、インテリセンスが効かないといった問題がありました。

Vuexの型課題を解決するために様々な方法が考案されており、Treeと称される型定義を使用したりそもそもVuexを利用しないで独自の状態管理を行うなど様々です。

今回はその中でも、Nuxt.js公式で推奨されているvuex-module-decoratorsを使用します。

セットアップ

Nuxt.jsでVuexを通常利用する場合には、storeディレクトリにモジュールと対応するファイルを設置します。
例えば、myModule.jsというファイルをstoreディレクトリに設置すれば、myModuleといモジュールで自動的に作成され、コンポーネントからアクセスすることができます。

ただし、今回のようにvuex-module-decoratorsを使用する場合には、下準備が必要です。

vuex-module-decoratorsをインストール

ます初めに、vuex-module-decoratorsを使用するためにインストールをします。

yarn add dev vuex-module-decorators
# OR
npm install -D vuex-module-decorators

store/index.ts

ここからはNuxt.jsで使用するために必要な手順です。公式のREAD MEの手順に従って実装していきます。

まずは、~/store/index.tsファイルを作成し、以下のコードを記述します。

~/store/index.ts
import { Store } from 'vuex'
import { initialiseStores } from '~/utils/store-accessor'
const initializer = (store: Store<any>) => initialiseStores(store)
export const plugins = [initializer]
export * from '~/utils/store-accessor'

このファイルは一度作成したら、基本編集しません。
コンポーネントからimport { todoStore } from '~/storeのようにできるようにするためここで初期化します。

utils/store-accsessor

次に、store/index.tsの中で利用されている~/utils/store-accsessor.tsです。

~/utils/store-accsessor.ts
/* eslint-disable import/no-mutable-exports */
import { Store } from 'vuex'
import { getModule } from 'vuex-module-decorators'
import Todo from '~/store/todo'

let TodoStore: Todo
function initialiseStores(store: Store<any>): void {
  TodoModule = getModule(Todo, store)
}

export { initialiseStores, TodoModule }

ここでは、作成したモジュールをインポートしてstoreに登録します。
新たにモジュールを作成するたびに、作成したモジュールをこのファイルに追加していきます。

これでVuexのセットアップは完了です。実際にモジュールを作成して使用してみましょう。

モジュールの作成

今回はみんな大好きTODOリストを作成して、vuex-module-decoratorsを体感します。

まずは、storeの作成です。todoというモジュールで作成するので、~/store/todo.tsという構造でファイルを作成します。

~/store/todo.ts
import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'
import { $axios } from '~/utils/api'

type Todo = {
  id?: Number
  title: String
  description: String
  done: Boolean
}

@Module({
  name: 'todo',
  stateFactory: true,
  namespaced: true
})
export default class Todos extends VuexModule {
  private todos: Todo[] = []

  public get getTodos() {
    return this.todos
  }

  public get getTodo() {
    return (id: Number) => this.todos.find((todo) => todo.id === id)
  }

  public get getTodoCount() {
    return this.todos.length
  }

  @Mutation
  private add(todo: Todo) {
    this.todos.push(todo)
  }

  @Mutation
  private remove(id: Number) {
    this.todos = this.todos.filter((todo) => todo.id !== id)
  }

  @Mutation set(todos: Todo[]) {
    this.todos = todos
  }

  @Action({ rowError: true })
  public async fetchTodos() {
    const { data } = await $axios.get<Todo[]>('/api/todos')
    this.set(data)
  }

  @Action({ rowError: true })
  public async createTodo(payload: Todo) {
    const { data } = await $axios.post<Todo>('/api/todo', payload)
    this.add(data)
  }

  @Action({ rowError: true })
  async deleteTodo(id: Number) {
    await $axios.delete(`/api/todo/${id}`)
    this.remove(id)
  }
}

こんな感じで作成してみました。
Vuexをクラスベースで作成するのが特徴です。
さらに、デコレータを使用してモジュールであることや、MutationActionメソッドであることを伝えます。
クラス内でなら、他のプロパティの要素にはthisでアクセスすることができます。

モジュールについて、一つづつ詳しく見てみましょう。

デコレータ、Nuxt アプリケーションインスタンスインポート

まずはファイルの先頭で必要なものをインストールします。

import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'
import { $axios } from '~/utils/api'

クラスの作成に必要なものをvuex-module-decoratorsからインポートします。
また、VuexのモジュールからはNuxtアプリケーションインスタンスにアクセスできないので、axiosなどを使用したいときには一手間必要です。

Vuexで$axiosを使用する方法

まずは、プラグインを作成します。plugins/axios-accessor.tsファイルを作成します。

import { Plugin } from '@nuxt/types'
import { initializeAxios } from '~/utils/api'

const accessor: Plugin = ({ $axios }) => {
  initializeAxios($axios)
}

export default accessor

nuxt.config.jspluginsに忘れずに追加します。

plugins: [
    '~/plugins/axios-accessor',
  ]

utils/api.tsファイルを作成して、そこからインポートする必要があります。

/* eslint-disable import/no-mutable-exports */
import { NuxtAxiosInstance } from '@nuxtjs/axios'

let $axios: NuxtAxiosInstance

export function initializeAxios(axiosInstance: NuxtAxiosInstance) {
  $axios = axiosInstance
}

export { $axios }

型宣言

モジュールで使用する独自の型を宣言しています。
typesフォルダを作成して、そこから型定義をインポートするのでもよいでしょう。

type Todo = {
  id: number
  title: string
  description: string
  done: boolean
}

クラス作成

モジュールクラスを作成します。
クラス宣言の前に@moduleデコレータを付与する必要があります。
また、stateFactory: trueを渡すことで、Nuxt.jsのモジュールであることを宣言します。
クラスはVuexModuleを継承して作成されます。

@Module({
  name: 'todo',
  stateFactory: true,
  namespaced: true
})
export default class Todos extends VuexModule {

state

stateは、クラスのプロパティとして作成します。

private todos: Todo[] = []

アクセス修飾子は必須ではありませんが、Vuexの流儀の従うのなら、外部からstateにアクセスさせたくないのでprivateで宣言しておくのがよいでしょう。
stateクラス内部でのみ扱うようにします。

getters

gettersはそのままクラスのget構文として作成します。
get構文には引数を渡すことができないので、関数をreturnすることで渡してあげることができます。

public get getTodos() {
    return this.todos
  }

  public get getTodo() {
    return (id: number) => this.todos.find((todo) => todo.id === id)
  }

  public get getTodoCount() {
    return this.todos.length
  }

mutasions

mutationsには、@Mutationsデコレータを付与します。

@Mutation
  private add(todo: Todo) {
    this.todos.push(todo)
  }

  @Mutation
  private remove(id: number) {
    this.todos = this.todos.filter((todo) => todo.id !== id)
  }

  @Mutation
  private set(todos: Todo[]) {
    this.todos = todos
  }

mutationsには本来外部から直接アクセスしても構わないですが、非同期の有無にかかわらず、actions経由での更新に統一するというルールにしたがってアクセス修飾子はprivateとしています。

actions

最後に、actionsです。@Actionデコレータを付与して作成します。

@Action({ rowError: true })
public async fetchTodos() {
  const { data } = await $axios.get<Todo[]>('/api/todos')
  this.set(data)
}

@Action({ rowError: true })
public async createTodo(payload: Todo) {
  const { data } = await $axios.post<Todo>('/api/todo', payload)
  this.add(data)
}

@Action({ rowError: true })
public async deleteTodo(id: number) {
  await $axios.delete(`/api/todo/${id}`)
  this.remove(id)
}

mutationsのアクセスにthisが使えるので、インテリセンスが使えるのでいい感じです。

コンポーネントから呼び出す

それでは、実際に作成したモジュールをコンポーネントから呼び出してみます。
従来のようなインテリセンスの効かないmapActionsmapGettersは使用せずに、methodscomputedに定義して使用します。

~/pages/todo.vue
<template>
  <div>
    <h1>TODOリスト</h1>
    <table>
      <tr>
        <th>ID</th>
        <th>TITLE</th>
        <th>DONE</th>
      </tr>
      <tr v-for="todo in todos" :key="todo.id">
        <td>{{ todo.id }}</td>
        <td>{{ todo.title }}</td>
        <td v-if="todo.done"></td>
        <td v-else></td>
      </tr>
    </table>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { TodoStore } from '~/store'

export default Vue.extend({
  async asyncData({ error }) {
    try {
      await TodoStore.fetchTodos()
    } catch (e) {
      console.log(e)
      error({
        statusCode: e.response.status,
        message: e.response.message
      })
    }
  },
  computed: {
    todos() {
      return TodoStore.getTodos
    }
  }
})
</script>

import { TodoStore } from '~/store'でモジュールをインポートして使用します。
下記の通り、インテリセンスがよく効いています。

スクリーンショット 20200624 22.49.42.png

computedプロパティにも型が効いています。

スクリーンショット 20200624 22.51.20.png

実際に正しく動作させるよう、/api/todosエンドポイントを作成する必要があります。
試しに適当にリストを返すものを作成しました。

router.use('/todos', (_req, res) => {
  res.json([
    {
      id: 1,
      title: 'リスト1',
      description: 'lorem ipsum',
      done: true
    },
    {
      id: 2,
      title: 'リスト2',
      description: 'lorem ipsum',
      done: false
    },
    {
      id: 3,
      title: 'リスト3',
      description: 'lorem ipsum',
      done: true
    }
  ])
})

ページを表示すると、全てが正しく動作していることがわかります。

スクリーンショット 20200624 22.54.42.png

注意する点

@Actionの{ rowError: true }を忘れると正しいエラーが得られない

Actionsメソッド内では、エラーを非同期処理などエラーを捕捉したい場面が多いかと思います。

例えば、次のようなこコードはAxiosのエラーを捕捉することを期待しています。

@Action
  public async createTodo(payload: Todo) {
    const { data } = await $axios.post<Todo>('/api/todo', payload)
    this.add(data)
  }
async asyncData({ error }) {
    try {
      await TodoStore.fetchTodos()
    } catch (e) {
      console.log(e)
      error({
        statusCode: e.response.status,
        message: e.response.message
      })
    }
  },

しかし、このままだと実際に補足するエラーは次のようになってしまいます。

ERR_ACTION_ACCESS_UNDEFINED: Are you trying to access this.someMutation() or this.someGetter inside an @action?
That works only in dynamic modules.

ERR_ACTION_ACCESS_UNDEFINEDと全く身に覚えがないエラーが補足されていますが、これは一体何のエラーなのでしょうか?

実は、@Actionの{ rowError: true }を指定しないと、デフォルトですべてのエラーはライブラリ内部で定義されている固定文言がthrowされます。

デフォルトでエラーを握りつぶしてしまう動作は予期しづらく、かつエラーメッセージも分かりづらいものになっているのでハマりどころだと思います。

他のモジュールがVuexを使用すると競合が発生する

ERR_STORE_NOT_PROVIDEDというエラーに悩まされていたのですが、原因はAuth-Moduleというモジュールを追加したことでした。

このモジュールに限らず、Vuexを使用しているモジュールを使用すると同様のエラーが発生すると思われます。

解決策は、nuxt.config.jsのモジュールの設定でvuex:falseを追加します。

  auth: {
    redirect: {
      login: '/login',
      logout: '/',
      callback: '/login',
      home: '/'
    },
    strategies: { 
       // 省略
    },
    vuex: false // これを追加
  },

このエラーのたちの悪いところは@Actionの{ rowError: true }を指定しないとさらにわけがわからなくなるところですね。

おわりに

はじめは、今までのVuexの記法と大きく違うクラス記法で慣れない部分もありましたが、いざ使ってみるとインテリセンス効きまくりで完全に虜になりました。普段からtypoしまくってる私にとってもうTypeScriptは手放せない存在になりつつあります。

Vuex + TypeScriptはまだ発展途上で、情報もあまり多くなくもしかしたら1年もしないうちにベストプラクティスが変わってしまう可能性はありますが、それを差し置いても導入するメリットはあると感じられました。

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

【Vue.js】v-bind:classで少し詰まった話。

教材を学習していて、少し詰まったので忘備録アウトプット。間違っていたらごめんなさい。

 概要

v-bind:classを使うことで、動的にクラス属性を変えることができる。ちなみにv-bindはclassだけでなく、属性全般に使用できる。

<div v-bind:class ={クラス名:プロパティ値}>hoge</div>

「プロパティ値」の部分がtrueならクラス名が付与。falseなら付与されない。

 例

<style>
       .blue{
         color: blue;
       }
</style>

<div id="app">
     <!--クリック時にchangeを発生-->
     <button v-on:click="change">クリック</button>
   <!--isDoneがtrueなら文字が青くなる-->
     <div v-bind:class ={blue:isDone}>hoge</div>
</div>

<script>

const app = new Vue ({
   //紐ずけ
  el: '#app',
  data:{
      //デフォ値を設定
      isDone:true,
   },
  //メソッドを設定
  methods{
    change:function(){
     this.isDone = !this.isDone
    }
  }
)}

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

【Vue.js】基礎用語集【忘備録】

アウトプット用の忘備録。間違っていたらすいません。

 データバインディング

データと描画を同期させる仕組み。何らかのデータと対象(view)を結びつけ、データや対象の変更をもう一方へ暗示的に変更すること。

  • 双方向バインディング
  • 単方向バインディング
  • ワンタイムバインディングなどがある。

 マスタッシュ構文

{{ 変数名 }}でvue上に定義した変数をテンプレートなどに埋め込むことができる。

 ディレクティブ

v-で始まる特別な属性。vue.jsに何らかの指令を行う。

 v-bind

属性へのデータバインディングに使われる。vue.jsから渡ってきたデータを属性で反映させたいときに使う。urlなどに使えそう。

<input type="text" v-bind:value="hoge">
<!-- helloが出力される -->
const app = new Vue({
  el: '#app',
    date: {
      hoge: 'hello'
    }
 })

 v-if

要素の表示・非表示を切り替える。DOMレベルで追加・削除される。

<!-- vue.js上でtoggleがtrueの場合はhogeが表示される。 -->
<div id="app">
  <p v-if="toggle">
    hoge
  </p>
</div>
const app = new Vue({
  el: '#app',
    date: {
      toggle: true
    }
 })

 v-show

要素の表示・非表示を切り替える。cssのdisplayプロパティを切り替える。頻繁に表示したり非表示にしたりする場合にはv-ifよりこちらの方がいいかも。

<!-- trueなら表示、falseなら非表示 -->
<div id="app">
  <p v-show="toggle">
    hoge
  </p>
</div>
const app = new Vue({
  el: '#app',
    date: {
      toggle: true
    }
 })

 v-for

オブジェクトの繰り返しを行う。下記の例では配列を取り出している。

<div id="app">
  <ol>
    <li v-for="animal in animals" >{{ animal }}</li>
  </ol>
</div>
const app = new Vue({
  el: '#app',
    date: {
     animals: ['dog', 'cat', 'tiger']
    }
 })

 v-on

イベント処理の時に使う。(toLocaleString() メソッドはデフォルトのロケールとデフォルトのオプションが返されるメソッド。)

<div id="app">
  <button v-on:click="onclick">
    Click
  </button>
  <p>
  {{ time }}
  </p>
</div>
const app = new Vue({
  el: '#app',
    date: {
    now:''
    },
   methods:{
     onclick: function(){
     this.time = new Date().toLocaleString() ;
     }
   }
 })

 v-model

双方向データバインディング。v-bindの双方向版。

<div id="app">
  <p>
    <input type="text" v-model="message">
  </p>
  <p>
    <input type="text" v-model="message">
  </p>
</div>
const app = new Vue({
  el: '#app',
    date: {
      message: 'hello'
    }
 })

 コンポーネント

  • ページを構成するUI部品
  • 再利用しやすい
<div id="app">
  <hello-component></hello-component>
  <!--helloが出力される-->
</div>
Vue.component('hello-component', {
  template: '<p>hello</p>'
})

const app = new Vue({
  el: '#app'
 })

 オプションオブジェクト

 el

idやclass名を指定して、Vueインスタンスと結びつける範囲を明示する。

 date

vueアプリ内で使用するデータ(変数など)を管理できる。

 methods

vueアプリ内で使用する関数を定義。v-onディレクティブを使うことでベントリスナーを加えて呼び出すことができる。

 watch

プロパティを監視して、変更時に関数を実行する。

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

【Vue】google-maps-api-loaderで郵便番号から住所を補完させる

概要

入力された郵便番号を元に、Google Maps API(google-maps-api-loader)を利用して「都道府県/市区町村/番地」を取得してオートコンプリートでフォームを補完させる方法

事前準備

  • ライブラリの導入
yarn add google-maps-api-loader
yarn add axios

実装

<template>
  <div>
    <label>郵便番号</label>
    <input
      @input="onChangePostalCodeFirst($event)"
    />
    -
    <input @input="onChangePostalCode($event)" />
    <select v-model="prefectureId">
      <option :value="null" disabled>選択してください</option>
      <option
        v-for="prefecture in prefectures"
        :key="`prefecture-${prefecture.value}`"
        :value="prefecture.value"
      >
        {{ prefecture.label }}
      </option>
    <input ref="address" v-model="address" />
  </div>
</template>

<script>
import GoogleMapsApiLoader from 'google-maps-api-loader'
import axios from 'axios'

export default {
  data() {
    return {
      address: '',
      prefectures: [
        { label: '北海道', value: 1 },
        { label: '青森県', value: 2 },
        ...省略...
      ]
    }
  },
  methods: {
    onChangePostalCodeFirst(event) {
      if (event.target.value.length === 3) {
        this.$refs.postalCodeSecond.focus()
      }
    },
    async onChangePostalCodeSecond(event) {
      if (event.target.value.length === 4) {
        const google = await GoogleMapsApiLoader({
          apiKey: *************,  // APIキーを入力
          libraries: ['places']
        })

        const address = new google.maps.Geocoder()
        address.geocode(
          {
            address: this.postalCode,
            region: 'jp'
          },
          (results, status) => {
            if (status === google.maps.GeocoderStatus.OK) {
              let address = results[0].address_components
              address.shift() // '日本'を削除
              address = address.reverse() // 住所を逆ソート
              address.shift()  // 郵便番号を削除
              address = address.map(data => {
                return data.long_name  // 住所のテキストを抜き出し
              })
              const prefecture = address.shift()  // 都道府県を抜き出し

              this.prefectureId = this.prefectures.find(p => {
                return p.label === prefecture
              }).value
              this.cityAddress = address.join('')
            }
          }
        )
      }
    }
  }
}

動作

Image from Gyazo

参考

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

google-maps-api-loaderで郵便番号から住所を補完させる

概要

入力された郵便番号を元に、Google Maps API(google-maps-api-loader)を利用して「都道府県/市区町村/番地」を取得してオートコンプリートでフォームを補完させる方法

事前準備

  • ライブラリの導入
yarn add google-maps-api-loader
yarn add axios

実装

<template>
  <div>
    <label>郵便番号</label>
    <input
      @input="onChangePostalCodeFirst($event)"
    />
    -
    <input @input="onChangePostalCode($event)" />
    <select v-model="prefectureId">
      <option :value="null" disabled>選択してください</option>
      <option
        v-for="prefecture in prefectures"
        :key="`prefecture-${prefecture.value}`"
        :value="prefecture.value"
      >
        {{ prefecture.label }}
      </option>
    <input ref="address" v-model="address" />
  </div>
</template>

<script>
import GoogleMapsApiLoader from 'google-maps-api-loader'
import axios from 'axios'

export default {
  data() {
    return {
      address: '',
      prefectures: [
        { label: '北海道', value: 1 },
        { label: '青森県', value: 2 },
        ...省略...
      ]
    }
  },
  methods: {
    onChangePostalCodeFirst(event) {
      if (event.target.value.length === 3) {
        this.$refs.postalCodeSecond.focus()
      }
    },
    async onChangePostalCodeSecond(event) {
      if (event.target.value.length === 4) {
        const google = await GoogleMapsApiLoader({
          apiKey: *************,  // APIキーを入力
          libraries: ['places']
        })

        const address = new google.maps.Geocoder()
        address.geocode(
          {
            address: this.postalCode,
            region: 'jp'
          },
          (results, status) => {
            if (status === google.maps.GeocoderStatus.OK) {
              let address = results[0].address_components
              address.shift() // '日本'を削除
              address = address.reverse() // 住所を逆ソート
              address.shift()  // 郵便番号を削除
              address = address.map(data => {
                return data.long_name  // 住所のテキストを抜き出し
              })
              const prefecture = address.shift()  // 都道府県を抜き出し

              this.prefectureId = this.prefectures.find(p => {
                return p.label === prefecture
              }).value
              this.cityAddress = address.join('')
            }
          }
        )
      }
    }
  }
}

動作

Image from Gyazo

参考

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

Vue.js(CLI)

Vue CLIインストール方法

ターミナル

npm install -g @vue/cli

vue create udemy-vue.js  udemy-vue.jsは自分の好きな名前

default (babel, eslint)  選択

cd udemy-vue.js       ディレクりに移動

npm run serve

Image from Gyazo

http://localhost:8080/  ローカル環境で作業開始

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

Nuxt の redirect では vue-router の name が変わらない件

この記事について

Nuxt の middleware や plugin でリダイレクトするのに
redirect('hogehoge');
のような書き方をしていたら、動作が想定外で困った&解決した話です。

困ったこと

router.push() では route.name が変わる

こんなコードがあったとして

pages/mypage.vue
export default Vue.extend({
    created() {
        this.$router.push('/login');
    }
});
pages/login.vue
export default Vue.extend({
    created() {
        console.log(this.$route.name);
        // "login"
    }
});

ブラウザから mypage を呼び出してリダイレクトされた場合でも、 route.namelogin になっています。

Nuxt の redirect を使うと route.name が変わらない

middleware/redirectLogin.vue
export default function({ redirect }) {
    // 〜ログイン済チェクとか〜
    redirect('/login');
}
login.vue
export default Vue.extend({
    created() {
        console.log(this.$route.name);
        // "mypage"
    }
});

ブラウザで見る限りは login にリダイレクトされているけれど、 route.name は最初にアクセスしようとした mypage のまま・・・

解決方法

redirect を使って route.name を変える方法

これだと、 middleware や plugin で route.name を参照している場合、リダイレクト後に想定しない挙動が起きてしまうことがあります。

middleware/redirectLogin.vue
export default function({ route, redirect }) {
    // login ページに来ていたらチェックしない
    if (route.name === 'login') {
        return;
    }
    // ブラウザから見たら login に来ているけど、 route.name が mypage のままなので通過してしまう

    // 〜ログイン済チェクとか〜
    redirect({ name: 'login' });
}

困ったな〜と思っていたら、ちゃんと解決方法がありました?
https://github.com/nuxt/nuxt.js/issues/2421

middleware/redirectLogin.vue
export default function({ redirect }) {
    // 〜ログイン済チェクとか〜
    redirect({ name: 'login' });
}
login.vue
export default Vue.extend({
    created() {
        console.log(this.$route.name);
        // "login"
    }
});

コードを見ると、以下のように router.resolve してくれていました。
これで安心!

utils.js
       if (pathType === 'object') {
         path = app.router.resolve(path).href
       }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

firestoreとvue.ts(vuetify)を用いて検索機能を作成する

firestoreで検索機能を作る

おそらくドキュメントしっかり読めばなんの苦労もなく実装できるけど、メモがてら残しておきます。

検索機能を作る前に

まず、検索機能はfirestoreのcollection().whereを使って実装します。
書き方はこちら
この時にその検索したいものの名前であったりとかID、作者などで一致したものだけを取得することができます

search.vue
<template>
<v-text-field v-model="searchTerm"/>
<v-btn @click="filteredList">
  検索
</v-btn>
</template>
<script lang="ts">
searchTerm: number | null = null
filteredData: Array<firebase.firestore.DocumentData> = []
filteredList() {
  firebase.firestore().collection('コレクションの名前').where('name', '==', this.searchTerm).get()
    .then((querySnapshot) => {
      querySnapshot.forEach((doc) => {
        this.filteredData.push(doc.data())
      })
    })
  }
</script>

この場合だと検索というボタンを押すとfilteredListが動いて、データベースのコレクション内のnameという値がv-modelに渡っているのと一致するドキュメントだけを取ってきてくれます。
fiteredDataに配列として入っているのでv-forで回せば複数個表示したい場合でも問題ないと思います。
一応表示する際のコードもおいておきます(html部分だけです)

search.vue
<template>
<v-layout wrap>
 <v-flex
    xs12
    sm6
    md4
    v-for="(data, i) in filteredData"
    :key="i"
  >
    <v-card
      width='350'
      height='200'
      class='my-5'
    >
      <v-card-title
        class='ml-2'
      >
        {{ data.title }}
      </v-card-title>
    </v-card>
  </v-flex>
</v-layout>
</template>

これで取ってきたいドキュメントのtitleだけが表示されていると思います

最後に

詳しいコードは
私のgithubのsrc/components/atoms/にあるのでよかったら覗いてみてください。
何か他にもっといい書き方、間違っているところありましたら、教えていただけると幸いです。

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

VueにTypeScriptを導入する3つのやり方を比較してみた

最近TypeScriptが熱いっていう話をよく聞きますね!
っていうことで私もTypeScriptを導入するには何が必要なのか知りたくなりました。

でも、Vue.jsにTypeScriptを導入する方法が現状では3つほどあります。

  • Class Style Component
  • Vue.extend(後に知ったところObject Styleというらしい)
  • Composition API(RFC)

それぞれの方法の導入記事はよくありますが、それらを比較してみたくなりました。
という内容のLTを半年ほど前にしたのですが、登壇資料はこちらです。
VueでTypeScriptを始める 3つのStyle/3 style to start vue with typescript - Speaker Deck

今回の記事について

  • TypeScript運用経験がとても浅いので、比較しきれてないかもしれません
    • 間違っているところがあれば教えて下さい!
  • Vuexについては扱いません
  • VueCLI v3で作ったプロジェクトで試しています

今回のコードの比較対象

vue.js + typescript = vue.ts ことはじめ - Qiita

この記事のコードを一度JavaScriptで書いて、
それからTypeScript化するという方法を試しました

コードはこちらにあります。(リポジトリ名が適当すぎた…)
fruitriin/vue-ts-test - GitHub
vue-ts-test/Home.vue at master · fruitriin/vue-ts-test
vue-ts-test/MyButton.vue at master · fruitriin/vue-ts-test
vue-ts-test/ResetButton.vue at master · fruitriin/vue-ts-test

Class Style Component

Step by stepで書かれているし、元になった記事なので、この記事のほうが詳しいのですが…
vue.js + typescript = vue.ts ことはじめ - Qiita

既存のVue CLI v3のプロジェクトがある場合、vue add typescript でTypeScriptの関連モジュール一式がインストールできます。
対話形式で質問に答えてください。
Class Style Componentの場合は、「Use class-style component syntax?」 にYESです。

Class Style Componentで書き換える必要があるのは主に以下の通りです。

1.<script><script lang=ts>に書き換える
2.vue-property-decoratorから型注釈をimportする

スクリーンショット 2020-01-24 17.08.34.png

3.export defaultexport default class {Class名} extends Vueに書き換える
4.classのすぐ上に@Componentデコレータを付ける
5.子コンポーネントがある場合は@Component()デコレータの中に移動する
スクリーンショット 2020-01-24 17.10.07.png

5.dataはClassのプロパティとして定義する
スクリーンショット 2020-01-24 17.12.06.png

6.propsは@Propデコレータ付きのプロパティとして定義する
スクリーンショット 2020-01-24 17.13.00.png

7.$emitは@Emit()デコレータ付きのメソッドにする
イベントの送出用に別のメソッドに分ける
スクリーンショット 2020-01-24 18.10.12.png

8.computedはクラスのgetterメソッドにする
スクリーンショット 2020-01-24 18.13.07.png

9.watchは@Watch()デコレータ付きのメソッドにする
スクリーンショット 2020-01-24 18.13.37.png

Class Style ComponentでのHome.vue, MyButton.vue, ResetButton.vueのコードはこちら
vue-ts-test/Home.vue at master · fruitriin/vue-ts-test
vue-ts-test/MyButton.vue at master · fruitriin/vue-ts-test
vue-ts-test/ResetButton.vue at master · fruitriin/vue-ts-test
かなり書き方が変わりますね。

Class Style Componentポイント

  • TypeScriptっぽい書き方になる
  • Vueの公式サブプロジェクト
  • Vue.js v0.0.1の頃からあるらしく、資料が豊富
    • Vue3以降、新しい情報が出てきにくくなる可能性はある
  • Vue3.xの本体に取り込まれることはない(RFCは提出されたがRejectされた)が、3.x以降もサポートされ続ける予定らしい(というのをどこかでみた?UIT Insideで聞いた?)
  • 後述のVue.extend(Object Style)と混在させても特に問題はないはず

Vue.extend(Object Style?)

[Vue+TypeScript] Vue.extend で Vue らしさを保ちつつ TypeScript で書くときの型宣言についてまとめた - Qiita
この記事が詳しいです。

vue add typescript して、Use class-style component syntax?にNOと答えます

1.<script><script lang=ts>に書き換える
2.import Vue from 'vue'する
3.export default {}export default Vue.extend({}) に書き換える
スクリーンショット 2020-01-24 18.28.51.png
4.componentsはそのまま
5.dataは全部を内包するオブジェクトの型を与える
スクリーンショット 2020-01-24 18.30.56.png
6.propsはObjectがなければそのまま(type指定は必要)
Objectの場合は type: Object as PropType<{ObjectType}> で型を付ける
7.methods, watch, computed は適宜引数や戻り値に型を付ける
スクリーンショット 2020-01-24 18.35.20.png

以上!おしまい。

Vue.extend()で書いたコードはこちら。
vue-ts-test/Home.vue at master · fruitriin/vue-ts-test
vue-ts-test/MyButton.vue at master · fruitriin/vue-ts-test
vue-ts-test/ResetButton.vue at master · fruitriin/vue-ts-test

Vue.extend() ポイント

  • Vue.js v2.4から本体に追加された機能
  • 従来の書き方にちょっと書き加えるだけでTypeScript化できちゃう
  • props の型の付け方以外は本当に素直に書いて型がつけられるはず
    • この辺よくわかってないので教えて下さい
  • 前述のClass Styleと混在させても問題ないはず
    • vue-property-decoratorがプロジェクトに入っていないなら、これをインストール必要はある
  • 資料を探しにくいかもしれない

Composition API (RFC)

Vue.js v3.xに取り込まれる予定のRFCです。
yarn add @vue/composition-apiすると2.xのプロジェクトでも試すことができます。

下準備としてmain.tsにVue.use()で注入する必要があります。

import VueCompositionApi from from '@vue/composition-api';
Vue.use(VueCompositionApi);

1.<script><script lang=ts>に書き換える
2.import {createComponent} from '@vue/composition-api'する
3.export default()export default createComponent({}) に書き換え
4.componentsはそのまま
5.setup()を生やす

<script lang="ts">
import { createComponent} from "@vue/composition-api";

// @ is an alias to /src
import MyButton from "@/components/MyButton.vue";
import ResetButton from "@/components/ResetButton.vue";

export default createComponent({
  setup() {
    return {};
  },
  components: {
    MyButton,
    ResetButton
  }
});

6.data()をreactive()にする。reactive()メソッドの返り値をsetup()のreturnに含める

data(){
  return {
    greatText: "Hello",
    count: 0
  }
}

// importするものにreactiveを追加する
import {createComponent, reactive} from '@vue/composition-api'

// setup()だけ抜き出し
setup(){
  const state = reactive({
    greatText: "Hello",
    count: 0    
  })
  return {
    state
  }
}

このとき、reactive()したものはdata配下のオブジェクトとして反映されているので、templateも修正する

<template>
  <div>
   <p>{{state.greatText}}</p>
   <p>{{state.count}}</p>
  </div>
</template>

8.propsはsetup関数の第一引数に渡される
9.emitはsetup関数の第二引数のcontextの中にある
10.methodsはsetup関数の中で定義してreturnするオブジェクトに含める

  setup(props, context) {
    let count = 0;
    const onClick = () => {
      alert(props.great);
      count++;
      context.emit("click", count);
    };

    return {
      onClick
    };
  }

11.computedはcomputed関数でWrapします(第一引数にコールバック)
Note: computed関数はimportが必要です
12.watchはwatch関数にWrapします(第一引数に監視対象、第二引数にコールバック)
Note: watch関数はimportが必要です
Note: watch関数は何度setup関数内で呼んでもよいし、setup関数内でreturnする必要もない

  setup() {
    const state = reactive({
      count: 0
    });
    const isRegulars = computed(() => {
      return state.count >= 3;
    });
    watch(
      () => state.count,
      () => {
        if (state.count === 3) {
          alert("常連になりました");
        }
      }
    );
    return {
      state,
      onMyButtonClicked,
      isRegulars
    };

Composition APIで書いたコードはこちら
- vue-ts-test/Home.vue at master · fruitriin/vue-ts-test
- vue-ts-test/MyButton.vue at master · fruitriin/vue-ts-test
- vue-ts-test/ResetButton.vue at master · fruitriin/vue-ts-test

Composition APIのポイント

  • Vue3.xからVueコアに導入されるAPIです
    • 2.xでは別のライブラリを導入しないと使えません
  • すべてのコンポーネントをCompotision APIで書く必要はありません
    • なんだか難しそう……と思った人は使わなくても大丈夫だし。
  • コードの変更点自体はおおきいかも
  • thisを使わないので型安全に書きやすくなります
  • コードの組織化に役立ちます
    • 複数の機能を併せ持つコンポーネントを、機能ごとに分割して定義することができます

まとめ

[あなた/プロジェクトチーム]にとって最適な方法を選ぶとよいですね!
その一助となれば幸いです。

比較してみたけど「特に好みとかないよ」「よくわかんない」って人は個人的にはVue.extend()がいいんじゃないかなあ。

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