0%

跨环境配置实践——前后端通用的环境变量管理方案

团队开发中遇到跨平台环境变量设置的问题,Windows 同事设置的环境变量在 Mac 上不起作用。cross-env 这个神器,可以完美解决不同操作系统的环境变量差异问题。

cross-env 简介

  cross-env 是一个跨平台的环境变量设置工具,它能够在 Windows、macOS 和 Linux 等不同操作系统上统一设置环境变量的语法。在前端开发中,我们经常需要在不同的环境下运行不同的命令,比如设置 NODE_ENV 为 development 或 production,但由于各操作系统 shell 语法的差异,同一套 package.Json 脚本在不同平台上可能会出现兼容性问题。

  在 Windows 系统中,环境变量通常使用SET KEY=VALUE语法,而在 Unix-like 系统(macOS、Linux)中使用KEY=VALUE语法。cross-env 正是为了解决这个问题而生,它提供了一个统一的命令行接口,让我们可以在任何平台上使用相同的环境变量设置语法。

为什么需要 cross-env?

1
2
3
4
5
6
7
8
# Windows PowerShell
SET NODE_ENV=production && webpack --mode production

# Windows Command Prompt
SET NODE_ENV=production && webpack --mode production

# macOS/Linux
NODE_ENV=production webpack --mode production

  如果没有 cross-env,我们就需要为不同平台维护不同的脚本,这会增加开发和维护成本。cross-env 让我们可以用一行命令解决跨平台环境变量设置的问题:

1
# 使用 cross-env,跨平台兼容 cross-env NODE_ENV=production webpack --mode production

安装和基本使用

安装 cross-env

1
2
3
4
5
6
7
# 作为开发依赖安装 npm install --save-dev cross-env

# 或使用 yarn
yarn add --dev cross-env

# 或使用 pnpm
pnpm add -D cross-env

基本使用方法

1
2
3
4
5
# 基本语法 cross-env ENV_VAR=value command

# 示例 cross-env NODE_ENV=production npm run build
cross-env DEBUG=app:* npm run dev
cross-env PORT=3001 npm start

在 package.Json 中使用

1
2
3
4
5
6
7
8
9
10
{
"scripts": {
"dev": "cross-env NODE_ENV=development webpack serve",
"build": "cross-env NODE_ENV=production webpack --mode production",
"test": "cross-env NODE_ENV=test jest",
"analyze": "cross-env ANALYZE=true webpack --mode production",
"start:prod": "cross-env NODE_ENV=production Node server.JS",
"start:staging": "cross-env NODE_ENV=staging PORT=4000 Node server.JS"
}
}

高级用法

1. 设置多个环境变量

1
2
3
4
5
6
7
{
"scripts": {
"build:staging": "cross-env NODE_ENV=staging API_URL=https://API.staging.com webpack --mode production",
"dev:debug": "cross-env NODE_ENV=development DEBUG=true LOG_LEVEL=verbose webpack serve",
"test:integration": "cross-env NODE_ENV=test DB_HOST=localhost DB_PORT=5432 jest --config integration.config.JS"
}
}

2. 条件环境变量设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// scripts/build.JS
const { spawn } = require('child_process');
const isProd = process.argv.includes('--prod');

const envVars = {
NODE_ENV: isProd ? 'production' : 'development',
BUILD_TIME: new Date().toISOString(),
VERSION: require('../package.Json').version
};

// 使用 cross-env 执行命令 const args = Object.entries(envVars)
.map(([key, value]) => `${key}=${value}`)
.concat(['webpack', '--mode', isProd ? 'production' : 'development']);

spawn('cross-env', args, { stdio: 'inherit' });

3. 环境变量预处理

1
2
3
4
5
6
7
{
"scripts": {
"build:windows": "cross-env-shell \"SET PUBLIC_URL=/my-app && webpack --mode production\"",
"build:unix": "cross-env-shell \"PUBLIC_URL=/my-app webpack --mode production\"",
"build:universal": "cross-env PUBLIC_URL=/my-app webpack --mode production"
}
}

实际应用场景

场景1: Webpack 构建配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// webpack.config.JS
const webpack = require('webpack');
const path = require('path');

module.exports = {
mode: process.env.NODE_ENV || 'development',
entry: './src/index.JS',
output: {
path: path.resolve(__dirname, 'dist'),
filename: process.env.NODE_ENV === 'production' ? '[name].[contenthash].JS' : '[name].JS',
publicPath: process.env.PUBLIC_URL || '/'
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': Json.stringify(process.env.NODE_ENV),
'process.env.API_URL': Json.stringify(process.env.API_URL),
'process.env.VERSION': Json.stringify(process.env.VERSION)
})
]
};
1
2
3
4
5
6
7
8
9
// package.Json
{
"scripts": {
"build:dev": "cross-env NODE_ENV=development PUBLIC_URL=/ webpack --mode development",
"build:prod": "cross-env NODE_ENV=production PUBLIC_URL=/my-app webpack --mode production",
"build:staging": "cross-env NODE_ENV=staging PUBLIC_URL=https://staging.example.com webpack --mode production",
"dev": "cross-env NODE_ENV=development PUBLIC_URL=/ webpack serve --mode development"
}
}

