Web production note

 【更新日 :

【詳細版】Viteでコーダーのコーディング環境(HTML(ejsライク:ハンドルバー化)・Sass・JS)を作る

Category:
開発環境

※本記事はejsを利用していません。

代わりにHTMLファイルをejsと同等の機能を追加する(ハンドルバー化する)プラグイン vite-plugin-handlebars を利用しています。ejsは以下プラグイン vite-plugin-ejs を利用すると実装できるようです。

本記事は各設定の詳細を記載しています。
構築する手順とファイル構成だけが知りたい場合は以下の【簡易版】をご参照ください。

Viteでコーダー向けの環境構築をしたサンプルです。
通常のコーディングと同じ感覚(HTML(ejsライク)・Sass・JS)でコーディングできる設定を目指しました。

Viteの環境構築にはターミナル(Mac)やコマンドプロンプト(Windows)を用います。
本記事はこれらの基礎知識があることを前提とした記事です。

Viteは「HTML、SCSS、JS」で1セットなので、特定のファイルのみビルドしたい場合は別のツールを利用してください。

node.js v16.16.0 で動作確認をしています。
不具合が出る場合は実行環境のバージョンを合わせてください。

動作確認はmacです。windowsでは未検証ですので予めご了承ください。

Viteについて

Vite(ヴィート)は高速な開発環境を構築することができるフロントエンドのビルドツールです。

公式サイトが日本語化されており、ドキュメントも日本語で読むことができます。

Viteは「バンドルする」ということがコンセプトなツールのようですので、種々のファイルを分割して扱っている通常のWebサイトコーディングでの利用は想定していなさそうな印象ですが、検証したところ次項のような構成は組むことができたのでサンプルを作成しました。

この記事のViteでできること

  • 複数のHTMLページ生成
  • HTMLファイルをejsのように扱う(HTMLをハンドルバー化してejsの代替とする)
  • ビルド後のHTMLファイルを整形(任意追加)
  • SCSSの書き出し(PostCSSによるオプション設定)
    • autoprefixer:ベンダープレフィックスの追加
    • css-declaration-sorter:プロパティ順のソート(smacss)
    • postcss-sort-media-queries:メディアクエリをソートして1つにまとめる
    • postcss-purgecss:CSSファイルから未使用のスタイルを削除する
    • postcss-normalize-charset:先頭にcharset追加
  • publicに内包したサブJS(特定のページのみ追加したいJS)の圧縮(任意追加)

この記事のViteでできないこと

画像の圧縮はしない

プロジェクトで利用する固定画像は個別にコントロールしたい場合もあるので、コーディング環境に一括設定はしない派です。

Viteでは vite-plugin-imagemin というプラグインが存在しているようですので、そちらを利用すれば設定可能だと思います。
本記事では扱いませんので必要な方は以下をご参照ください。

ベース環境の構築

ベース環境の構築には以下の記事を参考にさせていただきました。

Vite は Node.js >=14.6.0 のバージョンが必要ですので事前にインストールしておいてください。

プロジェクト作成

構築したいディレクトリへ移動

作業を始める前に設定したいプロジェクトのあるディレクトリまで移動してください。

Windowsでの移動

cd C:¥Users¥.....¥Viteを構築したいプロジェクトディレクトリ

Macでの移動

cd /Users/....../Viteを構築したいプロジェクトディレクトリ

macなら該当フォルダを右クリック→「フォルダに新規ターミナルタブ」を選択してターミナルを立ち上げるとスムーズです。

Viteをインストール

プロジェクトを作成したいディレクトリで以下を実行すると、①〜③の質問が始まるので順番に入力・選択してください。その後は④、⑤と続けて実行してください。

npm init vite@latest

①プロジェクト名(構築するプロジェクトの名称)を入力

? Project name: › vite-project

②利用するフレームワークを選択:vanillaを選択

? Select a framework: › - Use arrow-keys. Return to submit.
❯   vanilla
    vue
    react
    preact
    lit
    svelte

③テンプレートのバリエーションを選択:vanillaを選択

? Select a variant: › - Use arrow-keys. Return to submit.
❯   vanilla
    vanilla-ts

④作成したプロジェクトへ移動

cd vite-project

⑤作成したプロジェクトに初期インストール

npm install

動作チェック(省略可能)

ここまで完了するとベースの構築は完了です。
「npm run dev」で開発サーバが起動するようになります。
※あくまでもテスト起動なのでこの工程は省いても問題ありません。
※一度立ち上げたタスクは control + c で終了できます。

npm run dev

ファイルのビルド(省略可能)

「npm run build」でdist/ディレクトリに公開用のファイル一式が書き出されます。

npm run build

想定しているディレクトリ構成

初期インストールを実行するとViteのサンプルファイル一式が出力さていますが不要なものは全て削除します。
本記事では開発ファイルを src/で管理する構成です。
存在していないファイルは以降の手順を進めながら新規作成してください。

