Slide 1

Slide 1 text

Typed Vuex Data Flow @joe_re

Slide 2

Slide 2 text

Who am I? twitter: @joe_re github: @joe­re working in freee.K.K

Slide 3

Slide 3 text

What is Vuex

Slide 4

Slide 4 text

Q1: Vuex 使っている人 どれぐらいますか??

Slide 5

Slide 5 text

Q2: その中でTypeScript やFlow を使っている人い ますか??

Slide 6

Slide 6 text

Q3: その中で各レイヤー をまたいだ型付けまでし ている人いますか??

Slide 7

Slide 7 text

この発表は僕がVuex を試す中で、 試行錯誤した結果です。 ベストな解決策はまだまだ余地が あると思います。 手を上げていただいた方、このあとぜひお話しましょう!

Slide 8

Slide 8 text

DEMO https://github.com/joe­re/tubutler

Slide 9

Slide 9 text

TypeScript or Flow? 現状Vue において楽をしたいならTypeScript を選ぶべき 公式にもサポートについて名言されている https://vuejs.org/v2/guide/typescript.html 実はVue の内部はFlow で実装されているが、flow では this type を扱えないのでサポートが厳しそう https://github.com/vuejs/vue/pull/5027

Slide 10

Slide 10 text

内部実装の方も例のApatch の決定 によりTS に置き換わるかも? https://github.com/vuejs/vue/issues/6411

Slide 11

Slide 11 text

Typed Vue Component 現在(Vue v2.4.2) において、通常のVue インスタンスの生成 では推論が効かないため型安全性の保証が難しい Vue インスタンスに渡すオプションの中において、this type がany になってしまうため PR は出ているのでそのうちできるようになる模様 https://github.com/vuejs/vue/pull/5887 詳しくは@ktsn さんの発表資料をご参照ください Contextual ThisType and Vue.js https://speakerdeck.com/ktsn/contextual­thistype­and­ vue­dot­js

Slide 12

Slide 12 text

Class­Style Vue Component 現在のところはvue­class­component を使って、Class とし てComponent を定義するのが現実解 Class で書くことによりthis の解決を可能にするアプローチ import Vue from 'vue' import Component from 'vue-class-component' // The @Component decorator indicates the class is a Vue component @Component({ // All component options are allowed in here template: 'Click!' }) export default class MyComponent extends Vue { // Initial data can be declared as instance properties message: string = 'Hello!' // Component methods can be declared as instance methods onClick (): void { window.alert(this.message) } }

Slide 13

Slide 13 text

Typed Vuex Data Flow ( ここからVuex の話)

Slide 14

Slide 14 text

BattleAX https://github.com/joe­re/battle­ax そのままのVuex の定義では各レイヤーをまたいで 型安全性を保証するのが難しかった そこでVuex の薄いラッパーを作成した 以降の説明はこのライブラリの使用方法と実装アプローチ を元にお話しします

Slide 15

Slide 15 text

Deifinition of Actions

Slide 16

Slide 16 text

