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 渲染进程访问
文件监视 实时更新 需要额外依赖 开发调试

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

相关推荐
万少5 小时前
用腻了 WorkBuddy 的默认界面?这个开源小工具给它换上了毛玻璃
前端·javascript·后端
To_OC7 小时前
LC 15 三数之和:双指针不难,难的是把去重做对
javascript·算法·leetcode
触底反弹8 小时前
🔥 前端也能玩转 AI 流式输出!从二进制流到打字机效果,一篇讲透
javascript·人工智能·node.js
胡萝卜术8 小时前
深度重构:Agent Skills——从 Prompt 工程到能力工程
javascript·架构·github
海上彼尚9 小时前
Nodejs也能写Agent - 16.LangGraph篇 - 条件分支与循环
前端·后端·langchain·node.js
To_OC10 小时前
从 “卡死半天” 到 “打字机效果”:大模型流式输出前端实现全记录
前端·javascript·llm
rockey62710 小时前
基于AScript的JavaScript脚本语言发布啦
javascript·c#·.net·js·script
SoaringHeart10 小时前
Flutter最佳实践:键盘辅助视图输入框天添加图片附件
前端·flutter
kyriewen11 小时前
我看完这篇安全论文——AI推荐的npm包,92%是编出来的
前端·javascript·ai编程
心中有国也有家12 小时前
AtomGit Flutter 鸿蒙客户端:Canvas 绘制进阶-路径、渐变与混合模式
android·javascript·flutter·华为·harmonyos