[PR] 本記事にはプロモーションが含まれています。

【入門編 第4回】Vite を組み込みの言葉で理解する ― ビルド、開発サーバー、base(Step 1)

TypeScript

はじめに

前回は、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 devICE をつないだまま開発し、保存のたびに書き込みとリセットが自動で走る状態画面を作っている間
ビルドnpm run buildmake でコンパイル&リンクして、ROM イメージを作る公開する前

Step 0 で使った tsc との違い

Step 0 では、サーバー用のプログラムを tsc(TypeScript のコンパイラ)でビルドしました。Vite も同じ TypeScript を扱いますが、役割が違います。

tscVite
対象サーバー(Node.js)で動くコードブラウザで動くコード
やることTypeScript を JavaScript に変換するだけ変換、まとめる、住所の調整、開発サーバー
出力dist/server.jsdist/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〕

Bash
$ 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 の設定)

TypeScript
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(画面の入口)

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(動作確認用の最小プログラム)

TypeScript
const el = document.getElementById('status');
if (el) {
  el.textContent = 'main.ts が動きました';
}

main.ts は、id="status" の部品を探して、文字を書き換えるだけのプログラムです。if (el) は、部品が見つからなかった場合の保護で、C のヌルポインタチェックと同じです。

③ 起動して、手元のブラウザで見る

Bash
$ npm run dev

次のように表示されれば起動成功です。このコマンドは止めるまで動き続けます。組み込みでいえば、メインループに入ったままの状態です。

Plaintext
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本つなぐようなものです。

Vite を組み込みの言葉で理解する ― ビルド、開発サーバー、:③ 起動して、手元のブラウザで見る

「読み込み中…」が「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.htmlNode.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.jsdist/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つのベースアドレスを、受け取る側では引き算し、書き出す側では足し算している、という関係です。

Vite を組み込みの言葉で理解する ― ビルド、開発サーバー、:書き出す側は「足し算」する

開発サーバーで、base の外を叩いてみる

開発サーバーで base の外の住所を開くとどうなるかを、curl で確かめました。-I は応答の見出し(ヘッダ)だけを表示する指定です。

Bash
$ curl -sI http://127.0.0.1:5173/

結果は次のとおりです。

Plaintext
HTTP/1.1 302 Found
Location: /apps/unitconv/

302 は「探しているものは別の場所にある」という意味のステータスコードで、Location: がその移動先です。ブラウザはこれを受け取ると、自動で /apps/unitconv/ を要求し直します。curl は、そのままでは言われた先へ行きませんが、-L を付けるとブラウザと同じように移動先をたどります。

Bash
$ curl -sIL http://127.0.0.1:5173/

今度は、302 の応答に続いて、移動先の 200 OK の応答が表示されます。ブラウザの裏側では、この2往復が起きています。

ここで注意したいのは、「転送」という言葉に2つの意味があることです。私も最初は混同していました。

言葉誰が動くか組み込みでいうと
中継(Apache の ProxyPass)サーバーが要求を代わりに先へ渡す。ブラウザは気づかない中継器、ブリッジ
リダイレクト(302 や 301)サーバーは「あっちへ行って」と返すだけ。ブラウザが自分で要求し直すデバイスが「このアドレスに再送して」と返し、ホストが再送する

なお、この 302 は開発サーバーの親切機能です。本番では、/ への要求は Apache が別のページとして処理し、Node.js には届きません。

ビルドして中身を見る

画面とプログラムができたら、公開用にビルドします。ビルドのコマンドを登録して実行し、出力されたファイルを観察しました。

Bash
$ npm pkg set scripts.build:client="vite build"
$ npm run build:client
$ ls -R dist
$ grep -o 'src="[^"]*"' dist/public/index.html

build:client という名前にしたのは、後でサーバー側のビルド(build:server)も追加し、両方をまとめた build を作るためです。

結果は次のとおりでした(英数字の部分は、ビルドのたびに変わります)。

