- 投稿日:2020-06-25T22:53:32+09:00
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-decoratorsstore/index.ts
ここからはNuxt.jsで使用するために必要な手順です。公式のREAD MEの手順に従って実装していきます。
まずは、
~/store/index.ts
ファイルを作成し、以下のコードを記述します。~/store/index.tsimport { 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.tsimport { 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をクラスベースで作成するのが特徴です。
さらに、デコレータを使用してモジュールであることや、Mutation
・Action
メソッドであることを伝えます。
クラス内でなら、他のプロパティの要素には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.js
のplugins
に忘れずに追加します。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
が使えるので、インテリセンスが使えるのでいい感じです。コンポーネントから呼び出す
それでは、実際に作成したモジュールをコンポーネントから呼び出してみます。
従来のようなインテリセンスの効かないmapActions
やmapGetters
は使用せずに、methods
、computed
に定義して使用します。~/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'
でモジュールをインポートして使用します。
下記の通り、インテリセンスがよく効いています。
computed
プロパティにも型が効いています。実際に正しく動作させるよう、
/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 } ]) })ページを表示すると、全てが正しく動作していることがわかります。
注意する点
@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年もしないうちにベストプラクティスが変わってしまう可能性はありますが、それを差し置いても導入するメリットはあると感じられました。
- 投稿日:2020-06-25T22:53:32+09:00
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-decoratorsstore/index.ts
ここからはNuxt.jsで使用するために必要な手順です。公式のREAD MEの手順に従って実装していきます。
まずは、
~/store/index.ts
ファイルを作成し、以下のコードを記述します。~/store/index.tsimport { 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.tsimport { 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をクラスベースで作成するのが特徴です。
さらに、デコレータを使用してモジュールであることや、Mutation
・Action
メソッドであることを伝えます。
クラス内でなら、他のプロパティの要素には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.js
のplugins
に忘れずに追加します。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
が使えるので、インテリセンスが使えるのでいい感じです。コンポーネントから呼び出す
それでは、実際に作成したモジュールをコンポーネントから呼び出してみます。
従来のようなインテリセンスの効かないmapActions
やmapGetters
は使用せずに、methods
、computed
に定義して使用します。~/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'
でモジュールをインポートして使用します。
下記の通り、インテリセンスがよく効いています。
computed
プロパティにも型が効いています。実際に正しく動作させるよう、
/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 } ]) })ページを表示すると、全てが正しく動作していることがわかります。
注意する点
@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年もしないうちにベストプラクティスが変わってしまう可能性はありますが、それを差し置いても導入するメリットはあると感じられました。
- 投稿日:2020-06-25T18:27:10+09:00
【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>
- 投稿日:2020-06-25T17:55:10+09:00
【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
プロパティを監視して、変更時に関数を実行する。
- 投稿日:2020-06-25T17:02:10+09:00
【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('') } } ) } } } }動作
参考
- 投稿日:2020-06-25T17:02:10+09:00
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('') } } ) } } } }動作
参考
- 投稿日:2020-06-25T17:01:50+09:00
Vue.js(CLI)
- 投稿日:2020-06-25T16:24:02+09:00
Nuxt の redirect では vue-router の name が変わらない件
この記事について
Nuxt の middleware や plugin でリダイレクトするのに
redirect('hogehoge');
のような書き方をしていたら、動作が想定外で困った&解決した話です。困ったこと
router.push()
では route.name が変わるこんなコードがあったとして
pages/mypage.vueexport default Vue.extend({ created() { this.$router.push('/login'); } });pages/login.vueexport default Vue.extend({ created() { console.log(this.$route.name); // "login" } });ブラウザから
mypage
を呼び出してリダイレクトされた場合でも、route.name
はlogin
になっています。Nuxt の
redirect
を使うと route.name が変わらないmiddleware/redirectLogin.vueexport default function({ redirect }) { // 〜ログイン済チェクとか〜 redirect('/login'); }login.vueexport default Vue.extend({ created() { console.log(this.$route.name); // "mypage" } });ブラウザで見る限りは
login
にリダイレクトされているけれど、route.name
は最初にアクセスしようとしたmypage
のまま・・・解決方法
redirect
を使って route.name を変える方法これだと、 middleware や plugin で
route.name
を参照している場合、リダイレクト後に想定しない挙動が起きてしまうことがあります。middleware/redirectLogin.vueexport default function({ route, redirect }) { // login ページに来ていたらチェックしない if (route.name === 'login') { return; } // ブラウザから見たら login に来ているけど、 route.name が mypage のままなので通過してしまう // 〜ログイン済チェクとか〜 redirect({ name: 'login' }); }困ったな〜と思っていたら、ちゃんと解決方法がありました?
https://github.com/nuxt/nuxt.js/issues/2421middleware/redirectLogin.vueexport default function({ redirect }) { // 〜ログイン済チェクとか〜 redirect({ name: 'login' }); }login.vueexport default Vue.extend({ created() { console.log(this.$route.name); // "login" } });コードを見ると、以下のように
router.resolve
してくれていました。
これで安心!utils.jsif (pathType === 'object') { path = app.router.resolve(path).href }
- 投稿日:2020-06-25T10:40:19+09:00
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/にあるのでよかったら覗いてみてください。
何か他にもっといい書き方、間違っているところありましたら、教えていただけると幸いです。
- 投稿日:2020-06-25T00:15:00+09:00
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-testClass 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する3.
export default
をexport default class {Class名} extends Vue
に書き換える
4.classのすぐ上に@Component
デコレータを付ける
5.子コンポーネントがある場合は@Component()
デコレータの中に移動する
6.propsは
@Prop
デコレータ付きのプロパティとして定義する
7.$emitは@Emit()デコレータ付きのメソッドにする
イベントの送出用に別のメソッドに分ける
9.watchは@Watch()デコレータ付きのメソッドにする
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({})
に書き換える
4.componentsはそのまま
5.dataは全部を内包するオブジェクトの型を与える
6.propsはObjectがなければそのまま(type指定は必要)
Objectの場合はtype: Object as PropType<{ObjectType}>
で型を付ける
7.methods, watch, computed は適宜引数や戻り値に型を付ける
以上!おしまい。
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-testVue.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-testComposition APIのポイント
- Vue3.xからVueコアに導入されるAPIです
- 2.xでは別のライブラリを導入しないと使えません
- すべてのコンポーネントをCompotision APIで書く必要はありません
- なんだか難しそう……と思った人は使わなくても大丈夫だし。
- コードの変更点自体はおおきいかも
- thisを使わないので型安全に書きやすくなります
- コードの組織化に役立ちます
- 複数の機能を併せ持つコンポーネントを、機能ごとに分割して定義することができます
まとめ
[あなた/プロジェクトチーム]にとって最適な方法を選ぶとよいですね!
その一助となれば幸いです。比較してみたけど「特に好みとかないよ」「よくわかんない」って人は個人的にはVue.extend()がいいんじゃないかなあ。