🔶

vite-plugin-handlebarsでよく使う構文

2024/02/25に公開

はじめに

最近静的サイトを制作する際に、viteで使用できるvite-handlebarsをよく使うので、備忘録として投稿で残そうと思います。

環境構築

  1. viteのインストール 今回は静的サイトを目的としているのでVanillaの使用で進めます。
npm create vite@latest
  1. sassとvite-plugin-handlebarsのインストール
npm i sass vite-plugin-handlebars
  1. pageData.jsとvite.config.jsを作成
touch pageData.js vite.config.js
  1. フォルダ整理
    整頓すると以下のようになります。
├── package-lock.json
├── package.json
├── pageData.js
├── src
│   ├── common
│   │   ├── components
│   │   │   └── Header.html
│   │   │   └── Head.html
│   │   │   └── Card.html
│   │   ├── js
│   │   │   └── index.js
│   │   └── scss
│   │       └── index.scss
│   └── index.html
└── vite.config.js

vite.config.jsの記述

全体像はこのような形式です。普段は別途設定などはしますが、今回は割愛します。

/vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';
import handlebars from 'vite-plugin-handlebars';
import pageData from './pageData.js';
export default defineConfig({
  root: './src',
  base: '',
  build: {
    outDir: '../dist',
    rollupOptions: {
      input: {
        index: resolve(__dirname, './src/index.html'),
      },
      output: {
        chunkFileNames: 'common/js/[name].[hash].js',
        entryFileNames: 'common/js/[name].[hash].js',
        assetFileNames: ({ name }) => {
          if (/\.(jpe?g|png|gif|svg)$/.test(name ?? '')) {
            return 'common/img/[name].[hash][extname]';
          }
          if (/\.css$/.test(name ?? '')) {
            return 'common/css/[name].[hash][extname]';
          }
          return 'common/[name].[hash][extname]';
        },
      },
    },
  },
  plugins: [
    handlebars({
      partialDirectory: resolve(__dirname, './src/common/components'),
      context: (pagePath) => {
        return {
          data: pageData[pagePath],
           url: "https://zenn.dev/"
        };
      },
      helpers: {
        br: (contents) => {
          let str = contents;
          str = str.replace(/\r?\n/g, '<br>');
          return str;
        },
      },
    }),
  ],
});

pluginsの中でhandlebarsの各設定がありますが、それぞれの役割は以下になります。

partialDirectory

handlebarsではコードを分割して、各ページにコンポーネントとして使用することができます。
そのため、headやHeaderなど各ページに共通で読み込ませたいファイルをまとめておくフォルダを指定する設定になります。
使用方法としては以下のように使えます。
まず、/src/common/components/Header.htmlを作成します。
内容は以下のようにします。

/src/common/components/Header.html
<header class="ly_header">
  <h1 class="ly_header_ttl">ヘッダー</h1>
</header>

その上で/src/index.htmlに下記の記述を加えてビルドすると

/src/index.html
{{> Header}}

Header.htmlの記述がそのまま反映されます。

context

これは、ページ全体での変数定義する箇所になります。例えばurlと記述てあるものを

<a class="bl_card_link" href="{{url}}">リンク</a>

と書いて、ビルドすると

<a class="bl_card_link" href="https://zenn.dev/">リンク</a>

になります。ページ全体で共通化しておきたいものなどを定義しておくと、後から変更になったときにとても便利でうす。 また、さっきのHeader.htmlの中で以下のコードを追加します。

/src/common/components/Header.html
<header class="ly_header">
  <h1 class="ly_header_ttl">ヘッダー</h1>
  <a class="bl_card_link" href="{{url}}">ヘッダーリンク</a>
</header>

これをビルドすると

/dist/index.html
<header class="ly_header">
  <h1 class="ly_header_ttl">ヘッダー</h1>
  <a class="bl_card_link" href="https://zenn.dev/">ヘッダーリンク</a>
</header>

と表示されます。
なので、return内で定義したものは、コンポーネントの中でも使用ができます。 returnの中にpageDataと定義してありますが、これは後で説明します。

helpers

