配置
使用 Config 与 ConfigProvider 描述、加载、校验并测试应用配置。
Effect 把配置的描述与提供其值的来源分离开来:
Config<T>描述如何加载并解码一个类型为T的值。它同时也是一个Effect<T, ConfigError>,因此可以直接在Effect.gen中被 yield。ConfigProvider提供原始值。默认的 provider 从环境变量读取。
把这两件事分开,应用代码就可以只定义一次自己的需求,然后为生产环境、本地开发或测试选择不同的 provider。
定义并解析 Config
对于单个值,使用 Config 模块中的构造器;要把它们组合起来,使用 Config.all。
示例(使用指定 Provider 解析 Config)
import { Config, ConfigProvider, Effect } from "effect"
const AppConfig = Config.all({
host: Config.NonEmptyString("HOST"),
port: Config.Port("PORT"),
})
const provider = ConfigProvider.fromEnv({
env: {
HOST: "localhost",
PORT: "8080",
},
})
const result = Effect.runSync(AppConfig.parse(provider))
result // => { host: "localhost", port: 8080 }
当 provider 已经可以显式拿到时,调用 config.parse(provider) 会很有用,尤其是在测试中。
在应用代码里,Config 也可以改为作为一个 Effect 被 yield。此时它会使用安装在 Effect 上下文中的 ConfigProvider;如果没有显式安装 provider,Effect 会使用 ConfigProvider.fromEnv()。
示例(使用默认的环境 Provider)
import { Config, Effect } from "effect"
const AppConfig = Config.all({
host: Config.NonEmptyString("HOST"),
port: Config.Port("PORT").pipe(Config.withDefault(8080)),
})
const program = Effect.gen(function* () {
const { host, port } = yield* AppConfig
console.log(`Application started: ${host}:${port}`)
})
Effect.runPromise(program)
HOST=localhost PORT=3000 npx tsx app.ts
Application started: localhost:3000
内置的 Config 值
Effect 为常见的标量值提供了便捷构造器:
| 构造器 | 结果 |
|---|---|
String(name?) | 字符串 |
NonEmptyString(name?) | 非空字符串 |
Finite(name?) | 有限数值 |
Int(name?) | 整数 |
Port(name?) | 1 到 65,535 之间的整数 |
Boolean(name?) | 布尔值 |
Literal(value, name?) | 单个字面量值 |
Literals(values, name?) | 若干字面量值之一 |
Duration(name?) | 一个 Duration |
Date(name?) | 一个 Date |
URL(name?) | 一个 URL |
LogLevel(name?) | 一个 LogLevel |
Redacted(name?) | 一个 Redacted<string> |
对于普通的数值配置,优先使用 Config.Finite。Config.Number 也会接受 NaN 和 Infinity 这类非有限值,而它们很少是有效的配置值。
Config.Boolean 接受区分大小写的字符串 true、false、yes、no、on、off、1、0、y 和 n。
将 Config 与 Schema 一起使用
当某项配置需要自定义类型、校验或结构化表示时,使用 Config.schema。provider 提供编码后的表示,而生成的 Config 会产出该 schema 的 Type。
示例(校验配置值)
import { Config, ConfigProvider, Effect, Schema } from "effect"
const Username = Schema.String.check(
Schema.isMinLength(4, { message: "Expected at least 4 characters" }),
)
const username = Config.schema(Username, "USERNAME")
const provider = ConfigProvider.fromEnv({ env: { USERNAME: "alice" } })
Effect.runSync(username.parse(provider)) // => "alice"
Schema 也可以描述整个配置对象。
示例(读取结构化配置)
import { Config, ConfigProvider, Effect, Schema } from "effect"
const ServerConfig = Config.schema(
Schema.Struct({
host: Schema.String,
port: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 })),
}),
"server",
)
const provider = ConfigProvider.fromUnknown({
server: {
host: "localhost",
port: 8080,
},
})
Effect.runSync(ServerConfig.parse(provider)) // => { host: "localhost", port: 8080 }
Config.schema 会从 schema 推导出一种树状的字符串编码。这样一来,同一份结构化配置既可以通过环境 provider 读取 SERVER_HOST 和 SERVER_PORT,也可以通过 ConfigProvider.fromUnknown 读取 { server: { host, port } }。
更多细节参见 Schema 的 Config。
数组与 Record
Config.Array 和 Config.Record 用于读取这类值:既可以从结构化数据中读取,也可以从一个带分隔符的字符串中读取。两者都直接返回 Config。
示例(读取逗号分隔的数组)
import { Config, ConfigProvider, Effect, Schema } from "effect"
const exporters = Config.Array(Schema.String, "EXPORTERS")
const provider = ConfigProvider.fromEnv({
env: { EXPORTERS: "otlp,prometheus" },
})
Effect.runSync(exporters.parse(provider)) // => ["otlp", "prometheus"]
Config.Record(key, value, path) 类似地既接受一个 record,也接受形如 "service.name=api,service.version=1.0" 的字符串。这两个构造器都接受用于自定义分隔符的选项。
当 provider 必须提供结构化的数组或对象、而不是一个带分隔符的标量值时,普通的 Schema.Array 和 Schema.Record 依然有用。
组合 Config
Config.all 把多个 config 组合成一个元组或一个具名对象,同时保留输入的形状。
示例(组合并嵌套 Config)
import { Config, ConfigProvider, Effect } from "effect"
const DatabaseConfig = Config.all({
host: Config.NonEmptyString("HOST"),
port: Config.Port("PORT"),
}).pipe(Config.nested("DATABASE"))
const provider = ConfigProvider.fromEnv({
env: {
DATABASE_HOST: "localhost",
DATABASE_PORT: "5432",
},
})
Effect.runSync(DatabaseConfig.parse(provider)) // => { host: "localhost", port: 5432 }
Config.nested(config, path) 会为该 config 执行的每一次查找都加上一个字符串或路径前缀。在使用 ConfigProvider.fromEnv 时,路径片段之间用 _ 连接。
对于那些既接受已构建好的 Config<T>、又接受嵌套 config record 的 API,Config.Wrap<T> 和 Config.unwrap 分别提供对应的输入类型与转换。
默认值与可选值
Config.withDefault 仅在相关输入都不存在时才提供一个值。
示例(提供默认值)
import { Config, ConfigProvider, Effect } from "effect"
const port = Config.Port("PORT").pipe(Config.withDefault(8080))
const provider = ConfigProvider.fromUnknown({})
Effect.runSync(port.parse(provider)) // => 8080
无效输入不会被当作缺失处理。例如 PORT=not-a-port 仍然会失败,而不会悄悄产出 8080。组合 config 也遵循同样的规则:如果组中的一部分已提供,那么缺失或无效的兄弟项会让这个组不完整,而不是用默认值替换整个组。
如果希望缺失时产出 Option,请使用 Config.option。
示例(读取可选值)
import { Config, ConfigProvider, Effect, Option } from "effect"
const apiKey = Config.String("API_KEY").pipe(Config.option)
const provider = ConfigProvider.fromUnknown({})
Effect.runSync(apiKey.parse(provider)) // => Option.none()
在任何 Config 错误之后 fallback
Config.orElse 比 Config.withDefault 适用范围更广:它在出现任何 ConfigError(包括无效输入)之后都会尝试另一个 config。
示例(fallback 到另一个 Config)
import { Config, ConfigProvider, Effect } from "effect"
const host = Config.String("HOST").pipe(
Config.orElse(() => Config.String("FALLBACK_HOST")),
)
const provider = ConfigProvider.fromUnknown({ FALLBACK_HOST: "localhost" })
Effect.runSync(host.parse(provider)) // => "localhost"
由于 orElse 可以从校验错误中恢复,只有在你确实有意替换无效输入时才使用它。对于「只有缺失才触发 fallback」这种常见情形,请使用 withDefault。
转换值
对于不会失败的转换,使用 Config.map。
示例(映射 Config)
import { Config, ConfigProvider, Effect } from "effect"
const origin = Config.all({
host: Config.NonEmptyString("HOST"),
port: Config.Port("PORT"),
}).pipe(Config.map(({ host, port }) => `http://${host}:${port}`))
const provider = ConfigProvider.fromUnknown({
HOST: "localhost",
PORT: 8080,
})
Effect.runSync(origin.parse(provider)) // => "http://localhost:8080"
对于校验和解析,优先把规则表达在 schema 中并使用 Config.schema。如果某个转换需要改为返回 Effect<B, ConfigError>,可以使用 Config.mapEffect。
处理敏感值
Config.Redacted 会把字符串包装成 Redacted<string>,其字符串表示不会暴露该值。要访问这个秘密,必须显式调用 Redacted.value。
示例(保护机密值)
import { Config, ConfigProvider, Effect, Redacted } from "effect"
const apiKey = Config.Redacted("API_KEY")
const provider = ConfigProvider.fromEnv({
env: { API_KEY: "secret-value" },
})
const result = Effect.runSync(apiKey.parse(provider))
String(result) // => "<redacted>"
Redacted.value(result) // => "secret-value"
如果需要先解码某个值、再把它包装起来,可以把 Config.schema 与 Schema.RedactedFromValue 结合使用。
import { Config, ConfigProvider, Effect, Redacted, Schema } from "effect"
const secretNumber = Config.schema(
Schema.RedactedFromValue(Schema.FiniteFromString),
"SECRET_NUMBER",
)
const provider = ConfigProvider.fromEnv({
env: { SECRET_NUMBER: "42" },
})
const result = Effect.runSync(secretNumber.parse(provider))
Redacted.value(result) // => 42
Config Provider
ConfigProvider 模块包含针对几种常见来源的 provider:
| 构造器 | 来源 |
|---|---|
fromEnv | 环境变量 |
fromUnknown | 内存中的 JavaScript 值,包括解析后的 JSON |
fromDotEnvContents | .env 文件的内容 |
fromDotEnv | 通过 FileSystem service 读取的 .env 文件 |
fromDir | 目录树,包括挂载的 ConfigMap 与 Secret |
make | 自定义的后备存储 |
fromEnv 默认会合并 process.env 与 import.meta.env。传入 { env } 会替换掉这个来源,这使它便于在测试和非 Node 运行时中使用。
fromEnv 和 fromUnknown 默认都把空字符串视为缺失。当空字符串本身是有意义的值时,请传入 { preserveEmptyStrings: true }。
加载内存中的对象
对于 JavaScript 值或解析后的 JSON 对象,使用 ConfigProvider.fromUnknown。对象的键和数组索引会成为配置路径片段。
示例(加载解析后的 JSON 对象)
import { Config, ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromUnknown(
JSON.parse(`{"server":{"host":"localhost","port":8080}}`),
)
const server = Config.all({
host: Config.String("host"),
port: Config.Port("port"),
}).pipe(Config.nested("server"))
Effect.runSync(server.parse(provider)) // => { host: "localhost", port: 8080 }
加载 .env 文件与目录
ConfigProvider.fromDotEnvContents 解析已经加载好的字符串。ConfigProvider.fromDotEnv() 默认从当前目录读取 .env,并返回一个需要 FileSystem service 的 Effect。
ConfigProvider.fromDir() 把每个文件读取为一个叶子值,把每个目录读取为一个 record。这对于以文件形式挂载的配置很有用,例如 Kubernetes 的 ConfigMap 和 Secret。它需要 FileSystem 与 Path 这两个 service。
转换 Provider 路径
Provider 组合子会改变所有 config 查找其路径的方式:
ConfigProvider.nested为每一次查找加上前缀。ConfigProvider.constantCase把字符串路径片段转换为CONSTANT_CASE。ConfigProvider.mapInput执行任意的路径转换。
示例(把 camelCase 键适配为环境变量)
import { Config, ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromEnv({
env: { DATABASE_HOST: "localhost" },
}).pipe(ConfigProvider.constantCase)
const databaseHost = Config.String("databaseHost")
Effect.runSync(databaseHost.parse(provider)) // => "localhost"
组合 Provider
ConfigProvider.orElse(primary, fallback) 仅在 primary provider 于所请求路径上没有值时才查询 fallback。与 Config.orElse 不同,它不会从来源错误或 schema 校验错误中恢复。
示例(为 Provider 添加默认值)
import { Config, ConfigProvider, Effect } from "effect"
const environment = ConfigProvider.fromEnv({
env: { HOST: "production.example.com" },
})
const defaults = ConfigProvider.fromUnknown({
HOST: "localhost",
PORT: 8080,
})
const provider = ConfigProvider.orElse(environment, defaults)
const app = Config.all({
host: Config.String("HOST"),
port: Config.Port("PORT"),
})
Effect.runSync(app.parse(provider)) // => { host: "production.example.com", port: 8080 }
安装 Provider
使用 ConfigProvider.layer(provider) 可以替换那些作为 Effect 被 yield 的 config 所使用的 provider。ConfigProvider.layerAdd(provider) 则是为当前 provider 添加一个 fallback;传入 { asPrimary: true } 可以让新增的 provider 优先。
示例(提供 ConfigProvider Layer)
import { Config, ConfigProvider, Effect } from "effect"
const provider = ConfigProvider.fromUnknown({ PORT: 8080 })
const ProviderLayer = ConfigProvider.layer(provider)
const program = Effect.gen(function* () {
return yield* Config.Port("PORT")
})
Effect.runSync(Effect.provide(program, ProviderLayer)) // => 8080
对于单个 config,config.parse(provider) 更简单,而且不会改变 Effect 程序中其余部分所使用的 provider。