Electron 读取 JSON 配置文件教程

📁 项目结构

复制代码
your-electron-app/
├── main.js              # 主进程
├── preload.js           # 预加载脚本
├── package.json
├── config/
│   └── app-config.json  # 配置文件
├── index.html
└── renderer.js          # 渲染进程

1. 创建配置文件

config/app-config.json

复制代码
{
  "appName": "我的Electron应用",
  "version": "1.0.0",
  "theme": {
    "primaryColor": "#4CAF50",
    "darkMode": false
  },
  "server": {
    "host": "localhost",
    "port": 3000,
    "apiUrl": "http://localhost:3000/api"
  },
  "features": {
    "autoUpdate": true,
    "notifications": true,
    "analytics": false
  },
  "users": [
    { "id": 1, "name": "管理员", "role": "admin" },
    { "id": 2, "name": "普通用户", "role": "user" }
  ]
}

2. 主进程读取配置 (main.js)

方法一:使用 fs 模块同步读取

复制代码
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs')

// 配置文件路径
const configPath = path.join(__dirname, 'config', 'app-config.json')

/**
 * 读取JSON配置文件(同步)
 */
function loadConfigSync() {
  try {
    const data = fs.readFileSync(configPath, 'utf-8')
    return JSON.parse(data)
  } catch (error) {
    console.error('读取配置文件失败:', error.message)
    return getDefaultConfig()
  }
}

/**
 * 读取JSON配置文件(异步)
 */
function loadConfigAsync() {
  return new Promise((resolve, reject) => {
    fs.readFile(configPath, 'utf-8', (error, data) => {
      if (error) {
        console.error('读取配置文件失败:', error.message)
        resolve(getDefaultConfig()) // 返回默认配置
        return
      }
      try {
        resolve(JSON.parse(data))
      } catch (parseError) {
        console.error('解析JSON失败:', parseError.message)
        resolve(getDefaultConfig())
      }
    })
  })
}

/**
 * 写入配置到文件
 */
function saveConfig(newConfig) {
  try {
    const data = JSON.stringify(newConfig, null, 2) // 格式化输出
    fs.writeFileSync(configPath, data, 'utf-8')
    return true
  } catch (error) {
    console.error('保存配置失败:', error.message)
    return false
  }
}

/**
 * 默认配置
 */
function getDefaultConfig() {
  return {
    appName: 'My App',
    version: '1.0.0',
    theme: { primaryColor: '#2196F3', darkMode: false },
    server: { host: 'localhost', port: 3000, apiUrl: '' },
    features: { autoUpdate: true, notifications: true, analytics: false }
  }
}

// 应用启动时加载配置
let appConfig = loadConfigSync()
console.log('应用配置已加载:', appConfig.appName)

let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false
    }
  })

  mainWindow.loadFile('index.html')

  // 将配置传递给渲染进程
  mainWindow.webContents.on('did-finish-load', () => {
    mainWindow.webContents.send('config-loaded', appConfig)
  })
}

// IPC 处理渲染进程的配置请求
ipcMain.handle('get-config', async () => {
  return await loadConfigAsync()
})

ipcMain.handle('save-config', async (event, newConfig) => {
  const success = saveConfig(newConfig)
  if (success) {
    appConfig = { ...appConfig, ...newConfig } // 更新内存中的配置
  }
  return success
})

ipcMain.handle('get-config-value', (event, key) => {
  // 支持点号分隔的路径,如 "theme.darkMode"
  return getNestedValue(appConfig, key)
})

// 获取嵌套对象的值
function getNestedValue(obj, path) {
  return path.split('.').reduce((current, key) => {
    return current && current[key] !== undefined ? current[key] : undefined
  }, obj)
}

app.whenReady().then(createWindow)

3. 预加载脚本 (preload.js)

复制代码
const { contextBridge, ipcRenderer } = require('electron')

// 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld('configAPI', {
  // 获取完整配置
  getConfig: () => ipcRenderer.invoke('get-config'),
  
  // 保存配置
  saveConfig: (config) => ipcRenderer.invoke('save-config', config),
  
  // 获取单个配置项
  get: (key) => ipcRenderer.invoke('get-config-value', key),
  
  // 监听配置变化
  onConfigUpdate: (callback) => {
    ipcRenderer.on('config-updated', (event, config) => callback(config))
  }
})

4. 渲染进程使用 (renderer.js)

复制代码
// 方式1:等待主进程主动发送
window.electronAPI.onConfigLoaded((config) => {
  console.log('收到配置:', config)
  initApp(config)
})

// 方式2:主动请求配置
async function loadConfig() {
  try {
    const config = await window.configAPI.getConfig()
    console.log('配置已加载:', config)
    
    // 使用配置
    document.title = config.appName
    applyTheme(config.theme)
    
    return config
  } catch (error) {
    console.error('获取配置失败:', error)
  }
}

// 方式3:获取单个配置项
async function getServerUrl() {
  const url = await window.configAPI.get('server.apiUrl')
  return url
}