■プロジェクトディレクトリ
 ┣ dist (ビルドしたファイルが出力される場所)
 ┣ package.json (プロジェクトのjsonファイル)
 ┣ postcss.config.cjs (PostCSSの設定ファイル)
 ┣ vite.config.js (viteの設定ファイル)
 ┣ .jsbeautifyrc (任意:HTML整形プラグインjs-beautifyの設定ファイル)
 ┃
 ┣ node_modules (編集不要:自動生成されるコアファイル格納場所)
 ┣ package-lock.json (編集不要:インストールしたパッケージ情報などが記載されている)
 ┃
 ┗ src
    ┣ index.html
    ┣ xxx.html (複数ページを追加する場合)
    ┃
    ┣ components (HTMLのコンポーネントパーツを格納)
    ┃  ┗ header.html
    ┃    xxx.html ...
    ┃
    ┣ js (メインのモジュールJSファイルを格納)
    ┃  ┗ main.js
    ┃
    ┣ public (Viteの変換対象外のディレクトリ。distに中身がそのままコピーされます。)
    ┃  ┗ assets (そのまま移動させたいファイルを必要に応じて格納していく)
    ┃     ┣ fonts
    ┃     ┃  ┗ xxx.woff2 ...
    ┃     ┣ js
    ┃     ┃  ┗ xxx.js ...
    ┃     ┗ images
    ┃       ┗ xxx.jpg ...
    ┃
    ┗ scss
       ┣ style.scss
       ┗ (各記法に合わせたディレクトリ構成)

vite.config.jsを作成

ディレクトリの変更などViteの各設定をカスタマイズするためのvite.config.jsをルートに作成します。

■プロジェクトディレクトリ
 ┣ vite.config.js (※新規作成)
 ┃
 ┣ package-lock.json
 ┣ package.json
 ┗ src

vite.config.js

import { defineConfig } from 'vite';

export default defineConfig({
  root: './src', //開発ディレクトリ設定
  build: {
    outDir: '../dist', //出力場所の指定
  },
});

Viteの初期設定ではnpm run buildするとファイルの出力結果が以下のような構成になります。

■dist
 ┣ index.html
 ┗ assets
    ┣ index.js
    ┗ index.css

この構成のまま納品することはまずないと思いますので、「assets/js/main.js」「assets/css/style.css」の構成になるようvite.config.jsのbuildプロパティに rollupOptions の設定を追加します。

vite.config.js

import { defineConfig } from 'vite';

export default defineConfig({
  root: './src', //開発ディレクトリ設定
  build: {
    outDir: '../dist', //出力場所の指定
    rollupOptions: { //ファイル出力設定
      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)) {
            extType = 'images';
          }
          //ビルド時のCSS名を明記してコントロールする
          if(extType === 'css') {
            return `assets/css/style.css`;
          }
          return `assets/${extType}/[name][extname]`;
        },
        chunkFileNames: 'assets/js/[name].js',
        entryFileNames: 'assets/js/[name].js',
      },
    },
  },
});

これで以下のような構成で出力されるようになります。

■dist
 ┣ index.html
 ┗ assets
    ┣ fonts (Webフォントが存在する場合)
    ┃ ┗ xxx.woff2
    ┣ js
    ┃ ┗ index.js
    ┗ css
      ┗ style.css

rollupOptionsの設定は以下を参考にさせていただきました。

rollupOptionsの詳細は以下の公式リファレンスをご参照ください。

Sass(SCSS)を使う

Sassモジュールのインストール

sassモジュールをインストールするとSassファイルを扱えるようになるので、以下のコマンドでインストールします。

npm install -D sass

Sassファイルの設定

Viteは特に設定を追記せずとも SCSSファイルを認識してくれるので、モジュールをインストールした後は既存のCSSをSCSSに置き換えるだけです。

  • src/のcssディレクトリをscssに変更
  • style.cssをstyle.scssに変更
■プロジェクトディレクトリ
 ┗ src
    ┣ index.html
    ┣ js
    ┃  ┗ main.js
    ┗ scss
       ┗ style.scss

cssファイルの読み込み方法

Viteの初期設定ではCSSは main.js 内で読み込んでいますが、
通常のコーディングではHTMLでCSSを読み込む方が一般的なので、HTMLで読み込むよう変更します。

main.js(未編集)

import './style.css'

〜省略〜

main.jsの中身を削除

CSSのimport設定やテストコードは全て不要なので削除してください。

main.js

//空にしてください。

index.htmlにlinkタグを追加

通常のコーディングと同じようにlinkタグで読み込ませます。
相対パスでSCSSのまま設定します。(buildすると自動的にCSSへ置き換わります。)
※lang属性はjaに変更し、初期に入っているfavicon設定は不要なので削除しています。

※main.jsも同じように相対パスを修正してください。

index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
    <!-- CSSの読み込みを追加 -->
    <link rel="stylesheet" href="./scss/style.scss">

  </head>
  <body>
    <div id="app"></div>
    <!-- 相対パスを修正 -->
    <script type="module" src="./js/main.js"></script>
  </body>
</html>

CSSビルド時にPostCSSによるオプション設定を追加する(任意)

ViteはPostCSSを扱えるので、ベンダープレフィックスを付与したり様々なオプションを追加することが可能です。

PostCSSのインストール

以下のコマンドでベースとなるPostCSSをインストールします。

npm install -D postcss

PostCSSの利用方法

PostCSSを利用するにはプロジェクトのルートディレクトリに「postcss.config.cjs」を作成し、プラグインを呼び出すコードを記述する必要があります。

