# How PicList's Main Process Interacts with the Renderer Process: A Complete IPC Guide

> Understand how PicList's main process interacts with the renderer process using Electron's IPC. Explore the custom RPC layer and preload script API for seamless communication.

- Repository: [Kuingsmile/piclist](https://github.com/kuingsmile/piclist)
- Tags: deep-dive
- Published: 2026-03-05

---

**PicList uses Electron's IPC mechanisms wrapped in a custom RPC layer, where the main process registers handlers in [`src/main/events/rpc/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/events/rpc/index.ts) and the renderer communicates through a preload script exposing a `window.piclist` API.**

As an Electron-based application, PicList (available at `kuingsmile/piclist`) separates its Node.js backend logic from the Chromium-based UI frontend. Understanding how PicList's main process interacts with the renderer process reveals a secure, well-architected communication pattern that leverages **context isolation** and a centralized RPC server.

## Architecture Overview of PicList's IPC Flow

PicList implements a strict three-layer communication model that prevents direct Node.js access from the renderer while enabling rich functionality.

**The Main Process** runs the core Node.js logic and registers IPC listeners through `ipcMain.handle` and `ipcMain.on` in [`src/main/events/rpc/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/events/rpc/index.ts). The **preload script** ([`src/preload/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/preload/index.ts)) acts as a secure bridge, using `contextBridge.exposeInMainWorld` to inject a controlled `piclist` object into the renderer's global scope. Finally, the **Renderer Process** (Vue.js UI) accesses this global object to invoke main-process methods without ever touching raw Electron APIs.

The flow follows these distinct channels:

- **Asynchronous Request/Response**: The renderer calls `window.piclist.invoke()`, which triggers `ipcRenderer.invoke` targeting `RPC_ACTIONS_INVOKE`. The main process handles this via `RpcServer` and returns a Promise resolution.
- **Fire-and-Forget Messaging**: For one-way communication, the renderer uses `window.piclist.send()`, mapped to `ipcRenderer.send` on the `RPC_ACTIONS` channel.
- **Main-to-Renderer Broadcasting**: The main process can push updates to all renderer windows using `webContents.send`, received via `window.piclist.on()` listeners.

## Core Implementation Files

The interaction relies on four critical source files that establish the RPC contract between processes.

### Main Process RPC Server ([`src/main/events/rpc/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/events/rpc/index.ts))

This file defines the central communication hub. It exports two channel constants, `RPC_ACTIONS` and `RPC_ACTIONS_INVOKE`, and implements the `RpcServer` class. During application startup (called from [`src/main/lifeCycle/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/lifeCycle/index.ts)), `rpcServer.start()` registers handlers for both synchronous and asynchronous IPC calls.

The server maintains an internal handler map that routes action names like `THEME_GET_BOOTSTRAP` or `UPLOAD_FILES` to specific business logic implementations scattered throughout `src/main/manage/`.

### Preload Bridge ([`src/preload/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/preload/index.ts))

This script creates the security boundary. It imports `contextBridge` and `ipcRenderer` from Electron, then exposes a `piclist` object on `window` containing three methods:

- `invoke(action, ...args)`: Wraps `ipcRenderer.invoke('RPC_ACTIONS_INVOKE', ...)` with JSON serialization via `getRawData()`
- `send(action, ...args)`: Wraps `ipcRenderer.send('RPC_ACTIONS', ...)` for fire-and-forget operations
- `on(channel, listener)`: Wraps `ipcRenderer.on()` with automatic JSON parsing of arguments

This isolation ensures that even if the renderer process is compromised, attackers cannot access Node.js APIs directly.

### Renderer Integration ([`src/renderer/main.ts`](https://github.com/kuingsmile/piclist/blob/main/src/renderer/main.ts))

The Vue.js frontend consumes the exposed API through the global `window.piclist` object. Components call `await window.piclist.invoke()` to fetch configuration data or `window.piclist.send()` to trigger uploads. Event listeners attach via `window.piclist.on()` to receive real-time updates from the main process, such as theme changes or upload progress notifications.

## Code Implementation Examples

### Registering IPC Handlers in the Main Process

The main process establishes its RPC endpoints during initialization in [`src/main/events/rpc/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/events/rpc/index.ts):

```typescript
import { ipcMain } from 'electron'

export const RPC_ACTIONS = 'RPC_ACTIONS'
export const RPC_ACTIONS_INVOKE = 'RPC_ACTIONS_INVOKE'

class RpcServer {
  start() {
    ipcMain.on(RPC_ACTIONS, this.handleEvent.bind(this))
    ipcMain.handle(RPC_ACTIONS_INVOKE, this.handleInvoke.bind(this))
  }

  private handleEvent(event, action, ...args) {
    const handler = this.handlers[action]
    if (handler) handler(event, ...args)
  }

  private async handleInvoke(event, action, ...args) {
    const handler = this.handlers[action]
    return handler ? await handler(event, ...args) : null
  }
}

export const rpcServer = new RpcServer()

```

### Exposing Safe APIs via Preload

The preload script in [`src/preload/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/preload/index.ts) serializes arguments and creates the bridge:

