Tech Hotoke Blog

IT観音とは私のことです。

Vue×SpringでSPA作成5【UIをいい感じにする】

f:id:TechHotoke:20211216134028p:plain

まえがき

こちらの記事の続編です。

techhotoke.hatenablog.com

目的

VueとSpringで作成したプロジェクトの構築手順の備忘録。

備忘録のため、詳細な説明を省略している部分があります。

前提

基本的なJavaの知識やSpring、Vueの知識があること。

環境

  • Java 11
  • Spring Boot2.5.6
  • Gradle 7.1.1
  • Vue2.6
  • IDESTS

ゴール

今はUIがこんな↓感じなので、

f:id:TechHotoke:20211209180456p:plain

これをいい感じ↓にしていきます。

f:id:TechHotoke:20211215213342p:plain

画面をいい感じにする

構成的には親コンポーネントにサイドバーとヘッダを入れて、メインコンテンツを子コンポーネントとして適宜切り替えていくイメージで実装を進めてまいります。

まずは親コンポーネントとなるコンポーネント達を作成します。 frontend/src直下にlayouts/defaultという命名ディレクトリを作成します。その直下にまず親コンポーネントとなるIndex.vueを作成します。

<template>
    <v-app>
        <default-bar />
    
        <default-drawer />
    
        <default-view />
    
        <default-footer />
    </v-app>
</template>

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

    components: {
        DefaultBar: () =>
            import (
                /* webpackChunkName: "default-app-bar" */
                './AppBar'
            ),
        DefaultDrawer: () =>
            import (
                /* webpackChunkName: "default-drawer" */
                './Drawer'
            ),
        DefaultFooter: () =>
            import (
                /* webpackChunkName: "default-footer" */
                './Footer'
            ),
        DefaultView: () =>
            import (
                /* webpackChunkName: "default-view" */
                './View'
            ),
    },
}
</script>
  • AppBar:ヘッダ
  • Drawer:サイドバー
  • Footrt:フッタ
  • View:メインコンテンツ

大体こんな構成でいこうかと思います。

コンポーネントを構成している部品が多く、冗長なので今回の記事ではヘッダ、フッタ、サイドバーについては説明を割愛しますので、GitHubをご参照ください。

github.com

メインコンテンツの実装

まず、default/layoutの直下にView.vueを作成します。

<template>
  <v-main>
    <v-container fluid>
      <router-view :key="$route.path" />
    </v-container>
  </v-main>
</template>

<script>
  export default {
    name: 'DefaultView',
  }
</script>
  • <router-view>のkeyに子コンポーネントのパスを渡すことで動的にViewを切り替えていきます。

公式サイトを見ると、Vue-routerでのコンポーネントのネストの実装方法が載っていましたので、こちらを参考にしつつ、

https://router.vuejs.org/ja/guide/essentials/nested-routes.html#%E3%83%8D%E3%82%B9%E3%83%88%E3%81%95%E3%82%8C%E3%81%9F%E3%83%AB%E3%83%BC%E3%83%88

まずはlodshをインストールしていきます。(lodashを使うとフォーマットの成形が楽になるので)

Lodash

次に、routingの制御を行う汎用的なメソッドを作成していきたいので、src直下にutilという名称のフォルダを作成し、routes.jsファイルを作成します。

// Imports
import { kebabCase } from 'lodash'

export function layout (layout = 'Default', children, path = '') {
  const dir = kebabCase(layout)

  return {
    children,
    component: () => import(
      /* webpackChunkName: "layout-[request]" */
      `@/layouts/${dir}/Index`
    ),
    path,
  }
}

export function route (name, component, path = '', publishingSettings) {
  component = Object(component) === component
    ? component
    : { default: name.replace(' ', '') }

  const components = {}

  for (const [key, value] of Object.entries(component)) {
    components[key] = () => import(
      /* webpackChunkName: "views-[request]" */
      `@/views/${value}`
    )
  }
  const meta = {isPublic: publishingSettings}

  return {
    name,
    components,
    path,
    meta
  }
}

続いて、router/index.jsを修正します。

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '../views/Login.vue'
import store from '../store/index.js'
import {
  layout,
  route,
} from '@/util/routes'

