This teaching plugin demonstrates a virtual environment module for Vite 5/6. Environment-file changes trigger a full-page reload in development; it does not replace the built-in env system.
import env from 'virtual:env'
console.log(env.VITE_API_URL)It will automatically read:
.env.env.local.env.development- Or any custom env file you define
And generate a virtual module virtual:env for any code to import directly.
This type of plug-in is very useful in real projects.
🚀 Final effect
You can write in any file:
import env from 'virtual:env'
console.log(env)Output example:
{
"VITE_API_URL": "https://api.xxx.com",
"VITE_TITLE": "My App"
}This module is automatically generated and requires no manual maintenance.
🔥 1. Complete plug-in code (plug-in file: plugins/virtualEnv.js)
The code uses Vite loadEnv for file precedence and variable expansion, and stores cross-hook state in a closure.
import path from 'node:path'
import { loadEnv } from 'vite'
export default function virtualEnvPlugin(options = {}) {
const id = 'virtual:env'
const resolvedId = '\0' + id
let config
let envDir
let clientEnv = {}
function readEnv() {
return loadEnv(config.mode, envDir, 'VITE_')
}
return {
name: 'vite-plugin-virtual-env',
configResolved(resolved) {
config = resolved
envDir = options.envDir
? path.resolve(config.root, options.envDir)
: config.envDir
clientEnv = readEnv()
},
resolveId(source) {
if (source === id) return resolvedId
},
load(source) {
if (source === resolvedId) {
return 'export default ' + JSON.stringify(clientEnv)
}
},
configureServer(server) {
const files = new Set([
'.env', '.env.local',
'.env.' + config.mode, '.env.' + config.mode + '.local'
].map(file => path.resolve(envDir, file)))
server.watcher.add([...files])
const refresh = file => {
if (!files.has(path.resolve(file))) return
clientEnv = readEnv()
const mod = server.moduleGraph.getModuleById(resolvedId)
if (mod) server.moduleGraph.invalidateModule(mod)
server.ws.send({ type: 'full-reload', path: '*' })
}
for (const event of ['add', 'change', 'unlink']) {
server.watcher.on(event, refresh)
}
server.watcher.once('close', () => {
for (const event of ['add', 'change', 'unlink']) {
server.watcher.off(event, refresh)
}
})
}
}
}📌 2. Use in vite.config.js
import { defineConfig } from 'vite'
import virtualEnvPlugin from './plugins/virtualEnv.js'
export default defineConfig({
plugins: [
virtualEnvPlugin()
]
})📁 3. Usage examples
Assuming your .env.development:
VITE_API_URL=https://api.dev.com
VITE_APP_NAME=Dev App
SECRET_KEY=xxxIn your code you use:
import env from 'virtual:env'
console.log(env)Result (only variables with the VITE_ prefix are exposed):
{
"VITE_API_URL": "https://api.dev.com",
"VITE_APP_NAME": "Dev App"
}🔄 4. Support HMR
When an environment file is added, changed, or removed, the plugin reloads its data, invalidates any loaded virtual module, and triggers a full-page reload.
For example, change .env.local:
-VITE_APP_NAME="Dev App"
+VITE_APP_NAME="Dev App Updated"The browser will receive:
full-reloadThis is behavior added by this plugin; restart the development server when changing built-in Vite env values as documented.
⚙️ 5. Why does Vite limit exposure to only the VITE_ prefix?
Vite’s official policy is also:
只有 VITE_ 前缀的变量才会暴露给浏览器This is a security policy to avoid leaking sensitive variables to the client.
You can decide whether to strictly limit it, which can be customized in the plug-in:
if (k.startsWith('CLIENT_'))🧠 6. Summary of plug-in principles (important)
Virtual module process:
resolveId('virtual:env')→ return\0virtual:envload('\0virtual:env')→ return env JS content- The browser can directly get the generated module when importing
.envchange → watcher → invalidate the module if present → full-page reload
This is the most beautiful part of Vite’s architecture: Virtual module + HMR + automatic dependency graph refresh