はじめに
前回は、Step 1 の単位換算ツールについて、要件定義書と設計書を紹介しました。今回からは、実際に作っていく過程に入ります。
Step 1 で初めて使ったのが、Vite(ヴィート)という開発ツールです。名前は聞いたことがあっても、「結局何をしてくれるものなのか」が、最初はまったくつかめませんでした。
この回では、Vite を組み込みの言葉に置き換えながら、次の3つを、手順と実行結果つきで紹介します。
- Vite の2つの顔:開発サーバーとビルド
- 私がいちばんつまずいた base の設定:住所の「足し算」と「引き算」
- 開発中と本番で、ブラウザに画面が届くまでの流れがどう違うか
Vite とは:コンパイラ・リンカと、デバッガをひとまとめにした道具
なぜ必要なのか
ブラウザが直接実行できるのは JavaScript だけで、TypeScript は読めません。C のソースをそのまま CPU に渡しても動かず、機械語にコンパイルする必要があるのと同じです。
さらに、プログラムを複数のファイルに分けて書いた場合(今回なら main.ts と conv.ts)、ブラウザにはネットワーク越しに1つずつ取りに来てもらうことになります。1つにまとめておいたほうが効率よく届けられます。これはリンクに相当します。
この「変換」と「まとめる」を引き受けてくれるのが Vite です。
2つの顔
Vite には、使う場面の違う2つの役割があります。
| 役割 | コマンド | 組み込みでいうと | 使う場面 |
|---|---|---|---|
| 開発サーバー | npm run dev | ICE をつないだまま開発し、保存のたびに書き込みとリセットが自動で走る状態 | 画面を作っている間 |
| ビルド | npm run build | make でコンパイル&リンクして、ROM イメージを作る | 公開する前 |
Step 0 で使った tsc との違い
Step 0 では、サーバー用のプログラムを tsc(TypeScript のコンパイラ)でビルドしました。Vite も同じ TypeScript を扱いますが、役割が違います。
| tsc | Vite | |
|---|---|---|
| 対象 | サーバー(Node.js)で動くコード | ブラウザで動くコード |
| やること | TypeScript を JavaScript に変換するだけ | 変換、まとめる、住所の調整、開発サーバー |
| 出力 | dist/server.js | dist/public/ |
Node.js は手元のファイルを自分で読み込めるので、変換だけで足ります。一方、ブラウザはネットワーク越しにファイルを取りに来るので、まとめる処理や住所の調整が必要になります。ターゲットごとにツールチェーンを使い分けるのと同じ感覚です。
開発サーバーを動かす
まずは、「画面に文字を出すだけ」の最小構成で、Vite の開発サーバーを動かしてみました。作業は、自宅サーバーに VS Code の Remote-SSH でつないで行っています。
WSL で試す場合:この回の Node.js・Vite のコマンドは、Windows の WSL(Ubuntu など)でもそのまま使えます。違うのは、手元のブラウザで見る方法だけです。WSL2 では、WSL の中で起動した開発サーバーに、Windows のブラウザから
http://localhost:5173/apps/unitconv/で直接つながるので、VS Code のポート転送は必要ありません。
① Vite と TypeScript を入れる
ツールのフォルダで、次を実行します。〔Code Block Pro の言語:Shell〕
$ npm init -y # package.json を作る
$ npm pkg set type=module # 新しい import 形式を使うと宣言する
$ npm install -D vite typescript # 開発用の部品として入れる
$ npm pkg set scripts.dev="vite" # npm run dev で Vite を起動できるようにする行頭の $ は、一般ユーザーで実行するコマンドであることを示す記号(プロンプト)です。実際に入力するのは $ の後ろからです。
| 用語 | 意味 | 組み込みでいうと |
|---|---|---|
| npm | 部品(ライブラリ)の取得と、ビルド手順の実行を担う道具 | パッケージマネージャ+make |
| package.json | 使う部品の一覧と、実行コマンドの定義 | Makefile と部品表 |
-D | 開発中だけ使う部品という指定。完成品には含まれない | ホスト側のツールチェーン |
| node_modules/ | 部品の実体が入るフォルダ。いつでも再取得できるので Git には入れない | SDK のライブラリフォルダ |
② 3つのファイルを作る
vite.config.ts(Vite の設定)
import { defineConfig } from 'vite';
export default defineConfig({
base: '/apps/unitconv/', // 置かれる住所(後で詳しく説明)
build: {
outDir: 'dist/public', // ビルドの出力先
emptyOutDir: true, // ビルド前に古い出力を消す
},
server: {
host: '127.0.0.1', // 開発サーバーは外に出さない
port: 5173,
strictPort: true, // 5173 が使用中なら、別の番号にせずエラーにする
},
});index.html(画面の入口)
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>検査値の単位換算</title>
</head>
<body>
<h1>検査値の単位換算</h1>
<p id="status">読み込み中…</p>
<script type="module" src="/src/main.ts"></script>
</body>
</html>src/main.ts(動作確認用の最小プログラム)
const el = document.getElementById('status');
if (el) {
el.textContent = 'main.ts が動きました';
}main.ts は、id="status" の部品を探して、文字を書き換えるだけのプログラムです。if (el) は、部品が見つからなかった場合の保護で、C のヌルポインタチェックと同じです。
③ 起動して、手元のブラウザで見る
$ npm run dev次のように表示されれば起動成功です。このコマンドは止めるまで動き続けます。組み込みでいえば、メインループに入ったままの状態です。
VITE v7.x.x ready in 300 ms
➜ Local: http://127.0.0.1:5173/apps/unitconv/開発サーバーは、サーバー自身(127.0.0.1)からしかつながりません。そこで、VS Code の「ポート」タブで 5173 番を転送し、手元の PC のブラウザで http://localhost:5173/apps/unitconv/ を開きます。SSH の接続の中を通して、サーバーの 5173 番を手元の PC につなぐ仕組みで、外部には一切公開されません。デバッガのケーブルを1本つなぐようなものです。