handelbarsでは独自の関数を定義して戻り値をそのまま反映することができます。
vite.config.jsで定義しているのは引数として渡した文字の中に\nがあれば
タグに変換する関数を定義しています。
なぜこれを定義しているかというと、contextや、後ほど解説するコンポーネントへの値などはすべて文字列として処理されてしまい、文章内に
を含めてもそのまま表示されてしまいます。
なので、helpersで定義したものでcontextの値などを改行できるようにしています。使用方法として以下です。

/vite.config.js
  context: (pagePath) => {
    return {
        brTxt:"これはテスト文章です。\nこれはテスト文章です。"
    };
  },
 helpers: {
    br: (contents) => {
      let str = contents;
      str = str.replace(/\r?\n/g, '<br>');
      return str;
    },
  },
<p>{{{br brTxt}}}</p>

ビルドすると

index.html
<p>これはテスト文章です。<br>これはテスト文章です。</p>

となります。

pageData.jsの記述

context内でpageData[pagePath]をdataとして定義していました。
pageDataの中身はこんな感じです。

/pageData.js
const baseData = {
  metaTtl: "トップページ",
  ttl: "タイトル -- "
}


const pageData = {
  '/index.html': {
    metaTtl: baseData.metaTtl,
    ttl: baseData.ttl + "トップ",
    "snsList": [
      {
        "title": "Facebook",
        "link": "https://www.facebook.com",
        "text": "世界最大のソーシャルネットワーキングサービスで、友人や家族とのつながりを深めることができます。ニュースフィード、写真の共有、イベントの作成などの機能があります。",
        "linkTxt": "Facebookを訪問する"
      },
      {
        "title": "Twitter",
        "link": "https://www.twitter.com",
        "text": "短いメッセージ(ツイート)を投稿することに特化したプラットフォーム。ニュースの共有や意見の交換に使われ、リアルタイムでの情報交換が特徴です。",
        "linkTxt": "Twitterを訪問する"
      },
      {
        "title": "Instagram",
        "link": "https://www.instagram.com",
        "text": "写真や動画を中心に共有するSNS。ストーリーズ機能やライブ配信など、視覚的なコンテンツの共有に特化しています。",
        "linkTxt": "Instagramを訪問する"
      },
      {
        "title": "LinkedIn",
        "link": "https://www.linkedin.com",
        "text": "プロフェッショナル向けのネットワーキングサイト。キャリアアップや仕事の機会を見つけるためのプラットフォームとして利用されます。",
        "linkTxt": "LinkedInを訪問する"
      },
      {
        "title": "TikTok",
        "link": "https://www.tiktok.com",
        "text": "短い動画を共有するプラットフォーム。音楽やダンス、ユーモアなど、エンターテインメント性の高いコンテンツが人気です。",
        "linkTxt": "TikTokを訪問する"
      }
    ],
  },
}
export default pageData;

baseDataでページ全体の共通のものを定義しておきます。
pageDataでは各オブジェクトの名前をファイル名にして、それぞれの定義をしています。
基本的に各ページのオブジェクト内には、baseDataで定義した値を同じ命名で定義しておきます。
ページによってbaseDataの値と結合したり書き換えたい場合は、上書きします

pageData.js
ttl: baseData.ttl + "トップ",
// => "タイトル -- トップ"と出力

また、snsListなど独自の定義をしておくことでページごとに繰り返しの記述がある箇所などをデータとして定義しておき、#each snsListで繰り返し処理が可能になります。

使い方

変数

使い方としては、context内で定義した値を{{}} で囲んで使用するだけです。

/src/index.html
<h1>{{data.ttl}}</h1>
<p>{{url}}</p>

ビルドすると

/dist/index.html
<h1>タイトル -- トップ</h1>
<p>https://zenn.dev/</p>

条件分岐(if unless)

ヘッダーのメニューリンクなどでis-active状態を各ページで管理したいときなどがあると思います。
その場合は、pageData.jsで以下のように定義をしておけばclassやコンポーネントの条件分岐が可能になります。

pageData.js
  '/index.html': {
    metaTtl: baseData.metaTtl,
    ttl: baseData.ttl + "トップ",
    // snsList
    top:true,
  },
/src/common/components/Header.html
    <a
      href=""
      class="link {{#if data.top}}is-active{{/if}}">リンク
    </a>

ビルド後

/dist/html
    <a href="" class="link is-active">リンク</a>

