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.
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
└── typesThe 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.styl4. 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
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
