SAKURUG TECHBLOG

ViteとHandlebarsのコンポーネント型HTML環境

timestampauthor-name
Akiyoshi

はじめに

こんにちは。
皆さんは静的サイトをコーディングする際にどんな環境をお使いでしょうか。
今回は、Astro、Nuxtjs、NextJsなどを使わずに、
なるべく純粋なHTMLで実装するために
Handlebarsを使ったコーディング環境を紹介します

どんな人向けか

  • 出力がZero JavaScriptな環境でコンポーネントを使いたい
  • UI Framework を使うほどの規模でなくてもコンポーネントベースのコーディングがしたい
  • 大きな規模出なくてもサクッと構文チェックライブラリを導入したい
  • 色んな人がメンテナンスするサイトでできる限りクセのない実装を提供したい
  • 複数のサイトのコーディングのベース環境が欲しい

バージョン

Node

18以上

Vite

4.4.0

Vite-plugin-handlebars

1.6.0

環境の補足

CSSについて

  • Sass
  • Tailwindcss

まずコーディングのベースとして機能するには
サイトの実装時に毎回書くようなものをテンプレート化させることを目指します
タイポグラフィやプライマリーカラーなどはscssの変数を使います
これらに依存するレイアウトやコンポーネントはscssにて管理します
コンポーネントに関しては、複数サイトで使いまわせるようにしたいため、
Handlebarsで切り分けたコンポーネントのhtmlファイル内でCSSを記述したいです
そこはtailwindcss を使うことで解決できます
タイポグラフィや色はCSSの変数使えばさらに汎用性が高まりますが、そこまでやるならそれなりの規模かと思うのでUI Frameworkを検討しても良いと思います
なので、多少の汎用性は犠牲にしてラフに実装していきます

  • Markuplint
  • stylelint
  • eslint

    構文チェックはlinterに任せます

  • husky

    コミット単位でlint通すために導入します

参考リンク

公式のリンクをまとめます

ディレクトリ構成

- .husky
- src
  - assets
    - images
    - js
    - scss
  - data
    - pageData.json
  - index.html
  - other.html
- .markuplintrc.json
- .stylelintrc.json
- .eslintrc.json
- .gitignore
- tailwind.config.js
- postcss.config.cjs
- vite.config.js
- package.json
- package.lock.json

解説

各種configファイル

コンポーネントの活用をすると、そのままのhtmlではサーバーにアップしてもうまく表示されません
Viteを用いたビルドが必要です
今回はlinterやtailwindも入れているので、それらの設定ファイルも必要ではありますが、
他の設定ファイルは割愛します。

  • vite.config.jsファイル
import { defineConfig } from 'vite';
//import設定を追記

import { resolve } from 'path';
/**
 * HTMLの複数出力を自動化する
 * ./src配下のファイル一式を取得
 * 2階層目まで取得する
 */

import fs from 'fs';
// const fileNameList = fs.readdirSync(resolve(__dirname, './src/'));
const fileNameList = [];
const getFiles = (dir, fileList, nested) => {
  // assets, components, data, publicは除外
  if (/assets|components|data|public/i.test(dir)) return;
  const files = fs.readdirSync(dir);
  files.forEach(file => {
    if (fs.statSync(dir + '/' + file).isDirectory()) {
      getFiles(dir + '/' + file, fileList, nested ? nested + '/' + file : file);
    }
    else {
      nested ? fileList.push(nested + '/' + file) : fileList.push(file);
    }
  }
  );
};

getFiles(resolve(__dirname, './src/'), fileNameList, '');

//htmlファイルのみ抽出
const htmlFileList = fileNameList.filter(file => /.html$/.test(file));

//build.rollupOptions.inputに渡すオブジェクトを生成
const inputFiles = {};
for (let i = 0; i < htmlFileList.length; i++) {
  const file = htmlFileList[i];
  inputFiles[file.slice(0,-5)] = resolve(__dirname, './src/' + file );
}

/**
 * ページ単位の基本情報
 */
//import設定を追記
import handlebars from 'vite-plugin-handlebars';
import pageDataJson from './src/data/pageData.json';

//HTML上で出し分けたい各ページごとの情報
const pageData = pageDataJson;

/**
 * config情報
 */

