今回からはフロントエンド側のUIをモックアップとして作成して行く。今回のアプリケーションのフロントはVue.js+Vuetifyで作ることになり、仕様的にはデータベースへの問い合わせを行う部分以外はほぼJavaScriptで開発することになる。
 今時の応答性の高いWEBアプリはReactやVue、(ちょいと落ち目な)Angular、(これから大旋風を巻き起こしそうな)GatsbyなどのJSフレームワークで作られることがマストな感じになって来ているので、WEB系エンジニアであれば、どれか一つは使えるようにしておかないと、業界内で市場価値が得られないかもしれない。
 世界的にはまだReact一強時代が続いているようだが、個人的にReact使ってみて肌に合わなかったので、今は専らVueばっかり使っている。まぁ、コンセプト的にどちらも似たようなストラクチャーなので、どちらかでも使えるようにしておけば、当分WEB業界で食いっぱぐれることはなさそうだw
 ──と、話が逸れたが、では早速Vue.jsでフロントエンドを制作して行こう。

Vuetifyのインストール

 第4回の時にVue.jsで今回のアプリケーションのプロジェクトを作成したが、これだけだとリッチなUIコンポーネントを自作しなければならない。そこで、Vue.js用のUIフレームワークであるVuetifyを使うことにする。Vuetifyにあらかじめ用意された豊富なコンポーネント群を組み合わせていくことで、コンポーネント自体の製造にかかる時間的・労力的なコストを削減して生産性の向上を図るのである。
 フロントエンドのUI/UXの開発においては開発手法の選択が最も重要だ。奇抜なデザイン性に重きを置くようなアプリケーションならば、デザイナーや開発者が自由にコンポーネントを作成できる方が良いだろう。その場合は、Tailwind CSSなどのように最小限度の機能性を持ったユーティリティ・ファーストなフレームワークを採択すべきだろう。一方で、今回のような特異なデザイン性は不要なアプリケーションでは、極力「使い易さ」を重視したユニバーサルデザイン的な方向性が望ましい。そういう点で、WEBページで使い慣れている&使い易いUIを簡単に実装できるCSSフレームワーク(今回採用したVuetifyや、古くはBootstrap等)が有効なのだ。
 前置きはここまでにして、では早速Vuetifyをインストールしていこう。

 まず、今までのプロジェクトディレクトリの開発内容をGitHubに登録する。コマンドラインから、第4回で作成したプロジェクトディレクトリー直下に移動して、

git remote add origin https://github.com/ka215/mhr-skill-simulator.git
git branch -M main
git push -u origin main

 なお、事前にGitHub側にリポジトリだけは作っておく必要がある。これで、イニシャル・コミットは完了だが、前回までの開発内容が反映されていないので、改めてコミットを作成する。
──と、その前に、データベースのDSN情報を格納しているdatabase.jsonをコミットから外すべく、.gitignoreへ下記のように追記しておく。

database.json

 そして、コミットしてプッシュする。

git add .
git commit -m "first commit"
git push origin main

 これでOKだ。
 この時点の、私のGitHubリポジトリのコミット内容はこちらで確認できる。

 では、いよいよVuetifyをインストールする。

vue add vuetify
? Choose a preset: Default (recommended)

 インストール中に、プリセットの種別を訊かれるので、「Default (recommended)」を選択しておくこと(「V3」に後ろ髪を引かれるが……)。これで、Vuetifyのインストールは完了だ。動作確認のため、ウォッチ・ビルドしてみよう。

yarn serve

 コンパイルが完了したら、ブラウザでlocalhost:8080にアクセスして、Vuetifyのページが表示されればOKだ。

