团队开发中遇到跨平台环境变量设置的问题,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
| SET NODE_ENV=production && webpack --mode production
SET NODE_ENV=production && webpack --mode production
NODE_ENV=production webpack --mode production
|
如果没有 cross-env,我们就需要为不同平台维护不同的脚本,这会增加开发和维护成本。cross-env 让我们可以用一行命令解决跨平台环境变量设置的问题:
安装和基本使用
安装 cross-env
1 2 3 4 5 6 7
|
yarn add --dev cross-env
pnpm add -D cross-env
|
基本使用方法
1 2 3 4 5
|
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
| 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 };
.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
| 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
| { "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
|
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
| 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 3 4 5
| NODE_ENV=development API_URL=http: PORT=3000 DEBUG=true
|
1 2 3 4 5
| NODE_ENV=production API_URL=https: 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
| const path = require('path');
? '.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
| 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
| 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_VERSION = version; process.env.BUILD_ENV = env;
try { 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`);
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
| 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&chars' webpack --mode production",
"dev:complex": "cross-env DB_CONNECTION_STRING=\"postgresql://user:pass@host:5432/db?ssl=true\" npm run dev",
"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
| 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": { "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",
"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",
"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
| NODE_ENV: 'production', VERSION: '1.0.0', BUILD_TIME: '2024-01-01T00:00:00Z', USER: 'someuser', HOME: '/home/someuser', PATH: '/usr/bin:/bin', };
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
| 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
|
cross-env NODE_ENV=production webpack --mode production
|
现代替代方案
1 2 3 4 5 6 7 8 9
| { "scripts": { "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
|
...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