Skip to content

配置文件读取与管理

CLI 工具通常需要从配置文件读取选项。

JSON 配置

最简单的方式:

javascript
const fs = require('fs');
const path = require('path');

function loadConfig(configPath) {
  try {
    const content = fs.readFileSync(configPath, 'utf-8');
    return JSON.parse(content);
  } catch (err) {
    if (err.code === 'ENOENT') {
      return {};  // 文件不存在返回空对象
    }
    throw new Error(`配置文件解析错误: ${err.message}`);
  }
}

const config = loadConfig('./config.json');
console.log(config);

查找配置文件

向上级目录查找:

javascript
const fs = require('fs');
const path = require('path');

function findConfig(filename, startDir = process.cwd()) {
  let dir = startDir;
  
  while (true) {
    const configPath = path.join(dir, filename);
    
    if (fs.existsSync(configPath)) {
      return configPath;
    }
    
    const parent = path.dirname(dir);
    if (parent === dir) {
      return null;  // 到达根目录
    }
    dir = parent;
  }
}

const configPath = findConfig('myapp.config.json');
if (configPath) {
  console.log('找到配置:', configPath);
}

多配置文件查找

javascript
function findConfigMultiple(filenames, startDir = process.cwd()) {
  let dir = startDir;
  
  while (true) {
    for (const filename of filenames) {
      const configPath = path.join(dir, filename);
      if (fs.existsSync(configPath)) {
        return configPath;
      }
    }
    
    const parent = path.dirname(dir);
    if (parent === dir) return null;
    dir = parent;
  }
}

// 支持多种配置文件名
const configPath = findConfigMultiple([
  'myapp.config.js',
  'myapp.config.json',
  '.myapprc',
  '.myapprc.json'
]);

使用 cosmiconfig

自动查找和加载配置:

bash
npm install cosmiconfig
javascript
const { cosmiconfig } = require('cosmiconfig');

async function loadConfig() {
  const explorer = cosmiconfig('myapp');
  
  // 自动查找以下配置:
  // - package.json 中的 myapp 字段
  // - .myapprc
  // - .myapprc.json
  // - .myapprc.yaml
  // - .myapprc.yml
  // - .myapprc.js
  // - myapp.config.js
  
  const result = await explorer.search();
  
  if (result) {
    console.log('配置来源:', result.filepath);
    return result.config;
  }
  
  return {};
}

loadConfig().then(console.log);

默认配置合并

javascript
const defaultConfig = {
  port: 3000,
  host: 'localhost',
  output: './dist',
  minify: false,
  sourcemap: true
};

function mergeConfig(userConfig) {
  return { ...defaultConfig, ...userConfig };
}

// 使用
const userConfig = loadConfig('./config.json');
const config = mergeConfig(userConfig);

深度合并

javascript
function deepMerge(target, source) {
  const result = { ...target };
  
  for (const key of Object.keys(source)) {
    if (
      source[key] &&
      typeof source[key] === 'object' &&
      !Array.isArray(source[key])
    ) {
      result[key] = deepMerge(target[key] || {}, source[key]);
    } else {
      result[key] = source[key];
    }
  }
  
  return result;
}

const defaultConfig = {
  server: {
    port: 3000,
    host: 'localhost'
  },
  build: {
    minify: false,
    sourcemap: true
  }
};

const userConfig = {
  server: {
    port: 8080
  }
};

const config = deepMerge(defaultConfig, userConfig);
// {
//   server: { port: 8080, host: 'localhost' },
//   build: { minify: false, sourcemap: true }
// }

环境变量覆盖

javascript
function loadConfigWithEnv(configPath) {
  const fileConfig = loadConfig(configPath);
  
  // 环境变量优先级最高
  return {
    ...fileConfig,
    port: process.env.PORT || fileConfig.port,
    host: process.env.HOST || fileConfig.host,
    debug: process.env.DEBUG === 'true' || fileConfig.debug
  };
}

配置验证

javascript
function validateConfig(config) {
  const errors = [];
  
  if (typeof config.port !== 'number' && typeof config.port !== 'undefined') {
    errors.push('port 必须是数字');
  }
  
  if (config.port && (config.port < 1 || config.port > 65535)) {
    errors.push('port 必须在 1-65535 范围内');
  }
  
  if (config.output && typeof config.output !== 'string') {
    errors.push('output 必须是字符串');
  }
  
  if (errors.length > 0) {
    throw new Error(`配置错误:\n  ${errors.join('\n  ')}`);
  }
  
  return config;
}

完整配置管理器

javascript
const fs = require('fs');
const path = require('path');
const { cosmiconfig } = require('cosmiconfig');

class ConfigManager {
  constructor(name, defaultConfig = {}) {
    this.name = name;
    this.defaultConfig = defaultConfig;
    this.config = null;
    this.configPath = null;
  }
  
  async load(searchFrom = process.cwd()) {
    const explorer = cosmiconfig(this.name);
    const result = await explorer.search(searchFrom);
    
    if (result) {
      this.configPath = result.filepath;
      this.config = this.merge(result.config);
    } else {
      this.config = { ...this.defaultConfig };
    }
    
    // 环境变量覆盖
    this.applyEnv();
    
    return this.config;
  }
  
  merge(userConfig) {
    return this.deepMerge(this.defaultConfig, userConfig);
  }
  
  deepMerge(target, source) {
    const result = { ...target };
    
    for (const key of Object.keys(source)) {
      if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
        result[key] = this.deepMerge(target[key] || {}, source[key]);
      } else {
        result[key] = source[key];
      }
    }
    
    return result;
  }
  
  applyEnv() {
    const prefix = this.name.toUpperCase() + '_';
    
    for (const [key, value] of Object.entries(process.env)) {
      if (key.startsWith(prefix)) {
        const configKey = key.slice(prefix.length).toLowerCase();
        this.config[configKey] = this.parseEnvValue(value);
      }
    }
  }
  
  parseEnvValue(value) {
    if (value === 'true') return true;
    if (value === 'false') return false;
    if (!isNaN(value)) return Number(value);
    return value;
  }
  
  get(key) {
    return key.split('.').reduce((obj, k) => obj?.[k], this.config);
  }
  
  set(key, value) {
    const keys = key.split('.');
    const last = keys.pop();
    const target = keys.reduce((obj, k) => obj[k] = obj[k] || {}, this.config);
    target[last] = value;
  }
}

// 使用
const configManager = new ConfigManager('myapp', {
  port: 3000,
  debug: false,
  output: './dist'
});

async function main() {
  await configManager.load();
  
  console.log('配置来源:', configManager.configPath || '默认配置');
  console.log('端口:', configManager.get('port'));
  console.log('完整配置:', configManager.config);
}

main();

配置文件示例

.myapprc.json

json
{
  "port": 8080,
  "debug": true,
  "output": "./build"
}

package.json

json
{
  "name": "my-project",
  "myapp": {
    "port": 8080
  }
}

本章小结

  • JSON 是最简单的配置格式
  • 向上级目录查找配置文件
  • cosmiconfig 自动处理多种格式
  • 默认配置 + 用户配置 + 环境变量的合并策略
  • 配置验证防止运行时错误

下一章我们将综合运用,开发完整的 CLI 工具。

配置文件读取与管理 has loaded