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

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

相关推荐
xiaoxiangsiyan20 小时前
运维之前端反调试学习
运维·前端·学习·状态模式
小徐_233320 小时前
TRAE WORK 实战,之前写一篇水文要半天,现在用 TRAE Work 摸鱼2分钟交差,真香!
前端·trae
明月_清风20 小时前
🤗 Hugging Face 模型上传完全指南:从本地到 Hub 的 4 种姿势
前端·后端·ai编程
sugar__salt20 小时前
跟着 Demo 学 Pinia:两种仓库写法 + 完整 TodoList 复现
前端·javascript·vue.js·前端框架·vue
To_OC21 小时前
从一个名字编辑组件开始,我把 React + TS 的数据流和副作用彻底搞明白了
前端·react.js·typescript
程序员黑豆21 小时前
Java入门第一步:从零开始编写你的第一个Hello World程序
java·前端·ai编程
凌涘21 小时前
前端路由(二):React Router 组件化路由实战
前端
鸽鸽1 天前
别急着装第三方库,这些事浏览器原生就能做到——Web Native API 实战指南
前端
做前端的娜娜子1 天前
从 0 到 1 打造一个配置可视化后台:一次前端配置中心化改造的实战复盘
前端·设计模式·数据可视化