■プロジェクトディレクトリ
 ┣ postcss.config.cjs (※新規作成)
 ┃
 ┣ package-lock.json
 ┣ package.json
 ┗ src

postcss.config.cjs

module.exports = {
  plugins: {
    //呼び出すプラグインを記述していく
  },
}

本記事のプラグインを一括でインストールする

本記事のプラグインを一括でインストールする場合は以下のコードを実行してください。
精査したい場合は以降を参照しそれぞれ個別にインストールしてください。

npm install -D postcss autoprefixer postcss-sort-media-queries css-declaration-sorter @fullhuman/postcss-purgecss postcss-normalize-charset

autoprefixerの追加

autoprefixerはベンダープレフィックスを自動付与してくれるPostCSSのプラグインです。
以下のコマンドでautoprefixerをインストールします。

npm install -D autoprefixer

browserslistの設定

package.jsonに対象となるブラウザ範囲「browserslist」を追記してください。

package.json

{
  〜省略〜
  "devDependencies": {
   〜省略〜
  },
  "browserslist": [
    "last 3 versions",
    "> 5%",
    "Firefox ESR",
    "not dead"
  ]
}

browserslistは環境に合わせて適宜変更してください。
設定の詳細は以下をご参照ください。

autoprefixerを有効化する

postcss.config.cjsに追記してください。

postcss.config.cjs

module.exports = {
  plugins: {
    autoprefixer: {},
  },
}

特別対応が必要でなければオプションなしで十分だと思います。

細かいオプション設定が必要な場合は環境に合わせて適宜追加してください。autoprefixerのオプション設定の詳細は以下をご参照ください。

postcss-sort-media-queries

メディアクエリをソートして1つにまとめてくれるPostCSSのプラグインです。

npm install -D postcss-sort-media-queries

postcss.config.cjs

module.exports = {
  plugins: {
    autoprefixer: {},
    'postcss-sort-media-queries': {},
  },
}

css-declaration-sorter

CSSプロパティの順番をソートするPostCSSのプラグインです。

npm install -D css-declaration-sorter

postcss.config.cjs

module.exports = {
  plugins: {
    autoprefixer: {},
    'postcss-sort-media-queries': {},
    'css-declaration-sorter':{order:'smacss'},
  },
}

orderの設定値は「alphabetical」「concentric-css」の3種類です。
詳しくは以下をご参照ください。

postcss-purgecss

CSSファイルから未使用のスタイルを削除するPostCSSのプラグインです。

案件によっては利用しない方が良いこともありますので、本当に必要かどうか必ず検討してください。

npm install -D @fullhuman/postcss-purgecss

postcss.config.cjs

module.exports = {
  plugins: {
    autoprefixer: {},
    'postcss-sort-media-queries': {},
    'css-declaration-sorter':{order:'smacss'},
    '@fullhuman/postcss-purgecss': {
      content: ['./src/**/*.html','./src/js/**/*.js'],
      //除外設定 https://purgecss.com/safelisting.html
      safelist: ['hoge']
    },
  }
}

postcss-normalize-charset

CSSファイルの先頭にcharset追加するPostCSSのプラグインです。

昔文字化けした経験があって明記しておきたい派なので個人的に導入していますが、最近はブラウザがよしなに判定してくれるようなので多くの場合は不要だと思います。

npm install -D postcss-normalize-charset

postcss.config.cjs

module.exports = {
  plugins: {
    'postcss-normalize-charset': {},
    autoprefixer: {},
    'postcss-sort-media-queries': {},
    'css-declaration-sorter':{order:'smacss'},
    '@fullhuman/postcss-purgecss': {
      content: ['./src/**/*.html','./src/js/**/*.js'],
      //除外設定 https://purgecss.com/safelisting.html
      safelist: ['hoge']
    },
  }
}

HTMLを複数出力したい時の設定

Viteの初期設定では「npm run build」するとHTMLファイルは1つにまとめられてしまうので、HTMLを複数出力したい時には、vite.config.jsに出力するページを追記していく必要があります。

出力したいHTMLファイル

今回はindex.htmlに加えてlist.htmlを出力する想定とします。

■プロジェクトディレクトリ
 ┗ src
    ┣ index.html
    ┗ list.html

複数出力の詳細は公式リファレンスのマルチページアプリの項をご参照ください。

vite.config.jsの設定

冒頭のimport設定と、先程追加したrollupOptionsの中にinputの設定を追加します。

vite.config.js

import { defineConfig } from 'vite';

//import設定を追記
import { resolve } from 'path';

export default defineConfig({
  root: './src', //開発ディレクトリ設定
  build: {
    outDir: '../dist', //出力場所の指定
    rollupOptions: { //ファイル出力設定
      output: {
        〜省略〜
      },
      input: {
        index: resolve(__dirname, './src/index.html'),
        /*
          複数HTMLページを出力したい時にここへ追記していく
          xxx: resolve(__dirname, './src/xxx.html'),
        */
        list: resolve(__dirname, './src/list.html'),
      },
    },
  },
});

HTMLの複数出力を自動化したい時の設定

複数ページの出力を自動化する方法です。
vite.config.jsに./src配下にあるhtmlファイル一式を取得し自動出力する記述を追記します。

vite.config.js

import { defineConfig } from 'vite';

//import設定を追記
import { resolve } from 'path';

