Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Typed Vuex Data Flow

joe_re
August 24, 2017

Typed Vuex Data Flow

Meguro.es #11 @ oRo 発表資料
https://meguroes.connpass.com/event/61531/

joe_re

August 24, 2017
Tweet

More Decks by joe_re

Other Decks in Technology

Transcript

  1. Typed Vuex Data Flow
    @joe_re

    View Slide

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

    View Slide

  3. What is Vuex

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  11. 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

    View Slide

  12. 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)
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  15. Deifinition of Actions

    View Slide

  16. 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)

    View Slide

  17. ところでRedux

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

    View Slide

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

    View Slide

  19. 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
    で使いやすい形にした(
    後述)

    View Slide

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

    View Slide

  21. 実装はこんな感じ
    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
    }

    View Slide

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

    View Slide

  23. Deifinition of Mutations

    View Slide

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

    View Slide

  25. ところで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
    の動的チェックにより絞り込みを効
    かせることで型安全性を保証する

    View Slide

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

    View Slide

  27. 先ほど定義した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
    は決定できる

    View Slide

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

    View Slide

  29. Definition of Getters

    View Slide

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

    View Slide

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

    View Slide

  32. 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
    として与えることで保証する

    View Slide

  33. 実装はこんな感じ
    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
    の方、アドバイスください!

    View Slide

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

    View Slide

  35. こんな感じ
    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;

    View Slide

  36. Using store from view

    View Slide

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

    View Slide

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

    View Slide

  39. 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
    を呼び
    出せるようになる

    View Slide

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

    View Slide

  41. 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
    を受け取る

    View Slide

  42. 実装はこんな感じ
    export function inject>(container: any, store: BAStoreG, A, AC>) {
    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
    のバケツリレーからは逃れられないが、
    お行儀はよくなるというトレードオフ

    View Slide

  43. complete typed data
    flow on Vuex (for me)

    View Slide

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

    View Slide