Vite を使ってアプリケーションに環境変数を参照させる方法を考える
wakamsha
前置き: ビルドごとにアプリの振る舞いを変えたいことがある
アプリケーションコード(以下、アプリコード)に直接ハードコーディングせず、ビルド時に任意の環境変数を値として参照させたいケースがあります。
例えば Firebase のような SaaS を利用する際は、開発用と本番用とで別々の環境を用意することが多いでしょう。ということは開発時と本番デプロイ時とで異なる API Key をアプリコードに読み取らせねばなりません。
SaaS に限らず自前で開発するサーバにおいても開発時と本番デプロイ時とで疎通先が異なる場合は、 API の URL をビルド時にスイッチできるような仕組みが求められます。
https://dev-api.example.com/ # 開発環境用
https://stg-api.example.com/ # ステージング環境用
https://api.example.com/ # 本番環境用
ビルド時にグローバル定数を生成する
アプリコード内にハードコーディングしないのであれば、何かしらの方法で外部にグローバル定数として定義し参照できるようにすることになりますね。Vite では define
というオプションを使うことでビルド時にアプリコード内から参照可能なグローバル定数を生成できます。
import { defineConfig } from 'vite';
export default defineConfig({
// ...
define: {
ENV_API_KEY: JSON.stringify('KEY:XXX-XXX'),
},
});
ビルドすると ENV_API_KEY
というグローバル定数が生成され、アプリコード内から参照できます。
これでアプリケーションの振る舞いをビルド時の設定次第で変えられるようになりました。
さて、ここまでは良いのですが、開発時と本番デプロイ時とでいちいち vite.config
を書き直すのは非効率ですしケアレスミスの温床となります。やはりこういった値は環境変数として vite
コマンド実行時にコマンドライン引数として任意の値を渡したいところです。
Vite は コマンドライン引数から任意の環境変数を渡せない
webpack の場合ですとコマンドライン引数の --env
を使って任意の環境変数を webpack.config
に渡せます。
yarn webpack --env apiKey=KEY:XXX-XXX
渡した値は webpack.config
の module.exports
に渡す関数の引数 env
から参照できます。
module.exports = (env) => {
// Use env.<YOUR VARIABLE> here:
console.log(env.apiKey); //=> 'KEY:XXX-XXX'
return {
entry: './src/index.js',
// ...
};
};
コマンドラインオプションから渡した値は env
というオブジェクトに渡した変数が格納され、そこから参照できるという仕組みです。非常に柔軟性に富んだ機能ですが、残念ながら Vite にはこれに相当する機能がありません。よって Vite に適合した方法で環境変数を渡すことになります。
.env ファイルで環境変数を定義する方法
Vite は、ビルド時に vite.config
ファイルと同じディレクトリ1)どうやら任意のディレクトリには置けないようです。にある .env
というファイルに記載されたもののうち、名前が VITE_
で始まる環境変数だけを読み込む仕様となっています。読み込まれた値は import.meta.env
というグローバルオブジェクトに格納され、アプリコード内から参照できます。
VITE_API_KEY=KEY:XXX-XXX # 読み込まれる
API_KEY=KEY:YYY-YYY # 読み込まれない
console.log(import.meta.env.VITE_API_KEY); //=> KEY:XXX-XXX
console.log(import.meta.env.API_KEY); //=> undefined
webpack と違いコマンドライン引数でなくファイルに記述することでメンテナンス性を担保してるというわけですね。
.env を開発用と本番用とで別管理にする
.env
は用途別に複数作成できます。
.env # 全てのケースで必ず読み込まれる
.env.[mode] # --mode オプションで指定したものだけが読み込まれる
./app
├── src
├── .env # 共通の環境変数
├── .env.dev # 開発環境用
├── .env.stg # ステージング環境用
├── .env.prod # 本番環境用
└── vite.config.ts
これを踏まえ上記のように4種類の .env
ファイルを作成したとします。 vite
コマンドには --mode
というオプションがあり、これに指定した値と合致するファイル名が読み込まれます。つまり vite --mode dev
とコマンドを実行すると .env
と .env.dev
の2つが読み込まれ、 vite --mode stg
なら .env
と .env.stg
が読み込まれます。
--mode
オプションのデフォルト値は、開発時なら development
、本番デプロイ時なら production
の二種類だけなのですが、 stg
( staging ) のような独自のモードも作れるため、利用する現場の運用に則した柔軟な設計が可能です。
デメリット: .env の数が増えるにつれ管理が煩雑化する
環境変数のパターンが dev | stg | prod
と少なければ問題ないのですが、これに収まりきらなくなると運用に辛さが出てきます。
例えばスタディサプリ ENGLISH の開発現場では常に複数の機能開発案件が同時並行で行われているため、 開発用環境だけでも10個あります2)開発用以外にも QA 用、コンテンツ入稿確認用、プレリリース用など非常に多くの環境が運用されています。。これをそのまま .env
ファイル運用に乗せると実に 11以上もの .env
ファイルが作成されることとなり、いくらなんでも冗長が過ぎます。
とはいえ一度に疎通するのは 10ある環境のうちせいぜい1つなので、 .env.dev
に dev1 | dev2 | ... dev10
のうちどこと疎通するのかを環境変数として記述すれば済む話でもあります。
VITE_VARIANT=dev1 # dev1 環境と疎通する。dev10 と疎通したければ `dev10` と書き換える
ひとますこれで .env.dev
ファイルの大量発生は防げそうですが、疎通先を変えるたびにファイル編集せねばならないのはやはり非効率であり冗長と言わざるを得ません。 .env.dev
が Git 管理下にあると疎通先を変えただけで差分と見なされてしまい、これまた面倒です。
そもそも扱う環境変数が .env
ファイルだけで全て管理できるのであれば何も問題ありませんが、値のバリエーションが多くなるにつれコマンドライン引数から渡したくなってきます。
どうにかして webpack のようにコマンドライン引数を受け渡す方法
繰り返しになりますが、 Vite は webpack の --env
のように任意の引数を受け取れません。仮に無理やり vite --apiKey KEY:XXX-XXX
とコマンド実行したところで未定義の引数と見なされエラーとして処理されてしまいます。
yarn vite --apiKey KEY:XXX-XXX
/path/to/sample-project/node_modules/vite/dist/node/cli.js:431
throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
^
CACError: Unknown option `--apiKey`
at Command.checkUnknownOptions (//path/to/sample-project/node_modules/node_modules/vite/dist/node/cli.js:431:17)
at CAC.runMatchedCommand (/path/to/sample-project/node_modules/node_modules/vite/dist/node/cli.js:629:13)
at CAC.parse (/path/to/sample-project/node_modules/node_modules/vite/dist/node/cli.js:568:12)
at Object.<anonymous> (/path/to/sample-project/node_modules/node_modules/vite/dist/node/cli.js:13989:5)
at Module._compile (internal/modules/cjs/loader.js:1068:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1097:10)
at Module.load (internal/modules/cjs/loader.js:933:32)
at Function.Module._load (internal/modules/cjs/loader.js:774:14)
at Module.require (internal/modules/cjs/loader.js:957:19)
at require (internal/modules/cjs/helpers.js:88:18)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
error Command failed.
Exit code: 1
つまり任意の引数を受け取るには、 vite
コマンド以外の方法でビルドを実行させる必要があるというわけです。
コマンドライン引数を受け取り Vite を起動する Node.js スクリプトを自作する
Vite の機能の殆どは JavaScript ( or TypeScript )API として提供されており、これを駆使することで vite
コマンドを使わずビルド処理を実現できます。
下記は Vite でビルドして Dev サーバを起動する処理の例です。
const { createServer } = require('vite');
(async () => {
const server = await createServer({
// any valid user config options, plus `mode` and `configFile`
configFile: false,
// 以下、 vite.config と同じ設定項目を指定
// ※ 不要なのは省略可能
root: __dirname,
server: {
port: 3000,
},
});
await server.listen();
})();
createServer
という API を使って Dev サーバのインスタンスを生成し、 listen()
を実行すると Dev サーバが watch 状態で立ち上がります。つまり vite
コマンドを実行したのと同じ結果が得られます。
configFile
プロパティに vite.config
ファイルへのパスを指定するとその設定内容を読み込んだ上で起動しますが、 false
と指定することで読み込まなくなります。その他のプロパティは全て vite.config
にて記述してたのと同じものが使えます。
スクリプトファイルを作ったら Node.js コマンドを実行します。
node ./builder
成功すると http://localhost:3000
で Dev サーバが立ち上がります。
同様に vite build
コマンドを再現したければ build
という API を使えば OK です(引数の型は createServer
のそれと全く同じ)。
任意のコマンドライン引数を受け取る
実行するコマンドが vite
から単なる Node.js コマンドになったので、任意のコマンドライン引数を受け取れるようになりました。
node ./builder apiKey=KEY:XXX-XXX
これで builder.js
内で process.argv
から apiKey
を参照できます。
const { createServer } = require('vite');
console.log(process.argv);
// ...
[
'/usr/local/bin/node', // Node.jsの実行プロセスのパス
'/path/to/builder.js', // 実行したスクリプトファイルのパス
'apiKey=KEY:XXX-XXX', // 渡したコマンドライン引数
];
念願のコマンドライン引数を受け取れました。が、流石にこの構造のままでは扱いにくいので、 yargs
という npm パッケージを使って扱いやすい構造にパースします。
yarn add -D yargs
builder.js
にコマンドライン引数をパースする処理を追記します。
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const { apiKey } = yargs(hideBin(process.argv))
.option('apiKey', {
type: 'string',
describe: '疎通する API サーバのシリアルキーを指定します。',
}).argv;
console.log(apiKey);
// ...
これで apiKey
を string 型として取り出せます。この他にも number 型や Boolean 型として取り出せるだけでなく、Array 型として取り出すことも可能です。
--params foo bar baz #=> ['foo', 'bar', 'baz'] として取り出せる
これでコマンドライン引数を受け取る準備が整いました。 下記のコマンドを実行します。
node ./builder.js --apiKey KEY:XXX-XXX
KEY:XXX-XXX
無事に値を受け取れています。あとはこの値を define
を使ってグローバル定数化すれば完成です。
const { createServer } = require('vite');
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const { apiKey } = yargs(hideBin(process.argv))
.option('apiKey', {
type: 'string',
describe: '疎通する API サーバのシリアルキーを指定します。',
}).argv;
console.log({ apiKey });
if (!apiKey) {
console.error('apiKey が指定されていません。');
process.exit(1);
}
(async () => {
const server = await createServer({
// any valid user config options, plus `mode` and `configFile`
configFile: false,
// 以下、 vite.config と同じ設定項目を指定可能
root: __dirname,
server: {
port: 3000,
},
define: {
ENV_API_KEY: JSON.stringify(apiKey),
},
});
await server.listen();
})();
これで webpack の --env
と同等の機能と使い勝手を再現できました。
デメリット: Vite のシンプルさが損なわれる
せっかく vite.config
でシンプルかつイージーにビルド環境を構築できていたのが、最終的に webpack.config
と大差ない規模にまで膨れ上がってしまいました。これはデメリットと言わざるを得ないでしょう。また、Node.js スクリプト化したことでこれまで TypeScript でコーディングできていたのが VanillaJS になってしまったのもマイナス点といえます。 ts-node 等を導入するか tsc
コマンドでのトランスパイルを挟んでから Node.js で実行することで TypeScript のまま設定を記述することも可能ではありますが、果たしてそこまでして採算に見合うかは悩ましいところです。
願わくば vite コマンド も webpack の --env
のように任意の引数を受け取れるようになってもらいたいところですが、当面はこういった「運用でカバー」して凌ぐことも考慮しておいた方が良いかもしれません。
当エントリが皆様のお役に立てれば幸いです。