Vuex の提供するAction actions: { hoge (context, payload) { context.commit('hoge', payload) } } 第1 引数にaction のcontext 、第2 引数view から渡された payload を受け取る action のcontext には、ここからmutation へ渡すための関数で あるcommit やstate やgetter などのプロパティを含む (型定義を直接見た方が使える機能の把握は早い https://github.com/vuejs/vuex/blob/dev/types/index.d.ts)

Slide 17

Slide 17 text

ところでRedux は function addTodo(text) { return { type: ADD_TODO, text } } 純粋関数として提供される 一切の副作用がない view から直接呼ばれる形式で定義するので、 そのままこの定義を使うだけで型安全性の保証ができる

Slide 18

Slide 18 text

Vuex の難しいところ view 側には当然action のcontext などは存在しないので、こ の定義をview からはそのまま使えない

Slide 19

Slide 19 text

action の定義を改変することにした import { ActionCreatorHelper } from 'battle-ax'; import { Product } from '../types'; import { State } from './index'; import shop from '../api/shop'; export type ActionTypes = { ADD_TO_CART: { id: number }, CHECKOUT_REQUEST: null, //... 省略 }; export const actions = ActionCreatorHelper()({ addToCart (payload: { id: number }) { return ({ commit }) => { commit({ type: 'ADD_TO_CART', payload: { id: payload.id } }); } }, //... 省略 } payload を第1 引数として受け取り、ActionContext は返り値 の関数定義にinject されるようにした ActionTypes の定義はMutation で使いやすい形にした( 後述)

Slide 20

Slide 20 text

これでView から直接呼び出せる形 のAPI になったので、View の呼び 出し時に型付けすることができる

Slide 21

Slide 21 text

実装はこんな感じ export interface ActionContext { dispatch: Dispatch; commit: (params: { type: K, payload?: A[K] }) => void, state: S; getters: any; rootState: R; rootGetters: any; } type Action = (payload: P) => (injectee: ActionContext) => any; export type ActionTree = { [key: string]: (payload: any) => (ctx: ActionContext) => any } export function ActionCreatorHelper() { return >(ac: T): T => ac }

Slide 22

Slide 22 text

ActionCreatorHelper の定義が奇妙な感じに ActionCreatorHelper: () => >(ac: T): T => ac 本当は ActionCreatorHelper (AC は引数で受け取る ActionCreator の関数群) という感じにしたかった しかしこのAC だけは推論を効かせたい しかしTS のgenerics では一部にだけ推論を効かせることは できない( ヒントを与えるなら全部に与えないといけない) そこで高階関数にして、段階を分けることで対応した 他にいい方法をご存知の方は是非おしえてください!

Slide 23

Slide 23 text

Deifinition of Mutations

Slide 24

Slide 24 text

Vuex の提供するMutation mutations: { ['hoge'] (state, payload) { state.hoge = payload.hoge; } } ActionCreator でcommit されたkey で定義した関数が実行さ れる この関数は第1 引数にstate 、第2 引数にcommit された payload を受け取る

Slide 25

Slide 25 text

ところでRedux では function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) default: return state } } dispatch されたAction はreducer でそのまま受け取り、Action のtype property で処理を分岐する 静的型付けの観点では、ActionType をunion type として定義 しておき、type property の動的チェックにより絞り込みを効 かせることで型安全性を保証する

Slide 26

Slide 26 text

Vuex の難しいところ Mutation が動作するタイミングではすでに振り分けが完了し ているので、union でAction を定義しても絞り込みを効かせ るタイミングはすでに過ぎている Mutation を定義した時点で受け取るpayload が決定されてい る必要がある

Slide 27

Slide 27 text

