🚲

HTMLのサイトだけどコンポーネント指向でつくりたい(Vue3)

2023/07/31に公開

前提条件

  1. 新しいウェブサイトを立ち上げる。
  2. 今後の保守運営を考えてコンポーネント指向で設計する。(シンプルな操作で素早い更新とデザインの統一性が保たれるようにしたい)
  3. 今後メインで更新業務を行うのはマークアップしか知識のない人なのでHTMLファイル・CSSファイルだけいじれば更新が行えるようにする。
  4. そのため、Node.jsでの環境構築などはできない。(ターミナルが使えない)
  5. ただし、将来的にメンバーの技術力アップがあったときにViteやNuxtに移行する可能性も考えて設計する。

以上のような条件でサイトを制作した過程をご紹介します。
かなり特殊ケースかもしれませんが、これから本格的にVue.jsを導入したいがチーム内の技術力にばらつきがある場合などの参考にしていただければ幸いです。

なお、開発環境はVSCodeでLive Serverなどのプラグインを利用します。

やること

  1. scriptタグでVue.jsとモジュールを読み込む。
  2. コンポーネントごとにJSファイルを分けてつくる。
  3. コンポーネントをまとめたレイアウトコンポーネントをつくる。
  4. UIのサンプルページを用意する。

scriptタグでVue.jsとモジュールを読み込む

各HTMLファイルのヘッダーからscriptタグで読み込みます。
SPAと違ってページ遷移するたびに読み込みが行われるので遅いのがデメリットです。

Vue.jsの読み込み

CDNから読み込むのが一番簡単です。

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

あるいは一度npmでダウンロードしたものを読み込むこともできます。

<script src="./node_modules/vue/dist/vue.global.js"></script>

今回は2つ目のnpmを利用した方法をとっています。

main.jsの読み込み

main.jsはコンポーネントなどをインポートして、Vueのインスタンスを<div id="app">にマウントするのが役割です。
そのため、type="module"をつけるのが重要です。※IE非対応

<script src="./js/main.js" type="module"></script>

HTMLファイルの基本的な構造

以上を踏まえたうえですべてのHTMLファイルは以下のような構造になります。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>サンプルサイト</title>
    <link rel="shortcut icon" href="./favicon.ico" />
    <link rel="stylesheet" href="./css/main.css" />
    <script src="./node_modules/vue/dist/vue.global.js"></script>
    <script src="./js/main.js" type="module"></script>
  </head>
  <body>
    <div id="app" v-cloak>
    <!-- この中でVue.jsのコンポーネントが使える -->
  </div>
    <!-- /#app -->
  </body>
</html>

id="app"のところにv-cloakとついているのは、ページが読み込まれた際に一瞬スクラッチで書かれたテキストやマスタッシュタグなどが見えてしまうのを防止するためです。
cssに以下の記述も必要です。

[v-cloak] {
  display: none;
}

main.jsの構造

可読性向上のためにDefaultLayoutとBannerという二つのコンポーネントしか読み込んでいませんが、実際にはほかにも多数のコンポーネントを読み込んでいます。

js/main.js
import DefaultLayout from "./layouts/DefaultLayout.js";
import Banner from "./components/Banner.js";

const { createApp } = Vue;

const app = createApp({
  components: {
    "default-layout": DefaultLayout,
    "banner": Banner,
  },
});

app.mount("#app");

以上でHTMLファイルでVue.jsを利用する準備はできました。
次からコンポーネントをつくっていきます。

コンポーネントごとにJSファイルを分けてつくる。

コンポーネントはjsディレクトリの中にさらにcomponentsディレクトリをつくって、その中に1コンポーネント1ファイルで作っていきます。
可読性と将来的にSFCに移行しやすいように。

例としてバナーコンポーネントをつくるとしたら以下のようになります。

js/components/Banner.js
const { computed } = Vue;

