🏀

Vue.jsでHTMLをインクルードする

2023/01/13に公開

1. 概要

趣味でやっているバスケットボールチームの公式HPを作ろうという話になり、簡単にHTMLを書き、Bootstrapでデザインを整え、Netlifyを使って公開しました。
3ページしかない小さなサイトですが、ヘッダーやメンバー紹介のカードなどHTMLを共通化したい部分がいくつかあったので、Vue.jsのコンポーネントを使って共通化しました!

2. なぜVue.jsなのか

端的に言えば、使ったことがなかったから、そして一番簡単にできそうな手法だったからです。
「HTML インクルード」等で検索するとJavaScriptやPHPを使った手法がヒットします。HTMLの<iframe>タグでもインクルードできますが、Bootstrapのデザインがうまく適用されなかったので諦めました。

3. Vue.jsの導入

CDNを用いてVue.jsを使えるので、HTMLに1行追加するだけOKです。

<script src="https://cdn.jsdelivr.net/npm/vue@2.7.11"></script>

4. 各要素をインクルード

まずはフッター

各ページに以下のようなフッターを書いていました。

<footer>
    <p>&copy; 2023</p>
</footer>

これはインクルードするまでもないかもしれませんが、一番簡単なのでこれからインクルードします。
app.jsという名前で新たにファイルを作成し、以下のようにコンポーネントを作成します。

app.js
Vue.component('footer-template', {
    template: `
        <footer>
            <p>&copy; 2023 Salcow</p>
        </footer>
    `,
})

new Vue({
    el: '#app',
})

footer-templateというコンポーネントを作ることができました。
このコンポーネントをnew Vueで作成されたルート Vue インスタンス内でカスタム要素として使用することができます。
HTML上のフッターを挿入したい箇所に

<div id="app">
    <footer-template></footer-template>
</div>
<!-- app.jsを読み込む -->
<script src='./app.js'></script>

とすることでフッターを表示させられます!

new Vue({
    el: '#app',
})

ここで何をやっているんだろうと思ったのですが、こちらに詳しく説明されています。

elというオプションが指定した要素がVue.jsが作用を及ぼす範囲になります。

という認識で考えるとわかりやすかったです。そのため<footer-template></footer-template>id=appとしたdivタグの中に記述する必要があるんだと思います。

次にカード

HP上ではBootstrapのCardを使ってメンバー紹介をしています。

image.png

HTMLはこんな感じです。

<div class="card" style="width: 18rem;">
    <img src="demo.png" class="card-img-top" alt="demo">
    <div class="card-body">
        <h5 class="card-title">name</h5>
        <h6 class="card-subtitle mb-2 text-muted">position, height</h6>
        <p class="card-text">detail</p>
    </div>
</div>

これをメンバーの数だけコピペしてHTMLに書き込みたくないのでインクルードします。先ほどと違うのは、メンバーによって、imgタグの画像パスや名前等を変更したいという点です。HTML側からそれらを指定できるようにします。
chatGPTと相談しながら次のように書きました。

まずはVueのコンポーネント

Vue.component('card-template', {
    props: {
        img_src: {
            type: String,
            default: './static/images/member/demo.png'
        },
        member_name: {
            type: String,
            default: 'name'
        },
        basic: {
            type: String,
            default: 'position, height'
        },
        text: {
            type: String,
            default: 'detail'
        }
    },
    template: `
        <div class="card" style="width: 18rem;">
            <img v-bind:src="img_src" class="card-img-top" alt="demo">
            <div class="card-body">
                <h5 class="card-title">{{ member_name }}</h5>
                <h6 class="card-subtitle mb-2 text-muted">{{ basic }}</h6>
                <p class="card-text" v-html="text"></p>
            </div>
        </div>
    `,
})

new Vue({
    el: '#app',
})

先ほどと同じようにcard-templateというコンポーネントを作成しました。propsによってこのコンポーネント内で使う名前やポジションなどの属性を定義します。上記のようにすることで型やデフォルト値を設定することができます。
こちらのサイトが非常にわかりやすく参考になりました。

props は、templateなどのコンポーネント内部で使用する属性を定義します。

画像のパスを指定するにはv-bindを用います。

v-bind は、HTML タグの属性値に変数名を割り当てます。: は v-bind: の省略系です。v-bind:属性名 と :属性名は同じ意味を持ちます。

text属性は改行できるようにしたかったのでv-htmlを用いました。

通常のテキストは、<, >, & が <, >, & に置換されて出力されますが、v-html を用いた場合はタグがそのまま出力されます。迂闊に使用するとセキュリティ問題を起こすことがあるので、クロスサイトスクリプティングに注意しながら使用してください。

v-htmlを用いることで文字列内に<br>を入れることで改行ができます。
HTMLでは次のように記述します。

<div id="app">
    <card-template img_src="minions.jpg" member_name="スチュアート" basic="SG, 61cm" text="のんびりで、ギター好き。<br>イディスと少し似ている。"></card-template>
</div>
<!-- app.jsを読み込む -->
<script src='./app.js'></script>

image.png

61cmは公式設定です笑
ポジションはSGっぽい気がするのでSGにしておきます。