Vue.use(VueRouter)

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  scrollBehavior: (to, from, savedPosition) => {
    if (to.hash) return { selector: to.hash }
    if (savedPosition) return savedPosition

    return { x: 0, y: 0 }
  },
  routes: [
    {
          path: '/',
          name: 'Login',
          component: Login,
          meta: {isPublic: true}
    },
    layout('Default', [
      route('Home', null, '/home', false)
    ])
  ]
})

router.beforeEach((to, from, next) => {
  console.log(store)
  // 非公開コンポーネントで未ログインの場合ログイン画面にリダイレクト
  if (
    to.matched.some(
      // TODO:Store.stateのアクセス方法が不明確なので今後修正を行うこと
      record => (record.meta.isPublic || store.state)
    )
  ) {
    next();
  } else {
    next({ path: "/", query: { redirect: to.fullPath } });
  }
});

export default router

公式曰く、

<router-view> コンポーネントは与えられたパスに対してマッチしたコンポーネントを描画する関数型コンポーネント

なので、Homeコンポーネントのパスをkeyに渡すことでview-routerを動的に切り替えてみようと思います。これで、子コンポーネントのあたるメインコンテンツをroutingに応じて切り替えられ(るはず)ます。

おまけ〜Vuex-pathify〜

VuexのSotreにアクセスする記述方法が手続き的で毎回記述するのも冗長だと感じていたところ、Vuex-pathifyというライブラリを発見しましたので、試しに使ってみました。

davestewart.github.io

一応公式にもこのような形でvuex-pthifyを導入することで得られるメリットなどが記載されています。

  • less cognitive overhead
  • zero store boilerplate
  • one-liner wiring
  • cleaner code
  • lighter files

先頭のcongnitive overheadが何を指しているか分かりにくいのですが、おそらくコードをぱっと見たときの感覚的に理解できる時間が短くなるよ的なことかと。。。

ネックなのは参考になる日本語の記事が見当たらなかった点くらいですかね。。。

それでは早速、 こちらをインストールしてみます。

  • インストール
npm i vuex-pathify

こちらができたら、storeのエントリポイントとなるindex.jsにimportして、

import pathify from 'vuex-pathify'

Vuexインスタンスにマウントします。

export default new Vuex.Store({
  plugins: [ pathify.plugin ], // activate plugin
});

そして、続いてstore直下にmodulesという名称のフォルダを作成して配置。 その直下にapp.jsというファイルを作成します。

// Pathify
import { make } from 'vuex-pathify'

// Data
const state = {
  drawer: null,
  drawerImage: true,
  mini: false,
  items: [
    {
      title: 'Home',
      icon: 'mdi-home-circle-outline',
      to: '/home',
    },
  ],
}

const mutations = make.mutations(state)

const actions = {
  ...make.actions(state),
  init: async ({ dispatch }) => {
    //
  },
}

const getters = {}

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters,
}

  • vue-pathifyを利用するため、mutationsにmakeメソッドを代入します。

例として、今回説明を省いたDrawer.vueを下記に引用します。

<template>
  <v-navigation-drawer
    id="default-drawer"
    v-model="drawer"
    :right="$vuetify.rtl"
    :mini-variant.sync="mini"
    mini-variant-width="80"
    app
    width="260"
    color="#424242"
  >

    <div class="px-2">
      <default-drawer-header />

      <v-divider class="mx-3 mb-2" />

      <default-list :items="items" />
    </div>

    <div class="pt-12" />
  </v-navigation-drawer>
</template>

<script>
  // Utilities
  import { get, sync } from 'vuex-pathify'

  export default {
    name: 'DefaultDrawer',

    components: {
      DefaultDrawerHeader: () => import(
        /* webpackChunkName: "default-drawer-header" */
        './widgets/DrawerHeader'
      ),
      DefaultList: () => import(
        /* webpackChunkName: "default-list" */
        './List'
      ),
    },

    computed: {
      ...get('app', [
        'items',
        'version',
      ]),
      ...sync('app', [
        'drawer',
        'drawerImage',
        'mini',
      ]),
    },
  }
</script>
  • computedプロパティ内で、...get(),...sync()などを用いてStoreにアクセスすることが可能です。

個人的にはこちらの方が宣言的で好みでした。

今回はここまでとなります。

お付き合いいただきありがとうございます。