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 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
  2. 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: '<button @click="onClick">Click!</button>' }) 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) } }
  3. 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)
  4. ところでRedux は function addTodo(text) { return { type: ADD_TODO, text

    } } 純粋関数として提供される 一切の副作用がない view から直接呼ばれる形式で定義するので、 そのままこの定義を使うだけで型安全性の保証ができる
  5. 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<State, State, ActionTypes>()({ addToCart (payload: { id: number }) { return ({ commit }) => { commit({ type: 'ADD_TO_CART', payload: { id: payload.id } }); } }, //... 省略 } payload を第1 引数として受け取り、ActionContext は返り値 の関数定義にinject されるようにした ActionTypes の定義はMutation で使いやすい形にした( 後述)
  6. 実装はこんな感じ export interface ActionContext<S, R, A> { dispatch: Dispatch; commit:

    <K extends keyof A>(params: { type: K, payload?: A[K] }) => void, state: S; getters: any; rootState: R; rootGetters: any; } type Action<S, R, A, P> = (payload: P) => (injectee: ActionContext<S, R, A>) => any; export type ActionTree<S, R, A> = { [key: string]: (payload: any) => (ctx: ActionContext<S, R, A>) => any } export function ActionCreatorHelper<S, R, A>() { return <T extends ActionTree<S, R, A>>(ac: T): T => ac }
  7. ActionCreatorHelper の定義が奇妙な感じに ActionCreatorHelper<S, R, A>: () => <T extends ActionTree<S,

    R, A>>(ac: T): T => ac 本当は ActionCreatorHelper (AC は引数で受け取る ActionCreator の関数群) という感じにしたかった しかしこのAC だけは推論を効かせたい しかしTS のgenerics では一部にだけ推論を効かせることは できない( ヒントを与えるなら全部に与えないといけない) そこで高階関数にして、段階を分けることで対応した 他にいい方法をご存知の方は是非おしえてください!
  8. Vuex の提供するMutation mutations: { ['hoge'] (state, payload) { state.hoge =

    payload.hoge; } } ActionCreator でcommit されたkey で定義した関数が実行さ れる この関数は第1 引数にstate 、第2 引数にcommit された payload を受け取る
  9. ところで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 の動的チェックにより絞り込みを効 かせることで型安全性を保証する
  10. 先ほど定義したActionTypes を generics として受け取ることで解決した import { MutationTree } from 'battle-ax';

    import { ActionTypes } from './actions'; import { State } from '../store'; export const mutations: MutationTree<State, ActionTypes> = { ['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 は決定できる
  11. 実装はこんな感じ export type Mutation<S, P> = (state: S, payload: P)

    => any; export type MutationTree<S, A> = { [P in keyof A]?: Mutation<S, A[P]>; };
  12. Vuex の提供するGetter getters: { doneTodos: state => { return state.todos.filter(todo

    => todo.done) } } state や他のgetter などを引数として受け取り、 それを元に値を計算する関数 副作用はない
  13. 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<State, State, Getters> = { 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 として与えることで保証する
  14. 実装はこんな感じ export type GetterResult = { [key: string]: any }

    export type Getter<S, R, G, V> = (state: S, getters: G, rootState: R, rootGetters: any) => V; export type GetterTree<S, R, G extends GetterResult> = { [P in keyof G]: Getter<S, R, G, G[P]>; } 先にGetter の計算結果を定義する形にして解決したけど、 GetterTree の定義から推論できれば定義が要らなくなるか も? TS 力が足りなくていい方法は浮かばなかった TS glue の方、アドバイスください!
  15. こんな感じ 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;
  16. 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: ` <div class="app"> <counter></counter> </div> ` }) root にstore を渡すことで、呼び出されるすべての component の this.$store プロパティにstore の参照が入る (DI) これにより、すべてのcomponent から透過的にstore を呼び 出せるようになる
  17. 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 を受け取る
  18. 実装はこんな感じ export function inject<S, G, A, AC extends ActionTree<S, S,

    A>>(container: any, store: BAStore<S, G, 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 のバケツリレーからは逃れられないが、 お行儀はよくなるというトレードオフ