Plaintext
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.htmlno-cache毎回、最新かどうかをサーバーに確かめる
index-xxxx.jspublic, max-age=31536000, immutable1年間そのまま使ってよい(中身が変われば名前が変わるので)

index.html さえ毎回最新になれば、その中に書かれた JS の名前も最新になるので、古い JS が使われることはありません。

Vite を組み込みの言葉で理解する ― ビルド、開発サーバー、:ファイル名に英数字が付いている

開発中と本番の違い

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

Vite を組み込みの言葉で理解する ― ビルド、開発サーバー、:開発中と本番の違い

図の青い矢印がブラウザからの要求、緑の矢印がサーバーからの応答、灰色の矢印がサーバーによるファイルの読み込みです。それぞれの手順を、文章でも追ってみます。

開発中(Vite の開発サーバー、5173 番)

  1. ブラウザが /apps/unitconv/ を要求する
  2. Vite が index.html を読み、script の住所をその場で書き換え、自動更新の仕組みも差し込んで返す
  3. ブラウザが HTML の中の script を見つけ、…/src/main.ts を要求する
  4. Vite が main.ts を読み、要求されたその瞬間に JavaScript へ変換して返す
  5. ブラウザが main を実行し始めると、import に気づいて …/src/conv.ts を追加で要求する
  6. Vite が conv.ts もその場で変換して返す
  7. 部品がそろい、画面が完成する

本番(自作の Node.js サーバー、5002 番)

  1. (事前)npm run build で、Vite が main.ts と conv.ts を1つの JS にまとめておく
  2. ブラウザが /apps/unitconv/ を要求する
  3. Node.js が base を引き算し、ビルド済みの index.html をそのまま返す
  4. ブラウザが …/assets/index-xxxx.js を要求する
  5. Node.js がビルド済みの JS をそのまま返す
  6. 画面が完成する(conv.ts もまとめてあるので、追加の要求は起きない)

違いのまとめ

開発中本番
変換するタイミング要求が来るたびに、その場でビルドのときに一度だけ
ファイルの取り方main.ts、conv.ts を別々に1つにまとめた JS を1回で
サーバーの仕事変換して返す読んでそのまま返す
重視していること書いてすぐ確かめられること速く、変わらないものを確実に配ること
組み込みでいうとICE につないで、保存のたびに書き込みROM を焼き終えて出荷した状態

どちらの場合も、画面が完成した後の換算計算は、ブラウザの中だけで行われます。サーバーは「画面のファイルを届ける窓口」で、計算には関わりません。

まとめと次回予告

Vite を組み込みの言葉に置き換えると、次のようになります。

Vite の概念組み込みでいうと
ビルド(npm run build)コンパイル&リンクして ROM イメージを作る
開発サーバーと HMRICE につないで、保存のたびに書き込みとリセット
baseリンカスクリプトで指定する配置アドレス
サーバーが base を引くペリフェラルのアドレスデコード
ファイル名のハッシュファーム名に CRC を含めて版の取り違えを防ぐ

最初は「何をしてくれるのかわからない道具」でしたが、要求の流れを1手ずつ追ってみると、やっていることは組み込みでおなじみの作業ばかりでした。特に base は、受け取る側の引き算と、書き出す側の足し算をセットで考えると、すっきり理解できました。

次回は、Step 1 で書いた TypeScript のコードを、C と比べながら読んでいきます。構造体や enum、タグ付き共用体にあたる書き方が、TypeScript ではどう表現されるのかを紹介します。

この記事は、シリーズ「組み込みエンジニアの Web 開発入門」の1本です。ほかの回は、シリーズの目次からどうぞ。

ほかの開発・プログラミングの記事は、開発・プログラミングメモの目次から、テーマ別に探せます。

このブログは、にほんブログ村と人気ブログランキングに参加しています。記事が役に立ったら、下のランキングのバナーのどれかを押して応援してもらえると励みになります。

ブログランキング・にほんブログ村へにほんブログ村

サーバー構築・運用ランキング プログラムランキング

TypeScript
watasoftdevをフォローする

コメント