https://despicableme.fandom.com/wiki/Stuart

最後にナビゲーションバー

ヘッダーはBootstrapのナビゲーションバーを使っているのでこれも共通化していきます。

image.png

このナビゲーションバーは現在開いているページの文字が黒くなるようになっています。すなわち、Memberページでは以下のように描画されます。

image.png

ページごとのこの変化を共通化しながら実現していきます。

元のHTML
<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <div class="container-fluid">
    <a class="navbar-brand" href="#">Navbar</a>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav me-auto mb-2 mb-lg-0">
        <li class="nav-item">
          <a class="nav-link active" aria-current="page" href="#">Home</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="#">Link</a>
        </li>
        <li class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
            Dropdown
          </a>
          <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
            <li><a class="dropdown-item" href="#">Action</a></li>
            <li><a class="dropdown-item" href="#">Another action</a></li>
            <li><hr class="dropdown-divider"></li>
            <li><a class="dropdown-item" href="#">Something else here</a></li>
          </ul>
        </li>
        <li class="nav-item">
          <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
        </li>
      </ul>
      <form class="d-flex">
        <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
        <button class="btn btn-outline-success" type="submit">Search</button>
      </form>
    </div>
  </div>
</nav>

まずはVue.jsのコンポーネント

Vue.component('header-template', {
    data: function() {
        return {
            currentRoute: null
        }
    },
    template: `
        <div class="sticky-top">
            <nav class="navbar navbar-expand-lg navbar-light bg-light">
                <div class="container-fluid">
                    <a class="navbar-brand" href="/">Salchow</a>
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                    </button>
                    <div class="collapse navbar-collapse" id="navbarSupportedContent">
                        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                            <li class="nav-item">
                                <a class="nav-link" :class="{ 'active': currentRoute === '/' }" href="/">Home</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" :class="{ 'active': currentRoute === '/member.html' }" href="/member.html">Member</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" :class="{ 'active': currentRoute === '/contact.html' }" href="/contact.html">Contact</a>
                            </li>
                        </ul>
                    </div>
                </div>
            </nav>
        </div>
    `,
    created: function() {
        this.currentRoute = window.location.pathname;
    }
})

new Vue({
    el: '#app',
})

header-templateというコンポーネントを作成しました。
現在のルートを取得するために、window.location.pathnameを使用し、それをcurrentRouteデータプロパティに割り当てることでどのclassをactiveにするかを制御しました。

5. 躓いたところ

<div id="app">
    <header-template></header-template>
</div>
<div id="app">
    <card-template></card-template>
</div>
<div id="app">
    <footer-template></footer-template>
</div>
<script src='./app.js'></script>

のように記述するべきだと思っていたのですが、これだと1つ目しか表示されません。

<div id="app">
    <header-template></header-template>
    <card-template></card-template>
    <footer-template></footer-template>
</div>
<script src='./app.js'></script>

が正しい表記でした。

完成した app.js
Vue.component('header-template', {
    data: function() {
        return {
            currentRoute: null
        }
    },
    template: `
        <div class="sticky-top">
            <nav class="navbar navbar-expand-lg navbar-light bg-light">
                <div class="container-fluid">
                    <a class="navbar-brand" href="/">Salchow</a>
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                    </button>
                    <div class="collapse navbar-collapse" id="navbarSupportedContent">
                        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                            <li class="nav-item">
                                <a class="nav-link" :class="{ 'active': currentRoute === '/' }" href="/">Home</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" :class="{ 'active': currentRoute === '/member.html' }" href="/member.html">Member</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" :class="{ 'active': currentRoute === '/contact.html' }" href="/contact.html">Contact</a>
                            </li>
                        </ul>
                    </div>
                </div>
            </nav>
        </div>
    `,
    created: function() {
        // 現在のルートを取得するために、window.location.pathnameを使用し、それをcurrentRouteデータプロパティに割り当てる
        this.currentRoute = window.location.pathname;
    }
})

Vue.component('footer-template', {
    template: `
        <footer>
            <p>&copy; 2023 Salcow</p>
        </footer>
    `,
})

Vue.component('card-template', {
    props: {
        img_src: {
            type: String,
            default: './static/images/member/demo.png'
        },
        member_name: {
            type: String,
            default: 'name'
        },
        basic: {
            type: String,
            default: 'position, height'
        },
        text: {
            type: String,
            default: 'detail'
        }
    },
    template: `
        <div class="card" style="width: 18rem;">
            <img v-bind:src="img_src" class="card-img-top" alt="demo">
            <div class="card-body">
                <h5 class="card-title">{{ member_name }}</h5>
                <h6 class="card-subtitle mb-2 text-muted">{{ basic }}</h6>
                <p class="card-text" v-html="text"></p>
            </div>
        </div>
    `,
})

new Vue({
    el: '#app',
})

6. まとめ

Vue.jsを用いることでコードが見やすくなり、また編集もやりやすくなりました。
詰まったときはchatGPTに「このコードをVue.jsでインクルードしたいんだけど、どうしたらいい?」って聞いたら瞬時に解決策を教えてくれました。有能すぎる、、、
もっといいやり方があったら教えてください。

Discussion