Fastify 最佳实践 #1: 环境变量
前言:为什么环境变量如此重要?
在现代应用开发中,将 配置 与 代码分离 是一条黄金法则。我们的应用需要部署在不同环境中,如本地开发、持续集成(CI)、测试环境和生产环境。每个环境都有其独特的配置,例如数据库连接字符串、API 密钥、端口号等。
环境变量正是实现这种分离的最佳工具。正确地管理它们,可以极大地提升我们应用的灵活性、安全性和可维护性。
这篇博客将从 Fastify 开发中的常见做法讲起,分析其痛点,并最终引导你掌握一套更纯粹、更强大、更符合云原生时代要求的环境变量管理方案。
1. 常见做法:在代码中使用 dotenv
对于大多数 Node.js 开发者来说,dotenv 是一个非常熟悉的库。它能将 .env 文件中的变量加载到 process.env 中。
一个典型的项目结构可能包含以下文件:
.env:用于本地开发环境。.env.test:用于测试环境。.env.production:用于生产环境。
为了让 Fastify 应用加载这些变量,我们通常会在入口文件(如 index.ts)的顶部添加如下代码:
// index.ts
import dotenv from 'dotenv';
import path from 'path';
// 根据 NODE_ENV 加载不同的 .env 文件
const envFile = process.env.NODE_ENV ? `.env.${process.env.NODE_ENV}` : '.env';
dotenv.config({ path: path.resolve(__dirname, envFile) });
// ... 接下来是 Fastify 的启动代码
这种方式虽然能工作,但是它存在一些明显的痛点:
- 配置与代码耦合:环境变量的加载逻辑侵入了业务代码。代码的职责应该是实现业务功能,而不是关心自己运行在哪个环境以及如何加载配置。
- 构建产物不纯粹:打包时,所有环境的
.env文件(.env.test,.env.production等)都可能被包含在最终的构建产物中。这不仅增加了包体积,还可能在无意中泄露其他环境的敏感配置。 - 配置来源不统一:当使用 Docker 或 CI/CD 工具时,一部分配置可能来自服务器的环境变量,另一部分来自项目内的
.env文件,导致管理混乱,排查问题时也更加困难。
2. 最佳实践:让配置在运行时注入
我觉得代码需要尽量纯粹,打包之后的代码里不应该有和业务逻辑无关的东西。因此,配置应该存储在环境中,也就是说应用程序本身应该是环境无关的,它只从运行环境中读取配置,而不需要知道这些配置是如何被注入的。
因此,我们需要将环境变量的加载过程从应用代码中剥离,移到应用启动之前。
实现方式一:使用 dotenv-cli(通用方案)
dotenv-cli 是一个简单有效的命令行工具,它可以在执行任何命令之前,先将指定的 .env 文件加载到环境中。
第一步:安装 dotenv-cli 作为开发依赖。
npm i -D dotenv-cli
# 或者
yarn add -D dotenv-cli
第二步:修改 package.json 中的 scripts 命令。
我们不再需要在代码中 import 'dotenv',而是直接在启动命令前加上 dotenv 指令。
// package.json
{
"scripts": {
"start:dev": "dotenv -e .env -- ts-node index.ts",
"start:prod": "dotenv -e .env.production -- node dist/index.js",
"test": "dotenv -e .env.test -- jest"
}
}
实现方式二:使用 Node.js 原生能力(现代方案)
好消息是,从 Node.js v20.6.0 开始,Node.js 已经原生支持 —env-file 标志。这意味着我们不再需要任何外部依赖就能实现同样的效果!
用法如下:
// package.json
{
"scripts": {
"start:dev": "node --env-file=.env --loader=ts-node/esm index.ts",
"start:prod": "node --env-file=.env.production dist/index.js"
}
}
3. 更进一步:校验你的环境变量
仅仅加载环境变量还不够,一个健壮的应用还需要确保这些变量是存在且有效的。例如,PORT 必须是一个数字,API_URL 必须是一个合法的 URL。
Fastify 的官方插件 @fastify/env 就是为此而生的利器。它会在应用启动时,根据你定义的 schema 来校验环境变量,如果校验失败,应用会立即报错退出,从而避免了在运行时出现因配置错误导致的潜在问题。
第一步:安装插件。
npm i -D @fastify/env
# 或者
yarn add -D @fastify/env
第二步:在 Fastify 中注册并配置它。
import fastifyEnv from '@fastify/env';
const schema = {
type: 'object',
required: ['PORT', 'DATABASE_URL'],
properties: {
PORT: {
type: 'string',
default: '3000',
},
DATABASE_URL: {
type: 'string',
},
LOG_LEVEL: {
type: 'string',
default: 'info',
},
},
};
const options = {
confKey: 'config', // 注册到 fastify 实例下的键名,默认为 'config'
schema: schema,
};
await fastify.register(fastifyEnv, options);
// 在你的应用中,可以通过 fastify.config 来访问这些经过校验和类型转换的变量
// fastify.config.PORT
通过这种方式,我们不仅加载了环境变量,还为它们建立了一道可靠的“防火墙”。
4. 总结
我们回顾一下这条演进路径:
- 初始阶段:在代码中用
dotenv加载配置,导致代码与配置耦合。 - 最佳实践:使用
dotenv-cli或 Node.js 原生的--env-file在应用启动前注入配置,实现代码与配置的彻底分离。 - 进阶增强:通过
@fastify/env对环境变量进行启动时校验,提升应用的健壮性。
这种将配置从代码中分离并在运行时注入的模式,与 Docker、Kubernetes 等容器化技术的思想完全契合。在 Docker 中,我们可以通过 docker run --env-file .env.production ... 来启动容器;在 Kubernetes 中,我们可以使用 ConfigMap 和 Secret 来管理配置。我们的应用程序无需任何改动,就能无缝地部署在这些现代化的基础设施上。
希望这篇分享能帮助你构建更专业、更可靠的 Fastify 应用。