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

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

相关推荐
jvxiao32 分钟前
你真的懂作用域吗?从编译原理角度深度 JS 的作用域
前端·javascript
Darling噜啦啦34 分钟前
二叉树与递归算法实战:从树结构到 LeetCode 爬楼梯,一文吃透前端数据结构与递归思维
前端·javascript·数据结构
星栈39 分钟前
Rust + Makepad 应用怎么打包发布:Windows、macOS、Linux 全平台交付
前端·rust
Aolith1 小时前
React 路由守卫:我用一个组件替代了 Vue 的 beforeEach
前端·react.js
Daybreak1 小时前
从 PDD、DDD、SDD 到 TDD:我是如何用一套 Agent 工程方法论推进 My-Notion 的
前端
HjhIron1 小时前
从零实现一个待办事项应用:前端必学的Ajax与Node.js实战
前端·后端
yingyima1 小时前
JavaScript 正则表达式:从零开始的实战对比
前端
xsbcme1 小时前
VueTabRouter 插件实践(一):多标签页不是一排 TabBar
vue.js
Sammyyyyy2 小时前
月之暗面 Kimi Code 0.4.0 发布,终端 AI 编码助手全面采用 TypeScript,实现毫秒级启动
前端·javascript·人工智能·ai·typescript·servbay
范什么特西2 小时前
配置文件xml和properties
xml·前端