- はじめに
- Viteとは
- 結局、何のために使うの?
- 実際にViteをつかってみる
- Vite+TypescriptでTodoList
- 従来のビルドツール(Webpackなど)の違い
- 開発ビルドと本番ビルドは何が違う??
- 本番ビルドの必要性
- 軽くプロジェクトフォルダの構成
- おわりに
- 参考
はじめに
こんにちは!さいとーです。
皆さんはフロントエンドの開発で開発用ビルドと本番用ビルドを意識して開発していますか??
開発用ビルドと本番用ビルドはビルドツールによって違う方法でビルドされています。
自分は恥ずかしながら全く意識していなかったです。
というのもビルドツールが便利すぎて、不便を感じなかったのでこれまで調べることがなかったのです。
↑言い訳ではないです、、、、
とにかく今回ビルドツールの歴史や軽くやってることを調べてみました。
もう、ビルドツール(Vite)に足を向けて寝られない、、、、
Viteとは
Vite(フランス語で「素早い」という意味の単語で /vit/ ヴィートのように発音)は、現代の Web プロジェクトのために、より速く無駄のない開発体験を提供することを目的としたビルドツールです。2 つの主要な部分で構成されています: ・非常に高速な Hot Module Replacement (HMR) など、ネイティブ ES モジュールを利用した豊富な機能拡張を提供する開発サーバー。 ・Rollup でコードをバンドルするビルドコマンド。プロダクション用に高度に最適化された静的アセットを出力するように事前に設定されています。
https://ja.vite.dev/guide/#概要 より
- Hot Module Replacement(HMR)とは
- ローカルサーバーにおいてjavascriptの変換処理と連携させて、ファイル変更時に変更されたモジュールだけを差し替えるのでブラウザの表示結果にすぐ反映できる。 都度変換する手間やブラウザを手動で再読み込みする作業が省けるため、確認作業の効率化につながります。確認までの待ち時間が長いと、間違いがあっても修正に時間がかかり、開発のスムーズさが損なわれます。
- バンドルとは
- 複数のモジュールに分割して開発したり、npmからインストールしたライブラリをモジュールとして利用した場合に依存関係を整理しつつ統合する処理(1つのファイルにする)
結局、何のために使うの?
ずばり、モダンなフロントエンド開発のために作られたビルドツールで、開発サーバーの起動や更新がとても速く、ストレスのない開発体験が得られます。
開発のときに気をつける点は以下のものがあります。
- ローカルサーバーが起動するか
- コード変更して即座にブラウザに反映されるか
- ブラウザで解釈できるように、HTMLやjavascriptへの変換ができるか
- TypescriptやReact、Vueなどはブラウザで解釈できずjavascriptに変換してあげる必要あり
- ビルド(バンドル+トランスパイル(変換)+圧縮+最適化)できるか
→これを解決してくれるのがViteになります
従来のビルドツールWebpackに比べて開発サーバー起動や変更反映が速い、設定がシンプルなどのメリットがあります。
実際にViteをつかってみる
最初の Vite プロジェクトを生成する
今回はnpmでプロジェクトを作成します。
他に関してはこちらをご覧ください
https://ja.vite.dev/guide/#最初の-vite-プロジェクトを生成する
- 注意
Vite は [**Node.js**](https://nodejs.org/en/) 18+ または 20+ のバージョンが必要です。
このコマンドを打つ
npm create vite@latest
このような感じででてくるのでy
を押す。
create-vite@6.3.1
のあとの数字はviteの最新バージョンの数字なので実際には異なる可能性あり。
プロジェクトの名前が決められます。
そのままエンターを押すと、vite-project
になります。
今回は勉強のためにプロジェクトを作成するのでvite-study-project
にしてみたいと思います。
次は使用するフレームワークを選べます。
今回はVanilla
であるtypescript
を選択します。
この表示がでたらviteのプロジェクトが作成されています。
ここに表示されているコマンドを3つを順番にやっていきます。
実際にフォルダを見てみると必要そうなフォルダやファイルができていることを確認できます。
cd vite-study-project
→npm install
→npm run dev
の順番でコマンドをうつ。
ここでnpm install
時にnode_modules
という新しいファイルが作成されます。
package_json
というファイルが設定ファイルになっていて、package.json
に書かれた依存ライブラリやモジュールを全部node_modules
にインストールします。devDependencies
にはtypescript
とvite
が書いてありtypescript
とvite
に必要なライブラリやモジュールがnode_modules
に入るようになります。
実際のnode_modules
の中身
なんだかたくさん入っています
次にnpm run dev
と打つと
http://localhost:5173/
にアクセスすると、、このように開発サーバーが起動します。
Vite+TypescriptでTodoList
実際にvite+Typescriptで作成してみた。
まず、index.html
の中身を下記に変更
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <title>Simple ToDo</title> <style> body { font-family: sans-serif; margin: 2em; } ul { list-style: none; padding: 0; } li { margin: 0.5em 0; display: flex; align-items: center; } button { margin-left: 1em; } </style> </head> <body> <h1>ToDoリスト</h1> <form id="todo-form"> <input type="text" id="todo-input" placeholder="やることを入力" required /> <button type="submit">追加</button> </form> <ul id="todo-list"></ul> <script type="module" src="/src/main.ts"></script> </body> </html>
次にvite-study-project/src/main.ts
を下記に変更
const form = document.getElementById("todo-form") as HTMLFormElement; const input = document.getElementById("todo-input") as HTMLInputElement; const list = document.getElementById("todo-list") as HTMLUListElement; form.addEventListener("submit", (e) => { e.preventDefault(); const text = input.value.trim(); if (text === "") return; const li = document.createElement("li"); li.textContent = text; const btn = document.createElement("button"); btn.textContent = "削除"; btn.addEventListener("click", () => list.removeChild(li)); console.log('検証用'); li.appendChild(btn); list.appendChild(li); input.value = ""; });
ちゃんとHMRできてます!
従来のビルドツール(Webpackなど)の違い
Viteは従来のビルドツールに比べて、開発サーバーの起動が高速で、変更時の反映も速いという点があります。これはバンドルせずにネイティブESMを活用することで、必要なモジュールだけをオンデマンドで読み込むため、特に開発中の体験が軽快になります。
なぜこれは実現できているのか。
「開発サーバー」の仕組みの違いによるものです。
こちらに詳しく書いてあります。
https://ja.vite.dev/guide/why.html#遅いサーバー起動
用語解説
entry→スタート地点のJavaScriptファイル(main.tsなど)、typescriptのファイルでも開発サーバー上で自動的に .js
に変換して配信。
ブラウザが最初に読み込むファイルで、そこから他のモジュールが読み込まれます
route→ページ単位のコンポーネントファイル
module→JavaScript の個別ファイルで関数やコンポーネントなどの部品単位で構成されている
server ready→開発サーバーが起動完了し、リクエストを受け付けられる状態になったこと。
従来のビルドツール「開発サーバー」の仕組み(バンドルベース)
この図のようにバンドルベースはserver ready
状態になるまで全ファイルの依存関係を解析してバンドルする必要があり、サーバー起動(server ready
)するのに時間がかかります。
これはコードを変更するたびに行われるため、非常に時間を取られることになります。
vite「開発サーバー」の仕組み(ネイティブ ESM ベース)
一方こちらは事前バンドルなしでserver ready
になります。ファイル(コード)を変更するたびにそのモジュールだけを動的に再読み込みして即配信することができます。再バンドルが不要で、開発中の反映速度が非常に速くなります。
まとめると
従来型が変更を検知 → 再バンドル → 再配信(バンドル済みの巨大な1ファイルまたは数ファイル)
ネイティブESMベースが変更を検知→変更されたファイルだけ即配信
なぜネイティブESMベースを使っていなかったか
従来のビルドツール(Webpack)が使われていた頃、ブラウザがECMAScript Modules(ESM)に対応してなかったから。(import
/ export
構文が使えない)
import
/ export
が使えないためWebpackでブラウザで読み込めるように複数のモジュールを1つのファイルにバンドルする必要があった。
しかしESMがブラウザが対応したことによって下記のような構文をブラウザが理解できることにより、モジュールの依存関係を自分で解決し、必要なファイルを1つずつHTTPリクエストできるようになりました。これによりバンドルなしで即時に必要なモジュールだけ配信・実行できるようになりました。
import { add } from './math.js';
開発ビルドと本番ビルドは何が違う??
開発ビルド(run dev)
- 基盤はESM
- バンドルはせず、モジュール単位で配信。配信形式はHTTP経由でファイルを逐次変換して返す
- 変更はHMRで変更箇所のみ即反映
- 変換後のJavaScriptコードと、元のソースコード(例:TypeScriptやSCSSなど)との対応関係を記録したファイル(ソースマップ)があり圧縮されたJSコードではなく、元のソースでデバッグできる。
元のtypescriptコードがブラウザで表示される
本番ビルド(run build)
- 基盤はバンドラー
- バンドルして最小化、圧縮された静的ファイルで配信
- ファイル変更反映は再ビルドが必要
- 圧縮されているため、可読性が低い
バンドルされたjsコードしかブラウザで表示されない
本番ビルドの必要性
1.読み込みを最適化
→開発中はモジュールが100個あっても個別に読み込むのに対して
本番ビルドは1つまたは少数にまとめてバンドルすることでリクエスト数を減らしてページの読み込みを高速化できる
2.未使用部分の削除
→使用されていない関数を削除する(Tree-shaking)ことによってファイルサイズが小さくなって読み込み・実行が速くなる
試してみる↓
先程のtodolistのコードに新しくutil.ts
ファイルを作成して下記のコードを追加して本番ビルドしてみる
export function notUsed() { console.log("これは使われていません"); } export function alertUsed() { console.log("これは使われています"); }
alertUsed
とnotUsed
をインポートしてalertUsed
だけmain.ts
で実行する。
const btn = document.createElement("button"); btn.textContent = "削除"; btn.addEventListener("click", () => list.removeChild(li)); alertUsed();
本番ビルドしたコードを見てみる
alertUsed
だけコードに含まれていてnotUsed
は省かれています。これはTree-shakingされていることがわかります。
ちなみに開発ビルドだとnotUsed
はTree-shaking
されていません
3.不要なコードの削除
→スペースや改行、変数名(例:let title
→ let a
)などを削減・短縮(minify)してファイルサイズを軽量化することができる。
先程の本番ビルドしてバンドルしたコードを見てみると空欄がなくなっていたり、変数名が変更されていることがわかります。
ビルド前のコード
function alertUsed() { console.log("これは使われています"); }
ビルド後のコード
function d(){console.log("これは使われています")}
また、本番ビルド画像はvsコードの改行設定を使っているので改行されていますが、本当は1行で書かれています。
4.キャッシュ対策のためにファイル名をハッシュ付きにする
→ブラウザは、同じファイル名のリソースをキャッシュ(保存)して、次回アクセス時には再リクエストせずにキャッシュから読み込むことがある。中身を更新しても古いファイルを見続けてしまうのでファイル更新してファイル名にハッシュを付ける。コードを変更しないとハッシュは更新されない。
そうすることでブラウザは別の新しいファイルとして扱う。
1回目のビルドしたファイル名
2回目
5.設定することで元のコードを隠したりコメント除外してビルドできる
設定でソースマップを無効にすることで圧縮されたままのJSしか見れず、ユーザーに不要な情報を見せずに済む。(デフォルトでは無効になっている。)
ビルドの設定ファイル(vite.config.js
)を作成してソースマップをビルドする設定(sourcemap: true
)すると、mapファイルができてブラウザでも元のコードがユーザーからも見れてしまいます
import { defineConfig } from 'vite'; export default defineConfig({ build: { minify: 'terser', sourcemap: true, terserOptions: { compress: { drop_console: true, } } } });
バンドルされたファイルの下にmapファイル(ソースマップ)ができることで本番ビルドでも元のコードが確認できるようになります。(基本的に無効が好ましい)
また、開発途中でいれたconsole.log(’開発用検証’)
が入ったまま本番ビルドしてまうとユーザーからも不要なconsole.log
が見えてしまったり、debugger
が入っていると本番環境で処理が止まってしまうので本番ビルド時は自動で削除できたら便利です。
ちなみに先程のコードの設定でdebugger
という文字はデフォルトで削除してくれるみたいです。
drop_console: true
をいれると削除できる。
何も設定しないとバンドルされたファイルにはconsole.log(’開発用検証’)
が入っちゃってます。
設定してビルド後、console.log
を省くことができます。
軽くプロジェクトフォルダの構成
dist
本番ビルド最終成果物を格納するディレクトリです。
src
ビルド前のソースコードが格納されるディレクトリであり、ここにあるファイルのimport
されたものが基本的にはバンドルされる。最終的にdist/assets/
に格納される
public
この配下のファイルはdistディレクトリにそのままコピーされて格納されます。バンドルや圧縮の対象にはなりません。ロゴ画像(logo.png
)やWebフォント(.woff
、.woff2
、 .ttf
)、svg
などはここにいれます。
おわりに
これまで開発途中でいきなりブラウザで元のコードが見えなくなってデバッグできなくて焦っていたりしたこともありますが、理由がわかってよかったです。あと、ビルドの設定も他にもいろいろあるみたいなので調べていきたいです。コードの書き方だけではなくこのように開発を助けてくれているものを勉強をすると結果的に得られること(今回だったらブラウザのことなど)が多いのでこれからもやっていきたいと思います。
iimonではコードの書き方はもちろん、その他いろいろなことを学べる環境が整っています。興味を持って頂いた方はぜひカジュアル面談でお話しできればと思います!