ルートコンポーネントの作成

 Vue.jsによってビルドされるHTMLページの大枠のテンプレートは、開発用ビルド(yarn serveの実行)時はpublic/index.htmlで、公開用ビルド(yarn buildの実行)後はdist/index.htmlとなる。それぞれのindex.html内の<div id="app"></div>に実際のコンポーネントが挿入される建付けだ(セレクタは任意に変更可能)。
 そして、挿入されるコンポーネント群の親となるのがルートコンポーネントであり、そのファイルはsrc/App.vueである。ここには、アプリケーションの骨格となる基礎的なレイアウト要素を追加するのが一般的だ。

<template>
  <v-app>
    <v-navigation-drawer
      v-model="drawer"
      absolute
      temporary
    >
      <v-list
        nav
        dense
        class="mt-16"
      >
        <v-list-item-group
          v-model="group"
          :active-class="`${darkTheme ? 'cyan--text text--darken-1': 'cyan--text text--accent-4'}`"
        >
          <v-list-item>
            <v-list-item-title>Menu Item 1</v-list-item-title>
          </v-list-item>
          <v-list-item>
            <v-list-item-title>Menu Item 2</v-list-item-title>
          </v-list-item>
          <v-list-item>
            <v-list-item-title>Menu Item 3</v-list-item-title>
          </v-list-item>
        </v-list-item-group>
      </v-list>
    </v-navigation-drawer>

    <v-app-bar
      app
    >
      <v-app-bar-nav-icon @click.stop="drawer = !drawer" />
      <v-toolbar-title>{{ labels.title }}</v-toolbar-title>
      <v-spacer />
      <div class="d-flex my-a">
        <v-switch
          v-model="darkTheme"
          color="amber accent-4"
          class="align-center"
          dense
          hide-details
        >
          <template v-slot:label>
            <v-icon
              :color="`${darkTheme ? 'amber': 'red'} accent-2`"
            >mdi-theme-light-dark</v-icon>
          </template>
        </v-switch>
      </div>
    </v-app-bar>

    <v-main>
      <v-container
        fluid
      >
        <v-card
          :loading="loading"
          outlined
          tile
          min-height="calc(100vh - 64px - 150px)"
          class="d-flex justify-center align-stretch"
        >
          <div
            class="align-self-center"
          >Main Content Area</div>
        </v-card>
      </v-container>
    </v-main>

    <v-footer
      app
      padless
      class="transparent"
    >
      <v-card
        flat
        tile
        width="100%"
        class="transparent text-center"
      >
        <v-card-text>
          <v-btn
            v-for="item in icons"
            :key="item.icon"
            class="mx-4 font-weight-light"
            :href="item.href"
            text
            small
          >
            <v-icon
              size="20px"
              class="mr-1 grey--text"
            >mdi-{{ item.icon }}</v-icon>
            <span
              :class="['grey--text', {'text--lighten-2': darkTheme}, {'text--darken-2': !darkTheme}]"
            >{{ item.label }}</span>
          </v-btn>
        </v-card-text>
        <v-divider />
        <v-card-text
          class="grey--text text--darken-1"
        >
          {{ labels.version }} &mdash; {{ new Date().getFullYear() }} &copy; <strong>{{ labels.copyright }}</strong> {{ labels.poweredby }}
        </v-card-text>
      </v-card>
    </v-footer>
  </v-app>
</template>

<script>
export default {
  name: 'App',

  components: {
    //
  },

  data: () => ({
    drawer: false,
    group: null,
    darkTheme: true,
    labels: {
      title: 'MHRise: Skill Simulator',
      version: 'Ver.0.1.0',
      copyright: 'Monaural Sound ka2.org,',
      poweredby: 'Powered by MAGIC METHODS',
    },
    icons: [
      { icon: 'home', href: '', label: 'Home' },
      { icon: 'email', href: '', label: 'Contact' },
      { icon: 'alert-circle-outline', href: '', label: 'Issues' },
    ],
    loading: false,
  }),

  watch: {
    group () {
      this.drawer = false
      this.loading = true
    },
    darkTheme (value) {
      this.$vuetify.theme.isDark = value
    }
  },

  created() {
    this.darkTheme = this.$vuetify.theme.isDark
  },

};
</script>

 アプリケーションのワイヤーフレームとして、App.vueを上記のようにコーディングしてみる。各Vueコンポーネントの説明は省くので、詳しく知りたい場合は公式サイトを参照して欲しい。
 このApp.vueをビルドすると、下記のように表示される。

 Vuetifyを使うと、結構リッチなUIを持つアプリケーションが簡単に作れるようになるのだ。