// 方式4:保存配置
async function updateConfig() {
  const newConfig = {
    theme: {
      darkMode: true,
      primaryColor: '#ff5722'
    },
    features: {
      analytics: true
    }
  }
  
  const success = await window.configAPI.saveConfig(newConfig)
  if (success) {
    alert('配置已保存!')
  } else {
    alert('保存失败!')
  }
}

// 应用配置
function applyTheme(theme) {
  if (theme.darkMode) {
    document.body.classList.add('dark-theme')
  } else {
    document.body.classList.remove('dark-theme')
  }
  document.documentElement.style.setProperty('--primary-color', theme.primaryColor)
}

// 初始化
loadConfig()

5. 完整 HTML 示例 (index.html)

复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>配置演示</title>
  <style>
    :root {
      --primary-color: #4CAF50;
    }
    
    body {
      font-family: Arial, sans-serif;
      padding: 20px;
      background-color: #f5f5f5;
    }
    
    .config-panel {
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
      max-width: 600px;
      margin: 0 auto;
    }
    
    h2 {
      color: var(--primary-color);
      margin-top: 0;
    }
    
    .form-group {
      margin-bottom: 15px;
    }
    
    label {
      display: block;
      margin-bottom: 5px;
      font-weight: bold;
    }
    
    input[type="text"],
    input[type="number"] {
      width: 100%;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      box-sizing: border-box;
    }
    
    .switch {
      position: relative;
      display: inline-block;
      width: 50px;
      height: 24px;
    }
    
    .switch input {
      opacity: 0;
      width: 0;
      height: 0;
    }
    
    .slider {
      position: absolute;
      cursor: pointer;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: #ccc;
      transition: 0.4s;
      border-radius: 24px;
    }
    
    .slider:before {
      position: absolute;
      content: "";
      height: 18px;
      width: 18px;
      left: 3px;
      bottom: 3px;
      background-color: white;
      transition: 0.4s;
      border-radius: 50%;
    }
    
    input:checked + .slider {
      background-color: var(--primary-color);
    }
    
    input:checked + .slider:before {
      transform: translateX(26px);
    }
    
    button {
      background-color: var(--primary-color);
      color: white;
      border: none;
      padding: 10px 20px;
      border-radius: 4px;
      cursor: pointer;
      margin-right: 10px;
    }
    
    button:hover {
      opacity: 0.9;
    }
    
    .config-display {
      background: #f9f9f9;
      padding: 15px;
      border-radius: 4px;
      margin-top: 20px;
      font-family: monospace;
      white-space: pre-wrap;
      max-height: 200px;
      overflow-y: auto;
    }
  </style>
</head>
<body>
  <div class="config-panel">
    <h2>⚙️ 应用配置</h2>
    
    <div class="form-group">
      <label>应用名称</label>
      <input type="text" id="appName" placeholder="输入应用名称">
    </div>
    
    <div class="form-group">
      <label>服务器端口</label>
      <input type="number" id="serverPort" placeholder="输入端口号">
    </div>
    
    <div class="form-group">
      <label>暗黑模式</label>
      <label class="switch">
        <input type="checkbox" id="darkMode">
        <span class="slider"></span>
      </label>
    </div>
    
    <div class="form-group">
      <label>自动更新</label>
      <label class="switch">
        <input type="checkbox" id="autoUpdate">
        <span class="slider"></span>
      </label>
    </div>
    
    <div>
      <button onclick="loadConfig()">📂 加载配置</button>
      <button onclick="saveConfig()">💾 保存配置</button>
      <button onclick="resetConfig()">🔄 重置</button>
    </div>
    
    <h3>当前配置</h3>
    <div class="config-display" id="configDisplay">点击"加载配置"查看</div>
  </div>

  <script src="renderer.js"></script>
  <script>
    // 页面内联脚本
    let currentConfig = {}

    async function loadConfig() {
      currentConfig = await window.configAPI.getConfig()
      displayConfig(currentConfig)
      fillForm(currentConfig)
    }

    function displayConfig(config) {
      document.getElementById('configDisplay').textContent = 
        JSON.stringify(config, null, 2)
    }

    function fillForm(config) {
      document.getElementById('appName').value = config.appName || ''
      document.getElementById('serverPort').value = config.server?.port || 3000
      document.getElementById('darkMode').checked = config.theme?.darkMode || false
      document.getElementById('autoUpdate').checked = config.features?.autoUpdate ?? true
    }

    async function saveConfig() {
      const newConfig = {
        appName: document.getElementById('appName').value,
        server: {
          ...currentConfig.server,
          port: parseInt(document.getElementById('serverPort').value) || 3000
        },
        theme: {
          ...currentConfig.theme,
          darkMode: document.getElementById('darkMode').checked
        },
        features: {
          ...currentConfig.features,
          autoUpdate: document.getElementById('autoUpdate').checked
        }
      }
      
      const success = await window.configAPI.saveConfig(newConfig)
      if (success) {
        currentConfig = newConfig
        displayConfig(currentConfig)
        alert('配置保存成功!')
      } else {
        alert('保存失败!')
      }
    }

    function resetConfig() {
      fillForm(currentConfig)
    }
  </script>
</body>
</html>

6. 配置文件热重载

主进程添加文件监视