场景2: Jest 测试环境

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"scripts": {
"test": "cross-env NODE_ENV=test jest",
"test:watch": "cross-env NODE_ENV=test jest --watch",
"test:coverage": "cross-env NODE_ENV=test jest --coverage",
"test:e2e": "cross-env NODE_ENV=test TEST_TYPE=e2e jest --config e2e.config.JS",
"test:unit": "cross-env NODE_ENV=test TEST_TYPE=unit jest --config unit.config.JS"
},
"jest": {
"testEnvironment": "Node",
"setupFilesAfterEnv": ["<rootDir>/test/setup.JS"]
}
}
1
2
3
4
5
6
// test/setup.JS
// 根据环境变量设置测试配置 if (process.env.TEST_TYPE === 'e2e') {
console.log('Running end-to-end tests...');
} else if (process.env.TEST_TYPE === 'integration') {
console.log('Running integration tests...');
}

场景3: 数据库环境切换

1
2
3
4
5
6
7
8
9
10
{
"scripts": {
"db:migrate": "cross-env NODE_ENV=development knex migrate:latest",
"db:seed": "cross-env NODE_ENV=development knex seed:run",
"db:migrate:prod": "cross-env NODE_ENV=production DATABASE_URL=postgres://prod_db knex migrate:latest",
"db:reset:test": "cross-env NODE_ENV=test npm run db:drop && npm run db:create && npm run db:migrate",
"start:dev": "cross-env NODE_ENV=development DB_HOST=localhost DB_PORT=5432 npm run dev",
"start:prod": "cross-env NODE_ENV=production DB_HOST=prod-db-server DB_PORT=5432 npm run start"
}
}

场景4: API 环境切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// config/API.JS
const configs = {
development: {
apiUrl: process.env.API_URL || 'http://localhost:3000',
timeout: 5000,
retries: 3
},
staging: {
apiUrl: process.env.API_URL || 'https://API.staging.example.com',
timeout: 10000,
retries: 2
},
production: {
apiUrl: process.env.API_URL || 'https://API.example.com',
timeout: 8000,
retries: 1
}
};

module.exports = configs[process.env.NODE_ENV || 'development'];
1
2
3
4
5
6
7
8
{
"scripts": {
"start:dev": "cross-env NODE_ENV=development API_URL=http://localhost:3000 nodemon server.JS",
"start:staging": "cross-env NODE_ENV=staging API_URL=https://API.staging.example.com Node server.JS",
"start:prod": "cross-env NODE_ENV=production API_URL=https://API.example.com Node server.JS",
"API:test": "cross-env NODE_ENV=development API_URL=http://test-API.example.com npm run test:API"
}
}

与其它工具的集成

1. 与 dotenv 集成

