CSSのLiveReloadする！開発環境でのCSSのビルドをViteに任せてバックエンドとViteの開発サーバーを連携させる

この記事は「[はてなエンジニア Advent Calendar 2022](https://qiita.com/advent-calendar/2022/hatena)」の10日目の記事です。9日目は[@dekokun](https://qiita.com/dekokun)さんの「[みんなで会議をよくしていこう！と思ってやってるTips](https://dekotech.dekokun.info/entry/2022/12/09/143600)」でした。

----

# はじめに

Webページはさまざまなファイルから成り立っており、それぞれのファイルはソースファイルをビルドすることで作られています。従って、ソースファイルの更新をブラウザから確認するためには、単純に考えればソースファイルをビルドしてファイルを配置してからWebページをブラウザから更新する必要があるように思えます。しかし、開発中これらの作業を都度するのは大変なので、開発サーバーを立ち上げてビルドの工程を自動化します。すなわち、ソースファイルの更新に応じてブラウザのリロードなしにWebページの表示にその変更を反映させるようにします。この開発体験を`Live Reload`と呼びます。

今回はフロントエンドビルドツールの`Vite`に備わる開発サーバー立ち上げ機能を使って`CSS`のライブリロードを実現します。また`Vite`を既存の開発サーバーと連携させる方法をまとめます。


# Viteについて


[Vite](https://ja.vitejs.dev)は便利なビルドツールで、デフォルトの設定でも`HTML`, `JavaScript`, `CSS`からなる開発サーバーを立ち上げることができます。

https://vitejs.dev

Viteの触り心地を試すために軽く`HTML`, `TypeScript`, `Sass`から成り立つ開発環境を立ててみましょう。

## Viteの触り心地を試す編

今回はトップページに`TypeScript`, `Sass`を読み込ませてみます。

### インストール

必要なライブラリを[Yarn](https://yarnpkg.com)経由でインストールします。`Vite`のデフォルト設定でも`JavaScript`や`CSS`のバンドルを自動でやってくれますが、`TypeScript`や`Sass`などを使うためにはそれぞれ`devDependencies`に追加する必要があります。

```terminal:terminal
yarn add -D vite sass typescript water.css
```

:::note

[water.css](https://watercss.kognise.dev)はNo-classなCSSフレームワークで、読み込むだけでシュッとしたスタイルが適用されます。これは見た目を整えるために入れています。<font color="SlateGray">なんたって見た目をカッコ良くするとやる気が出ます！</font>従ってインストールは完全なオプションです。
**参考:** [No-Class CSS フレームワークをいろいろ比較するサイトを作った | blog.ojisan.io](https://blog.ojisan.io/no-class-css-fw/)

:::

なお、執筆時の`package.json`のライブラリのバージョンは以下の通りです。

<details><summary>package.json</summary>

```package.json
{
  "name": "vite",
  "devDependencies": {
    "sass": "^1.56.2",
    "typescript": "^4.9.4",
    "vite": "^3.2.5",
    "water.css": "^2.1.1"
  }
}
```

</details>

今回、`src/`以下ソースコードを書いていきましょう。そのために、`Vite`の設定ファイルたる`vite.config.js`を`package.json`と同じディレクトリに以下の内容で追加します。なお、`Vite`はデフォルトで全ての`.html`ファイルをビルドの対象にします。

```vite.config.js
module.exports = {
  root: 'src'
}
```

今回、`TypeScript`の設定の`tsconfig.json`にはこだわりがないので次のコマンドで雛形を用意してもらいます。なお、`tsc`は`typescript`を`devDependencies`に追加していれば使えます。


```terminal:terminal
yarn tsc --init
```

ただし、`Vite`を使う上で、`tsconfig.json`の`compilerOptions`の設定にはいくつか制限があり、`"isolatedModules": true`にする必要があります。

[Features#Type Script | Vite](https://vitejs.dev/guide/features.html#typescript)


</details>

### ソースコードを書いてみる

簡単に`index.html`を用意します

```src/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="scss/hoge.scss">
    <script type="module" src="button.ts"></script>
</head>
<body>

<content>
    <h1>テスト</h1>
    <p class="hoge">hogehoge</p>
    <p class="piyo">piyopiyo</p>
    <button id="hello">Hello</button>
</content>

</body>
</html>
```

注目したいポイントは次の2行で、`TypeScript`ソースを使いたいところは`.ts`のまま`Sass`ならば`.scss`のまま、`index.html`からの相対パスで記載している点です。
```html
<link rel="stylesheet" href="scss/hoge.scss">
<script type="module" src="button.ts"></script>
```

`Vite`の開発サーバーで`index.html`を読み込んでいるならば、`TypeScript`や`Sass`のファイルは`Vite`によって管理されているので、ファイルの変更に応じて自動的に表示も更新されます。開発サーバーを立ち上げる前に`hoge.scss`の内容と`button.ts`の内容も作ってしまいましょう。

```src/scss/hoge.scss
@import "water.css";
@import "fuga/_piyo.scss";

.hoge {
  color: tomato;
}
```

```src/scss/fuga/_piyo.scss
.piyo {
  color: blue;
}
```


```src/button.ts
const element = document.getElementById("hello");
if (element) {
    element.addEventListener("click", (e) => {
        alert(`hello: ${element.innerText}`)
    });
}
```


ここまでのディレクトリ構成は以下のようになっています。

<details><summary>ディレクトリ構成</summary>

```txt
.
├── .DS_Store
├── .tool-versions
├── index.html
├── package.json
├── src
│   ├── button.ts
│   ├── index.html
│   └── scss
│       ├── fuga
│       │   └── _piyo.scss
│       └── hoge.scss
├── tsconfig.json
├── vite.config.js
└── yarn.lock


```

</details>


### 開発サーバーを立ち上げる

次のコマンドで`Vite`は`vite.config.js`の内容をもとに開発サーバーを立ち上げます。

```terminal:terminal
yarn vite
```

出力は次のようになり、この時点で`http://localhost:5173/`にブラウザからアクセスすることで`index.html`を見ることができます。

```terminal:terminal
❯❯❯ yarn vite
yarn run v1.22.19
warning package.json: No license field
$ ********************

  VITE v4.0.0  ready in 254 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help
```

![スクリーンショット 2022-12-10 20.02.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/9bf3a49c-5528-527a-308b-b98b9d3d04ea.png)

開発サーバーを立ち上げたままソースファイルを編集してみましょう。ブラウザを閉じることなく、変更が表示に反映されることが確認できるはずです。

### ビルド

開発サーバーでは`.ts`ファイルや`.scss`を読み込んでいますが、開発サーバーを経由しない本番用のファイルを作るためには次のコマンドを使ってビルドします。

```
yarn vite build
```

<details><summary>ディレクトリ構成</summary>

```
├── src
│   ├── button.ts
│   ├── dist
│   │   ├── assets
│   │   │   ├── index-5a956950.css
│   │   │   └── index-7f113ec2.js
│   │   └── index.html
│   ├── hoge.scss
│   └── index.html
```

</details>

これによって本番用の`index.html`と`css`, `js`が`src/dist/`以下に作成されます。このとき、`css`と`js`はミニファイされて、配信に最適な形になっています。

:::note

ビルド結果を`src`配下にしたくない場合は、`vite.config.js`の`build≫outDir`を設定することで変更できます。例えば、今回は次のような設定にすると`package.json`と同じディレクトリに出力することができます。

```vite.config.js
module.exports = {
  root: 'src',
  build: {
    outDir: '../dist'
  }
}
```

<details><summary>ビルド結果のディレクトリ構成</summary>

```
.
├── .tool-versions
├── dist
│   ├── assets
│   │   ├── index-3c0c8df7.css
│   │   └── index-7f113ec2.js
│   └── index.html
├── index.html
├── package.json
├── src
│   ├── button.ts
│   ├── index.html
│   └── scss
│       ├── fuga
│       │   └── _piyo.scss
│       └── hoge.scss
├── tsconfig.json
├── vite.config.js
└── yarn.lock
```

</details>

:::


----

# Viteとバックエンドを連携する

## 基本的なアイデア

このように、全てを`Vite`に任せるのであれば簡単に開発サーバーを利用できます。

今回想定する開発環境では`Vite`ではない開発サーバーがあって、その開発サーバーから`Vite`の開発サーバーを呼び出し、`CSS`の`Live Reload`を実現してみます。


そのために、`Vite`の開発サーバーのクライアントを使います。

[Backend Integration | Vite](https://vitejs.dev/guide/backend-integration.html)

## 開発サーバーのクライアントの使い方

これまで作ってきたものに加えて、開発サーバーのクライアントの挙動を試すために、`Vite`で管理しない`html`ファイルを作ります。

```index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="module" src="http://localhost:5173/@vite/client"></script>
    <link rel="stylesheet" href="http://localhost:5173/scss/hoge.scss">
</head>
<body>
<content>
    <h1>テスト</h1>
    <p class="hoge">hogehoge</p>
    <p class="piyo">piyopiyo</p>
</content>
</body>
</html>

```

重要な行は次の行で、`script`タグで`Vite`の開発サーバーのクライアントを読み込み、使いたい`scss`ファイルを`http://localhost:5173/(Viteの設定ファイルのrootからの相対パス)`で呼び出す点です。


```html
    <script type="module" src="http://localhost:5173/@vite/client"></script>
    <link rel="stylesheet" href="http://localhost:5173/scss/hoge.scss">
```

そして`Vite`の開発サーバーを立ち上げます。

```terminal:terminal
yarn vite
```

今作った`html`ファイルをブラウザにドラッグ&ドロップして開いてみると、開発サーバーが立ち上がっている限り、ブラウザーで`Live Reload`が機能していることが確認できます。

![スクリーンショット 2022-12-10 20.29.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/5f1176f1-9531-8166-8c2d-31677f2de7dd.png)

:::note warn

今回は`html`ファイルを直に開いて確認していますが、`Vite`の開発サーバーのクライアントを使いたい開発サーバーを`https`スキームで立ち上げている時、`Vite`の開発サーバーもまた`https`な`localhost`で立ち上げる必要があります。`Vite`の開発サーバは、サーバーオプションに有効な証明書を渡してやることで、`https`で立ち上げられます。次の記事が参考になりました。

**参考:** 
* [Server Options | Vite](https://vitejs.dev/config/server-options.html)
* [viteでhttpsなlocalhostを起動する（devcert編） |  DevelopersIO](https://dev.classmethod.jp/articles/vite-https-localhost-devcert/)


:::


## ビルド

:::note alert

次のやり方はVite 4では使えません。ビルドはできるものの、出力されるディレクトリが変わります。

:::

今回`Sass`から`CSS`へのビルドは次のようにしてみます。

- `src/scss/`以下の`.scss`を全てビルドする
- ただし、`_`から始まるファイルはビルドしない
  - なお`_`をインポートして使うことはできる
- 最終的に`dist/css`にディレクトリ構造とファイル名を保ったま出力する
  - ただし、`.css`に拡張子を変える

先に完成形をお見せすると、`vite.config.js`を次のようにします。

```vite.config.js
import glob from 'fast-glob';

module.exports = {
  root: 'src',
  build: {
	outDir: '../dist',
	rollupOptions: {
	  /**
	   * scssのビルド
	   * - scss/以下の.scssをビルドする
	   * - ただし，_から始まるファイルはビルドしない
	   */
	  input: glob.sync(['src/scss/**/*.scss', '!**/_*']),
	  output: {
		assetFileNames: ({name}) => `${name?.replace('scss/', 'css/')}`,
	  }
	}
  }
}
```

個別に解説します。

### `src/scss/`以下の`.scss`を全てビルドする

`.scss`をビルドの対象にするには、全ての`.scss`ファイルを列挙し、それらをビルドの対象にすることで実現できます。

`glob`のように複数ファイルを列挙するには`npm`パッケージの`fast-glob`が便利です。

https://github.com/mrmlnc/fast-glob

```
yarn add -D fast-glob
```

`Vite`は内部で`rollup.js`を使っているので、ビルドの対象を変更するには、`vite.config.js`で`rollup.js`の設定を渡せる`build»rollupOptions`を設定します。

https://rollupjs.org/guide/en/


```vite.config.js
import glob from 'fast-glob';

module.exports = {
  root: 'src',
  build: {
	outDir: '../dist',
	rollupOptions: {
	  /**
	   * scssのビルド
	   * - scss/以下の.scssをビルドする
	   */
	  input: glob.sync(['src/scss/**/*.scss']),
	}
  }
}
```

ここまでの設定でビルドをすると出力ディレクトリは以下のようになります。

```terminal
yarn vite build
```

```
.
├── dist
│   └── assets
│       ├── _piyo.167b97ea.css
│       └── hoge.3c0c8df7.css
├── index.html
├── package.json
├── src
│   ├── button.ts
│   └── scss
│       ├── fuga
│       │   └── _piyo.scss
│       └── hoge.scss
├── tsconfig.json
├── vite.config.js
└── yarn.lock

```

ここで注目したいのは、デフォルトの設定では出力ファイルは`assets`配下になり、かつ、ハッシュ値がつけられており、元のファイルとの対応を取る必要がありそうだという点です。

この解決策は`Vite`のドキュメントに書かれています。具体的には、出力ファイルと元ファイルの対応表である`manifest.json`を`manifest: true`オプションを設定することで作成するこ、`manifest.json`から対応を引いてくる方法が書かれています。

[Backend Integration | Vite](https://vitejs.dev/guide/backend-integration.html)

今回は、`dist`ディレクトリにディレクトリ構造とファイル名を保ったま出力してみましょう。

### `dist/css`にディレクトリ構造とファイル名を保ったま出力する

これを実現するには、出力先オプションを設定すればオッケーです！

```vite.config.js
import glob from 'fast-glob';

module.exports = {
  root: 'src',
  build: {
	outDir: '../dist',
	rollupOptions: {
	  /**
	   * scssのビルド
	   * - scss/以下の.scssをビルドする
	   * - ただし，_から始まるファイルはビルドしない
	   */
	  input: glob.sync(['src/scss/**/*.scss']),
	  output: {
		assetFileNames: ({name}) => `${name?.replace('scss/', 'css/')}`,
	  }
	}
  }
}
```

これでビルド結果は次のようになります。

```
├── dist
│   └── css
│       ├── fuga
│       │   └── _piyo.css
│       └── hoge.css
```

`assetFileNames`は`css`の出力先を変更できるオプションで引数で渡ってくるオブジェクトの中に元のファイル名である`name`フィールドがあるので、`name`をそのまま返してやればハッシュ値がつきません。また、この時にディレクトリを変えておくと`scss`ディレクトリに`.css`ファイルが吐き出されるというねじれた状態から、`css`ディレクトリに`.css`ファイルを吐き出すといったことが実現できます。


:::note

ところで、`asset`とはなんでしょう？この記事では`asset`の説明は省いています。ここまで見てきたように、`Vite`は内部で`rollup.js`を使っており、`asset`の説明をするためには`rollup.js`の解説も必要になってきます。この記事を読むにあたっては`rollup.js`には`chunk`,`entry`,`asset`というグループがあり、`css`は`asset`に含まれるもので、`css`の出力はデフォルトでは`assets/`に出力されれ、`css`のファイル名を変更するには`assetFileNames`というオプションを使えば良さそうということを覚えておけば大丈夫です。

:::


:::note alert
Vite 4系ではこのオプションを設定していても次のように出力されます。ハッシュは外せますが、ディレクトリは入っていないといった状態。
```
├── dist
│   ├── _piyo.css
│   └── hoge.css
```
:::


### `_`から始まるファイルはビルドしない

これは簡単に実現できて、`glob`で`_`始まりを弾いてやればよく、ここまですると冒頭で示した完成系と一致します。

```vite.config.js
import glob from 'fast-glob';

module.exports = {
  root: 'src',
  build: {
	outDir: '../dist',
	rollupOptions: {
	  /**
	   * scssのビルド
	   * - scss/以下の.scssをビルドする
	   * - ただし，_から始まるファイルはビルドしない
	   */
	  input: glob.sync(['src/scss/**/*.scss', '!**/_*']),
	  output: {
		assetFileNames: ({name}) => `${name?.replace('scss/', 'css/')}`,
	  }
	}
  }
}
```

### 本番では

本番環境では`Vite`のクライアントを呼び出さないようにして、`css`ファイルを読み込むようにしたら完成です！お疲れ様でした！

```index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="dist/css/hoge.css">
</head>
<body>
<content>
    <h1>テスト</h1>
    <p class="hoge">hogehoge</p>
    <p class="piyo">piyopiyo</p>
</content>
</body>
</html>
```

# 執筆後記

アドベントカレンダーのネタを温めておいて、いざ書こうとしたら`Vite 4`が出たんですよ（執筆の昨日）！そしたら破壊的変更の影響受けてしまっておおわらわしている。

https://vitejs.dev/blog/announcing-vite4.html