「読み込み中…」が「main.ts が動きました」に変われば、TypeScript が JavaScript に変換され、ブラウザで実行されたことになります。
④ 保存すると、勝手に画面が変わる
開発サーバーを動かしたまま、main.ts の文字を書き換えて保存すると、ブラウザを操作しなくても、すぐに表示が変わります。これは HMR(Hot Module Replacement、動作中のプログラムの差し替え)という機能です。
組み込みでいえば、「ソースを直す → ビルド → 書き込み → リセット」の一連の作業が、保存1回で終わる状態です。
base:住所の「足し算」と「引き算」
vite.config.ts に書いた base: '/apps/unitconv/' は、Step 1 で私がいちばん理解に時間がかかった設定です。
住所は2種類ある
この話には、2種類の住所が出てきます。
| 住所 | 例 | 使う人 |
|---|---|---|
| 外から見た住所(URL のパス) | /apps/unitconv/index.html | ブラウザ、Apache |
| サーバーの中のファイルの場所 | dist/public/index.html | Node.js |
Apache はパスを書き換えずに中継するので、Node.js には /apps/unitconv/… という住所がそのまま届きます。この2つを正しく対応づけるのが、今回の話です。
受け取る側は「引き算」する
Node.js のサーバーは、届いた住所から先頭の /apps/unitconv を取り除いて、ファイルを選びます。
| 届いた住所 | 引き算した後 | 返すファイル |
|---|---|---|
/apps/unitconv/ | /(index.html とみなす) | dist/public/index.html |
/apps/unitconv/assets/main.js | /assets/main.js | dist/public/assets/main.js |
組み込みでいえば、ペリフェラルのアドレスデコードです。CPU から見た番地が 0x4000_1004 で、周辺回路のベースが 0x4000_1000 なら、周辺回路の中ではオフセット 0x004 だけを見てレジスタを選びますよね。それと同じです。
書き出す側は「足し算」する
問題は、画面(index.html)の中に書かれた「この JS も読み込んでね」という住所です。ブラウザは HTML を受け取ると、そこに書かれた住所へ、改めてファイルを取りに行きます。
base を設定しないと、Vite はこの住所を /assets/main.js と書き出します。すると、ブラウザは /assets/main.js を取りに行きますが、/apps/unitconv で始まらないので Apache は Node.js へ中継せず、結果は 404(見つからない)になります。画面の枠は出るのに、計算が動かない、という状態です。
base: '/apps/unitconv/' を設定すると、Vite はビルドのときに住所の先頭へ base を足して、/apps/unitconv/assets/main.js と書き出します。これならブラウザの要求が Apache から Node.js へ正しく届きます。
組み込みでいえば、リンカスクリプトで配置アドレスを指定するのと同じです。0x0000_0000 に置くつもりでリンクしたイメージを 0x0800_0000 に書き込むと、関数呼び出しが間違った番地に飛んで暴走します。実際に置かれる番地をリンカに教えておけば、正しい番地で呼び出せます。
| 担当 | やること | 理由 |
|---|---|---|
| Node.js(受け取る側) | 届いた住所から base を引く | ファイルの場所を探すため |
| Vite(書き出す側) | 画面の中の住所に base を足す | ブラウザが正しい窓口に取りに行けるようにするため |
1つのベースアドレスを、受け取る側では引き算し、書き出す側では足し算している、という関係です。