// HTMLの複数出力を自動化する
//./src配下のファイル一式を取得
import fs from 'fs';
import path from 'path';

const files = [];
function readDirectory(dirPath) {
  const items = fs.readdirSync(dirPath);

  for (const item of items) {
    const itemPath = path.join(dirPath, item);

    if (fs.statSync(itemPath).isDirectory()) {
      // componentsディレクトリを除外する
      if (item === 'components') {
        continue;
      }

      readDirectory(itemPath);
    } else {
      // htmlファイル以外を除外する
      if (path.extname(itemPath) !== '.html') {
        continue;
      }

      // nameを決定する
      let name;
      if (dirPath === path.resolve(__dirname, 'src')) {
        name = path.parse(itemPath).name;
      } else {
        const relativePath = path.relative(path.resolve(__dirname, 'src'), dirPath);
        const dirName = relativePath.replace(/\//g, '_');
        name = `${dirName}_${path.parse(itemPath).name}`;
      }

      // pathを決定する
      const relativePath = path.relative(path.resolve(__dirname, 'src'), itemPath);
      const filePath = `/${relativePath}`;

      files.push({ name, path: filePath });
    }
  }
}
readDirectory(path.resolve(__dirname, 'src'));
const inputFiles = {};
for (let i = 0; i < files.length; i++) {
  const file = files[i];
  inputFiles[file.name] = resolve(__dirname, './src' + file.path );
}
/*
  この形を自動的に作る
  input:{
    index: resolve(__dirname, './src/index.html'),
    list: resolve(__dirname, './src/list.html')
  }
*/

export default defineConfig({
  root: './src', //開発ディレクトリ設定
  build: {
    outDir: '../dist', //出力場所の指定
    rollupOptions: { //ファイル出力設定
      output: {
        〜省略〜
      },
      //生成オブジェクトを渡す
      input: inputFiles,
    },
  },
});

HTMLファイルをejsのように扱う (ハンドルバー化する)

Viteでejsを利用する代わりにHTMLファイルをejsのように扱うことができる(ハンドルバー化する)プラグイン vite-plugin-handlebars を利用します。

vite-plugin-handlebarsのインストール

npm install -D vite-plugin-handlebars

コンポーネントを管理するディレクトリをsrc/に追加

各ページで利用するコンポーネントファイル用のディレクトリ「components」を src/ 内に作成し、テスト用のheader.htmlを用意します。

■プロジェクトディレクトリ
 ┗ src
    ┣ index.html
    ┗ components
       ┗ header.html

vite.config.jsの設定

vite-plugin-handlebars を読み込む設定を追記します。
プラグインを有効化するとHTML内でif文や変数の出力ができるようになるので、ページごとに設定したい情報をまとめたオブジェクトも作成します。
(vite.config.jsに書かずに直接テンプレート側で変数を定義する方法もすぐ下に記載しています。)

vite.config.js

import { defineConfig } from 'vite';

import { resolve } from 'path';

〜省略〜

//import設定を追記
import handlebars from 'vite-plugin-handlebars';

//HTML上で出し分けたい各ページごとの情報
const pageData = {
  '/index.html': {
    isHome: true,
    title: 'Main Page',
  },
  '/list.html': {
    isHome: false,
    title: 'List Page',
  },
};

export default defineConfig({
  root: './src', //開発ディレクトリ設定
  build: {
    outDir: '../dist', //出力場所の指定
    rollupOptions: { //ファイル出力設定
      〜省略〜
    },
  },
  /*
    プラグインの設定を追加
  */
  plugins: [
    handlebars({
      //コンポーネントの格納ディレクトリを指定
      partialDirectory: resolve(__dirname, './src/components'),
      //各ページ情報の読み込み
      context(pagePath) {
        return pageData[pagePath];
      },
    }),
  ],
});

変数の出力、コンポーネントの読み込み

index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <!-- pageDataのtitle情報を出力 -->
    <title>{{title}}</title>

    <!-- CSSの読み込みを追加 -->
    <link rel="stylesheet" href="./scss/style.scss">

  </head>
  <body>
    <!-- header.htmlの読み込み -->
    {{> header}}

    <div id="app"></div>
    <!-- 相対パスを修正 -->
    <script type="module" src="./js/main.js"></script>
  </body>
</html>

header.html

{{!if文でテキストの出し分け}}
<header>
ヘッダー
{{#if isHome}}
  トップページ
{{else}}
  リストページ
{{/if}}
</header>

テンプレート上で変数を渡す

vite.config.jsに記載せずともコンポーネントの呼び出し内に変数を追記していくと、テンプレート上で直接値を渡すことができます。

index.html

{{> header isHome=true hoge="文字情報を渡す"}}

その他のできること

このプラグインはハンドルバーというテンプレート言語を利用できるようにするものです。
上記機能以外の検証確認はしていませんが、繰り返し文など一通り機能は揃っていそうです。

もっとできることが知りたい方は以下の公式リファレンスをご参照ください。

js-beautifyでビルドしたHTMLを整形する(任意)

Viteのプラグインなどではなく「npm run build」後にnpm scriptsでビルドされたHTMLに対してHTMLを整形する設定です。
環境に合わせて適宜導入を検討してください。

この項はViteの機能とは独立しており導入必須ではありません。

有名な整形ツールとしてprettierがありますが、本記事ではjs-beautifyを用いてHTMLを整形します。
js-beautifyはprettierと違って、整形しないタグを設定(除外設定)できたり、不用意な改行が入るといったことがないため、個人的にHTML整形においてはprettierより使いやすと思っています。

HTML整形プラグインjs-beautifyをインストール

npm install -D js-beautify

.jsbeautifyrcのファイルを作成(js-beautifyの設定)

開発ディレクトリに.jsbeautifyrcを作成し、JSON形式でHTML整形のオプション設定を追加します。

.jsbeautifyrc

{
  "html": {
    "indent_size": 2,
    "unformatted": ["svg", "pre"]
  }
}

オプション設定は環境に合わせて適宜変更してください。
詳細は以下をご参照ください。

.jsbeautifyrcに追記する書き方は以下が参考になります。

package.jsonへコマンドを追加

package.jsonの「build」コマンドにjs-beautifyのコマンドを追記します。

package.json

{
  〜省略〜
  "scripts": {
    "build": "vite build && html-beautify dist/**/*.html",
    〜省略〜
  },
  〜省略〜
}

実行方法

「npm run build」を実行すると自動的に dist/ にあるHTMLファイルが整形されるようになります。

変換しないファイルを格納する public/ディレクトリの設定

「npm run build」時に変換対象にしたくないファイル(そのまま使いたいファイル)は、メインのHTMLファイルと同じ階層に public/ ディレクトリを作成しそこへ内包します。

■プロジェクトディレクトリ
 ┗ src
    ┣ index.html
    ┣ js
    ┃  ┗ main.js
    ┣ scss
    ┃  ┗ style.scss
    ┃
    ┗ public (※新規作成)
      ┗ assets
        ┣ js
        ┗ images

publicのファイルを参照する時はpublicを除いてパスを記述します。

<img src="./assets/images/hoge.jpg" alt="">
<script src="./assets/js/fuga.js"></script>

「npm run build」を実行すると変換ファイルと一緒に出力されます。

■dist
 ┣ index.html
 ┣ list.html
 ┗ assets
    ┣ js
    ┃ ┣ main.js
    ┃ ┗ fuga.js
    ┃
    ┣ images
    ┃ ┗ hoge.jpg
    ┃
    ┗ css
      ┗ style.css

public/ディレクトリ内のJSファイルを圧縮する(任意)

Viteに備わっているesbuildを利用して特定のページのみに追加したJSファイルを圧縮します。
prettierと同様に「npm run build」後にnpm scriptsでesbuildを実行する方法です。

この項のesbuildコマンドは対象のJSファイルが存在しない場合、コンソール上でエラーが出るので機能自体が不要な場合は追記しないでください。
(一応Viteのビルドコマンドとは独立してるので、エラーが出てもベースのビルド自体は問題なく動作します。)

esbuildの実行コマンドをpackage.jsonに追記

package.jsonの “scripts” に記載されている “build” コマンドにesbuildの実行コマンドを追加します。
&&でesbuild src/public/assets/js/*.js –bundle –minify –outdir=dist/assets/js/を繋ぎます。

package.json

{
  〜省略〜
  "scripts": {
    "dev": "vite",
    "build": "vite build && html-beautify dist/**/*.html && esbuild src/public/assets/js/*.js --bundle --minify --outdir=dist/assets/js/",
    "preview": "vite preview"
  },
  〜省略〜
}

実行方法

「npm run build」を実行すると自動的に src/public/assets/js/ のjsファイルが圧縮されるようになります。

モジュール main.jsの書き方

本記事ではモジュールJS(main.js)に対して特に設定を追加しません。
元々Vite自体にJSをバンドルする機能が備わっていますので、外部JSファイルをimportして扱っても、webpackを利用している時と同じようにビルド時には自動的にバンドルしてくれます。

main.js

//特に何か設定せずともビルド時にはimport内容を自動的にバンドルしてくれます。
import { hoge } from './hoge';
hoge();

モジュールJSはimportが使えるというだけで、importの記述を強制するものではありません。
importを使わない普通のコードのみでも動作します。

モジュール main.jsの扱いについて

この項はmain.jsからモジュール機能(type=”module”属性)を外したいと思った方へ向けた内容です。
結論だけ先に申し上げると、JSファイルにモジュール設定が残っていても特に問題ないので、気にならない方はこの項は飛ばしていただいて大丈夫です。

基本的に普通のコーディングしかしたことがないので「モジュール機能は利用しないし外したいな」と最初に引っかかって色々調べました。
もしかすると同じように思う方がいるかもしれないので詳細を記載しておきます。

main.jsのtype=”module”属性は削除できない(でも問題ない)

Viteはフロントエンドのビルドツールで、基本的にバンドルすることを主目的としているためか、main.jsからtype=”module”属性を外すことはできません。(ファイル名を変えることはできます。)

「モダン開発でもないしモジュール機能自体がそもそも必要ない」という場合でも、type=”module”属性を残しておかなければなりません。

使わない機能は外したくなるかもしれませんが、type=”module”属性はimportをブラウザ上で利用する時に追加する属性で、通常のscriptに+αするものですので、これ自体が何か阻害したり制限になったりもしません。

通常利用の時と同じように書いたものがそのまま動きますので、main.jsはいつもと同じように「全ページに共通して読み込まれる普通のJSファイル」という考え方で扱っても問題ないと思います。

src/index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    〜省略〜
  </head>
  <body>
    <div id="app"></div>

    <!-- type="module"が追加されているmain.jsのscriptタグ -->
    <script type="module" src="./js/main.js"></script>

    <!-- public内のJSを読み込む時はtype属性を省く普通の方法で書く -->
    <script src="./assets/js/hoge.js"></script>

  </body>
</html>

ビルド後のmain.jsの出力位置(変更できないが問題ない)

「npm run build」で出力したHTMLファイルを確認するとmain.jsはheadタグの中(CSSの直前)に出力されます。

dist/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    〜省略〜

    <!-- 必ずCSSの上に出力される -->
    <script type="module" crossorigin src="/assets/js/main.js"></script>
    <link rel="stylesheet" href="/assets/css/style.css">
  </head>
  <body>
    〜省略〜
  </body>
</html>

この仕様は普段JSファイルを</body>の直前で読み込ませている方は結構気持ち悪さを感じるかもしれません。
しかし、type=”module”属性が付いたscriptは、defer属性をつけたscriptと同じ仕様で読み込み・実行されるので、</body>の直前に書いた時と同じような処理になるため速度面でも特に問題ありません。

defer属性は、HTMLの読み込みを阻害せず、HTMLの解析が完了した後にソースコード上に記載された順番でJSコードを実行する機能です。

詳細は以下の「モジュールの通常のスクリプトのその他の違い」項などをご参照ください。

モジュール main.jsの名称について

「npm run build」で出力したモジュールJSの名称は各設定の影響を受けることがあります。

単一ページの時 (モジュールJSの名称を変える方法)

rollupOptionsにinput設定がない単一ページの時に「npm run build」で出力したJSファイルの名称は index.jsになります。

これをコントロールしたい場合は、「chunkFileNames」「entryFileNames」に設定したい名称を明記します。

vite.config.js (rollupOptions.input設定がない時:jsのファイル名を明記した例)

〜省略〜
export default defineConfig({
  root: './src', //開発ディレクトリ設定
  build: {
    outDir: '../dist', //出力場所の指定
    rollupOptions: { //ファイル出力設定
      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)) {
            extType = 'images';
          }
	  //ビルド時のCSS名を明記してコントロールする
          if(extType === 'css') {
            return `assets/css/style.css`;
          }
          return `assets/${extType}/[name][extname]`;
        },
	//単一ページの時は設定したいファイル名を明記するとコントロールできる
        chunkFileNames: 'assets/js/main.js',
        entryFileNames: 'assets/js/main.js',
      },
  },
〜省略〜
});

複数ページの時 (モジュールJSの名称に数字が入る時の回避策)

rollupOptionsにinput設定がある複数ページの場合は「npm run build」で出力すると、input設定のプロパティ名にモジュールJSと同じ名称(main)が存在する場合は main2.jsのように名称に数字が入ることがあります。

vite.config.js (inputのプロパティ名とモジュールjsの名称が一致している例)

〜省略〜
input: {
  //プロパティ名: resolve(__dirname, './src/index.html'),
  main: resolve(__dirname, './src/index.html'),
  /*
    ↑モジュールJSと同じ名称のプロパティ名が存在すると
    ビルド結果のファイル名に数字が入ってしまう
    dist/asetts/js/main2.js
  */
  list: resolve(__dirname, './src/list.html'),
},
〜省略〜

これはinput設定のプロパティ名を変更することで回避できます。
以下のように、モジュールのJSと異なる名称にすることで数字がつかなくなります。

vite.config.js

〜省略〜
input: {
  //プロパティ名: resolve(__dirname, './src/index.html'),
  index: resolve(__dirname, './src/index.html'),
  /*
    ↑モジュールJSと異なる名称にすると数字がつかなくなる
  */
  list: resolve(__dirname, './src/list.html'),
},
〜省略〜

相対パスでビルドしたい場合

通常「npm run build」するとルートパスで出力されますが vite.config.js の defineConfig に base 設定を追加するとビルド時のパスをコントロールできるようになります。

vite.config.js

〜省略〜
export default defineConfig({
  base: './', //相対パスでビルドする
  root: './src', //開発ディレクトリ設定
  build: {
    〜省略〜
  },
  〜省略〜
});

詳しくは以下をご参照ください。

CSS内の画像を相対パスで参照する

ビルド後のディレクトリ環境で一旦ルートに戻ってからファイルを参照するように書くと上手くいくようです。

ビルド後のファイルの位置関係

■dist
 ┗ assets
    ┣ images
    ┃ ┗ hoge.jpg
    ┃
    ┗ css
      ┗ style.css

style.scss (assets/images/hoge.jpgを参照する場合)

.piyo {
  background-image: url(../../assets/images/hoge.jpg);
}

Network設定を有効化してIPアドレスを発行する

vite.config.js

〜省略〜
export default defineConfig({
  server: {
    host: true //IPアドレスを有効化
  },
  base: './', //相対パスでビルドする
  root: './src', //開発ディレクトリ設定
  build: {
    〜省略〜
  },
  〜省略〜
});

package.json

{
  〜省略〜
  "scripts": {
    "dev": "vite --host",
    〜省略〜
  },
  〜省略〜

「npm run dev」するとIPアドレスが発行されるようになります。
詳しくは以下をご参照ください。

CSSとJSファイルに更新パラメータを自動で追加する

CSSとJSファイルに更新パラメータを自動で追加する方法です。
Viteに用意されたHTMLファイルを変換するための専用フック transformIndexHtml を用いて、ファイルの末尾に更新パラメータを追加します。

vite.config.js にパラメータを生成するhtmlPlugin関数を追加し、plugins内で読み込ませます。
更新パラメータは日時の合計値とすることで値が被らないようにしています。

vite.config.js

〜省略〜

//CSSとJSファイルに更新パラメータを追加
const htmlPlugin = () => {
  return {
    name: 'html-transform',
    transformIndexHtml(html) {
      // npm run build のときのみ動作させる
      if(process.env.NODE_ENV !== 'production') {
        return;
      }

      //更新パラメータ作成
      const date = new Date();
      const param = date.getFullYear() + date.getMonth() + date.getDate() + date.getHours() + date.getMinutes() + date.getSeconds();

      // CSSファイルにパラメータを追加(httpsから始まる外部リンクは除外)
      let setParamHtml = html.replace(/(?=.*<link)(?=.*css)(?!.*https).*$/gm, match => {
        return match.replace(/\.css/, '.css?' + param);
      });

      // JSファイルにパラメータを追加して変更内容を返す(httpsから始まる外部リンクは除外)
      return setParamHtml.replace(/(?=.*<script)(?=.*js)(?!.*https).*$/gm, match => {
        return match.replace(/\.js/, '.js?' + param);
      });
    }
  }
}

export default defineConfig({
  〜省略〜
  build: {
    〜省略〜
  },
  〜省略〜
  plugins: [
    handlebars({
      〜省略〜
    }),
    htmlPlugin()
  ],
});

※パラメータ更新以外にも独自ルールでhtmlの内容を書き換えることが可能です。
詳しくは以下をご参照ください。

完成した構成ファイルのまとめ

ここまでの内容を構築したファイル構成のまとめです。

ディレクトリ構成

■プロジェクトディレクトリ
 ┣ dist (ビルドしたファイルが出力される場所)
 ┣ package.json (プロジェクトのjsonファイル)
 ┣ postcss.config.cjs (PostCSSの設定ファイル)
 ┣ vite.config.js (viteの設定ファイル)
 ┣ .prettierrc (prettierの設定ファイル)
 ┃
 ┣ node_modules (編集不要:自動生成されるコアファイル格納場所)
 ┣ package-lock.json (編集不要:インストールしたパッケージ情報などが記載されている)
 ┃
 ┗ src
    ┣ index.html
    ┣ list.html (複数ページを追加する場合)
    ┃
    ┣ components (HTMLのコンポーネントパーツを格納)
    ┃  ┗ header.html
    ┃    xxx.html ...
    ┃
    ┣ js (メインのモジュールJSファイルを格納)
    ┃  ┗ main.js
    ┃
    ┣ public (Viteの変換対象外のディレクトリ。distに中身がそのままコピーされます。)
    ┃  ┗ assets (そのまま移動させたいファイルを必要に応じて格納していく)
    ┃     ┣ fonts
    ┃     ┃  ┗ xxx.woff2 ...
    ┃     ┣ js
    ┃     ┃  ┗ xxx.js ...
    ┃     ┗ images
    ┃       ┗ xxx.jpg ...
    ┃
    ┗ scss
       ┣ style.scss
       ┗ (各記法に合わせたディレクトリ構成)

package.json

browserslistは環境に合わせて書き換えてください。

devDependencies”の各バージョンは記事執筆時のものですので、package.jsonをコピペで利用する場合は更新がないか確認をしてください。

package.json

{
  "name": "プロジェクト名",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build && html-beautify dist/**/*.html && esbuild src/public/assets/js/*.js --bundle --minify --outdir=dist/assets/js/",
    "preview": "vite preview"
  },
  "devDependencies": {
    "@fullhuman/postcss-purgecss": "^5.0.0",
    "autoprefixer": "^10.4.13",
    "css-declaration-sorter": "^6.3.1",
    "js-beautify": "^1.14.7",
    "postcss": "^8.4.21",
    "postcss-normalize-charset": "^5.1.0",
    "postcss-sort-media-queries": "^4.3.0",
    "sass": "^1.57.1",
    "vite": "^4.0.0",
    "vite-plugin-handlebars": "^1.6.0"
  },
  "browserslist": [
    "last 3 versions",
    "> 5%",
    "Firefox ESR",
    "not dead"
  ]
}

vite.config.js

vite.config.js

import { defineConfig } from 'vite';

import { resolve } from 'path';

//handlebarsプラグインimport
import handlebars from 'vite-plugin-handlebars';

// HTMLの複数出力を自動化する
//./src配下のファイル一式を取得
import fs from 'fs';

import path from 'path';

const files = [];
function readDirectory(dirPath) {
  const items = fs.readdirSync(dirPath);

  for (const item of items) {
    const itemPath = path.join(dirPath, item);

    if (fs.statSync(itemPath).isDirectory()) {
      // componentsディレクトリを除外する
      if (item === 'components') {
        continue;
      }

      readDirectory(itemPath);
    } else {
      // htmlファイル以外を除外する
      if (path.extname(itemPath) !== '.html') {
        continue;
      }

      // nameを決定する
      let name;
      if (dirPath === path.resolve(__dirname, 'src')) {
        name = path.parse(itemPath).name;
      } else {
        const relativePath = path.relative(path.resolve(__dirname, 'src'), dirPath);
        const dirName = relativePath.replace(/\//g, '_');
        name = `${dirName}_${path.parse(itemPath).name}`;
      }

      // pathを決定する
      const relativePath = path.relative(path.resolve(__dirname, 'src'), itemPath);
      const filePath = `/${relativePath}`;

      files.push({ name, path: filePath });
    }
  }
}
readDirectory(path.resolve(__dirname, 'src'));
const inputFiles = {};
for (let i = 0; i < files.length; i++) {
  const file = files[i];
  inputFiles[file.name] = resolve(__dirname, './src' + file.path );
}
/*
  この形を自動的に作る
  input:{
    index: resolve(__dirname, './src/index.html'),
    list: resolve(__dirname, './src/list.html')
  }
*/

//HTML上で出し分けたい各ページごとの情報
const pageData = {
  '/index.html': {
    isHome: true,
    title: 'Main Page',
  },
  '/list.html': {
    isHome: false,
    title: 'List Page',
  },
};

//CSSとJSファイルに更新パラメータを追加
const htmlPlugin = () => {
  return {
    name: 'html-transform',
    transformIndexHtml(html) {
      // npm run build のときのみ動作させる
      if(process.env.NODE_ENV !== 'production') {
        return;
      }

      //更新パラメータ作成
      const date = new Date();
      const param = date.getFullYear() + date.getMonth() + date.getDate() + date.getHours() + date.getMinutes() + date.getSeconds();

      // CSSファイルにパラメータを追加(httpsから始まる外部リンクは除外)
      let setParamHtml = html.replace(/(?=.*<link)(?=.*css)(?!.*https).*$/gm, match => {
        return match.replace(/\.css/, '.css?' + param);
      });

      // JSファイルにパラメータを追加して変更内容を返す(httpsから始まる外部リンクは除外)
      return setParamHtml.replace(/(?=.*<script)(?=.*js)(?!.*https).*$/gm, match => {
        return match.replace(/\.js/, '.js?' + param);
      });
    }
  }
}

export default defineConfig({
  server: {
    host: true //IPアドレスを有効化
  },
  base: './', //相対パスでビルドする
  root: './src', //開発ディレクトリ設定
  build: {
    outDir: '../dist', //出力場所の指定
    rollupOptions: { //ファイル出力設定
      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)) {
            extType = 'images';
          }
          //ビルド時のCSS名を明記してコントロールする
          if(extType === 'css') {
            return `assets/css/style.css`;
          }
          return `assets/${extType}/[name][extname]`;
        },
        chunkFileNames: 'assets/js/[name].js',
        entryFileNames: 'assets/js/[name].js',
      },
      input:
      //生成オブジェクトを渡す
      input: inputFiles,
    },
  },
  /*
    プラグインの設定を追加
  */
  plugins: [
    handlebars({
      //コンポーネントの格納ディレクトリを指定
      partialDirectory: resolve(__dirname, './src/components'),
      //各ページ情報の読み込み
      context(pagePath) {
        return pageData[pagePath];
      },
    }),
    htmlPlugin()
  ],
});

