Nuxt.js+Vuetifyの機能を理解しながらヘッダーとスライドメニューをカスタマイズした記録

..

フロントエンド学習の一環として、Nuxt.js+VuetifyでWebサイトを制作しています。
現時点ではヘッダー等の共通パーツまで形になっており、その部分をどう作ったか記録していきます。

やったこと

  • ヘッダーコンポーネント(v-app-bar)を導入
    • スタイルを調整
    • ボタンを配置
    • v-toolbar-title
      • 任意のフォントを適用
      • トップページへのリンクを追加
  • スライドメニューコンポーネント(v-navigation-drawer)を導入
    • スタイルを調整
    • メニュー用テキストのフォントを変更
    • SNSアカウントへのリンク付きアイコンを配置

編集対象のファイル

  • /layouts/default.vue
  • nuxt.config.js

default.vueは全て空っぽにしてから始めています。

default.vue
<template>

</template>

<script>
export default {}
</script>

ヘッダー部分

Vuetifyのv-app-barコンポーネントを使用。

コンポーネントを呼び出し

default.vue
<template>
  <v-app>
    <v-app-bar
      app
    >
    </v-app-bar>
    <v-main>
      <nuxt />
    </v-main>
  </v-app>
</template>

スクリーンショット 2021-11-01 11.25.46.png
app属性のみ付与しています。app属性のついた要素はアプリケーションのレイアウトの一部であるとみなされ、自動的にposition: fixed;を指定して配置を固定すると同時に、v-app-barの高さと同じpadding-topv-main(コンテンツ)要素に自動的に付与します。

###v-toolbar-titleでサイト名を表示

default.vue
<template>
  <v-app>
    <v-app-bar
      app
    >
      <v-toolbar-title>
        <h1>サイトのタイトル</h1>
      </v-toolbar-title>
    </v-app-bar>
    <v-main>
      <nuxt />
    </v-main>
  </v-app>
</template>

スクリーンショット 2021-11-01 11.42.43.png

中央に寄せる+トップへのリンクを付与する

default.vue
<template>
  <v-app>
    <v-app-bar
      app
    >
      <v-toolbar-title
        class="mx-auto"
      >
        <h1>
          <nuxt-link to="/">サイトのタイトル</nuxt-link>
        </h1>
      </v-toolbar-title>
    </v-app-bar>
    <v-main>
      <nuxt />
    </v-main>
  </v-app>
</template>

スクリーンショット 2021-11-01 12.27.44.png
Vuetifyで用意されているmx-autoクラスをつけることで、自動的に横方向のmarginautoにするCSSを当ててくれます。またaタグ同様、nuxt-linkタグにリンクを記載して囲うことで、中のテキストにリンクをつけることができます。

aタグのスタイルについてはCSSを書く必要があるので、例えばassets内にscssディレクトリを作り、その中にcommon.scssを作成して、サイト全体の共通スタイルとしてCSSを記述します。

common.scss
a {
  color: white !important;
  text-decoration: none;
  &:hover {
    opacity: 0.7;
  }
}

このCSSを読み込むために、nuxt.config.jsを編集。

nuxt.config.js
  css: [
    { src: '~/assets/scss/common.scss' },
  ],

スクリーンショット 2021-11-01 12.38.08.png
キャッシュごと更新すると、指定したスタイルが反映されたことが確認できました。
/inspireページでタイトルロゴを押すと、ちゃんとトップに移動するようにもなっています。
inspire-to-top.gif

フォントを変更する

Vuetifyで読み込んでいるRobotoフォント
スクリーンショット 2021-11-01 13.11.25.png日本語フォントやGoogle Fontsを使うためには、設定ファイルで読み込ませる必要があります。

  • デフォルトのフォントをシステムフォントに変更

特に指定しなかった場合のフォントを変える場合は、variables.scssにて指定します。
Vuetifyが用意しているスタイルの中にはSASS変数化されているものがあり、その変数を上書きすることで任意のスタイルを当てることができます。フォントを変更する場合は$body-font-familyを変更します。

variables.scss
+ $body-font-family: -apple-system, system-ui, "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif, "Segoe UI Emoji";