复制代码
const chokidar = require('chokidar') // npm install chokidar

// 在 createWindow 后添加
function watchConfig() {
  const watcher = chokidar.watch(configPath, {
    persistent: true,
    ignoreInitial: true
  })

  watcher
    .on('change', async (path) => {
      console.log('配置文件已更改,重新加载...')
      appConfig = await loadConfigAsync()
      
      // 通知所有窗口配置已更新
      BrowserWindow.getAllWindows().forEach(window => {
        window.webContents.send('config-updated', appConfig)
      })
    })
    .on('error', error => console.error('文件监视错误:', error))

  return watcher
}

// 在 createWindow 中调用
createWindow()
watchConfig()

渲染进程监听配置变化

复制代码
// 在页面加载时
window.electronAPI.onConfigUpdate((newConfig) => {
  console.log('配置已更新:', newConfig)
  // 重新应用配置
  applyTheme(newConfig.theme)
  // 刷新页面数据
  refreshData()
})

7. 不同环境配置

创建多环境配置

复制代码
// config/config-loader.js
const path = require('path')
const fs = require('fs')

const environments = ['development', 'production', 'staging']
let currentEnv = process.env.NODE_ENV || 'development'

// 确保环境有效
if (!environments.includes(currentEnv)) {
  currentEnv = 'development'
}

// 主配置文件
const mainConfigPath = path.join(__dirname, 'app-config.json')
// 环境特定配置
const envConfigPath = path.join(__dirname, `app-config.${currentEnv}.json`)

function deepMerge(target, source) {
  const result = { ...target }
  for (const key in source) {
    if (source.hasOwnProperty(key)) {
      if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
        result[key] = deepMerge(result[key] || {}, source[key])
      } else {
        result[key] = source[key]
      }
    }
  }
  return result
}

function loadConfig() {
  let config = {}
  
  // 加载主配置
  if (fs.existsSync(mainConfigPath)) {
    config = JSON.parse(fs.readFileSync(mainConfigPath, 'utf-8'))
  }
  
  // 加载环境特定配置(覆盖主配置)
  if (fs.existsSync(envConfigPath)) {
    const envConfig = JSON.parse(fs.readFileSync(envConfigPath, 'utf-8'))
    config = deepMerge(config, envConfig)
  }
  
  return config
}

module.exports = { loadConfig, currentEnv }

环境配置文件

config/app-config.development.json

复制代码
{
  "server": {
    "host": "localhost",
    "port": 3000,
    "apiUrl": "http://localhost:3000/api"
  },
  "features": {
    "debug": true,
    "logging": "verbose"
  }
}

config/app-config.production.json

复制代码
{
  "server": {
    "host": "0.0.0.0",
    "port": 8080,
    "apiUrl": "https://api.example.com"
  },
  "features": {
    "debug": false,
    "logging": "minimal"
  }
}

8. 安全注意事项

复制代码
// ❌ 危险:直接暴露整个配置
contextBridge.exposeInMainWorld('dangerousConfig', {
  data: require('./config.json')  // 不推荐!
})

// ✅ 安全:只暴露需要的字段
contextBridge.exposeInMainWorld('safeConfig', {
  get: (key) => {
    // 白名单检查
    const allowedKeys = ['appName', 'theme', 'server']
    if (allowedKeys.includes(key)) {
      return readConfigValue(key)
    }
    return null
  }
})

9. 运行项目

复制代码
# 安装依赖
npm init -y
npm install electron --save-dev
npm install chokidar --save  # 可选,用于热重载

# 在 package.json 中添加
# "scripts": { "start": "electron main.js" }

# 运行
npm start

方法对比

方法 优点 缺点 适用场景
同步读取 代码简单 阻塞主进程 启动时读取
异步读取 不阻塞 代码稍复杂 运行时读取
预加载暴露 安全 需要 IPC 渲染进程访问
文件监视 实时更新 需要额外依赖 开发调试

需要我详细解释哪个部分吗?

相关推荐
GreenTea4 小时前
GrokBot 核心成员 Lauren Tan:每月交付 2000 个 PR 的人,是怎么用 AI 的
前端·后端·架构
mCell5 小时前
用 Knip 清理 AI Coding 留下的冗余代码
前端·ai编程·前端工程化
dy17175 小时前
vue2树节点刷新后定位到上一节点展开
vue.js
笃行3506 小时前
使用Rokid AIUI做了一个童年推箱子游戏
前端
excel7 小时前
前端加密的作用与使用场景
前端
计算机魔术师8 小时前
纽约时报版权诉讼披露:微软高管内部称训练 AI 是人类历史上最大规模劳动窃取
前端
去伪存真8 小时前
大模型的参数量为什么那么大?
前端·人工智能
IT_陈寒8 小时前
Java空指针这次真把我坑惨了
前端·人工智能·后端
高级程序源9 小时前
django大学生创新创业项目管理系统94923-计算机课程设计、毕业设计
javascript·vue.js·spring boot·后端·python·django·课程设计
八荒启·交互动画10 小时前
# Web特效020—让 Web 特效真正动起来:时间循环应该怎么接
前端·webgl·网页特效·八荒启-交互动画·八荒启