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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?