(これ以外にどんな変数があるかはこちらのページから)

設定ファイルのvuetify部分に追記。

nuxt.config.js
vuetify: {
  + customVariables: ['~/assets/variables.scss'],
  + treeShake: true,
  
    ・
    ・
}

SASS変数を上書きした結果をローカル環境で閲覧するためにtreeShakeを有効化しています。
デベロッパーツールで確認すると、ちゃんとフォントが読み込まれています。
スクリーンショット 2021-11-01 13.12.22.png
今回は追加でシステムフォントを読み込んだだけですが、始めからデフォルトのRobotoを読み込ませない設定にすることもできます。

  • Google Fontsを使用する

Webフォントを読み込むためのライブラリを追加。

yarn add nuxt-webfontloader

設定ファイルのmodules以下を編集。

nuxt.config.js
  // Modules: https://go.nuxtjs.dev/config-modules
  modules: [
    'nuxt-webfontloader'
  ],

  // GoogleFont読み込み
  webfontloader: {
    google: {
      families: ['Raleway:400'] 
    }
  },

あとはCSSで指定するだけで使用可能です。

【備忘録】ビルドについて

treeShakeを有効化すると、ビルドにかかる時間がけっこう増えます。
設定次第で短縮させられるようです。

nuxt.config.js
  // Build Configuration: https://go.nuxtjs.dev/config-build
  build: {
    parallel: true, // マルチスレッドでビルド
    cache: true, // ビルド時のloader情報をキャッシュ化
    hardSource: true, // キャッシュ管理して二回目以降を高速化
  }

ここでキャッシュを残すようにしたことで、ビルド時にエラーが出るようになることがあります。
その対策としてpackage.jsonにキャッシュをクリアするスクリプトを追加し、エラーがあればyarn clear-hard-source-cacheを都度実行します。

package.json
  "scripts": {
    "dev": "HOST=0.0.0.0 PORT=4000 nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "test": "jest",
    + "clear-hard-source-cache": "rm -rf node_modules/.cache/hard-source/"
  },

ヘッダーにスライドメニューの開閉ボタンを追加

default.vue
<template>
  <v-app>
    <v-app-bar
      app
    >
      <v-toolbar-title
        class="mx-auto"
      >
        <h1><nuxt-link to="/">サイトのタイトル</nuxt-link></h1>
      </v-toolbar-title>
      + <v-app-bar-nav-icon 
        @click.stop="drawer = !drawer"
      >
      + </v-app-bar-nav-icon>
    </v-app-bar>
    <v-main>
      <nuxt />
    </v-main>
  </v-app>
</template>

スクリーンショット 2021-11-01 13.43.50.png
あとは、このボタンによって呼び出されるスライドメニューを実装します。

スライドメニュー部分

v-navigation-drawerコンポーネントを使用。

コンポーネントを呼び出し

default.vue
<template>
  <v-app>
    + <v-navigation-drawer app>

    + </v-navigation-drawer>
    <v-app-bar
      app
    >
      <v-toolbar-title
        class="mx-auto"
      >
        <h1><nuxt-link to="/">サイトのタイトル</nuxt-link></h1>
      </v-toolbar-title>
      <v-app-bar-nav-icon 
        @click.stop="drawer = !drawer"
      >
      </v-app-bar-nav-icon>
    </v-app-bar>
    <v-main>
      <nuxt />
    </v-main>
  </v-app>
</template>

###クリックイベントとスライドメニューを紐付け

default.vue
<template>
・
・
    <v-navigation-drawer 
      app
      v-model="drawer"
    >

    </v-navigation-drawer>
・
・
</template>

<script>
export default {
  data() {
    return {
      drawer: false,
    }
  }
}
</script>

メニューの開閉は、dataプロパティでdrawerを保持し、クリックイベントでこの値を書き換えることで行います。
v-navigation-drawer.gif
デフォルトではこのように、メニューの横幅の分mainの幅を縮小して表示する形式になっています。
いちいちコンテンツの並びに影響するのはやや鬱陶しいので、temporaryで覆い被さる形の表示になるよう指定しました。
v-navigation-drawer-temporary.gif
temporaryが指定されると、展開されたスライドメニューは要素の一番上に配置されます。同時に半透明の色でオーバーレイをかけ、画面のどこかを押すと閉じるようにしてくれます。

