s平面の左側

左側なので安定してます(制御工学の話は出てきません)

Vuetify.js でお手軽マテリアルデザイン + Atomic Design「風」のディレクトリ構成

Vue.js #4 Advent Calendar 2017 - Qiita の 19 日目のエントリー。

(先日の記事の続きを書こうと思っていたが、こちらの日が先に来てしまった)


最近、フロントエンドエンジニア・デザイナー不在のプライベートな開発において、フロントサイドの実装を任されることがあった。

その際に Vuetify.js を用いて比較的簡単にマテリアルデザインのページを実現できたので、その備忘録。

(意見・ツッコミなどは歓迎)

vuetifyjs.com

出来上がった画面のイメージはこんな感じ。

f:id:okashoi:20171218114611p:plain

※グラフ描画は Vuetify.js で提供していないので、 vue-chartjs を利用している。

このレベルであれば、Vue.js の基礎知識だけある状態から1日で辿り着くことが出来た。

概要

Vuetify.js はマテリアルデザインのコンポーネントを80種類以上提供している。

Bootstrap などでグリッドシステムに慣れていれば、部品を組み合わせる感覚でいい感じにページを作ることができる。

f:id:okashoi:20171218115307p:plain

導入方法として CDN からの読み込み、 npm でのインストールどちらにも対応している。

また、8種類の vue-cli テンプレートが提供されているので、新規プロジェクト作成時にはこちらを利用してもいい。

基本的な使い方の解説や、利用できるコンポーネントについては 公式ドキュメント や、世の中にある他の記事にお任せ。

ディレクトリ構成

前提

  • ビルドには Webpack を用いる
  • 1ページに対して、1 つの html が 1 つの JS ファイルを読み込む形(SPAではない)

また今回の試みとして、いい感じにディレクトリを切って .vue ファイルからなるコンポーネント郡を分類したかったので、その基準として Atomic Design*1 を参考にしてみた。

ただし、私が Atomic Design についてきちんと理解・正しい解釈ができていなかったため、本来の Atomic Design 考え方からは外れてしまっている点に留意。 *2

+ .babelrc
+ webpack.config.js
+ package.json
+ src/
  + entries/        # Webpack のエントリーポイント
  | + landing.js
  | + main.js
  | :
  + Components/     # コンポーネント郡
  | + Templates/
  | | + Footer.vue
  | | + Header.vue
  | | - Contents/
  | |   + Landing.vue
  | |   + Main.vue
  | |   :
  | + Pages/
  | | + Landing.vue
  | | + Main.vue
  | | :
  | - Organisms/
  |   + AForm.vue
  |   + ATable.vue
  |   + AChart.vue
  |   :
  - common.js       # 共通の処理を記述したモジュール

コンポーネント.vue ファイル)名は仮

ディレクトリの説明

以下ソースコードでは、 src/App というalias を設定している。

また前述の通り おそらく、本来の Atomic Design 考え方からは外れてしまっている という点に留意。

entries

Webpack のエントリーポイントが配置される。

エントリーポイントでは Vuetify.js の読み込みと、Pages コンポーネントの render を行っている。

例) src/entries/main.js

import Vue from 'vue';
import Page from 'App/Components/Pages/Main.vue';
import Vuetify from 'vuetify';
import 'vuetify/dist/vuetify.css';

Vue.use(Vuetify);

new Vue({
    el: '#app',
    render: h => h(Page)
});

Pages

「このページはヘッダがあって、フッタがあって、このコンテンツがある」といった具合にページ全体の構成を表す。

ヘッダやフッタ、コンテンツにあたる部分が Templates コンポーネントとなり、これを配置する。

例)src/Components/Pages/Main.vue

<template>
  <v-app id="inspire" v-cloak>
    <my-header></my-header>
    <my-content></my-content>
    <my-footer></my-footer>
  </v-app>
</template>

<style>
  [v-cloak] {
    display: none;
  }
</style>

<script>
    import Header from 'App/Components/Templates/Header.vue';
    import Footer from 'App/Components/Templates/Footer.vue';
    import Content from 'App/Components/Templates/Contents/Main.vue';

    export default {
        components: {
            'my-header': Header,
            'my-content': Content,
            'my-footer': Footer
        }
        // ページ全体に及ぶロジックを記述
    };
