How to Build a JSON Editor with Electron: From Setup to Production

This article walks through creating a GUI‑based JSON editor for the Social Cube platform using Electron, covering environment setup, dual webpack configurations, IPC communication, data persistence with Dexie, real‑time file syncing, context menus, logging, packaging with electron‑builder, and update handling, all illustrated with code snippets.

WecTeam
WecTeam
WecTeam
How to Build a JSON Editor with Electron: From Setup to Production

The Social Cube platform is JD's SNS activity builder that uses template JSON to generate forms; editing JSON by hand is error‑prone, so a GUI editor based on Electron (inspired by the Github Desktop client) was built to generate JSON from form inputs.

1. About Electron

Electron is an open‑source library developed by Github that lets you build cross‑platform desktop applications with HTML, CSS and JavaScript by bundling Chromium and Node.js into a single runtime.

Electron apps have a single main process (Node‑only) that creates browser windows, and one or more renderer processes (which have both DOM/BOM and Node APIs). The two processes communicate via Electron’s IPC interfaces.

2. Development Environment Setup

Because Electron has both a main and a renderer process, two separate webpack configuration files are required to achieve one‑click start, build, and publish.

One‑click start

One‑click build

One‑click publish

Development configuration ( webpack.dev.ts):

// webpack.dev.ts
const mainConfig = merge({}, base.mainConfig, config, {
  watch: true
});

const rendererConfig = merge({}, base.rendererConfig, config, {
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.styl$/, use: ['style-loader', 'css-loader', 'stylus-loader'] }
    ]
  },
  devServer: {
    contentBase: path.join(__dirname, base.outputDir),
    port: 8000,
    hot: true,
    inline: true,
    historyApiFallback: true,
    writeToDisk: true
  }
});
module.exports = [rendererConfig, mainConfig];

Production configuration ( webpack.prod.ts) uses TypeScript for type‑safe config files and extracts CSS with MiniCssExtractPlugin:

const config: webpack.Configuration = {
  mode: 'production',
  devtool: 'source-map'
};

const mainConfig = merge({}, base.mainConfig, config);
const rendererConfig = merge({}, base.rendererConfig, config, {
  module: {
    rules: [
      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] },
      { test: /\.styl$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader'] }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: 'renderer.css' }),
    new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false, reportFilename: 'renderer.report.html' })
  ]
});
module.exports = [mainConfig, rendererConfig];

Scripts in package.json provide commands for development, production, linting, and release. nodemon watches the compiled main process file and restarts the Electron app for live reload:

{
  "compile:dev": "webpack-dev-server --config scripts/webpack.dev.ts",
  "compile:prod": "npm run clean && webpack --config scripts/webpack.prod.ts",
  "app": "electron .",
  "app:watch": "nodemon --watch 'dest/main.js' --exec npm run app"
}
Nodemon monitors source changes and automatically restarts the server.

3. Directory Structure

1. Project layout

src
├── lib
│   ├── cube
│   ├── databases
│   ├── enviroment
│   ├── files
│   ├── local-storage
│   ├── log
│   ├── shell
│   ├── stores
│   ├── update
│   └── watcher
├── main
│   ├── app-window.ts
│   ├── event-bus.ts
│   ├── index.ts
│   ├── keyboard
│   └── menu
├── models
│   ├── popup.ts
│   └── project.ts
└── renderer
    ├── App.tsx
    ├── assets
    ├── components
    ├── index.html
    ├── index.tsx
    ├── pages
    └── types

The main folder holds the main‑process code (window creation, menus, shortcuts), renderer contains the UI layer, lib stores logic unrelated to UI, and models defines domain entities.

2. CSS conventions

The project uses BEM naming instead of CSS modules, keeping each React component’s styles in a separate .styl file for easy refactoring.

stylesheets
  ├── common.styl
  ├── components
  │   ├── editor.styl
  │   ├── empty-guide.styl
  │   ├── find-in-page.styl
  │   ├── reindex.styl
  │   ├── sidebar.styl
  │   ├── source-viewer.styl
  │   └── upload.styl
  ├── index.styl
  └── reset.styl

4. IPC Communication

Inter‑Process Communication (IPC) enables data exchange between different processes.

1. Main side

// In the main process
const { ipcMain } = require('electron');
ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg); // prints "ping"
  event.reply('asynchronous-reply', 'pong');
});

ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg); // prints "ping"
  event.returnValue = 'pong';
});

2. Renderer side

// In the renderer process
const { ipcRenderer } = require('electron');
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')); // prints "pong"

ipcRenderer.on('asynchronous-reply', (event, arg) => {
  console.log(arg); // prints "pong"
});
ipcRenderer.send('asynchronous-message', 'ping');

The renderer uses ipcRenderer.send for async messages and ipcRenderer.sendSync for sync messages.

5. Data Persistence and State Management

1. Complex data persistence

Simple JSON storage can be done with electron-store, but for richer needs the project switched to Dexie, a wrapper around IndexedDB, which solves exception handling, poor queries, and code complexity.

import Dexie from 'dexie';

export interface IDatabaseProject {
  id?: number;
  name: string;
  filePath: string;
}

export class ProjectsDatabase extends Dexie {
  public projects: Dexie.Table<IDatabaseProject, number>;
  constructor() {
    super('ProjectsDatabase');
    this.version(1).stores({
      projects: '++id,&name,&filePath',
    });
    this.projects = this.table('projects');
  }
}