###メニューバーの中身を作っていく

  • ページを追加してリンクを配置する

「このサイトについて」みたいなページを作って、トップへのリンクと併せてメニューバーからアクセスできるようにします。

default.vue
<script>
export default {
  data() {
    return {
      drawer: false,
      + items: [
      +   { 
      +     title: 'home',
      +     link: '/'
      +   },
      +   { 
      +     title: 'about',
      +     link: '/about'
      +   },
      + ],
    }
  }
}
</script>

data内に文字列とパスの配列を作り、

about.vue
<template>
  <v-container>
    <h1>About Page</h1>
    <p>このサイトについて</p>
  </v-container>
</template>

<script>
export default {

}
</script>

pages内にコンポーネントを作成し、アバウトページを作成します。
スクリーンショット 2021-11-01 15.50.32.png
そしてページリンクの配列から、v-forディレクティブとv-listコンポーネントでメニューの中身を出力します。

default.vue
    <v-navigation-drawer 
      app
      v-model="drawer"
      temporary
    >
    +   <v-list>
    +     <v-list-item
    +       v-for="item in items"
    +       :key="item.title"
    +       :to="item.link"
    +     >
    +       <v-list-item-content>
    +         <v-list-item-title>
    +           {{ item.title }}
    +         </v-list-item-title>
    +       </v-list-item-content>
    +     </v-list-item>
    +   </v-list>
    </v-navigation-drawer>

page-routing.gif

スタイルの調整

  • フォントの変更

v-list-itemにidを付与し、先ほど読み込んだGoogle FontsのRalewayを適用します。

default.vue
            <v-list-item-title id="menu-text">
              {{ item.title }}
            </v-list-item-title>
・
・
・
+ <style scoped>
+ #menu-text {
+   font-family: 'Raleway', sans-serif;
+ }
+ </style>

スクリーンショット 2021-11-01 16.07.33.png

  • v-list-itemmargin-bottomを追加する
default.vue
      <v-list>
        <v-list-item
          v-for="item in items"
          :key="item.title"
          :to="item.link"
        + class="mb-4"
        >
          <v-list-item-content>
            <v-list-item-title id="menu-text">
              {{ item.title }}
            </v-list-item-title>
          </v-list-item-content>
        </v-list-item>
      </v-list>

スクリーンショット 2021-11-01 16.12.20.png
Vuetifyで用意されているヘルパークラスを付与することで、1=4px換算なのでmargin-bottom: 16px;となります。

メニュー下部に横並びのSNSアイコンを追加する

default.vue
<script>
data() {
  return {
    ・
    ・
    ・
    + icons: [
    +   {
    +     path: require("@/assets/img/twitter.svg"),
    +     link: "https://twitter.com/hogehoge"
    +   },
    +   {
    +     path: require("@/assets/img/wantedly.svg"),
    +     link: "https://www.wantedly.com/id/hogehoge"
    +   },
    +   {
    +     path: require("@/assets/img/github.png"),
    +     link: "https://github.com/hogehoge"
    +   }
    + ],
  }
}
</script>

今回作成しているのがブログサイトなので、スライドメニュー内に各SNSアカウントへのリンク付きアイコンを配置したいと考えました。アイコン画像は公式のブランドリソースをダウンロードし、リンクと画像のパスを抱き合わせたオブジェクトの配列をdataから返します。

default.vue
・
・
・
      <template v-slot:append>
        <div id="sns-icons">
          <div
            v-for="icon in icons"
            :key="icon.path"
          >
            <v-btn
              depressed
              icon
            >
              <a class="sns-icon" v-bind:href="icon.link" target="_blank" rel="noopener noreferrer">
                <img v-bind:src="icon.path">
              </a>
            </v-btn>
          </div>
        </div>
      </template>

    </v-navigation-drawer>