postcss.config.cjs

postcss.config.cjs

module.exports = {
  plugins: {
    autoprefixer: {},
    'postcss-sort-media-queries': {},
    'css-declaration-sorter':{order:'smacss'},
    '@fullhuman/postcss-purgecss': {
      content: ['./src/**/*.html','./src/js/**/*.js'],
      //除外設定 https://purgecss.com/safelisting.html
      safelist: ['hoge']
    },
  }
}

.jsbeautifyrc

.jsbeautifyrc

{
  "html": {
    "indent_size": 2,
    "unformatted": ["svg", "pre"]
  }
}

開発を開始する

以下のコマンドを実行すると開発サーバが起動するのでコーディングを開始できます。

npm run dev

エラーが出た場合はコンソール上に詳細が書いてあるので、しっかり確認すると殆どの原因はすぐに特定できると思います。
よくありそうなエラーは、パスの記述ミス、vite.config.jsやpostcss.config.cjsファイルなどの構文エラー、存在しないファイルを参照しているなどが考えられます。

CSSでfont-familyに日本語を設定する場合の注意点

Viteに内蔵された機能でCSSを圧縮すると日本語文字列をUnicodeエスケープシーケンスに変換(例:\65e5\672c\8a9e)しますが、変換されるとWindows環境などでfont-family設定が効かなくなる場合があり、これを回避するには別途設定が必要です。
詳細は以下の記事にまとめましたので、必要に応じて追加で設定をしてください。

Tailwind CSSを導入する

ViteでTailwind CSSを導入する方法を以下の記事にまとめました。
Tailwind CSSの最適化(リセットCSSの削除、変数の削減、独自拡張)や、最適化した独自styleを混在させることができるのか検証しています。

関連リンク