export default {
  props: {
    color: String,
    size: String,
    href: String,
    openInNew: Boolean,
  },
  setup(props) {
    const bannerColor = computed(() => {
      switch (props.color) {
        case "none":
          return "hp_bgColor_none";
        case "primary":
          return "hp_bgColor_primary";
        case "info":
          return "hp_bgColor_info";
        case "success":
          return "hp_bgColor_success";
        case "error":
          return "hp_bgColor_error";
        case "warning":
          return "hp_bgColor_warning";
        default:
          return "hp_bgColor_gray";
      }
    });
    const bannerSize = computed(() => {
      switch (props.size) {
        case "free":
          return "el_bannerFreeSize";
        case "xs":
          return "el_bannerXS";
        case "sm":
          return "el_bannerSM";
        case "lg":
          return "el_bannerLG";
        case "xl":
          return "el_bannerXL";
        default:
          return "el_bannerMD";
      }
    });
    const linkTarget = computed(() => {
      if (props.openInNew) {
        return "_blank";
      } else {
        return "_self";
      }
    });
    return {
      bannerColor,
      bannerSize,
      linkTarget,
    };
  },
  template: `
    <div class="el_banner" :class="[bannerSize, bannerColor]">
      <a 
        v-if="href"
        class="el_banner_inner el_banner_link"
        :class="{ic_openInNew: openInNew, }"
        :href="href"
        :target="linkTarget"
      >
        <slot />
      </a>
      <span v-else class="el_banner_inner">
        <slot />
      </span>
    </div>`,
};

propsでcolorやsizeなどを受け取りそれによってclassを付加したり、書き出されるタグを制御しています。
CSSは別途定義されているものとしてご覧ください。
CSSはグローバルでつくるしかないので、PRECSSという設計思想で制作しました。
ただ、今になって思うとTailwind CSSを利用したほうがよかったかもしれません。

また、setup()の中身はcomposablesというディレクトリを用意して、そちらに分けて書いてインポートしたほうがよいです。今回は割愛。

HTMLファイルでは以下のようにコンポーネントを利用することができるようになりました。

<banner size="xs" color="primary">バナーXS</banner>

コンポーネントをまとめたレイアウトコンポーネントをつくる

次にレイアウトコンポーネントをつくります。
js/layoutsディレクトリを用意します。

layouts/DefaultLayout.js
// Components
import LyWrapper from "../components/LyWrapper.js";
import LyHeader from "../components/LyHeader.js";
import LyFooter from "../components/LyFooter.js";
import LyMain from "../components/LyMain.js";

const { ref, onMounted, Transition } = Vue;

export default {
  components: {
    "ly-wrapper": LyWrapper,
    "ly-header": LyHeader,
    "ly-footer": LyFooter,
    "ly-main": LyMain,
  },
  props: {
    siteTitle: String,
    nav: Array,
    copyright: String,
  },
  setup() {
    const loading = ref(true);
    onMounted(() => {
      loading.value = false;
    });

    return { loading };
  },
  template: `
   <Transition name="fade">
      <ly-wrapper v-show="!loading">
        <ly-header
          :title="siteTitle"
          :nav="nav"
        ></ly-header>
        <ly-main>
          <slot />
        </ly-main>
        <ly-footer>{{ copyright }}</ly-footer>
      </ly-wrapper>
    </Transition>`,
};

Wrapper・Header・Main・Footerというコンポーネントをそれぞれ用意してそれらをインポートしている想定です。
propsではグローバルで定義されたサイトタイトルやナビゲーションなどの情報を受け取る想定です。
また、全体をTransitionタグで囲みCSSを設定してフェードインさせています。
こちらも、loadingという変数を直接定義していますが、composablesにuseLoadingなどという名前で分けておいたほうがいいかと思います。今回は割愛。

HTMLファイルでは<div id="app">の直下にこのレイアウトコンポーネントを入れて利用します。

<div id="app" v-cloak>
  <default-layout
    :site-title="siteTitle"
    :nav="navigation"
    :copyright="copyright"
  >
      <!--ここにコンテンツを書いていく -->
  </default-layout>
</div>
<!-- /#app -->

UIのサンプルページを用意する。

最後に今後更新作業を行ってくれるメンバーや自分自身のために各コンポーネントの使い方ページを用意しましょう。

さいごに

紹介しておいてなんですが、改めてNodo.jsで開発環境を作れないデメリットが多々あります。
TypeScriptやSassは当然利用できないですし、リントツールの設定をどのように共通化するかなどの課題があります。
なによりも、テストしない・トランスパイルやコンパイルしないという運用方法になるため、ブラウザごとのチェックを入念にしないといけないため非効率的です。

もし、改善点やわかりづらい点などあればご指摘ください。
読んでいただきありがとうございました。

追加情報

こちらの内容を発展させた記事があります。
https://zenn.dev/shu_saginoya/articles/c50c216d04221e

Discussion