【コーダー向け】ViteでHTML(ejsライク)・Sass・JSな普通のコーディング環境を作ってみた

2022/07/04に公開約38,400字

VueやReactは使えないけどViteの爆速環境はとても魅力的!
ということでモダン開発ではないWebサイトコーディングでもなんとか恩恵にあやかれないかと思い調べてみた備忘録です。

コーダー向けの環境構築は殆ど情報が出まわっていないようなので、同じように思っている方の一助になれば良いなと思い今回の構築例を記事にまとめることにしました。


結果だけ知りたい方へ

先に本記事で構築する手順とファイル構成だけをまとめました。
全体の構築内容と結果だけ知りたい方はこの項のみご参照ください。

色々読んでも良いよという方は「経緯など」からお読みください。

・初期設定

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

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 npm run build が実行できるようになります。

・各モジュールやプラグインのインストール

━ Sass

npm install -D sass

━ PostCSS + プラグイン (不要なものは適宜省いてください。)

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

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

npm install -D vite-plugin-handlebars

━ prettierのインストール

npm install -D prettier

・ディレクトリ構成

開発ファイルを src/で管理する構成です。

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

・package.json

prettierとsrc/public/assets/js/内のJSファイルを圧縮するコマンド、browserslistを追記しています。

package.json
{
  "name": "プロジェクト名",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite --host",
    "build": "vite build --emptyOutDir && prettier --write dist/**/*.html && esbuild src/public/assets/js/*.js --bundle --minify --outdir=dist/assets/js/",
    "preview": "vite preview"
  },
  "devDependencies": {
    "@fullhuman/postcss-purgecss": "^4.1.3",
    "autoprefixer": "^10.4.8",
    "css-declaration-sorter": "^6.3.0",
    "postcss": "^8.4.14",
    "postcss-normalize-charset": "^5.1.0",
    "postcss-sort-media-queries": "^4.2.1",
    "prettier": "^2.7.1",
    "sass": "^1.54.1",
    "vite": "^3.0.0",
    "vite-plugin-handlebars": "^1.6.0"
  },
  "browserslist": [
    "last 3 versions",
    "> 5%",
    "iOS >= 9.0",
    "Android >= 5",
    "Firefox ESR"
  ]
}

・vite.config.js

handlebars用のページ情報(pageData)はサンプルですので不要なら省いてください。

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';
const fileNameList = fs.readdirSync(resolve(__dirname, './src/'));

//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 );
  /*
    この形を自動的に作る
    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',
  },
};

export default defineConfig({
  server: {
    host: true //IPアドレスを有効化
  },
  root: './src', //開発ディレクトリ設定
  build: {
    base: './', //相対パスでビルドする
    outDir: '../dist', //出力場所の指定
    rollupOptions: { //ファイル出力設定
      output: {
        assetFileNames: (assetInfo) => {
          let extType = assetInfo.name.split('.')[1];
          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: inputFiles,
    },
  },
  /*
    プラグインの設定を追加
  */
  plugins: [
    handlebars({
      //コンポーネントの格納ディレクトリを指定
      partialDirectory: resolve(__dirname, './src/components'),
      //各ページ情報の読み込み
      context(pagePath) {
        return pageData[pagePath];
      },
    }),
  ],
});

・postcss.config.cjs

npm install時に省いたものは削除してください。

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']
    },
  }
}

・.prettierrc

prettierの設定ファイルです。各自の環境に合わせて設定してください。

.prettierrc
printWidth: 200
singleQuote: true
tabWidth: 2
useTabs: false

・main.jsの中身を削除

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

main.js
//空にしてください。

・index.htmlにlinkタグを追加・パスの修正

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

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>

・開発を開始する

npm run dev

・開発したプロジェクトをビルドする

npm run build

・結論

  • 実装したい機能が足りない場合は必要に応じて各自で肉付けしてください。

  • ある程度ルール化された環境で動作するが機微にどこまで対応できるのかは未知数です。

  • SCSSを分離して書き出すことができませんでした。 できました。詳しくはこちらをご参照ください。

  • HTML、CSS、JSで1セットなので、特定のファイルのみビルドしたい場合は別のツールを利用するしかなさそうです。

  • めちゃ速いので使える時は積極的に利用していきたいです!


経緯など

