リファクタリングの動機
機能を追加したかったけど、このまま追加していくとすぐに変更が難しくなりそうだったから、
小さいうちに変更を加えやすいものにしておきたかった。
あと機能を作ることに集中していたけど、これからUIをきれいにしていきたい。
現状は似ているエレメントをコピペとかしているのでこのままだと統一的なUIにするのが難しそうだと思った
リファクタリング対象
GitHub: https://github.com/sterashima78/vue-webpage-builder
これ自体ついては別の記事に書いている。(これとこれ)
リファクタリング前は v0.4.1 である程度リファクタリングしたのが v0.5.1
意識したこと
- 役割を適切に分ける
- コンポーネントから大きめのロジックを取り除く
- コンポーネントはあくまでもViewなのでリアクティブなデータを保持する必要があるけどロジックは持たなくてもいい
Atomic Design
- atoms・molecules・organisms・templates・pagesにコンポーネントを分ける
- atoms・molecules・organismsの分け方はあまり厳密に考えすぎない
- 最初のリファクタリングなのでそれなりの大きさのコンポーネントに分けることを重視
- Vuexに依存するのはpagesだけ
- イベントをパスしまくるのはめんどいけど、いったんこれで
- pagesは自分のViewを持たずにtemplatesを表示するだけ
- templatesに値を注入してイベントハンドラを設定するだけ
- .vueファイルで記述したけど結局HOCなので.ts書いてもよかった
ディレクトリ構成
主要なもののみ
変更前
VueCLI の create で生成した所からスタートしているので割と一般的な構成だと思う
src
├── App.vue
├── components
│ ├── Ace.vue
│ ├── ComponentEditor.vue
│ ├── ComponentTree.vue
│ ├── ComponentsList.vue
│ ├── ExternalResource.vue
│ ├── Viewer.vue
│ └── tags.ts
├── main.ts
├── observer #Rx
├── store #Vuex
├── types.ts
├── util
│ ├── LocalVue.ts
│ ├── NodeUtils.ts
│ └── toString.ts
└── views
└── Home.vue
変更後
少し移行が完了していない箇所がある
src
├── App.vue
├── application
│ └── components
│ ├── atoms
│ ├── molecules
│ ├── organisms
│ ├── pages
│ │ └── ViewerPage.vue
│ └── templates
│ └── ViewerTemplate.vue
├── domain # ドメインロジックが入る
│ ├── model
│ └── service
├── main.ts
├── store #Vuex
├── types.ts
├── util
│ ├── LocalVue.ts
│ └── NodeUtils.ts
└── views
└── Home.vue
進める手順
あらかじめテストを書いてあればそれをガイドに進めたけど、まずテストが書きにくかったので無い。
- コンポーネントを分ける
- コピペエレメントが減った
- ステートの更新と、更新するためのデータを作ったりするロジックを分ける
- この辺でテストを書こうと思えるようになってきた
- データを作成するロジックを外に出す
- この辺で無駄な処理とかの整理ができた
例
Vuexに依存している (Nodesモジュール)
<template>
<v-card flat style="overflow-y: scroll; height: calc(100vh - 50px);">
<v-card-text>
<v-select :items="['html', 'component']" v-model="type"/>
<v-text-field v-model="filter" label="filter"/>
<v-container grid-list-xl>
<v-layout wrap>
<v-flex xs6 v-for="name in filteredComponents" :key="name">
<v-sheet
draggable="true"
height="75"
:elevation="20"
style="word-break: break-all;cursor: move;"
@dragstart.native.stop="cmpDragStart(name)"
@dragend.native.stop="cmpDragEnd(name)"
@dragover="$event.preventDefault()"
>{{name}}</v-sheet>
</v-flex>
</v-layout>
</v-container>
</v-card-text>
</v-card>
</template>
<script lang="ts">
import { Component, Vue, Watch, Prop } from "vue-property-decorator";
import Nodes from "../store/modules/nodes";
import HTMLTags from "./tags";
@Component
export default class ComponentsList extends Vue {
private filter = "";
private type = "html";
public get filteredComponents(): string[] {
switch (this.type) {
case "html":
return HTMLTags.filter(c =>
new RegExp(this.filter, "i").test(c)
).sort();
case "component":
return Nodes.components.filter(c =>
new RegExp(this.filter, "i").test(c)
);
default:
return [];
}
}
public cmpDragStart(name: string) {
Nodes.SET_NEW_COMPONENT_NAME(name);
}
public cmpDragEnd(name: string) {
Nodes.REMOVE_NEW_COMPONENT_NAME(name);
}
}
</script>
コンポーネントを分ける
小さいコンポーネントにわけることで重複がなくなる。
タブアイテムとして配置するという興味 (MenuTabItem) と コンポーネントを選ばせるという興味 (ComponentSelector) が分かれる
更新処理部分をevent発行に置き換えて上位コンポーネントに移譲する
<template>
<MenuTabItem>
<ComponentSelector
:components="components"
@dragStart="dragStart"
@dragEnd="dragEnd"
/>
</MenuTabItem>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import ComponentSelector from "@/application/components/molecules/ComponentSelector.vue";
import MenuTabItem from "@/application/components/atoms/MenuTabItem.vue";
@Component({
components: {
ComponentSelector,
MenuTabItem
}
})
export default class ComponentsList extends Vue {
@Prop({ default: () => [] })
private components!: string[];
private dragStart(name: string) {
this.$emit("dragStart", name);
}
private dragEnd(name: string) {
this.$emit("dragEnd", name);
}
}
</script>
おわりに
ロジックを分離したから単体試験が書きやすくなる。
状態は外から注入して、操作の結果はイベント発行なのでStoryBookも書きやすくなる。