1
2
# 安装 dotenv
npm install dotenv
1
2
3
4
5
// .env.development
NODE_ENV=development
API_URL=http://localhost:3000
PORT=3000
DEBUG=true
1
2
3
4
5
// .env.production
NODE_ENV=production
API_URL=https://API.example.com
PORT=80
DEBUG=false
1
2
3
4
5
6
7
{
"scripts": {
"dev": "cross-env-shell \"Node -r dotenv/config ./scripts/load-env.JS && webpack serve\"",
"start": "cross-env-shell \"Node -r dotenv/config ./server.JS\"",
"build": "cross-env-shell \"Node -r dotenv/config ./scripts/build.JS\""
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// scripts/load-env.JS
const path = require('path');

// 根据 NODE_ENV 加载对应的环境文件 const envFile = process.env.NODE_ENV === 'production'
? '.env.production'
: process.env.NODE_ENV === 'staging'
? '.env.staging'
: '.env.development';

require('dotenv').config({ path: path.resolve(process.cwd(), envFile) });

console.log(`Loaded environment: ${process.env.NODE_ENV}`);
console.log(`API URL: ${process.env.API_URL}`);

2. 与环境检测脚本集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// scripts/check-env.JS
function checkRequiredEnv() {
const required = ['NODE_ENV', 'API_URL'];
const missing = required.filter(key => !process.env[key]);

if (missing.length > 0) {
console.error('Missing required environment variables:', missing);
process.exit(1);
}

console.log('Environment variables check passed');
}

function setDefaults() {
process.env.PORT = process.env.PORT || '3000';
process.env.DB_TIMEOUT = process.env.DB_TIMEOUT || '5000';
process.env.LOG_LEVEL = process.env.LOG_LEVEL || 'info';
}

checkRequiredEnv();
setDefaults();
1
2
3
4
5
6
7
{
"scripts": {
"prestart": "cross-env Node scripts/check-env.JS",
"start": "cross-env NODE_ENV=production Node server.JS",
"dev": "cross-env NODE_ENV=development nodemon server.JS"
}
}

3. 与构建脚本集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// scripts/build.JS
const { execSync } = require('child_process');
const fs = require('fs');

function runBuild() {
const startTime = Date.now();
const env = process.env.NODE_ENV || 'development';
const version = require('../package.Json').version;

// 设置构建相关环境变量 process.env.BUILD_TIME = new Date().toISOString();
process.env.BUILD_VERSION = version;
process.env.BUILD_ENV = env;

try {
// 执行构建命令 const buildCommand = 'webpack --mode ' + (env === 'production' ? 'production' : 'development');
console.log(`Starting build for ${env} environment...`);

execSync(`cross-env ${buildCommand}`, {
stdio: 'inherit',
env: { ...process.env }
});

const duration = Date.now() - startTime;
console.log(`Build completed in ${duration}ms`);

// 生成构建报告 const report = {
environment: env,
version,
buildTime: new Date().toISOString(),
duration
};

fs.writeFileSync('build-report.Json', Json.stringify(report, null, 2));

} catch (error) {
console.error('Build failed:', error.message);
process.exit(1);
}
}

runBuild();

最佳实践

1. 环境变量命名规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"scripts": {
// 使用大写字母和下划线
"build:prod": "cross-env NODE_ENV=production API_BASE_URL=https://API.example.com BUILD_VERSION=1.0.0 webpack --mode production",

// 避免使用连字符
"dev": "cross-env NODE_ENV=development npm run dev",

// 分类管理环境变量
"start:local": "cross-env NODE_ENV=development SERVER_PORT=3000 CLIENT_PORT=3001 DB_HOST=localhost npm run dev",

// 使用有意义的前缀
"test:integration": "cross-env NODE_ENV=test INTEGRATION_TIMEOUT=30000 jest --config integration.config.JS"
}
}

2. 环境变量验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// utils/validate-env.JS
function validateEnvironment() {
const validations = {
NODE_ENV: (value) => ['development', 'staging', 'production'].includes(value),
PORT: (value) => !isNaN(value) && value > 0 && value < 65536,
API_URL: (value) => /^https?:\/\/.+/.test(value),
LOG_LEVEL: (value) => ['error', 'warn', 'info', 'verbose', 'debug', 'silly'].includes(value)
};

const errors = [];

Object.entries(validations).forEach(([key, validator]) => {
const value = process.env[key];
if (value !== undefined && !validator(value)) {
errors.push(`Invalid value for ${key}: ${value}`);
}
});

if (errors.length > 0) {
throw new Error('Environment validation failed:\n' + errors.join('\n'));
}

return true;
}

module.exports = { validateEnvironment };

3. 条件环境变量设置

1
2
3
4
5
6
7
{
"scripts": {
"dev": "cross-env-shell \"[ \\\"$NODE_ENV\\\" = \\\"production\\\" ] && echo \\\"Cannot run dev in production\\\" || npm run dev:start\"",
"build:conditional": "cross-env-shell \"if [ \\\"$NODE_ENV\\\" = \\\"production\\\" ]; then npm run build:prod; else npm run build:dev; fi\"",
"test:env": "cross-env-shell \"if [ \\\"$NODE_ENV\\\" = \\\"test\\\" ]; then jest; else echo \\\"Wrong environment for tests\\\"; fi\""
}
}

常见问题和解决方案

1. 环境变量包含特殊字符

1
2
3
4
5
6
7
8
9
10
11
12
{
"scripts": {
// 处理包含特殊字符的值
"build:complex": "cross-env API_TOKEN='token_with_$pecial_chars' SECRET_KEY='secret=with&amp;chars' webpack --mode production",

// 使用双引号包围
"dev:complex": "cross-env DB_CONNECTION_STRING=\"postgresql://user:pass@host:5432/db?ssl=true\" npm run dev",

// 处理 Json 字符串
"config:Json": "cross-env APP_CONFIG='{\\\"API\\\":\\\"https://API.com\\\",\\\"timeout\\\":5000}' webpack --mode development"
}
}

2. 长命令行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// scripts/complex-build.JS
const { spawn } = require('child_process');
const envVars = {
NODE_ENV: 'production',
API_URL: 'https://API.example.com',
BUILD_VERSION: require('../package.Json').version,
BUILD_TIME: new Date().toISOString(),
ANALYZE: 'true',
SOURCE_MAPS: 'false'
};