になります。 ただし、{{#if data.top === "true"}}````など演算子は使用できないので、ページごと違う命名をして記述するのが望ましいです。 {{#unless data.top }}````にすると!trueになります。

繰り返し構文(#each)

VueやReactと同様に#eachを使用して繰り返し処理が行えます。pageData.jsで定義していたsnsListをeachで処理すると以下のようになります。

/src/index.html
{{#each data.snsList}}
    <div class="bl_card">
      <h3 class="bl_card_ttl">{{title}}</h3>
      <p class="bl_card_txt">{{text}}</p>
      <a class="bl_card_link" href="{{link}}">{{{br linkTxt}}}</a>
    </div>
{{/each}}

ビルドすると以下のようになります。

/dist/index.html
<div class="bl_card">
      <h3 class="bl_card_ttl">Facebook</h3>
      <p class="bl_card_txt">世界最大のソーシャルネットワーキングサービスで、友人や家族とのつながりを深めることができます。ニュースフィード、写真の共有、イベントの作成などの機能があります。</p>
      <a class="bl_card_link" href="https://www.facebook.com">Facebookを訪問する</a>
</div>
// 繰り返し

linkTxtをhelpersのbr関数を使用することによって渡された文章を改行することができます。
また、#eachの中でも#if文は使用できるので、仮に渡すデータの一部でtextがない場合は以下のように書くことも可能です。

/src/index.html
{{#each data.snsList}}
    <div class="bl_card">
      {{#if title}}<h3 class="bl_card_ttl">{{title}}</h3>{{/if}}
      <p class="bl_card_txt">{{text}}</p>
      <a class="bl_card_link" href="{{link}}">{{{br linkTxt}}}</a>
    </div>
{{/each}}

これで、値がないときはタグ自体も非表示にできます。

子コンポーネント(値受け渡し)

公式の方には記載がなかったのですが、どうやら各ページからコンポーネントに対して値を渡すことができるようです。
例として先ほど#eachで使用したCardをコンポーネントとして作成します。

/src/common/components/Card.html
<div class="bl_card">
  <h3 class="bl_card_ttl">{{title}}</h3>
  <p class="bl_card_txt">{{text}}</p>
  <a class="bl_card_link" href="{{link}}">{{linkTxt}}</a>
</div>

先程はcontextに定義していたdata.snsListなどを渡していましたが今回は通常の文字列を渡して見たいと思います。

/src/index.html
{{>Card
  title="子コンポーネントタイトル"
  text="子コンポーネントテキスト"
  link="https://zenn.dev/"
  linkTxt="Zennのリンク"
}}

構文の中で指定した命名に値を渡せば大丈夫です。
一つ注意点があります。この値を渡すところに、contextで定義していある値を直接渡すことはできません。
例えば、data.ttlをtitleに対して渡すとします。  その場合title="{{data.ttl}}"と記述すればうまくいきそうですが、これだと{{data.ttl}}がそのまま出力されます。
例外として#each文などで渡す値を子コンポーネントにわたす際は問題がありません。
例えば、先ほど作成したCard.htmlを使用してdata.snsListを繰り返し処理するとします。そうすると記述は以下のようになります。

/src/index.html
{{#each data.snsList}}
    {{>Card
    :title="{{title}}"
    :text="{{text}}"
    :link="{{link}}"
    :linkTxt="{{linkTxt}}"
    }}
{{/each}}

こうすると、繰り返し表示されます。
また、helperで定義したbr関数を使用する場合はCard.htmlで変換したい箇所を追加で{}足して、先頭にbrをつけます。

/src/common/components/Card.html
<div class="bl_card">
  <h3 class="bl_card_ttl">{{title}}</h3>
  <p class="bl_card_txt">{{{{br text}}}</p>
  <a class="bl_card_link" href="{{link}}">{{{br linkTxt}}}</a>
</div>

src/index.html内の:link=Txt="{{linkTxt}}"の値をbrで囲んでも半角スペースが生成されるだけで改行はされません。なので子コンポーネントから渡した値を関数で処理したい場合は、使用するコンポーネント内で処理が必要になります。

さいご

中規模サイトやパーツ作成が必要な場合ではとても有効なプラグインだと思うので、少しでも参考になればと思います。間違っている箇所があればコメントにてご意見お待ちしております!!
また、コメントで他にも便利なプラグインがあれば教えていただけると助かります。

Discussion