<template v-slot:append></template>を使うと、スライドメニューの底部にコンポーネントを差し込むことができます。
そこにv-btnコンポーネントをv-forディレクティブで表示します。

default.vue
<style scoped>



  + img {
  +   width: 24px;
  +   height: 24px;
  + }

  + .sns-icon {
  +   display: flex;
  +   justify-content: center;
  +   align-items: center;
  + }

  + #sns-icons {
  +   display: flex;
  +   gap: 0 3%;
  +   margin: 40px 52px;
  + }
</style>

そうして配置したアイコンを、CSSでサイズを調整したりフレックスボックスで均等に並べます。
スクリーンショット 2021-11-01 18.50.31.png
綺麗にアイコンが並びました。

ソース全体

default.vue
<template>
  <v-app>
    <v-navigation-drawer 
      app
      v-model="drawer"
      temporary
    >
      <v-list>
        <v-list-item
          v-for="item in items"
          :key="item.title"
          :to="item.link"
          class="mb-4"
        >
          <v-list-item-content>
            <v-list-item-title id="menu-text">
              {{ item.title }}
            </v-list-item-title>
          </v-list-item-content>
        </v-list-item>
      </v-list>

      <template v-slot:append>
        <div id="sns-icons">
          <div
            v-for="icon in icons"
            :key="icon.path"
          >
            <v-btn
              depressed
              icon
            >
              <a class="sns-icon" v-bind:href="icon.link" target="_blank" rel="noopener noreferrer">
                <img v-bind:src="icon.path">
              </a>
            </v-btn>
          </div>
        </div>
      </template>

    </v-navigation-drawer>

    <v-app-bar
      app
    >
      <v-toolbar-title
        class="mx-auto"
      >
        <h1><nuxt-link to="/">サイトのタイトル</nuxt-link></h1>
      </v-toolbar-title>
      <v-app-bar-nav-icon 
        @click.stop="drawer = !drawer"
      >
      </v-app-bar-nav-icon>
    </v-app-bar>
    <v-main>
      <nuxt />
    </v-main>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      drawer: false,
      items: [
        { 
          title: 'home',
          link: '/'
        },
        { 
          title: 'about',
          link: '/about'
        },
      ],
      icons: [
        {
          path: require("@/assets/img/twitter.svg"),
          link: "https://twitter.com/nawa3ng"
        },
        {
          path: require("@/assets/img/wantedly.svg"),
          link: "https://www.wantedly.com/id/ryoga_nawa"
        },
        {
          path: require("@/assets/img/github.png"),
          link: "https://github.com/nawawa"
        }
      ],
    }
  }
}
</script>

<style scoped>
  #menu-text {
    font-family: 'Raleway', sans-serif;
  }

  img {
    width: 24px;
    height: 24px;
  }

  .sns-icon {
    display: flex;
    justify-content: center;
    align-items: center;
  }

  #sns-icons {
    display: flex;
    gap: 0 15%;
    margin: 40px 52px;
  }
</style>

これ以外にスタイルやパーツなど、いくらでもコードが増えることはあると思います。
その際はヘッダー・メニュー・SNSアイコンなどの単位で小さいコンポーネントに分けて、default.vueで読み込む形にするとスッキリすると思います。

まとめ

  • 現時点までの、Vuetifyを使ってみた感想

UIフレームワークということで、ちょっとした記述でUIをマテリアルデザイン調にできるのは楽でした。今後の個人開発等では大活躍する予感です。

ただその分自由度は高くなく、作り込んだデザインカンプを用意してマークアップするような場面には全く合わないのだと思いました。事前デザインはワイヤーフレーム程度でおさえて、あとはUIコンポーネントフル活用でササっと形にしてしまうような使い方をするものなんだな、ということがわかります。

逆にみっちり画面デザインをして、世界観を作り込んだ上での開発もやってみたいところではあるので、今度はCSSフレームワークというやつを試してみたいとも思いました。流行ってるらしいのでTailwindCSSなど使ってみたいです。


勉強は退屈なのでさっさと手を動かしてしまいましたが、徐々に機能を理解してリファクタリングしていくつもりです。