普段のコーディング環境(ejs、Sass、JS)では主にタスクランナーのGulpを使っているコーダーです。

Gulpは速度がそれなりに安定していて目的のファイルを不足なく書き出せるので長らく利用していますが、書き方の問題なのか変更検知してくれないことも結構多く、そういう部分で日々ストレスを抱えています。

最近あまりにもいけずが過ぎてきたので先日npm scriptsを試してみましたが、コード量の多いSassファイルの書き出し速度に不満があり実用化は見送りました。

webpackも途中参加の案件などで他の方が構築した環境で少々体験していますが、こちらもファイル量が多いと速度にやや問題ありのイメージを持っており、風のうわさで爆速と聞いたViteを試してみることにしました。

Viteについて

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

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

https://ja.vitejs.dev/guide/

Viteは「バンドルする」ということがコンセプトなツールのようですので、そもそも種々のファイルを分割して扱っている通常のWebサイトコーディングでの利用は想定していない印象を受けました。

https://ja.vitejs.dev/guide/why.html
今後アップデートが進んでいくと本記事の構築内容が使えなくなる日も来るかもしれません。

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

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

この記事のViteでしない(できない)こと

・画像の圧縮はしない

プロジェクトで利用する固定画像はなるべく圧縮率を良くしたいのでタスクランナーで一括設定はしない派です。

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

https://github.com/vbenjs/vite-plugin-imagemin

※個人的にSVGはImageOptim、その他の画像圧縮にはよく以下の外部サービスを利用しています。

https://compressor.io/

・【できた】SCSSを分離して書き出すことはできない

理想はこれもやりたいのですが、今の時点では良い方法が見つからずです。

本記事の環境では、style.scssとは別にxxx.scssを準備しても、ビルド結果はstyle.cssに全て統合され1つのファイルになってしまいます。

━ 追記:2022.07.05

Twitterで YEND@フロントエンドエンジニア さんより本件のコメントをいただきました。

Sassなんですが、別々のhtmlで別々の Sass読み込んだ時は分割されないですかね?
例えば index.htmlでindex.scssを読んで、list.htmlでlist.scssを読んだ時みたいなパターンです。
逆に分割する必要がない時はvite側で自動的にまとめられるっぽいですね。

まさにこの通りで、検証した時はCSSファイルを複製しファイル名のみ変更して確認していたので、Vite側で分割不要と判断されていたようです。
CSSの中身が異なれば分割されましたのでこの問題は解決しました。
(YENDさんありがとうございました!🙇🏻)

ベース環境の構築

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

https://ics.media/entry/210708/

・プロジェクト作成

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

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

Windowsでの移動

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

Macでの移動

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

━ Viteをインストール

以下のコマンドを実行すると、設定項目が立ち上がるので順番に入力・選択していきます。

npm init vite@latest

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

? Project name: › vite-project

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

今回利用するのは素のJavaScriptなのでvanillaを選択します。

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

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

これも今回利用するのは素のJavaScriptなのでvanillaを選択します。

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

━ 初期インストールと動作チェック

ここまで実行するとターミナル上に以下のようなメッセージが表示され、入力したプロジェクト名のディレクトリが自動生成されていると思いますので順番に実行していきます。

Done. Now run:

  cd vite-project(入力したプロジェクト名)
  npm install
  npm run dev

①作成したプロジェクトディレクトリへ移動

cd vite-project(入力したプロジェクト名)

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

npm install

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

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

npm run dev

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

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

npm run build

開発ディレクトリの整理(src/管理に変更)

Viteを構築すると初期の開発ディレクトリの構成は全てルートで管理する最小構造になっています。

このままでは少々扱いにくいので、srcディレクトリに開発用ファイルをまとめる構成に変更していきます。
※faviconとcounter.js (vite3.0以降に自動生成されるファイル)は不要なので削除してください。

■プロジェクトディレクトリ
 ┣ package-lock.json
 ┣ package.json
 ┗ src
    ┣ index.html
    ┣ js
    ┃  ┗ main.js
    ┗ css
       ┗ style.css

・Viteの設定をカスタマイズする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];
          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',
      },
    },
  },
});

これで以下のような構成で出力されるようになります。
JSの名称コントロールについてはモジュール main.jsの名称についてをご参照ください。

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

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