コンポーネントを分割する

 前項で作ったApp.vueはちょっとワイヤーフレームとしてはリッチ過ぎるので、いくつかのコンポーネントは別ファイルに切り出しておくことで、各コンポーネントの保守性が高まる。
 例えば、引き出されるサイドメニューや、フッターなどを個別のコンポーネントファイルにしておくことで、今後メニューが追加されたり、フッターにリンクを追加する際に、基本的に対象となる個別のコンポーネントファイルだけを修正することで事足りるようになる。リソース全体としての機能の見通しが良くなるのだ。
 コンポーネントごとの再利用性を考えると、ボタンやリストといった極小コンポーネント別に切り出しておくのも有効だが、やり過ぎるとファイルが増えすぎて逆に保守性が失われるので、ある程度グループ化されたコンポーネントごとに個別ファイル化していくのが良いだろう。

 さて、今回はApp.vueからサイドメニューとメインコンテンツ、フッターの3つを別ファイルとして切り出してみる。
 まずはサイドメニュー用のコンポーネントファイルSideMenu.vuecomponentsフォルダの中に作り、下記のようにマークアップする。

<template>
  <v-list
    nav
    dense
    class="mt-16"
  >
    <v-subheader>{{ labels.subheader }}</v-subheader>
    <v-list-item-group
      v-model="group"
      active-class="cyan--text text--accent-4"
    >
      <v-list-item
        v-for="(item, i) in items"
        :key="i"
      >
        <v-list-item-icon>
          <v-icon
            v-text="`mdi-${item.icon}`"
            dense
          ></v-icon>
        </v-list-item-icon>
        <v-list-item-content>
          <v-list-item-title v-text="item.label" />
        </v-list-item-content>
      </v-list-item>
    </v-list-item-group>
  </v-list>
</template>

<script>
export default {
  name: 'SideMenu',

  data: () => ({
    group: null,
    labels: {
      subheader: '機能メニュー',
    },
    items: [
      { slug: 'main',        icon: 'alpha-m-circle-outline', label: 'メイン', },
      { slug: 'weapons',     icon: 'alpha-w-circle-outline', label: '武器', },
      { slug: 'armors',      icon: 'alpha-a-circle-outline', label: '防具', },
      { slug: 'decorations', icon: 'alpha-d-circle-outline', label: '装飾品', },
      { slug: 'talismans',   icon: 'alpha-t-circle-outline', label: '護石', },
      { slug: 'skills',      icon: 'alpha-s-circle-outline', label: 'スキル', },
    ],
  }),

  watch: {
    group () {
      this.$root.$emit('update:drawer', false)
    },
  },
}
</script>

 メニュー内に表示されるナビゲーションリンクを配列化することで、追加や削除が容易にできるようにした。
 また、リンクをクリックした際に、サイドメニュー自体を閉じるイベントは、コンポーネントを切り出したことで、子コンポーネントSideMenu.vue<v-list>要素から、親コンポーネントApp.vue<v-navigation-drawer>要素にイベントを受け渡してあげる必要が発生している。こういう子から親へのイベント伝播については、$emit()メソッドを使えば良い。まず、子コンポーネント側でメニューの選択状態を管理しているgroupという変数をwatchで監視して、値が変更される都度、$emit()update:drawerで定義されたイベントハンドラを呼び出す建付けだ。

 同じようにメインコンテンツとフッターも子コンポーネントとして切り出し、最後にApp.vue側でそれぞれのコンポーネントをインポートするようにすれば完了だ。最終的なApp.vueは次のようになっている。

