- Vue.js
Vueの環境変数の使い方|.envとVITE_・import.meta.env
Vue(Vite)の環境変数は、プロジェクト直下の .env ファイルに VITE_ で始まる名前で書き、コードからは import.meta.env.VITE_〜 で読み取ります。 VITE_ を付けていない変数はブラウザ側のコードに渡らず、undefined になります。開発用・検証用・公開用の値は、.env.development や .env.staging のようにモード別のファイルを置き、--mode で切り替えます。
この記事では、create-vue 3.24.0 で作った Vue 3.5.43 + Vite 8.3.2 のプロジェクトで実際にビルドした結果をもとに、.env ファイル4種類の優先順位、import.meta.env の組み込み値、undefined になる原因、vite.config の中で使う loadEnv、型定義、ビルド成果物に値が埋め込まれることの確かめ方、Vue CLI(VUE_APP_)からの移行までを説明します。
確認環境:Ubuntu 24.04 / Node.js 22.22.2 / Vue 3.5.43 / Vite 8.3.2 / vue-tsc 3.3.12 / TypeScript 6.0.3 / Chromium 141(Playwright 1.56.0)。画面の値は、ビルド結果を vite preview で配信して Chromium で開き、表の文字を読み取ったものです。出力中のパスは /home/taro/ に置き換えています。
Vueの環境変数の早見表
| やりたいこと | 書き方 |
|---|---|
| 変数を定義する | .env に VITE_API_URL=https://api.example.com |
| コンポーネントで読む | import.meta.env.VITE_API_URL |
| 開発中かどうかを知る | import.meta.env.DEV(true / false) |
| 公開用ビルドかどうかを知る | import.meta.env.PROD(true / false) |
| いまのモード名を知る | import.meta.env.MODE |
| モードを切り替えてビルドする | vite build --mode staging(.env.staging が読まれる) |
| 自分のPCだけの値にする | .env.local に書く(.gitignore の対象) |
| index.html で使う | %VITE_APP_TITLE% |
| vite.config の中で読む | loadEnv(mode, process.cwd()) |
| 型と補完を付ける | env.d.ts に ImportMetaEnv を定義 |
| 数値として使う | Number(import.meta.env.VITE_PORT) |
| 真偽値として使う | import.meta.env.VITE_DEBUG === 'true' |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vueの環境変数の使い方|.envとVITE_・import.meta.env」
最小の使い方(.env → コンポーネント)
1. .env を置く
package.json と同じ階層(プロジェクト直下)に .env を作ります。
# .env
VITE_APP_TITLE=タスク帳
VITE_API_URL=https://api.example.com
SECRET_TOKEN=abc123-secret
2. コンポーネントで読む
<!-- src/App.vue -->
<script setup lang="ts">
const title = import.meta.env.VITE_APP_TITLE
const apiUrl = import.meta.env.VITE_API_URL
const token = import.meta.env.SECRET_TOKEN
</script>
<template>
<h1>{{ title }}</h1>
<p>API: {{ apiUrl }}</p>
<p>SECRET_TOKEN: {{ token === undefined ? 'undefined' : token }}</p>
</template>
3. 実行結果(npm run dev)
開発サーバーを起動し、Chromium で開いて画面の文字を読み取った結果です。
タスク帳 | API: https://api.example.com | SECRET_TOKEN: undefined
VITE_ で始まる2つは読め、SECRET_TOKEN は undefined になりました。 これが Vite の環境変数のいちばん大事な決まりです。ブラウザに配るコードへうっかり値が混ざらないよう、VITE_ を付けたものだけが渡されます。
テンプレートに直接は書けない
import.meta.env は、script setup の中で変数に受けてからテンプレートで使います。テンプレートに {{ import.meta.env.VITE_API_URL }} と直接書くと、ビルドが次のエラーで止まりました。
error during build:
Build failed with 1 error:
RolldownError: Error parsing JavaScript expression: import.meta may appear only with 'sourceType: "module"' (1:1)
.envファイル4種類の優先順位を実行結果で確かめる
4種類のファイル
Vite の公式ドキュメント「環境変数とモード」には、次の4種類が載っています。
| ファイル | 読み込まれる場面 | Git に入れるか |
|---|---|---|
.env |
すべての場合 | 入れる(全員共通の値) |
.env.local |
すべての場合 | 入れない(自分のPCだけの値) |
.env.[mode] |
指定したモードのときだけ | 入れる(モード別の値) |
.env.[mode].local |
指定したモードのときだけ | 入れない(モード別かつ自分だけの値) |
create-vue が生成する .gitignore には *.local の行があるので、.local で終わるファイルは最初から Git の対象外になっています。
全部置いてビルドする
同じ変数 VITE_FROM を、4つのファイルすべてに「自分のファイル名」を値として書きました。どのファイルの値が使われたかが、ひと目でわかります。
# .env
VITE_APP_TITLE=タスク帳
VITE_API_URL=https://api.example.com
VITE_FROM=.env
VITE_ONLY_ENV=.env だけにある値
SECRET_TOKEN=abc123-secret
# .env.local
VITE_FROM=.env.local
VITE_ONLY_LOCAL=.env.local だけにある値
# .env.staging
VITE_API_URL=https://stg-api.example.com
VITE_FROM=.env.staging
VITE_ONLY_STAGING=.env.staging だけにある値
# .env.staging.local
VITE_FROM=.env.staging.local
画面には、変数を一覧で表示する次のコンポーネントを使います。
<!-- src/App.vue -->
<script setup lang="ts">
const env = import.meta.env
const rows = [
['VITE_APP_TITLE', env.VITE_APP_TITLE],
['VITE_API_URL', env.VITE_API_URL],
['VITE_FROM', env.VITE_FROM],
['VITE_ONLY_ENV', env.VITE_ONLY_ENV],
['VITE_ONLY_LOCAL', env.VITE_ONLY_LOCAL],
['VITE_ONLY_STAGING', env.VITE_ONLY_STAGING],
['SECRET_TOKEN', env.SECRET_TOKEN],
['MODE', env.MODE],
['DEV', env.DEV],
['PROD', env.PROD],
['BASE_URL', env.BASE_URL],
['SSR', env.SSR],
]
</script>
<template>
<h1>{{ env.VITE_APP_TITLE }}</h1>
<table>
<tr v-for="[name, value] in rows" :key="name">
<th>{{ name }}</th>
<td>{{ value === undefined ? 'undefined' : value }}</td>
</tr>
</table>
</template>
あわせて .env.production(VITE_API_URL=https://prod-api.example.com、VITE_FROM=.env.production)と .env.development(VITE_API_URL=http://localhost:3000、VITE_FROM=.env.development)も置いています。
npx vite build --mode staging
npx vite preview
VITE_APP_TITLE = タスク帳
VITE_API_URL = https://stg-api.example.com
VITE_FROM = .env.staging.local
VITE_ONLY_ENV = .env だけにある値
VITE_ONLY_LOCAL = .env.local だけにある値
VITE_ONLY_STAGING = .env.staging だけにある値
SECRET_TOKEN = undefined
MODE = staging
DEV = false
PROD = true
BASE_URL = /
SSR = false
読み取れることは3つです。
- 4つのファイルはすべて読み込まれ、値は合成される(それぞれにしか無い変数が全部そろっている)
- 同じ名前の変数は .env.staging.local の値が使われた
- MODE は staging だが、PROD は true(ビルドなので公開用の扱い)
ファイルを1つずつ外して順位を確かめる
VITE_FROM の値を、ビルド成果物から grep で取り出しました。上から順にファイルを外していった結果です。
| 置いてあるファイル | ビルド成果物の中の VITE_FROM |
|---|---|
シェルで VITE_FROM=shell を付けて実行(4ファイルあり) |
shell |
| .env / .env.local / .env.staging / .env.staging.local | .env.staging.local |
| .env / .env.local / .env.staging | .env.staging |
| .env / .env.local | .env.local |
| .env | .env |
優先順位は、高い順に次のとおりです。
flowchart LR
A[シェルの環境変数] --> B[.env.mode.local]
B --> C[.env.mode]
C --> D[.env.local]
D --> E[.env]
「モード別のファイルが共通のファイルより優先」「同じ段では .local 付きが優先」「コマンド実行時にすでにある環境変数がいちばん優先」と覚えると整理しやすいです。CI などで値を差し替えたい場合は、ファイルを書き換えずに VITE_API_URL=... npx vite build と実行時に渡せます。
モードごとの読み込み結果
同じファイル構成のまま、モードだけを変えた結果です。
| 項目 | npm run dev |
vite build |
vite build --mode staging |
|---|---|---|---|
| MODE | development | production | staging |
| DEV | true | false | false |
| PROD | false | true | true |
| VITE_API_URL | http://localhost:3000 | https://prod-api.example.com | https://stg-api.example.com |
| VITE_FROM | .env.development | .env.production | .env.staging.local |
| VITE_ONLY_LOCAL | .env.local だけにある値 | .env.local だけにある値 | .env.local だけにある値 |
| VITE_ONLY_STAGING | undefined | undefined | .env.staging だけにある値 |
dev と production の VITE_FROM が .env.local ではない点に注目してください。.env.local より .env.development や .env.production のほうが優先されるためです。「.env.local に書いたのに反映されない」ときは、モード別ファイルに同じ名前が無いかを見直します。
package.json にモード別のコマンドを用意する
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build:staging": "vite build --mode staging",
"preview": "vite preview"
}
}
import.meta.envの組み込み値一覧
.env に書かなくても最初から使える値が5つあります。
| 名前 | 型 | dev | build | build --mode staging | 意味 |
|---|---|---|---|---|---|
import.meta.env.MODE |
文字列 | development | production | staging | いまのモード名 |
import.meta.env.DEV |
真偽値 | true | false | false | 開発サーバーで動いているか |
import.meta.env.PROD |
真偽値 | false | true | true | 公開用ビルドで動いているか(DEV の逆) |
import.meta.env.BASE_URL |
文字列 | / | / | / | アプリを配信するベースのパス(vite.config の base) |
import.meta.env.SSR |
真偽値 | false | false | false | サーバー側で動いているか |
(dev・build の各列は、この記事の構成で実際に表示された値です。)
モードとNODE_ENVは別のもの
--mode staging でビルドしても PROD は true のままでした。モード(どの .env を読むか)と、開発用か公開用か(DEV / PROD)は別々に決まります。Vite の公式ドキュメントには次の対応が載っています。
| コマンド | NODE_ENV | モード |
|---|---|---|
vite build |
production | production |
vite build --mode staging |
production | staging |
NODE_ENV=development vite build |
development | production |
| 判定したいこと | 使う値 |
|---|---|
| 開発サーバーのときだけログを出す | import.meta.env.DEV |
| 検証用の環境だけバナーを出す | import.meta.env.MODE === 'staging' |
| 接続先を切り替える | モード別の .env に VITE_API_URL を書く |
使いどころの例
<script setup lang="ts">
const apiUrl = import.meta.env.VITE_API_URL
const isStaging = import.meta.env.MODE === 'staging'
if (import.meta.env.DEV) {
console.log('接続先:', apiUrl)
}
</script>
<template>
<p>{{ apiUrl }}</p>
<p v-if="isStaging">検証用の環境です</p>
</template>
--mode staging でビルドすると、接続先のURLと「検証用の環境です」が表示され、コンソールには何も出ませんでした。開発サーバーでは「検証用の環境です」は表示されず、コンソールに 接続先: https://api.example.com が出ます。
値はすべて文字列:数値・真偽値・引用符・変数展開
.env に書いた値は、数字や true / false に見えても、すべて文字列として届きます。 次の .env で確かめました。
VITE_PORT=8080
VITE_DEBUG=false
VITE_GREETING="Hello World"
VITE_API_V1=$VITE_API_URL/v1
VITE_NOTE=abc # ここはコメント
VITE_HASH="abc#def"
| 式 | 結果 | 補足 |
|---|---|---|
import.meta.env.VITE_PORT |
8080 | 見た目は数字 |
typeof import.meta.env.VITE_PORT |
string | 型は文字列 |
Number(import.meta.env.VITE_PORT) + 1 |
8081 | Number で変換してから計算する |
import.meta.env.VITE_DEBUG |
false | 文字列の 'false' |
import.meta.env.VITE_DEBUG ? 'ON' : 'OFF' |
ON | 空でない文字列は真として扱われる |
import.meta.env.VITE_DEBUG === 'true' |
false | 文字列どうしで比べる |
import.meta.env.VITE_GREETING |
Hello World | 引用符は取り除かれる |
import.meta.env.VITE_API_V1 |
https://stg-api.example.com/v1 | $VITE_API_URL が展開される |
import.meta.env.VITE_NOTE |
abc | # より後ろはコメント |
import.meta.env.VITE_HASH |
abc#def | 引用符で囲むと # も値に含まれる |
とくに VITE_DEBUG=false をそのまま if に入れると真になる点は、よく出会う落とし穴です。=== 'true' で比べる形にしておくと意図どおりに動きます。
変数展開($VITE_API_URL)は、staging でビルドすると staging の値(stg-api)で展開されました。展開されるのは、そのモードで最終的に決まった値です。# を値に含めたい場合は引用符で囲みます。$ をそのまま含めたい場合は、公式ドキュメントのとおり \ でエスケープします。
index.htmlで環境変数を使う
index.html では、%変数名% の形で埋め込めます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>%VITE_APP_TITLE%(%MODE%)</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
| 実行 | ページのタイトル |
|---|---|
npm run dev |
タスク帳(development) |
vite build --mode staging |
タスク帳(staging) |
undefinedになる原因チェック表
「import.meta.env.〜 が undefined になる」ときに見直す箇所を、実際の出力とあわせてまとめました。
| 原因 | 実際の結果 | 直し方 |
|---|---|---|
名前が VITE_ で始まっていない |
import.meta.env.API_KEY → undefined |
.env の名前を VITE_API_KEY にする(公開してよい値に限る) |
process.env で読んでいる(dev) |
ReferenceError: process is not defined |
import.meta.env.VITE_〜 に書き換える |
process.env で読んでいる(build 後) |
process.env.VITE_API_URL → undefined |
同上 |
| vite.config の中で読んでいる | import.meta.env?.VITE_API_URL → undefined |
loadEnv を使う(次の章) |
| 別のモードのファイルに書いた | staging 以外では VITE_ONLY_STAGING → undefined | そのモードで読まれるファイルに書く。--mode を確かめる |
| モード別ファイルに同じ名前がある | .env.local より .env.development が優先 | 優先順位の表で、どの値が使われるか確かめる |
| .env の置き場所が違う | src の中などに置くと読まれない | package.json と同じ階層に置く |
| ビルドし直していない | dist の中身は前回の値のまま | .env を変えたら build をやり直す |
| つづりの誤り | 既定の型定義では VITE_API_URl と書いても型エラーにならない |
型定義で検出する(後述の厳格モード) |
| テンプレートに直接書いた | ビルドが import.meta may appear only with ... で止まる |
script setup で変数に受ける |
process.env は dev と build で結果が違う
同じコード process.env.VITE_API_URL を、開発サーバーとビルド後の両方で実行した結果です。
開発サーバー : ReferenceError: process is not defined
ビルド後 : undefined
開発サーバーではエラーで気付けますが、ビルド後は静かに undefined になります。 vue-tsc の型チェックでは次のエラーが出るので、こちらでも気付けます。
src/App.vue(12,43): error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
このメッセージは @types/node の追加を勧めていますが、ブラウザで動くコードでは import.meta.env に書き換えるのが正しい対処です。
.env を変えたあとの再起動
Vite 8.3.2 の開発サーバーを起動したまま .env を書き換えると、次のログが出てサーバーが自動で再起動しました。
11:55:46 PM [vite] .env changed, restarting server...
11:55:46 PM [vite] server restarted.
ページを開き直すと、タイトルが新しい値(タスク帳2)に変わっていました。開発サーバーは .env の変更を検知して再起動してくれます。 一方、vite build で作った dist と、それを配信する vite preview は作り直すまで前の値のままです。値が変わらないときは、ログに再起動の行が出ているか、ビルドをやり直したかを確かめます。
動的なキーでも読める
const key = 'VITE_API_URL'
console.log(import.meta.env[key]) // https://stg-api.example.com
import.meta.env[key] のように変数でキーを指定しても読めました。ただし、この書き方でビルドすると、コードで使っていない VITE_ 変数の値までビルド成果物に含まれました(後の章で確かめます)。
vite.configの中ではloadEnvを使う
vite.config.ts は、.env が読み込まれる前に評価されます。そのため、設定ファイルの中では import.meta.env も process.env も .env の値を持っていません。 代わりに、Vite が用意している loadEnv を使います。
確かめるための設定
// vite.config.ts(動きを確かめる版)
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
// (1) vite.config の中では、.env の値はまだ読み込まれていない
console.log('(1) import.meta.env?.VITE_API_URL =', import.meta.env?.VITE_API_URL)
console.log('(1) process.env.VITE_API_URL =', process.env.VITE_API_URL)
// (2) loadEnv で自分で読み込む(第3引数を省くと VITE_ で始まるものだけ)
const viteEnv = loadEnv(mode, process.cwd())
console.log('(2) loadEnv のキー =', Object.keys(viteEnv).join(', '))
console.log('(2) VITE_API_URL =', viteEnv.VITE_API_URL)
// (3) 第3引数を '' にすると、接頭辞のない値も読める
const allEnv = loadEnv(mode, process.cwd(), '')
console.log('(3) SECRET_TOKEN =', allEnv.SECRET_TOKEN)
return {
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: Number(viteEnv.VITE_PORT),
},
}
})
npx vite build --mode staging の出力です。
(1) import.meta.env?.VITE_API_URL = undefined
(1) process.env.VITE_API_URL = undefined
(2) loadEnv のキー = VITE_API_URL, VITE_API_V1, VITE_APP_TITLE, VITE_DEBUG, VITE_FROM, VITE_GREETING, VITE_HASH, VITE_NOTE, VITE_ONLY_ENV, VITE_PORT, VITE_ONLY_LOCAL, VITE_ONLY_STAGING
(2) VITE_API_URL = https://stg-api.example.com
(3) SECRET_TOKEN = abc123-secret
| 番号 | 読み方 | 結果 |
|---|---|---|
| (1) | import.meta.env / process.env |
どちらも undefined |
| (2) | loadEnv(mode, process.cwd()) |
VITE_ で始まる変数だけが、モードに合わせて読める |
| (3) | loadEnv(mode, process.cwd(), '') |
接頭辞のない SECRET_TOKEN も読める |
普段使う形
確認用の console.log を外した形です。defineConfig に関数を渡すと、引数から mode を受け取れます。
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
return {
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: Number(env.VITE_PORT),
},
}
})
.env に VITE_PORT=8080 と書いた状態で npm run dev を実行すると、指定したポートで起動しました。
➜ Local: http://localhost:8080/
| 読む場所 | 使うもの | 読める範囲 |
|---|---|---|
| src 配下(ブラウザで動くコード) | import.meta.env |
VITE_ で始まる変数と組み込み値 |
| index.html | %VITE_〜% |
同上 |
| vite.config.ts(Node.js で動く設定) | loadEnv |
第3引数しだいで全変数 |
第3引数を '' にして読んだ値は、設定の中だけで使うようにします。たとえば開発サーバーのプロキシ先や、ビルド時にだけ必要な情報です。その値を define などでブラウザ側へ渡すと、成果物に埋め込まれます。
env.d.tsで型定義する
create-vue が生成する env.d.ts
TypeScript 構成のプロジェクトには、最初から次の1行だけの env.d.ts があります。
/// <reference types="vite/client" />
これで MODE・DEV・PROD などの組み込み値には型が付きます。自分で足した VITE_〜 は、この状態ではどんな名前でも通ります(補完は出ません)。
自分の変数に型を付ける
Vite の公式ドキュメントに沿って、ImportMetaEnv に自分の変数を書き足します。
// env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_API_URL: string
readonly VITE_PORT: string
readonly VITE_DEBUG: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
次のコードを npx vue-tsc --build で検査しました。
// src/typed.ts
export const title: string = import.meta.env.VITE_APP_TITLE
export const port: number = import.meta.env.VITE_PORT
export const url = import.meta.env.VITE_API_URl
export const mode: string = import.meta.env.MODE
src/typed.ts(2,14): error TS2322: Type 'string' is not assignable to type 'number'.
VITE_PORT を number として受け取ろうとした行がエラーになりました。 「値はすべて文字列」という決まりを、型が教えてくれます。一方、3行目のつづりの誤り(VITE_API_URl)は、この定義だけではエラーになりませんでした。
つづりの誤りも検出する(厳格モード)
ViteTypeOptions に strictImportMetaEnv を足すと、定義していないキーがエラーになります。
// env.d.ts
/// <reference types="vite/client" />
interface ViteTypeOptions {
// この1行で、定義していないキーを型エラーにできる
strictImportMetaEnv: unknown
}
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_API_URL: string
readonly VITE_PORT: string
readonly VITE_DEBUG: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
src/App.vue(7,21): error TS2339: Property 'VITE_FROM' does not exist on type 'ImportMetaEnv'.
src/App.vue(11,24): error TS2339: Property 'SECRET_TOKEN' does not exist on type 'ImportMetaEnv'.
src/typed.ts(2,14): error TS2322: Type 'string' is not assignable to type 'number'.
src/typed.ts(3,36): error TS2551: Property 'VITE_API_URl' does not exist on type 'ImportMetaEnv'. Did you mean 'VITE_API_URL'?
(同じ種類の行は一部省いています。)
つづりの誤りに「Did you mean 'VITE_API_URL'?」と候補まで出るようになりました。 定義していない VITE_FROM や、VITE_ を付けていない SECRET_TOKEN の参照も検出されています。
| env.d.ts の状態 | 型の誤り(string を number に代入) | つづりの誤り・定義していないキー |
|---|---|---|
| 生成されたまま(1行だけ) | 検出されない | 検出されない |
| ImportMetaEnv を定義 | TS2322 で検出 | 検出されない |
| strictImportMetaEnv も追加 | TS2322 で検出 | TS2339 / TS2551 で検出 |
ビルド成果物に値が埋め込まれることをgrepで確かめる
環境変数は、実行時に読みに行くのではなく、ビルドの時点でJavaScriptの中に文字として書き込まれます。 実際に dist の中を grep して確かめます。
手順
npx vite build --mode staging
grep -o "https://stg-api.example.com" dist/assets/*.js
grep -c "abc123-secret" dist/assets/*.js
https://stg-api.example.com
0
VITE_API_URL の値がそのまま JavaScript の中に入っていて、VITE_ を付けていない SECRET_TOKEN の値は入っていません(0件)。
VITE_ を付けた値は、誰でも読める
試しに、.env に次の2行を足しました。
API_KEY=no-prefix-value
VITE_SECRET_KEY=sk-do-not-put-here
コードでの参照の仕方を変えて、ビルド成果物の中に各値が含まれる行数を数えた結果です。
| コードの書き方 | VITE_SECRET_KEY の値 | SECRET_TOKEN の値 | API_KEY の値 |
|---|---|---|---|
| (a) どこからも参照していない | 0 | 0 | 0 |
(b) const env = import.meta.env と丸ごと参照 |
1 | 0 | 0 |
(b') import.meta.env[key] と動的なキーで参照 |
1 | ― | ― |
(c) import.meta.env.VITE_SECRET_KEY を参照 |
1 | 0 | 0 |
grep -rno "sk-do-not-put-here" dist
dist/assets/index-gBFGOR3b.js:1:sk-do-not-put-here
わかることは3つです。
VITE_を付けた値は、参照した時点で配信するファイルに書き込まれる。ブラウザの開発者ツールやファイルのダウンロードで、利用者の誰もが読めるimport.meta.envを丸ごと変数に入れたり、動的なキーで読んだりすると、使っていないVITE_変数もすべて入る((b)・(b') の行)VITE_を付けていない値は、どの書き方でも入らない
置いてよい値・置かない値
| 値の種類 | VITE_ を付けて .env に書く |
|---|---|
| APIのベースURL、アプリ名、表示の切り替えフラグ | 書いてよい(見られて困らない値) |
| 公開前提のキー(ブラウザから使うことを想定して発行されたもの) | 書いてよい。発行元の案内で利用元の制限をかける |
| データベースのパスワード、管理用のトークン、秘密鍵 | 書かない。サーバー側のプログラムだけが持つ |
「ブラウザに届いたものは隠せない」と考えるのが出発点です。Vite の公式ドキュメントも、VITE_ で始まる変数は秘密の情報に使わないよう案内しています。秘密の値が必要な処理は、サーバー側に用意したAPIを経由させます。
また、.env.local のような .local 付きのファイルは Git に入りませんが、ビルドすれば成果物には入ります。 「Git に入れない」と「配信物に含まれない」は別の話です。
ビルド後に値を変えたいとき
値はビルド時に埋め込まれるので、同じ dist を別の環境に持っていっても、値は変わりません。 環境ごとに値を変えるには、環境ごとに --mode を変えてビルドします。
Vue CLI(VUE_APP_)からの移行
Vue CLI で作ったプロジェクトを Vite に移すときの、環境変数まわりの対応です。Vue CLI の公式ドキュメント(Modes and Environment Variables)には、クライアント側に埋め込まれるのは NODE_ENV・BASE_URL と VUE_APP_ で始まる変数で、process.env.VUE_APP_〜 で読むと書かれています。
| 項目 | Vue CLI | Vite(create-vue) |
|---|---|---|
| 接頭辞 | VUE_APP_ |
VITE_ |
| 読み方 | process.env.VUE_APP_API_URL |
import.meta.env.VITE_API_URL |
| 公開用かどうか | process.env.NODE_ENV === 'production' |
import.meta.env.PROD |
| モード名 | process.env.NODE_ENV |
import.meta.env.MODE |
| ベースのパス | process.env.BASE_URL |
import.meta.env.BASE_URL |
| .env ファイルの種類 | .env / .env.local / .env.[mode] / .env.[mode].local | 同じ4種類 |
| 型定義 | ― | env.d.ts の ImportMetaEnv |
.env ファイルの種類と優先の考え方は共通なので、変えるのは「接頭辞」と「読み方」の2点です。
手順1:まず接頭辞だけ両対応にする(envPrefix)
.env の名前を一度に全部変えるのが大変なときは、envPrefix で VUE_APP_ も受け付けるようにできます。
# .env
VUE_APP_API_URL=https://legacy-api.example.com
envPrefix を書く前は、VUE_APP_ で始まる変数は読めません。
import.meta.env.VUE_APP_API_URL = undefined
import.meta.env.VITE_API_URL = https://stg-api.example.com
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
return {
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
envPrefix: ['VITE_', 'VUE_APP_'],
server: {
port: Number(env.VITE_PORT),
},
}
})
import.meta.env.VUE_APP_API_URL = https://legacy-api.example.com
import.meta.env.VITE_API_URL = https://stg-api.example.com
envPrefix に配列で2つ書くと、どちらの接頭辞も読めるようになりました。
手順2:読み方を置き換える
| 置き換え前 | 置き換え後 |
|---|---|
process.env.VUE_APP_API_URL |
import.meta.env.VUE_APP_API_URL(のちに VITE_API_URL) |
process.env.NODE_ENV === 'production' |
import.meta.env.PROD |
process.env.NODE_ENV === 'development' |
import.meta.env.DEV |
process.env.BASE_URL |
import.meta.env.BASE_URL |
置き換え漏れは、前の章のとおり vue-tsc が Cannot find name 'process' として教えてくれます。
envPrefix を空にはできない
「全部読めるようにしたい」と envPrefix を空文字にすると、Vite がエラーで止めてくれます。
error during build:
Error: envPrefix option contains value '', which could lead unexpected exposure of sensitive information.
at resolveEnvPrefix (file:///home/taro/env-app/node_modules/vite/dist/node/chunks/node.js:6097:36)
意図しない値が配信物に混ざるのを防ぐための仕組みです。接頭辞は必ず何か決めておきます。
つまずきやすい点のまとめ表
| 症状 | 見直す箇所 |
|---|---|
| undefined になる | VITE_ で始まっているか、置き場所、モード、つづり |
process is not defined |
import.meta.env に書き換える |
| if が常に真になる | === 'true' で比べているか |
| 計算結果がおかしい | Number() で変換しているか |
| .env.local の値が使われない | .env.development / .env.production に同じ名前が無いか |
| staging なのに開発用の表示が出ない | DEV は false。MODE === 'staging' で判定する |
| vite.config で値が取れない | loadEnv(mode, process.cwd()) を使う |
| 値を変えたのに公開環境が変わらない | ビルドをやり直したか |
よくある質問
Q. .env はGitに入れてよいですか?
A. 全員共通で、見られて困らない値だけを書いた .env は入れて構いません。自分のPCだけの値は .env.local に書きます。create-vue の .gitignore には *.local が入っているので、.local 付きのファイルは最初から対象外です。
Q. VITE_ を付ければ秘密の値も使えますか?
A. 動きはしますが、その値はビルド成果物に文字として入り、利用者が読める状態になります。この記事では grep で実際に見つかることを確かめました。秘密の値はサーバー側だけで持ち、ブラウザからはAPIを経由して使います。
Q. モードは development・production 以外も作れますか?
A. 作れます。.env.staging を置いて vite build --mode staging とすれば、MODE は staging になります。このとき PROD は true のままなので、「公開用ビルドかどうか」と「どの設定を読むか」を別々に扱えます。
Q. .env を書き換えたら、開発サーバーを再起動する必要がありますか?
A. Vite 8.3.2 では、.env を保存すると .env changed, restarting server... と表示されて自動で再起動しました。ページを開き直せば新しい値になります。ビルド済みの dist は自動では変わらないので、vite build をやり直します。
まとめ
- .env に
VITE_で始まる名前で書き、import.meta.env.VITE_〜で読む。VITE_の無い変数はブラウザ側では undefined - 優先順位は、シェルの環境変数 → .env.[mode].local → .env.[mode] → .env.local → .env。4ファイルは合成され、同じ名前だけが上書きされる
- 組み込み値は MODE・DEV・PROD・BASE_URL・SSR の5つ。
--mode stagingでも PROD は true - 値はすべて文字列。数値は
Number()、真偽値は=== 'true'で扱う - undefined の主な原因は、接頭辞なし・process.env の使用・モード違い・vite.config 内での参照。設定ファイルでは
loadEnvを使う - env.d.ts に ImportMetaEnv を定義すると型が付き、strictImportMetaEnv を足すとつづりの誤りも検出できる
VITE_を付けた値はビルド成果物に埋め込まれ、grep で見つかる。秘密の値は置かず、サーバー側で持つ- Vue CLI からは、接頭辞(VUE_APP_ → VITE_)と読み方(process.env → import.meta.env)を変える。envPrefix で両対応にして段階的に進められる
参考
- Vite 公式ドキュメント「環境変数とモード」 https://ja.vite.dev/guide/env-and-mode.html
- Vue CLI 公式ドキュメント「Modes and Environment Variables」 https://cli.vuejs.org/guide/mode-and-env.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vueの環境変数の使い方|.envとVITE_・import.meta.env」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。