開発サーバーで、base の外を叩いてみる
開発サーバーで base の外の住所を開くとどうなるかを、curl で確かめました。-I は応答の見出し(ヘッダ)だけを表示する指定です。
$ curl -sI http://127.0.0.1:5173/結果は次のとおりです。
HTTP/1.1 302 Found
Location: /apps/unitconv/302 は「探しているものは別の場所にある」という意味のステータスコードで、Location: がその移動先です。ブラウザはこれを受け取ると、自動で /apps/unitconv/ を要求し直します。curl は、そのままでは言われた先へ行きませんが、-L を付けるとブラウザと同じように移動先をたどります。
$ curl -sIL http://127.0.0.1:5173/今度は、302 の応答に続いて、移動先の 200 OK の応答が表示されます。ブラウザの裏側では、この2往復が起きています。
ここで注意したいのは、「転送」という言葉に2つの意味があることです。私も最初は混同していました。
| 言葉 | 誰が動くか | 組み込みでいうと |
|---|---|---|
| 中継(Apache の ProxyPass) | サーバーが要求を代わりに先へ渡す。ブラウザは気づかない | 中継器、ブリッジ |
| リダイレクト(302 や 301) | サーバーは「あっちへ行って」と返すだけ。ブラウザが自分で要求し直す | デバイスが「このアドレスに再送して」と返し、ホストが再送する |
なお、この 302 は開発サーバーの親切機能です。本番では、/ への要求は Apache が別のページとして処理し、Node.js には届きません。
ビルドして中身を見る
画面とプログラムができたら、公開用にビルドします。ビルドのコマンドを登録して実行し、出力されたファイルを観察しました。
$ npm pkg set scripts.build:client="vite build"
$ npm run build:client
$ ls -R dist
$ grep -o 'src="[^"]*"' dist/public/index.htmlbuild:client という名前にしたのは、後でサーバー側のビルド(build:server)も追加し、両方をまとめた build を作るためです。
結果は次のとおりでした(英数字の部分は、ビルドのたびに変わります)。
dist:
public
dist/public:
assets index.html
dist/public/assets:
index-CDQylnpc.js
src="/apps/unitconv/assets/index-CDQylnpc.js"ここから読み取れることが3つあります。
1. base の「足し算」が実際に起きている
元の index.html では、src="/src/main.ts" と書いていました。ビルド後は /apps/unitconv/assets/… に書き換わっています。前の節で説明した「Vite がビルドのときに住所を足す」のは、まさにこの部分です。
2. main.ts と conv.ts が1つの .js にまとまっている
2つの TypeScript ファイルが変換され、1つの JavaScript ファイルにまとめられています。中を見ると、空白や改行が削られて詰め込まれていますが、換算の係数や画面のメッセージの文字列が見つかります。組み込みでいえば、複数の .c をコンパイル・リンクして、1つの ROM イメージにした状態です。
3. ファイル名に英数字が付いている
index-CDQylnpc.js の英数字は、ファイルの中身から計算した値(ハッシュ)です。中身が1文字でも変わると、名前も変わります。
これが大事なのは、途中にある Cloudflare が、JS のようなファイルを手元に保存(キャッシュ)して使い回すからです。名前が同じままだと、プログラムを直しても、古い JS が配られ続けるおそれがあります。中身が変わると名前も変わるので、ブラウザや Cloudflare は必ず新しいファイルを取りに行きます。
組み込みでいえば、ファームのファイル名に CRC を含めて、版の取り違えを防ぐような仕組みです。
この仕組みを生かすために、配信するサーバー側では、応答に付ける保存の指示(Cache-Control)を次のように使い分けました。
| ファイル | 保存の指示 | 意味 |
|---|---|---|
| index.html | no-cache | 毎回、最新かどうかをサーバーに確かめる |
| index-xxxx.js | public, max-age=31536000, immutable | 1年間そのまま使ってよい(中身が変われば名前が変わるので) |
index.html さえ毎回最新になれば、その中に書かれた JS の名前も最新になるので、古い JS が使われることはありません。