<template>
  <v-app>
    <v-navigation-drawer
      v-model="drawer"
      absolute
      temporary
    >
      <SideMenu />
    </v-navigation-drawer>

    <v-app-bar
      app
    >
      <v-app-bar-nav-icon @click.stop="drawer = !drawer" />
      <v-toolbar-title>{{ labels.title }}</v-toolbar-title>
      <v-spacer />
      <div class="d-flex my-a">
        <v-switch
          v-model="darkTheme"
          color="amber accent-4"
          class="align-center"
          dense
          hide-details
        >
          <template v-slot:label>
            <v-icon
              :color="`${darkTheme ? 'amber': 'red'} accent-2`"
            >mdi-theme-light-dark</v-icon>
          </template>
        </v-switch>
      </div>
    </v-app-bar>

    <v-main>
      <v-container
        fluid
      >
        <MainView />
      </v-container>
    </v-main>

    <v-footer
      app
      padless
      class="transparent"
    >
      <Footer />
    </v-footer>
  </v-app>
</template>

<script>
import SideMenu from './components/SideMenu'
import MainView from './components/MainView'
import Footer   from './components/Footer'

export default {
  name: 'App',

  components: {
    SideMenu,
    MainView,
    Footer,
  },

  data: () => ({
    drawer: false,
    darkTheme: true,
    labels: {
      title: 'MHRise: Skill Simulator',
    },
  }),

  watch: {
    darkTheme (value) {
      this.$vuetify.theme.isDark = value
    }
  },

  created() {
    this.darkTheme = this.$vuetify.theme.isDark
  },

  mounted() {
    this.$root.$on('update:drawer', value => {
      this.drawer = value
    })
  },
};
</script>

 最初に較べると、だいぶスッキリしているのがわかる。
 ちなみに、ここまでのアプリケーションをビルドすると、次のようになる。

 だいぶ、モックアップっぽくなってきた。

開発時はウォッチビルド

 Vue.jsでの開発中はyarn serveで「ウォッチビルド」を継続しておく。この状態だと、常に変更リソースの監視が行われ、ファイルに変更が発生するとリアルタイムでビルドが行われるので、変更をすぐにブラウザで確認できるのだ。また、ビルド自体も内部キャッシュを使った差分のみのビルドになるため、レスポンスが抜群に良い。
 ただ、ローカルPC上のlocalhost:8080での動作確認になるため、サーバ側と通信が必要な処理は正常に動作しない。それについては、ダミー処理を作るなりの対策が必要になって来るので、詳しくは後の回にて説明しようと思っている。

本番用ビルドを使いモックアップをレビュー

 yarn buildで本番用ビルドを行うと、distフォルダに公開用ファイルがビルドされることは前述した。この公開用ファイルはHTMLとJavaScript、CSS等の静的なファイルセットなので、フォルダごと配布することで、ブラウザのある環境であればどこでも動作させられる。サーバサイドとのやり取りがある処理等はそのままではエラーになるが、ダミーデータやダミー処理を実装してやることで、都度アプリケーションのモックアップレビューを行うこともできるようになるのだ。
 これについても、詳しくは後の回で説明したいと思う。

 
 まぁ、Vue.jsとVuetifyによるフロントエンド開発はここまでの流れが基本となる。あとは必要なコンポーネントを都度追加しつつ、サーバサイドとの連携部分を作って行くことになる。

 ちゅーわけで、今回はここまで。
 なお、この章の最終リソースはGitHubのこちらのリポジトリで確認できる。いやぁ、GitHubのタグを使うと、開発中リソースの変遷がとても紹介しやすくてイイな……w
 次回は、VueRouterを導入してメインコンテンツ部分のページ遷移(表示切替)を実装していく。