function buildComplexCommand() {
const command = 'webpack';
const args = ['--mode', 'production', '--progress'];

if (envVars.ANALYZE === 'true') {
args.push('--analyze');
}

if (envVars.SOURCE_MAPS === 'false') {
args.push('--no-devtool');
}

return { command, args, envVars };
}

function runBuild() {
const { command, args, envVars } = buildComplexCommand();

const child = spawn('cross-env', [
...Object.entries(envVars).map(([key, value]) => `${key}=${value}`),
command,
...args
], {
stdio: 'inherit',
cwd: process.cwd()
});

child.on('error', (err) => {
console.error('Build failed:', err);
process.exit(1);
});
}

runBuild();

3. 与其他工具的兼容性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"scripts": {
// 与 npm-run-all 结合使用
"build:parallel": "npm-run-all --parallel build:client build:server",
"build:client": "cross-env NODE_ENV=production TARGET=client webpack --mode production",
"build:server": "cross-env NODE_ENV=production TARGET=server webpack --mode production",

// 与 concurrently 结合使用
"dev:both": "concurrently \"cross-env NODE_ENV=development npm run dev:client\" \"cross-env NODE_ENV=development npm run dev:server\"",
"dev:client": "cross-env NODE_ENV=development CLIENT_ONLY=true webpack serve",
"dev:server": "cross-env NODE_ENV=development SERVER_ONLY=true nodemon server.JS",

// 与 rimraf 结合使用
"clean:build": "cross-env-shell \"rimraf dist coverage && mkdir -p dist && cross-env BUILD_DIR=dist webpack --mode production\""
}
}

性能考虑

1. 减少不必要的环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 不好的做法:设置大量不必要的环境变量 const badEnv = {
NODE_ENV: 'production',
VERSION: '1.0.0',
BUILD_TIME: '2024-01-01T00:00:00Z',
USER: 'someuser',
HOME: '/home/someuser',
PATH: '/usr/bin:/bin', // 这些通常不需要重新设置
// ... 其他系统环境变量
};

// 好的做法:只设置应用需要的环境变量 const goodEnv = {
NODE_ENV: 'production',
API_URL: 'https://API.example.com',
CUSTOMER_ID: '12345'
};

2. 环境变量缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// utils/env-cache.JS
class EnvironmentCache {
constructor() {
this.cache = new Map();
}

get(key, defaultValue = null) {
if (!this.cache.has(key)) {
const value = process.env[key] || defaultValue;
this.cache.set(key, value);
}
return this.cache.get(key);
}

clear() {
this.cache.clear();
}
}

const envCache = new EnvironmentCache();

module.exports = envCache;

迁移和替代方案

从原生命令迁移到 cross-env

1
2
3
4
5
6
# 迁移前 (不跨平台)
# Windows: SET NODE_ENV=production && webpack --mode production
# Unix: NODE_ENV=production webpack --mode production

# 迁移后 (跨平台)
cross-env NODE_ENV=production webpack --mode production

现代替代方案

1
2
3
4
5
6
7
8
9
{
"scripts": {
// 使用 npm-run-all 进行更复杂的脚本编排
"build": "run-s clean compile assets",
"clean": "rimraf dist",
"compile": "cross-env NODE_ENV=production tsc",
"assets": "cross-env NODE_ENV=production webpack --mode production"
}
}
1
2
3
4
5
6
7
8
9
// 使用现代 Node.JS 环境变量处理
// Node v18+ 中的实验性功能 const env = {
...process.env,
NODE_ENV: 'production',
API_URL: 'https://API.example.com'
};

const { spawn } = require('child_process');
spawn('webpack', ['--mode', 'production'], { env });

总结

  • cross-env 是解决跨平台环境变量设置问题的有效工具
  • 正确使用可以避免团队协作中的环境配置问题
  • 与 dotenv 等工具配合使用效果更佳
  • 需要注意环境变量的安全性和验证
  • 合理的命名规范和分类管理很重要
  • 在现代开发中仍是重要的构建工具之一

使用 cross-env 后,团队的跨平台开发效率提升了好多,再也不用担心 Windows 和 Mac 同事之间的环境配置差异了。一个简单的工具解决了大问题,这就是好工具的魅力所在。

扩展阅读

  • Cross-env Official Documentation
  • Environment Variables Best Practices
  • Node.JS Process Environment
  • Webpack Environment Variables
  • Cross Platform Development Tips

参考资料

  • Cross-env GitHub Repository: https://github.com/kentcdodds/cross-env
  • Node.JS Documentation: https://nodejs.org/
  • Webpack Configuration: https://webpack.JS.org/configuration/
  • NPM Scripts Guide: https://docs.npmjs.com/cli/v8/using-npm/scripts
bulb