export default defineConfig({
  base: './', //ルートパスの設定。デフォルトは'/
  root: './src', //開発ディレクトリ設定
  plugins: [
    handlebars({

      //コンポーネントの格納ディレクトリを指定
      partialDirectory: resolve(__dirname, './src/components'),

      helpers: {
        // 変数内のhtmlタグを描画する
        html: (contents) => {
          const str = contents
          return str;
        }
      },

      //各ページ情報の読み込み
      context(pagePath) {
        return pageData[pagePath];
      },
    }),
  ],

  build: {
    outDir: '../dist', //出力場所の指定
    rollupOptions: { //ファイル出力設定
      input: inputFiles,
      output: {
        assetFileNames: (assetInfo) => {
          let extType = assetInfo.name.split('.')[1];
          //Webフォントファイルの振り分け
          if (/ttf|otf|eot|woff|woff2/i.test(extType)) {
            extType = 'fonts';
          }
          if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
            return assets/images/[name][extname];
          }
          //ビルド時のCSS名を明記してコントロールする
          if(extType === 'css') {
            if(assetInfo.name === 'index.css') {
              return assets/css/index-[hash].css;
            }
            return assets/css/[name].css;
          }
          return assets/${extType}/[name][extname];
        },
        chunkFileNames: 'assets/js/[name].js',
        entryFileNames: 'assets/js/[name].js',
      },
    },
  },
});

以下の記事が大変参考になりました
詳細はぜひ下記をご参照ください
参考 : 【詳細版】Viteでコーダーのコーディング環境(HTML(ejsライク:ハンドルバー化)・Sass・JS)を作る

  • markuplintrc.json

    markuplintは補足させていただきます

    こちらもコンポーネントで切り分けていると、構文チェックがうまくできません

    そこで、コンポーネントなどを全て処理した後で構文チェックをしてくれるようにparserを入れます

npm i -D @markuplint/mustache-parser

上記をインストールした上で、以下のように設定ファイルを記述してください。

{
  "extends": [
    "markuplint:recommended"
  ],
  "parser": {
    ".mustache$|.hbs$|.html$": "@markuplint/mustache-parser"
  },
  "rules": {}
}

コンポーネントの使い方

例、ボタン

- components
   - baseButton.hmtl
<a
  href="{{href}}" class="c-baseButton"{{#if id}} id={{id}}{{/if}} {{#if isDisabled}}disabled{{/if}}
>
  {{text}}
</a>

使い方

{{> baseButton
  href="URL"
  text="ボタンのテキスト"
  id="id"
  isDisabled=false  
}}

json データから各ページにデータを流し込む方法

Jsonは下記ディレクトリに配置します

- src
  - data
    - pageData.json
{
  "/index.html": {
    "meta": {
      "title": "タイトルが入ります",
      "description": "ディスクリプションが入ります"
    },
  }
}

ページが多い場合はファイル分けても良いと思います

※応用例としては、ページのmeta情報です

json から流し込むテキストだって改行させたい

ヘルパー関数を定義するとできます
ヘルパー関数はvite.config.jsファイルに記述します

  1. 改行コードをbrタグに変換する

     参考文献をご参照ください

     参考: vite-plugin-handlebarsでよく使う構文

  2. そもそもHTMLタグをそのまま動かせるようにする

     この方法だとaタグもJSON内部にかけます

     応用例としては、

     よくある質問のQ/AをJSONに書いたときの、

     改行やフォームへのリンクです

  • ヘルパー関数内容
export default defineConfig({
  ...
  plugins: [
    handlebars({
      ...
      helpers: {
        // 変数内のhtmlタグを描画する
        html: (contents) => {
          const str = contents
          return str;
        }
      },
      ...
    }),
  ],
  ...
  • 使い方
{{#html 変数}}{{/html}}

※ローカルにおきますので
外部から悪意のあるコードを差し込まれる可能性などは考慮しません

さいごに

いかがでしたか
制作の開発環境について、品質向上のヒントになっていると嬉しいです

記事をシェアする

ABOUT ME

author-image
Akiyoshi
17卒の新卒入社。VPoE候補として、SAKURUGのエンジニアリングユニットを盛り上げられるよう頑張ってます。