https://stackoverflow.com/questions/71180561/vite-change-ouput-directory-of-assets

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

https://ja.vitejs.dev/config/build-options.html#build-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'

document.querySelector('#app').innerHTML = `
  <h1>Hello Vite!</h1>
  <a href="https://vitejs.dev/guide/features.html" target="_blank">Documentation</a>
`

━ main.jsの中身を削除

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

main.js
//空にしてください。

━ index.htmlにlinkタグを追加

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

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

・autoprefixerの追加

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

npm install -D autoprefixer

━ browserslistの設定

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

package.json
{
  〜省略〜
  "devDependencies": {
   〜省略〜
  },
  "browserslist": [
    "last 3 versions",
    "> 5%",
    "iOS >= 9.0",
    "Android >= 5",
    "Firefox ESR"
  ]
}

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

https://github.com/browserslist/browserslist#queries

━ autoprefixerのオプション設定

プロジェクトのルートディレクトリにpostcss.config.cjsを作成し設定を記述します。

■プロジェクトディレクトリ
 ┣ postcss.config.cjs (※新規作成)
 ┃
 ┣ vite.config.js
 ┣ package-lock.json
 ┣ package.json
 ┗ src
postcss.config.cjs
module.exports = {
  plugins: {
    autoprefixer: {},
  },
}

特別対応が必要でなければオプションなしで十分だと思います。
細かいオプション設定が必要な場合は環境に合わせて適宜追加してください。

autoprefixerのオプション設定の詳細は以下をご参照ください。

https://github.com/postcss/autoprefixer#options

・その他のオプションを追加

個人的に利用しているものを列挙します。
残りのオプションは好みの領域でもありますので、それぞれの環境に合わせて導入を検討してください。

━ メディアクエリをソートして1つにまとめる postcss-sort-media-queries

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

インストール・オプション設定

npm install -D postcss-sort-media-queries
postcss.config.cjs
module.exports = {
  plugins: {
    autoprefixer: {},
    'postcss-sort-media-queries': {},
  },
}

https://www.npmjs.com/package/postcss-sort-media-queries

━ CSSプロパティの順番をソートする 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 smacss concentric-css の3種類です。
詳しくは以下をご参照ください。

https://www.npmjs.com/package/css-declaration-sorter

━ CSSファイルから未使用のスタイルを削除する 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']
    },
  }
}

https://purgecss.com/plugins/postcss.html

━ CSSファイルの先頭にcharset追加する 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']
    },
  }
}

https://www.npmjs.com/package/postcss-normalize-charset

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

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

・出力したいHTMLファイル

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

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

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

https://ja.vitejs.dev/guide/build.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の設定を変更する

vite.config.js
import { defineConfig } from 'vite';

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

// HTMLの複数出力を自動化する
//./src配下のファイル一式を取得
import fs from 'fs';
const fileNameList = fs.readdirSync(resolve(__dirname, './src/'));

//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 );
  /*
    この形を自動的に作る
    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 があったのでこれを利用します。

https://github.com/alexlafroscia/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
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
<header>
ヘッダー
{{#if isHome}}
  トップページ
{{else}}
  リストページ
{{/if}}
</header>

・その他のできること

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

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

https://handlebarsjs.com/guide/

HTMLファイルを整形する prettierの導入

Viteのプラグインなどではなくnpm run build後にnpm scriptsでビルドされたHTMLに対してprettierを実行する方法です。

・prettierのインストール

以下のコマンドでprettierをインストールします。

npm install -D prettier

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

package.jsonの "scripts" に記載されている "build" コマンドにprettierの実行コマンドを追加します。
&&prettier --write dist/**/*.htmlを繋ぎます。

package.json
{
  〜省略〜
  "scripts": {
    "dev": "vite",
    "build": "vite build && prettier --write dist/**/*.html",
    "preview": "vite preview"
  },
  〜省略〜
}

・prettierのオプション設定

プロジェクトのルートディレクトリに.prettierrcを作成し設定を記述します。

■プロジェクトディレクトリ
 ┣ .prettierrc (※新規作成)
 ┃
 ┣ postcss.config.cjs
 ┣ vite.config.js
 ┣ package-lock.json
 ┣ package.json
 ┗ src