</script>

Templates

共通のヘッダやフッダ、各ページのメインコンテンツにあたる部分など、ページを構成する各要素。

各ページのメインコンテンツにあたるコンポーネントは Contents/ というディレクトリを作成し、その下に置いた。

Organisms コンポーネントを、グリッドシステムに基づいて配置する。

例)src/Components/Templates/Contents/Main.vue

<template>
  <v-content>
    <v-container grid-list-lg>

      <v-layout row>
        <v-flex md12>
          <my-chart
            :chart-data="chartData"
          ></my-chart>
        </v-flex>
      </v-layout>

      <v-layout row>
        <v-flex md4>
          <my-table
            :table-items="tableItems"
          >
          </my-table>
        </v-flex>

        <v-flex md8>
          <v-layout row>

            <v-flex md6>
              <my-form
                v-model="formValue"
              >
              </my-form>
            </v-flex>

            <v-flex md6>
              <my-card
                :card-text="cardText"
              >
              </my-card>
            </v-flex>

          </v-layout>
        </v-flex>
      </v-layout>

    </v-container>
  </v-content>
</template>

<script>
    import MyCard from 'App/Components/Organisms/ACard.vue';
    import MyChart from 'App/Components/Organisms/AChart.vue';
    import MyForm from 'App/Components/Organisms/AForm.vue';
    import MyTable from 'App/Components/Organisms/ATable.vue';

    export default {
        components: {
            'my-chart': MyChart,
            'my-table': MyTable,
            'my-form': MyForm,
            'my-card': MyCard
        },
        data: () => ({
            chartData: [],
            tableItems: [],
            formValue: {},
            cardText: ''
        })
        // ロジックを記述
    };
</script>

Organisms

特定のフォームや表など、機能を持つ個別の部品。

ルート要素を <v-layout> にすることで Templates 内で利用する際に、グリッドシステムの恩恵を受けられる。

v-layout > v-flex の構造をネストさせてもレイアウトに影響しないよう)

Molecules を組み合わせて作る。

Vuetify.js が提供しているコンポーネントがその Molecules にあたる。

(下の例では v-data-table

例)src/Components/Organisms/ATable.vue

<template>
  <v-layout row>
    <v-flex md12>
      <h2 class="ml-3 mb-2">表1</h2>
      <v-data-table
        :headers="headers"
        :items="tableItems"
        hide-actions
        class="elevation-1"
      >
        <template slot="items" slot-scope="props">
          <td class="text-xs-center">{{ value1 }}</td>
          <td class="text-xs-center">{{ value2 }}</td>
          <td class="text-xs-center">{{ value3 }}</td>
        </template>
      </v-data-table>
    </v-flex>
  </v-layout>
</template>

<script>
    export default {
        props: {
            tableItems: {
                type: Array,
                required: true
            }
        },
        data: () => ({
            headers: [
                {
                    text: '要素1',
                    value: 'value1'
                },
                {
                    text: '要素2',
                    value: 'value2'
                },
                {
                    text: '要素3',
                    value: 'value3'
                }
            ]
        })
    }
</script>

Molecules / Atoms について

原則、 Vuetify.js が提供するコンポーネントがこれらに該当するので今回は自分では作成しなかった。

他のライブラリを用いるなどして部品を作る際には Molecules を作るかもしれない。

感想

今回は慣れないフロントエンド周りの設定・実装方針策定を1からやったので、いろいろ試行錯誤しながら進めた。

Webpack の設定含めて迷うことも多かったが、一旦納得行く形には落ち着いた(Atomic Design をきちんと取り込めていないという問題はあるが、現時点でこのプロジェクト内での破綻は起きていない)。

システムが肥大化するに従って新たな問題は出てくるとは思うが、そのときにきちんと対応できるようにフロントエンドの設計思想を学んでいきたい。

*1:http://atomicdesign.bradfrost.com/chapter-2/ を参照

*2:今回の例で言えば、例えば Pages と Templates の関係が逆なような気がしている。

Atomic Design にあわせたコンポーネント分割のしかたは次のリポジトリが参考になるとのこと(ただし React の例)。

github.com