```typescript
import { contextBridge, ipcRenderer } from 'electron'

const getRawData = (args: any[]) => args.map(arg => JSON.stringify(arg))

contextBridge.exposeInMainWorld('piclist', {
  invoke: (action: string, ...args: any[]) =>
    ipcRenderer.invoke('RPC_ACTIONS_INVOKE', action, ...getRawData(args)),
    
  send: (action: string, ...args: any[]) =>
    ipcRenderer.send('RPC_ACTIONS', action, ...getRawData(args)),
    
  on: (channel: string, listener: (...data: any[]) => void) => {
    const wrapper = (_: any, ...data: any[]) => listener(...data.map(JSON.parse))
    ipcRenderer.on(channel, wrapper)
    return () => ipcRenderer.removeListener(channel, wrapper)
  },
})

```

### Calling Main Process Methods from the Renderer

Vue components interact with the backend through the typed global interface:

```typescript
// In any Vue component
export default {
  async mounted() {
    // Request theme configuration via RPC
    const theme = await window.piclist.invoke('THEME_GET_BOOTSTRAP')
    this.applyTheme(theme)

    // Listen for main-process broadcasts
    window.piclist.on('THEME_UPDATE', (newTheme) => {
      this.applyTheme(newTheme)
    })
  },

  methods: {
    uploadFiles(files) {
      // Fire-and-forget upload request
      window.piclist.send('UPLOAD_FILES', files)
    }
  }
}

```

## Security and Design Benefits

PicList's IPC architecture prioritizes **context isolation**, ensuring the renderer process never holds a direct reference to `require('electron')`. By forcing all communication through the preload bridge, the application mitigates XSS vulnerabilities while maintaining clean separation of concerns.

The RPC pattern also enables **modular testing**. Developers can mock `ipcMain` and `ipcRenderer` to test handler logic without spawning Electron windows, while the centralized action naming convention (using constants like `RPC_ACTIONS_INVOKE`) prevents channel collision bugs.

## Summary

- **Main process** registers IPC handlers in [`src/main/events/rpc/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/events/rpc/index.ts) using `ipcMain.handle` and `ipcMain.on` via the `RpcServer` class
- **Preload script** ([`src/preload/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/preload/index.ts)) safely exposes `ipcRenderer` methods through `contextBridge.exposeInMainWorld('piclist', ...)`
- **Renderer process** accesses main functionality through `window.piclist.invoke()`, `send()`, and `on()` without direct Node.js access
- **Channel constants** (`RPC_ACTIONS`, `RPC_ACTIONS_INVOKE`) centralize communication naming to prevent typos
- **Security** is enforced by context isolation, ensuring the Chromium UI cannot access filesystem or system APIs directly

## Frequently Asked Questions

### How does PicList prevent the renderer process from accessing Node.js APIs directly?

PicList implements **context isolation** by using Electron's `contextBridge` module in [`src/preload/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/preload/index.ts). The preload script is the only file with access to both Node.js and DOM APIs; it selectively exposes only the `invoke`, `send`, and `on` methods on `window.piclist`. The renderer code runs in a isolated context without `nodeIntegration`, making direct `require()` calls impossible and protecting against XSS attacks.

### What is the difference between `window.piclist.invoke()` and `window.piclist.send()` in PicList?

`window.piclist.invoke()` triggers `ipcRenderer.invoke()` on the `RPC_ACTIONS_INVOKE` channel and returns a Promise that resolves with the main process handler's return value, making it ideal for requesting data like `THEME_GET_BOOTSTRAP`. `window.piclist.send()` uses `ipcRenderer.send()` on the `RPC_ACTIONS` channel for fire-and-forget operations like `UPLOAD_FILES` where no response is expected by the caller.

### Where does PicList initialize the IPC communication layer?

The RPC server initializes in [`src/main/lifeCycle/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/lifeCycle/index.ts) early in the application startup sequence. The file imports `rpcServer` from [`src/main/events/rpc/index.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/events/rpc/index.ts) and calls `rpcServer.start()` before any browser windows are created, ensuring all IPC handlers are registered before the renderer attempts to communicate.

### Can third-party plugins add new IPC channels to PicList?

According to the source architecture in `src/main/manage/apis/*.ts` (such as [`webdavplist.ts`](https://github.com/kuingsmile/piclist/blob/main/webdavplist.ts)), domain-specific handlers register additional `ipcMain.on` listeners for specialized actions like `cancelLoadingFileList`. While the core RPC mechanism uses the centralized `RPC_ACTIONS` channels, individual API modules can extend functionality by registering direct listeners on `ipcMain`, provided they follow the preload bridge security model.