Vue.js___デザインパターン_によるコンポーネント実装_v2.pdf

B8403d102456248570005ee7fb2ba0f7?s=47 philomagi
October 24, 2019

 Vue.js___デザインパターン_によるコンポーネント実装_v2.pdf

Vue.js + デザインパターンによるコンポーネント実装。
2019/10/17 Yumemi.vue で発表した内容の一部差し替え版です。

Notification の代わりに Strategy が入っています。

B8403d102456248570005ee7fb2ba0f7?s=128

philomagi

October 24, 2019
Tweet

Transcript

  1. 4.

    4

  2. 6.

    6 <template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png"> <HelloWorld msg="Welcome

    to Your Vue.js + TypeScript App"/> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator' import HelloWorld from '@/components/HelloWorld.vue' @Component({ components: { HelloWorld } }) export default class Home extends Vue {} </script>
  3. 14.

    Vue.js + デザインパターン 14 Atomic Designは? • UI設計に対する方法論 ◦ デザイン上の視点がメイン

    ◦ 細かく作ることでデザイン上の再利用性は上がる ◦ 状態管理や振る舞いの設計はスコープ外 コンポーネントの状態/振る舞いを設計/実装するには また別の方法を用意する必要がある
  4. 15.

    Vue.js + デザインパターン 15 あらゆる処理をSFCに組み込むと辛い ◦ 実装が肥大化する ◦ 読み込まないと意図が分からない ◦

    テストが大変 SFCには具体的なコンポーネントの振る舞いを記述し、 振る舞いの詳細は別に管理した方が良い
  5. 26.

    26

  6. 27.

    27

  7. 29.

    29

  8. 31.

    31

  9. 32.

    32 <template> <li class="todo-task" :style="taskState.style" > <div> {{ content }}&nbsp;{{

    taskState.notification }} </div> <div class="todo-task__limit"> <div> 期限:&nbsp; {{ limit }} </div> </div> </li> </template>
  10. 33.

    33 <template> <li class="todo-task" :style="taskState.style" > <div> {{ content }}&nbsp;{{

    taskState.notification }} </div> <div class="todo-task__limit"> <div> 期限:&nbsp; {{ limit }} </div> </div> </li> </template>
  11. 34.

    34 <template> <li class="todo-task" :style="taskState.style" > <div> {{ content }}&nbsp;{{

    taskState.notification }} </div> <div class="todo-task__limit"> <div> 期限:&nbsp; {{ limit }} </div> </div> </li> </template> TODOタスクの表示調整内容を taskStateオブジェクトから取得
  12. 35.

    35 <template> <li class="todo-task" :style="taskState.style" > <div> {{ content }}&nbsp;{{

    taskState.notification }} </div> <div class="todo-task__limit"> <div> 期限:&nbsp; {{ limit }} </div> </div> </li> </template> TODOタスクの通知テキストを taskStateオブジェクトから取得
  13. 36.

    36 get taskState (): TaskState { const today = new

    Date() const limit = new Date(this.limit) const diff = limit.getTime() - today.getTime() const oneDay = 1000 * 60 * 60 * 24 if (diff > oneDay * 3) { return new NormalState() } if (diff >= 0) { return new CloseToLimitState() } return new LimitOverState() }
  14. 37.

    37 get taskState (): TaskState { const today = new

    Date() const limit = new Date(this.limit) const diff = limit.getTime() - today.getTime() const oneDay = 1000 * 60 * 60 * 24 if (diff > oneDay * 3) { return new NormalState() } if (diff >= 0) { return new CloseToLimitState() } return new LimitOverState() }
  15. 38.

    38 get taskState (): TaskState { const today = new

    Date() const limit = new Date(this.limit) const diff = limit.getTime() - today.getTime() const oneDay = 1000 * 60 * 60 * 24 if (diff > oneDay * 3) { return new NormalState() } if (diff >= 0) { return new CloseToLimitState() } return new LimitOverState() } 条件に応じて、 適切なStateオブジェクトを選択
  16. 43.

    43

  17. 45.

    45

  18. 46.

    46

  19. 48.

    48

  20. 50.

    50

  21. 51.

    export interface ButtonBehavior { readonly label: string onClick (): void

    } export class OnLoading implements ButtonBehavior { onClick () { // ignore } } export class OnSuccess implements ButtonBehavior { onClick () { alert('Success!!') } } export class OnFailed implements ButtonBehavior { onClick () { location.reload() } } 51
  22. 52.

    export interface ButtonBehavior { readonly label: string onClick (): void

    } export class OnLoading implements ButtonBehavior { onClick () { // ignore } } export class OnSuccess implements ButtonBehavior { onClick () { alert('Success!!') } } export class OnFailed implements ButtonBehavior { onClick () { location.reload() } } 52
  23. 53.

    export interface ButtonBehavior { readonly label: string onClick (): void

    } export class OnLoading implements ButtonBehavior { onClick () { // ignore } } export class OnSuccess implements ButtonBehavior { onClick () { alert('Success!!') } } export class OnFailed implements ButtonBehavior { onClick () { location.reload() } } 53 ・ボタンの振る舞いをIFで定義 ・クリック時の処理をメソッドとして定義
  24. 54.

    export interface ButtonBehavior { readonly label: string onClick (): void

    } export class OnLoading implements ButtonBehavior { onClick () { // ignore } } export class OnSuccess implements ButtonBehavior { onClick () { alert('Success!!') } } export class OnFailed implements ButtonBehavior { onClick () { location.reload() } } 54
  25. 55.

    export interface ButtonBehavior { readonly label: string onClick (): void

    } export class OnLoading implements ButtonBehavior { onClick () { // ignore } } export class OnSuccess implements ButtonBehavior { onClick () { alert('Success!!') } } export class OnFailed implements ButtonBehavior { onClick () { location.reload() } } 55
  26. 56.

    export interface ButtonBehavior { readonly label: string onClick (): void

    } export class OnLoading implements ButtonBehavior { onClick () { // ignore } } export class OnSuccess implements ButtonBehavior { onClick () { alert('Success!!') } } export class OnFailed implements ButtonBehavior { onClick () { location.reload() } } 56 ボタンクリック時の振る舞いを バリエーション毎に定義
  27. 57.

    57 export default class Loading extends Vue { selected: Selector

    = this.selectors[0] // (略) get buttonBehavior (): ButtonBehavior { switch (this.selected.value) { case 'loading': return new OnLoading() case 'success': return new OnSuccess() default: return new OnFailed() } } }
  28. 58.

    58 export default class Loading extends Vue { selected: Selector

    = this.selectors[0] // (略) get buttonBehavior (): ButtonBehavior { switch (this.selected.value) { case 'loading': return new OnLoading() case 'success': return new OnSuccess() default: return new OnFailed() } } } 状態に応じた ボタンの振る舞いを選択
  29. 59.

    59 <template> <div class="loading"> <h1>Loading Button</h1> <loading-state-selector-list :selectors="selectors" :selected="selected" @click="onClick"

    /> <div class="button" @click="buttonBehavior.onClick" > {{ buttonBehavior.label }} </div> </div> </template>
  30. 60.

    60 <template> <div class="loading"> <h1>Loading Button</h1> <loading-state-selector-list :selectors="selectors" :selected="selected" @click="onClick"

    /> <div class="button" @click="buttonBehavior.onClick" > {{ buttonBehavior.label }} </div> </div> </template>
  31. 61.

    61 <template> <div class="loading"> <h1>Loading Button</h1> <loading-state-selector-list :selectors="selectors" :selected="selected" @click="onClick"

    /> <div class="button" @click="buttonBehavior.onClick" > {{ buttonBehavior.label }} </div> </div> </template>
  32. 62.

    62 <template> <div class="loading"> <h1>Loading Button</h1> <loading-state-selector-list :selectors="selectors" :selected="selected" @click="onClick"

    /> <div class="button" @click="buttonBehavior.onClick" > {{ buttonBehavior.label }} </div> </div> </template> クリック時の具体的な振る舞いは buttonBehaviorに移譲
  33. 67.

    67

  34. 68.

    68

  35. 70.

    70

  36. 72.

    72

  37. 74.

    74

  38. 75.

    75 export class CartItemList { private constructor (private readonly cartItems:

    CartItem[]) { } // (中略) onlyWillPurchase (): CartItemList { return this.filterInner(cartItem => cartItem.state.willPurchase) } add (item: Item): CartItemList { // 省略 } remove (item: Item): CartItemList { return this.filter(itemInCart => itemInCart.id !== item.id) } buyLater (item: Item): CartItemList { return this.changeState(item, { willPurchase: false }) } // 省略 }
  39. 76.

    76 export class CartItemList { private constructor (private readonly cartItems:

    CartItem[]) { } // (中略) onlyWillPurchase (): CartItemList { return this.filterInner(cartItem => cartItem.state.willPurchase) } add (item: Item): CartItemList { // 省略 } remove (item: Item): CartItemList { return this.filter(itemInCart => itemInCart.id !== item.id) } buyLater (item: Item): CartItemList { return this.changeState(item, { willPurchase: false }) } // 省略 }
  40. 77.

    77 export class CartItemList { private constructor (private readonly cartItems:

    CartItem[]) { } // (中略) onlyWillPurchase (): CartItemList { return this.filterInner(cartItem => cartItem.state.willPurchase) } add (item: Item): CartItemList { // 省略 } remove (item: Item): CartItemList { return this.filter(itemInCart => itemInCart.id !== item.id) } buyLater (item: Item): CartItemList { return this.changeState(item, { willPurchase: false }) } // 省略 } ・コレクションに対する操作に  メソッドとして名前を付けられる ・「どのように」ではなく  「〜をしたい」に集中できる
  41. 78.

    78 export default class Cart extends Vue { cartItems =

    CartItemList.initialize() .add({ // 省略 }) get cartItemsWillPurchase (): CartItemList { return this.cartItems.onlyWillPurchase() } remove (item: Item) { this.cartItems = this.cartItems.remove(item) } buyLater (item: Item) { this.cartItems = this.cartItems.buyLater(item) } buyNow (item: Item) { this.cartItems = this.cartItems.buyNow(item) } }
  42. 79.

    79 export default class Cart extends Vue { cartItems =

    CartItemList.initialize() .add({ // 省略 }) get cartItemsWillPurchase (): CartItemList { return this.cartItems.onlyWillPurchase() } remove (item: Item) { this.cartItems = this.cartItems.remove(item) } buyLater (item: Item) { this.cartItems = this.cartItems.buyLater(item) } buyNow (item: Item) { this.cartItems = this.cartItems.buyNow(item) } } Vueコンポーネントでの実装は ・コレクションの初期化 ・イベントと操作の紐付け ・用途に応じた絞り込み のみで良い
  43. 81.

    81 First Class Collectionパターンの効果 • (オマケ) immutableな実装にしやすい ◦ 添字参照で配列を更新しても、vueでは検知されない ▪

    破壊的/暗黙的な実装が入りやすい ◦ first class collection 自体をimmutableにする ▪ コレクションの更新 = 再代入になるので確実 ◦ 性能的に問題が有り、オブジェクトを再利用したい ▪ 破壊的であることを明示したメソッドを定義
  44. 84.

    Vue + デザインパターン 84 • デザインパターン = システム設計の定番パターンなので、フロ ントのコンポーネント設計/実装でも活用可能 •

    複雑なロジックはコンポーネント内部に押し込まず、 専門のモジュールに任せる • 専門のモジュールを組み立てるにあたって、デザインパターン が活用できる