開発中と本番の違い
同じ画面が表示されても、開発中と本番では、裏側の流れがかなり違います。ブラウザが画面を要求してから表示されるまでを、順に並べてみました。

図の青い矢印がブラウザからの要求、緑の矢印がサーバーからの応答、灰色の矢印がサーバーによるファイルの読み込みです。それぞれの手順を、文章でも追ってみます。
開発中(Vite の開発サーバー、5173 番)
- ブラウザが
/apps/unitconv/を要求する - Vite が index.html を読み、script の住所をその場で書き換え、自動更新の仕組みも差し込んで返す
- ブラウザが HTML の中の script を見つけ、
…/src/main.tsを要求する - Vite が main.ts を読み、要求されたその瞬間に JavaScript へ変換して返す
- ブラウザが main を実行し始めると、
importに気づいて…/src/conv.tsを追加で要求する - Vite が conv.ts もその場で変換して返す
- 部品がそろい、画面が完成する
本番(自作の Node.js サーバー、5002 番)
- (事前)
npm run buildで、Vite が main.ts と conv.ts を1つの JS にまとめておく - ブラウザが
/apps/unitconv/を要求する - Node.js が base を引き算し、ビルド済みの index.html をそのまま返す
- ブラウザが
…/assets/index-xxxx.jsを要求する - Node.js がビルド済みの JS をそのまま返す
- 画面が完成する(conv.ts もまとめてあるので、追加の要求は起きない)
違いのまとめ
| 開発中 | 本番 | |
|---|---|---|
| 変換するタイミング | 要求が来るたびに、その場で | ビルドのときに一度だけ |
| ファイルの取り方 | main.ts、conv.ts を別々に | 1つにまとめた JS を1回で |
| サーバーの仕事 | 変換して返す | 読んでそのまま返す |
| 重視していること | 書いてすぐ確かめられること | 速く、変わらないものを確実に配ること |
| 組み込みでいうと | ICE につないで、保存のたびに書き込み | ROM を焼き終えて出荷した状態 |
どちらの場合も、画面が完成した後の換算計算は、ブラウザの中だけで行われます。サーバーは「画面のファイルを届ける窓口」で、計算には関わりません。
まとめと次回予告
Vite を組み込みの言葉に置き換えると、次のようになります。
| Vite の概念 | 組み込みでいうと |
|---|---|
ビルド(npm run build) | コンパイル&リンクして ROM イメージを作る |
| 開発サーバーと HMR | ICE につないで、保存のたびに書き込みとリセット |
| base | リンカスクリプトで指定する配置アドレス |
| サーバーが base を引く | ペリフェラルのアドレスデコード |
| ファイル名のハッシュ | ファーム名に CRC を含めて版の取り違えを防ぐ |
最初は「何をしてくれるのかわからない道具」でしたが、要求の流れを1手ずつ追ってみると、やっていることは組み込みでおなじみの作業ばかりでした。特に base は、受け取る側の引き算と、書き出す側の足し算をセットで考えると、すっきり理解できました。
次回は、Step 1 で書いた TypeScript のコードを、C と比べながら読んでいきます。構造体や enum、タグ付き共用体にあたる書き方が、TypeScript ではどう表現されるのかを紹介します。
この記事は、シリーズ「組み込みエンジニアの Web 開発入門」の1本です。ほかの回は、シリーズの目次からどうぞ。



コメント