环境变量
环境变量的用途是将数据库密码、应用密钥或 API key 等密钥,以及应用代码库之外的其他配置存储起来。
此外,环境变量还可用于为不同环境提供不同的配置。例如,你可以在测试期间使用内存 mailer,在开发期间使用 SMTP mailer,在生产环境使用第三方服务。
由于所有操作系统、部署平台和 CI/CD 流水线都支持环境变量,它们已成为存储密钥和环境相关配置的事实标准。
在本指南中,我们将学习如何在 AdonisJS 应用中利用环境变量。
读取环境变量
Node.js 原生通过 process.env 全局属性将所有环境变量作为对象暴露出来,你可以按如下方式访问它们。
process.env.NODE_ENV
process.env.HOST
process.env.PORT
使用 AdonisJS 的 env 模块
通过 process.env 对象读取环境变量在 AdonisJS 侧无需任何配置,因为 Node.js 运行时本身支持。不过,在本文档的其余部分,我们将使用 AdonisJS 的 env 模块,原因如下。
- 能够从多个
.env文件中存储和解析环境变量。 - 在应用启动时就校验环境变量。
- 对经过校验的环境变量提供静态类型安全。
env 模块在 start/env.ts 文件中实例化,你可以在应用的其他位置按如下方式访问它。
import env from '#start/env'
env.get('NODE_ENV')
env.get('HOST')
env.get('PORT')
// 当 PORT 未定义时返回 3333
env.get('PORT', 3333)
与 Edge 模板共享 env 模块
如果你想在 edge 模板中访问环境变量,则必须将 env 模块作为全局变量与 edge 模板共享。
你可以在 start 目录中创建 view.ts 作为预加载文件,并在其中写入以下代码行。
这样做不会将 env 模块暴露给浏览器。env 模块仅在服务端渲染期间可用。
import env from '#start/env'
import edge from 'edge.js'
edge.global('env', env)
校验环境变量
环境变量的校验规则使用 Env.create 方法定义在 start/env.ts 文件中。
当你首次导入该文件时,校验会自动执行。通常,start/env.ts 文件会被你项目中的某个配置文件导入。如果没有,AdonisJS 会在启动应用之前隐式导入该文件。
Env.create 方法接受校验 schema 作为键值对。
- 键是环境变量的名称。
- 值是执行校验的函数。它可以是一个自定义的 inline 函数,也可以是对预定义 schema 方法(如
schema.string或schema.number)的引用。
import Env from '@adonisjs/core/env'
/**
* App root is used to locate .env files inside
* the project root.
*/
const APP_ROOT = new URL('../', import.meta.url)
export default await Env.create(APP_ROOT, {
HOST: Env.schema.string({ format: 'host' }),
PORT: Env.schema.number(),
APP_KEY: Env.schema.string(),
APP_NAME: Env.schema.string(),
CACHE_VIEWS: Env.schema.boolean(),
SESSION_DRIVER: Env.schema.string(),
NODE_ENV: Env.schema.enum([
'development',
'production',
'test'
] as const),
})
静态类型信息
同一套校验规则也被用于推断静态类型信息。使用 env 模块时即可获得该类型信息。
校验器 schema API
schema.string
schema.string 方法确保值为有效的字符串。空字符串会导致校验失败,你必须使用可选变体来允许空字符串。
{
APP_KEY: Env.schema.string()
}
// 标记为可选
{
APP_KEY: Env.schema.string.optional()
}
// 带条件地标记为可选
{
APP_KEY: Env.schema.string.optionalWhen(() => process.env.NODE_ENV === 'production')
}
字符串值可以针对其格式进行校验。以下是可用格式的列表。
host
校验值为有效的 URL 或 IP 地址。
{
HOST: Env.schema.string({ format: 'host' })
}
url
校验值为有效的 URL。可选地,你可以通过允许 URL 不带 protocol 或 tld 来放宽校验。
{
S3_ENDPOINT: Env.schema.string({ format: 'url' })
// 允许不带协议的 URL
S3_ENDPOINT: Env.schema.string({ format: 'url', protocol: false })
// 允许不带 tld 的 URL
S3_ENDPOINT: Env.schema.string({ format: 'url', tld: false })
}
校验值为有效的电子邮件地址。
{
SENDER_EMAIL: Env.schema.string({ format: 'email' })
}
schema.boolean
schema.boolean 方法确保值为有效的布尔值。空值会导致校验失败,你必须使用可选变体来允许空值。
字符串形式的 'true'、'1'、'false' 和 '0' 会被转换为布尔数据类型。
{
CACHE_VIEWS: Env.schema.boolean()
}
// 标记为可选
{
CACHE_VIEWS: Env.schema.boolean.optional()
}
// 带条件地标记为可选
{
CACHE_VIEWS: Env.schema.boolean.optionalWhen(() => process.env.NODE_ENV === 'production')
}
schema.number
schema.number 方法确保值为有效的数字。数字值的字符串形式会被转换为数字数据类型。
{
PORT: Env.schema.number()
}
// 标记为可选
{
PORT: Env.schema.number.optional()
}
// 带条件地标记为可选
{
PORT: Env.schema.number.optionalWhen(() => process.env.NODE_ENV === 'production')
}
schema.enum
schema.enum 方法针对某个预定义值校验环境变量。枚举选项可以指定为值数组,或 TypeScript 原生枚举类型。
{
NODE_ENV: Env
.schema
.enum(['development', 'production'] as const)
}
// 标记为可选
{
NODE_ENV: Env
.schema
.enum
.optional(['development', 'production'] as const)
}
// 带条件地标记为可选
{
NODE_ENV: Env
.schema
.enum
.optionalWhen(
() => process.env.NODE_ENV === 'production',
['development', 'production'] as const
)
}
// 使用原生枚举
enum NODE_ENV {
development = 'development',
production = 'production'
}
{
NODE_ENV: Env.schema.enum(NODE_ENV)
}
自定义函数
自定义函数可以执行 schema API 未覆盖的校验。
该函数接收环境变量的名称作为第一个参数,值作为第二个参数。它必须返回校验后的最终值。
{
PORT: (name, value) => {
if (!value) {
throw new Error('Value for PORT is required')
}
if (isNaN(Number(value))) {
throw new Error('Value for PORT must be a valid number')
}
return Number(value)
}
}
定义环境变量
在开发环境
开发期间,环境变量定义在 .env 文件中。env 模块会在项目根目录中查找该文件,并在其存在时自动解析。
PORT=3333
HOST=0.0.0.0
NODE_ENV=development
APP_KEY=sH2k88gojcp3PdAJiGDxof54kjtTXa3g
SESSION_DRIVER=cookie
CACHE_VIEWS=false
在生产环境
生产环境中,建议使用你的部署平台来定义环境变量。当今大多数现代部署平台都原生支持通过其 Web UI 定义环境变量。
假设你的部署平台不提供定义环境变量的方式。你可以在项目根目录或生产服务器上的其他位置创建一个 .env 文件。
AdonisJS 会自动从项目根目录读取 .env 文件。不过,当 .env 文件存储在其他位置时,你必须设置 ENV_PATH 变量。
# 尝试从项目根目录读取 .env 文件
node server.js
# 从 "/etc/secrets" 目录读取 .env 文件
ENV_PATH=/etc/secrets node server.js
在测试环境
特定于测试环境的变量必须定义在 .env.test 文件中。该文件中的值会覆盖 .env 文件中的值。
NODE_ENV=development
SESSION_DRIVER=cookie
ASSETS_DRIVER=vite
NODE_ENV=test
SESSION_DRIVER=memory
ASSETS_DRIVER=fake
// 在测试期间
import env from '#start/env'
env.get('SESSION_DRIVER') // memory
所有其他 dot-env 文件
除了 .env 文件外,AdonisJS 还会处理来自以下 dot-env 文件的环境变量。因此,你可以按需可选地创建这些文件。
排名最靠前的文件会覆盖排名靠后文件中的值。
| 排名 | 文件名 | 说明 |
|---|---|---|
| 第 1 | .env.[NODE_ENV].local |
为当前 NODE_ENV 加载。例如,如果 NODE_ENV 设置为 development,则会加载 .env.development.local 文件。
|
| 第 2 | .env.local |
在所有环境中加载,但 test 和 testing 环境除外 |
| 第 3 | .env.[NODE_ENV] |
为当前 NODE_ENV 加载。例如,如果 NODE_ENV 设置为 development,则会加载 .env.development 文件。
|
| 第 4 | .env |
在所有环境中加载。当其中存储密钥时,你应将该文件加入 .gitignore。 |
使用标识符进行插值
你可以定义并使用"标识符"来改变插值行为。标识符是一个前缀环境变量值的字符串,让你可以自定义值的解析方式。
import { EnvParser } from '@adonisjs/env'
EnvParser.defineIdentifier('base64', (value) => {
return Buffer.from(value, 'base64').toString()
})
const envParser = new EnvParser(`
APP_KEY=base64:U7dbSKkdb8wjVFOTq2osaDVz4djuA7BRLdoCUJEWxak=
`)
console.log(await envParser.parse())
在上面的示例中,base64: 前缀告诉 env 解析器在返回值之前先将其从 base64 解码。
或者,你可以使用 defineIdentifierIfMissing 方法定义标识符。该方法不会覆盖已有的标识符。
EnvParser.defineIdentifierIfMissing('base64', (value) => {
return Buffer.from(value, 'base64').toString()
})
你可以直接在 start/env.ts 文件中使用这些方法。
import { Env } from '@adonisjs/core/env'
Env.defineIdentifier('base64', (value) => {
return Buffer.from(value, 'base64').toString()
})
export default await Env.create(APP_ROOT, {
APP_KEY: Env.schema.string()
})
在 dot-env 文件中使用变量
在 dot-env 文件中,你可以使用变量替换语法引用其他环境变量。
在以下示例中,我们从 HOST 和 PORT 属性计算出 APP_URL。
HOST=localhost
PORT=3333
URL=$HOST:$PORT
$ 符号之后的所有字母、数字和**下划线(_)**会被用来组成变量名。如果变量名中包含除下划线以外的特殊字符,则必须将变量名用大括号 {} 包裹起来。
REDIS-USER=admin
REDIS-URL=localhost@${REDIS-USER}
转义 $ 符号
要将 $ 符号作为值使用,你必须对其进行转义以防止变量替换。
PASSWORD=pa\$\$word