概要
Vue.jsをTypeScriptで開発する際にVuexを利用するのにしっくりくる実装方法を模索中で、いくつか方法を試してみました。
GitHubに利用したプロジェクトをUPしています。実際に試してみたい方どうぞ^^
準備
ここではDockerを利用して環境構築していますが、ローカルで構築してもらってもOKです。
> mkdir 任意のディレクトリ
> cd 任意のディレクトリ
> vi Dockerfile
> vi docker-compose.yml
FROM node:10.8.0-stretch
RUN npm install --global @vue/cli
WORKDIR /projects
version: '3'
services:
app:
build: .
ports:
- "8080:8080"
volumes:
- ".:/projects"
tty: true
> docker-compose up -d
> docker-compose exec app bash
> vue create app
Vue CLI v3.0.1
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Vuex, Linter, Unit
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript for auto-detected polyfills? Yes
? Pick a linter / formatter config: TSLint
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Mocha
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
? Pick the package manager to use when installing dependencies: (Use arrow keys)
❯ Use Yarn
Use NPM
> cd app
> yarn serve
これで実装の準備が整いました。
Vue-Cli標準
vue create
コマンドでプロジェクトを作成するとsrc直下にstore.tsが作成されているので、そこに実装をいれて利用するパターンです。
stateにcounter
ってのを持っていて、それをインクリメントするアクションがあるだけです。
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
interface State {
conuter: number;
}
export default new Vuex.Store({
state: {
conuter: 0,
} as State,
getters: {
getCounter: (state, getters) => () => {
return state.conuter;
},
},
mutations: {
increment(state, payload) {
state.conuter += 1;
},
},
actions: {
incrementAction(context) {
context.commit('increment');
},
},
});
App.vueで使ってみます。
超適当ですが、画像にclickイベント定義して、HelloWorldコンポーネントでstateに定義しているcounter
を表示してます。
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" @click="increment">
<HelloWorld :msg="`Welcome to Your Vue.js + TypeScript App ${this.counter}`"/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from './components/HelloWorld.vue';
@Component({
components: {
HelloWorld,
},
})
export default class App extends Vue {
private get counter(): number {
return this.$store.getters.getCounter();
}
private increment(): void {
this.$store.dispatch('incrementAction');
}
}
</script>
(略)
ブラウザで確認すると、画像をクリックするとcounterがインクリメントするのが、確認できます。
気になる点
実装はシンプルで良いのですが、規模が大きくなってくると、store.tsが肥大化してくるのが目に見えます。辛い。
this.$store
を利用するので、メソッド名などを間違えていても実行時にしかエラーにならないので、せっかくTypeScript使っているのになーです。
export default class App extends Vue {
private get counter(): number {
return this.$store.getters.getCounter2(); // 実行時にエラー
}
private increment(): void {
this.$store.dispatch('incrementAction2'); // 実行時にエラー
}
}
vuex-type-helperを利用する
下記記事で紹介されていたvuex-type-helperを利用してみます。
Vue.js + Vuex + TypeScript を試行錯誤してみた
https://logs.utahta.com/blog/2017/09/02/110000
ktsn/vuex-type-helper
https://github.com/ktsn/vuex-type-helper
vuex-type-helperとvuex-classってのを追加します。
> yarn add vuex-type-helper vuex-class
storeの実装を追加します。ここではモジュール化してみます。
> mkdir -pv store2/modules
> touch store2/index.ts
> touch store2/modules/app.ts
import Vue from 'vue';
import Vuex from 'vuex';
import { app } from '@/store2/modules/app';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
app,
},
});
import Vuex, { createNamespacedHelpers } from 'vuex';
import { DefineActions, DefineGetters, DefineMutations } from 'vuex-type-helper';
export interface State {
counter: number;
}
export interface Getters {
counter: number;
}
export interface Mutations {
increment: {};
}
export interface Actions {
incrementAction: {};
}
export const state: State = {
counter: 0,
};
export const getters: DefineGetters<Getters, State> = {
counter: (state) => state.counter,
};
export const mutations: DefineMutations<Mutations, State> = {
increment(state, {}) {
state.counter += 1;
},
};
export const actions: DefineActions<Actions, State, Mutations, Getters> = {
incrementAction({ commit }, payload) {
commit('increment', payload);
},
};
export const {
mapState,
mapGetters,
mapMutations,
mapActions,
} = createNamespacedHelpers<State, Getters, Mutations, Actions>('app');
export const app = {
namespaced: true,
state,
getters,
mutations,
actions,
};
利用できるようにmain.tsとApp.vueを編集します。
import Vue from 'vue';
import App from './App.vue';
-import store from './store';
+import store from './store2';
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld from './components/HelloWorld.vue';
import { Getter } from 'vuex-class';
import * as app from './store2/modules/app';
@Component({
components: {
HelloWorld,
},
methods: {
...app.mapActions(['incrementAction']),
},
})
export default class App extends Vue {
@Getter('app/counter') private counter!: number;
private incrementAction!: (payload: {}) => void;
private increment(): void {
this.incrementAction({});
}
}
(略)
はい。
気になる点
こちらの利点としてはモジュール化しやすい点と、アクション名を間違えてたときにビルドエラー吐いてくれる点でしょうか。
methods: {
...app.mapActions(['incrementAction2']),
},
Argument of type '"incrementAction2"[]' is not assignable to parameter of type '"incrementAction"[]'.
Type '"incrementAction2"' is not assignable to type '"incrementAction"'.
getterで間違ってる場合は、ブラウザ側でエラーになります。惜しい。
@Getter('app/counter2') private counter!: number;
ブラウザでエラー
vuex-classを利用するとActionなんかも以下のような定義ができるけれど、getterと同様にメソッド名間違いがブラウザでしか検知できないので、微妙。
@Action('app/incrementAction') private incrementAction!: (payload: {}) => void;
うーん。独自実装いれたらもっとブラウザエラーを回避できそうですが、どこまで実装しようか悩ましいところです。
Vue.js+TypeScriptで開発するときの参考記事まとめ
https://qiita.com/kai_kou/items/19b494a41023d84bacc7