Refer to the Dexie documentation for full API details.

2. Simple data persistence

UI flags are stored in localStorage. Helper functions for boolean values simplify get/set operations:

export function getBoolean(key: string, defaultValue?: boolean): boolean | undefined {
  const value = localStorage.getItem(key);
  if (value === null) return defaultValue;
  if (value === '1' || value === 'true') return true;
  if (value === '0' || value === 'false') return false;
  return defaultValue;
}

export function setBoolean(key: string, value: boolean) {
  localStorage.setItem(key, value ? '1' : '0');
}

Source code can be found in the Github Desktop repository.

6. Feature Implementation

1. Real‑time disk/editor sync

File changes caused by Git or manual edits can desynchronize the in‑memory copy. Using chokidar to watch add, change, and unlink events provides reliable cross‑platform file watching, avoiding the pitfalls of Node’s native fs.watch and fs.watchFile.

2. Context‑menu

The Desktop client builds native context menus in the main process. The renderer creates an array of MenuItem objects, sends it via IPC, the main process constructs the actual Menu, and on click the index is sent back to the renderer to invoke the associated action.

onContextMenu => showContextualMenu (store MenuItems, ipcRenderer.send) => ipcMain => menu.popup() => MenuItem.onClick(index) => event.sender.send(index) => MenuItem.action()

A simplified implementation uses the remote module to hide the IPC complexity:

import { remote } from 'electron';
const { MenuItem, dialog, getCurrentWindow, Menu } = remote;

const onContextMenu = (project) => {
  const menu = new Menu();
  const menus = [
    new MenuItem({
      label: 'Open in Terminal',
      visible: __DARWIN__,
      click() { new FileAccessor(project.filePath).openInTerminal(); }
    }),
    new MenuItem({
      label: 'Open in VSCode',
      click() { new FileAccessor(project.filePath).openInVscode(); }
    })
  ];
  menus.forEach(menu.append);
  menu.popup({ window: getCurrentWindow() });
};

7. Logging

The project uses winston for both console and daily‑rotated file logging. A global log object exposes debug, info, warn, and error methods in both main and renderer processes, routing renderer logs to the main process via IPC.

8. Packaging, Release and Update

Packaging is handled by electron-builder, which supports multi‑platform builds, signing, and auto‑updates. In a restricted internal network the app cannot auto‑update, so a manual check compares the current version with a remote package.json using semver and notifies the user.

import got from 'got';
import semver from 'semver';
import { app, remote, BrowserWindow } from 'electron';

const realApp = app || remote.app;
const currentVersion = realApp.getVersion();

export async function checkForUpdates(window, silent = false) {
  const url = `http://yourcdn/package.json?t=${Date.now()}`;
  try {
    const response = await got(url);
    const pkg = JSON.parse(response.body);
    if (semver.lt(currentVersion, pkg.version)) {
      window.webContents.send('update-available', pkg.version);
    } else {
      window.webContents.send('update-not-available', silent);
    }
  } catch (e) {
    window.webContents.send('update-error', silent);
  }
}

The check runs on app start (silently) and when the user selects “Check for Updates” from the menu.

9. Other

1. DevTools

During development, Chrome DevTools and extensions such as React and MobX devtools are installed via electron-devtools-installer:

const { default: installExtension, MOBX_DEVTOOLS, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer');
const extensions = [REACT_DEVELOPER_TOOLS, MOBX_DEVTOOLS];
for (const extension of extensions) {
  try { installExtension(extension); } catch (e) { /* log.error(e); */ }
}

2. Preserve window size

The electron-window-state library automatically saves and restores window bounds across launches:

const windowStateKeeper = require('electron-window-state');
let win;
app.on('ready', () => {
  const mainWindowState = windowStateKeeper({ defaultWidth: 1000, defaultHeight: 800 });
  win = new BrowserWindow({
    x: mainWindowState.x,
    y: mainWindowState.y,
    width: mainWindowState.width,
    height: mainWindowState.height
  });
  mainWindowState.manage(win);
});

References

Electron: http://electronjs.org

Github Desktop client: https://github.com/desktop/desktop

nodemon: https://nodemon.io

electron-webpack: https://github.com/electron-userland/electron-webpack

electron-builder: https://www.electron.build

electron-store: https://github.com/sindresorhus/electron-store#readme

lowdb: https://github.com/typicode/lowdb

nedb: https://github.com/louischatriot/nedb

Dexie: https://github.com/dfahlander/Dexie.js

Dexie documentation: https://dexie.org/docs/

Source code example: https://github.com/desktop/desktop/blob/development/app/src/lib/local-storage.ts

chokidar: https://github.com/paulmillr/chokidar

winston: https://github.com/winstonjs/winston#readme

electron-builder: https://www.electron.build

semver: https://www.npmjs.com/package/semver

electron-window-state: https://github.com/mawie81/electron-window-state#readme

TypeScriptElectronIPCelectron-builderdesktop appDexieJSON editor
WecTeam
Written by

WecTeam

WecTeam (维C团) is the front‑end technology team of JD.com’s Jingxi business unit, focusing on front‑end engineering, web performance optimization, mini‑program and app development, serverless, multi‑platform reuse, and visual building.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.