先ほど定義したActionTypes を generics として受け取ることで解決した import { MutationTree } from 'battle-ax'; import { ActionTypes } from './actions'; import { State } from '../store'; export const mutations: MutationTree = { ['ADD_TO_CART'] (state, payload) { state.lastCheckout = null; const record = state.added.find(p => p.id === payload.id); if (!record) { state.added.push({ id: payload.id, quantity: 1 }); } else { if (record.quantity) record.quantity++; } const target = state.all.find(p => p.id === payload.id); if (target && target.inventory) target.inventory--; }, // ... 省略 } Mutation を定義した時点でstatem 、 payload は決定できる

Slide 28

Slide 28 text

実装はこんな感じ export type Mutation = (state: S, payload: P) => any; export type MutationTree = { [P in keyof A]?: Mutation; };

Slide 29

Slide 29 text

Definition of Getters

Slide 30

Slide 30 text

Vuex の提供するGetter getters: { doneTodos: state => { return state.todos.filter(todo => todo.done) } } state や他のgetter などを引数として受け取り、 それを元に値を計算する関数 副作用はない

Slide 31

Slide 31 text

Vuex の難しいところ 副作用がない純粋な関数なので特になし あえて言えば関数なのでview から直接呼べる形式の定義で はないので、ちょっと変換が必要

Slide 32

Slide 32 text

API は変えずに定義できた import { State } from './index'; import { FullItem } from '../types/Item'; import { GetterTree } from 'battle-ax'; export type Getters = { nextVideo: FullItem | null; nextVideoId: string | null; relatedVideos: FullItem[]; }; export const getters: GetterTree = { nextVideo: (state, getters) => { const next = state.relatedVideos.filter((v) => !state.playedVedeoIds.includes(v.id.videoId)); if (next.length === 0) { return null; } return next[0]; }, nextVideoId: (state, getters) => { return getters.nextVideo && getters.nextVideo.id.videoId; }, relatedVideos: (state, getters) => { return state.relatedVideos.filter(v => v !== getters.nextVideo); } } 先にGetter の計算結果を定義しておく(Getters) getters の返す値はgenerics として与えることで保証する

Slide 33

Slide 33 text

実装はこんな感じ export type GetterResult = { [key: string]: any } export type Getter = (state: S, getters: G, rootState: R, rootGetters: any) => V; export type GetterTree = { [P in keyof G]: Getter; } 先にGetter の計算結果を定義する形にして解決したけど、 GetterTree の定義から推論できれば定義が要らなくなるか も? TS 力が足りなくていい方法は浮かばなかった TS glue の方、アドバイスください!

Slide 34

Slide 34 text

Create a Store ここで今まで改変した定義の帳尻を合わせる 使い方はVuex とほぼ変わらないので、型付の観点で特筆す ることはありません

Slide 35

Slide 35 text

こんな感じ import { mutations } from './mutations' import { actions } from './actions'; import { FullItem } from '../types/Item'; import getters from './getters'; import { createStore } from 'battle-ax'; export type State = { items: FullItem[], relatedVideos: FullItem[], playedVedeoIds: string[], miniPlayerMode: boolean, transparentRate: number, loading: boolean } const state: State = { items: [], relatedVideos: [], playedVedeoIds: [], miniPlayerMode: false, transparentRate: 0, loading: false } const store = createStore({ strict: process.env.NODE_ENV !== 'production', state, actions, mutations, getters, }); export default store;

Slide 36

Slide 36 text

Using store from view

Slide 37

Slide 37 text

ここまででstore は型付けできてい るので、直接import するだけで型 安全性は保証できる

Slide 38

Slide 38 text

しかし直接import すると依存が 生まれてしまいテストに苦労する

Slide 39

Slide 39 text

Vuex のアプローチ const app = new Vue({ el: '#app', // provide the store using the "store" option. // this will inject the store instance to all child components. store, components: { Counter }, template: `
` }) root にstore を渡すことで、呼び出されるすべての component の this.$store プロパティにstore の参照が入る (DI) これにより、すべてのcomponent から透過的にstore を呼び 出せるようになる

Slide 40

Slide 40 text

個人的に気に入らないところ これをやってしまうと、PresentationalComponent として 設計しても、store を呼び出すことができてしまう その結果store の構造に依存したcomponent になってしまう 性善説で言えば、再利用性を高めたいComponent ではstore の値を直接呼ばないようにすることで問題は起きない しかし人は善だけでは構成されていない

Slide 41

Slide 41 text

inject という関数を用意した import Vue from 'vue'; import VueRouter from 'vue-router'; import Root from './containers/RootContainer.vue'; import Home from './containers/HomeContainer.vue'; import PlayVideo from './containers/PlayVideoContainer.vue'; import miniPlayer from './containers/miniPlayerContainer.vue'; import store from './store/index'; import { inject } from 'battle-ax' Vue.use(VueRouter); const routes = [ { path: '/', component: inject(Root, store), children: [ { path: '/mini-player/:id?', name: 'miniPlayer', component: inject(miniPlayer, store) }, { path: '/', name: 'home', component: inject(Home, store) }, { path: '/:id', name: 'player', component: inject(PlayVideo, store) }, ] }, ]; export default new VueRouter({ routes }); 第1 引数にstore 、第2 引数にvue component を受け取る 返り値としてactions, getters, state がinject されたcomponent を受け取る

Slide 42

Slide 42 text

実装はこんな感じ export function inject>(container: any, store: BAStore) { const name = container.options.name || 'unknown-container'; return { name: `injected-${name}`, components: { [name]: container }, data: () => ({ actions: store.actions, state: store.state, getters: store.getters }), template: '<${name} :actions="actions" :state="state" :getters="getters" />' } } HOC パターンを使って、actions 、state 、getters をprops と して注入しているだけ Vue のHOC の実装例を見つけられなかったけど、これで良 かったんだろうか これを使うとProps のバケツリレーからは逃れられないが、 お行儀はよくなるというトレードオフ

Slide 43

Slide 43 text

complete typed data flow on Vuex (for me)

Slide 44

Slide 44 text

ありがとうございました!