.prettierrc
printWidth: 200
singleQuote: true
tabWidth: 2
useTabs: false

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

https://prettier.io/docs/en/options.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を除いてパスを記述します。

index.html
<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の実行コマンドを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 && prettier --write 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();

モジュール main.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>の直前に書いた時と同じような処理になるため速度面でも特に問題ありません。

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

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

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

rollupOptionsinput設定がない単一ページの時に 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];
          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の名称に数字が入る時の回避策)

rollupOptionsinput設定がある複数ページの場合は 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 の defineConfigbase 設定を追加するとビルド時のパスをコントロールできるようになります。

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

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

https://ja.vitejs.dev/config/shared-options.html#base

・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アドレスを発行する

同じwifiに接続した外部端末などからアクセスできるIPアドレスを発行する方法です。
vite.config.js に server.host 設定を追加し、package.json のdevコマンドに --host オプションを追加します。

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

npm run dev するとIPアドレスが発行されるようになります。

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

https://ja.vitejs.dev/config/server-options.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 (そのまま移動させたいファイルを必要に応じて格納していく)
    ┃     ┣ js
    ┃     ┃  ┗ xxx.js ...
    ┃     ┗ images
    ┃       ┗ xxx.jpg ...
    ┃
    ┗ scss
       ┣ style.scss
       ┗ (各記法に合わせたディレクトリ構成)

・package.json

package.json
{
  "name": "プロジェクト名",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite --host",
    "build": "vite build --emptyOutDir && prettier --write dist/**/*.html && esbuild src/public/assets/js/*.js --bundle --minify --outdir=dist/assets/js/",
    "preview": "vite preview"
  },
  "devDependencies": {
    "@fullhuman/postcss-purgecss": "^4.1.3",
    "autoprefixer": "^10.4.8",
    "css-declaration-sorter": "^6.3.0",
    "postcss": "^8.4.14",
    "postcss-normalize-charset": "^5.1.0",
    "postcss-sort-media-queries": "^4.2.1",
    "prettier": "^2.7.1",
    "sass": "^1.54.1",
    "vite": "^3.0.0",
    "vite-plugin-handlebars": "^1.6.0"
  },
  "browserslist": [
    "last 3 versions",
    "> 5%",
    "iOS >= 9.0",
    "Android >= 5",
    "Firefox ESR"
  ]
}

・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';
const fileNameList = fs.readdirSync(resolve(__dirname, './src/'));

//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 );
  /*
    この形を自動的に作る
    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',
  },
};

export default defineConfig({
  server: {
    host: true //IPアドレスを有効化
  },
  base: './', //相対パスでビルドする
  root: './src', //開発ディレクトリ設定
  build: {
    outDir: '../dist', //出力場所の指定
    rollupOptions: { //ファイル出力設定
      output: {
        assetFileNames: (assetInfo) => {
          let extType = assetInfo.name.split('.')[1];
          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];
      },
    }),
  ],
});

・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']
    },
  }
}

・.prettierrc

.prettierrc
printWidth: 200
singleQuote: true
tabWidth: 2
useTabs: false

開発を開始する

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

npm run dev

開発したプロジェクトをビルドする

以下のコマンドを実行するとdist/ディレクトリに公開用のファイル一式が書き出されます。

npm run build

最後に

自分が欲しいなと思った範囲の機能で構築を目指したので、本記事の内容では全然要件を満たしていない方もいらっしゃるかもしれません。足りない場合は各自で肉付けしていっていただければと思います。
(Viteのプラグインに目的の機能がなければ、prettierなどのようにnpm scriptsで実装する方法を探すと見つかるかもしれません。)

とりあえずある程度ルール化された環境で動作するものは作ることができましたが、まだ殆ど運用できていないので機微にどこまで対応できるのかは未知数です。

SCSSを分離して書き出すことができなかったので今後の課題にしたいと思います。
(時々必要になるのでこの機能は是非実現したいです。)
SCSSの分割もできました。詳しくはこちらをご参照ください。

また、ViteはHTML、css、JSで1セットなので、特定のファイルのみビルドしたい場合は別のツールを利用するしかなさそうです。

とりあえず軽く使った感じではめちゃくちゃ爆速だったので、これから使える時は積極的に利用していきたいと思います!

Discussion

ログインするとコメントできます