electron

Node.js 原生模块调用

By AI-Writer 13 min read

前言

Electron 的主进程运行在 Node.js 环境中,理论上可以安装和使用任何 npm 包。但原生模块(Native Modules,即包含 C/C++ 编译代码的包)有一个关键问题:Node.js 版本与 Electron 内置 Node.js 版本不匹配。本文将详细讲解原生模块的安装、编译和常见场景的最佳实践。

原生模块的版本问题

Electron 使用的是自己的 Node.js

Electron 内置的 Node.js 版本(如 v20.x)和系统全局安装的 Node.js 版本可能不同。原生模块(如 sqlite3)使用 node-gyp 编译时,链接的是全局 Node.js 的 .node 文件,在 Electron 中运行时就会出现版本不匹配的崩溃:

plaintext
Error: The module '/node_modules/sqlite3/build/Release/sqlite3.node'
was compiled against a different Node.js version.

解决方案一览

方案原理适用场景
electron-rebuild用 Electron 版本重新编译原生模块需要使用原生模块时
@electron/rebuildelectron-rebuild 的更新维护版同上
纯 JS 替代方案用纯 JavaScript 实现相同功能性能要求不高的场景
prebuild-install预编译二进制分发CI/CD 环境
node-pre-gyp运行时动态下载预编译二进制跨平台发布

使用 electron-rebuild

安装与配置

bash
# 安装 electron-rebuild
pnpm add -D @electron/rebuild

# 安装原生模块(以 sqlite3 为例)
pnpm add sqlite3

编译原生模块

bash
# 手动触发一次编译
pnpm exec electron-rebuild

# 指定 Electron 版本
pnpm exec electron-rebuild -v 35.2.0

# 只编译特定模块
pnpm exec electron-rebuild -m ./node_modules/sqlite3

集成到 package.json

json
{
  "scripts": {
    "postinstall": "electron-rebuild",
    "rebuild": "electron-rebuild"
  }
}

postinstall 脚本会在每次 pnpm install 后自动执行,确保原生模块与当前 Electron 版本兼容。

Forge 项目中的 electron-rebuild

Electron Forge 内置了 @electron-forge/plugin-auto-unpack-natives,会在打包时自动处理原生模块。但为了保险起见,仍建议在开发环境中运行 electron-rebuild

bash
# Forge 项目中重新编译
pnpm rebuild

常见原生模块使用

better-sqlite3(推荐替代 sqlite3)

socket3 是纯 JavaScript 实现的 SQLite,性能略低于原生模块但安装更简单:

bash
pnpm add better-sqlite3
pnpm exec electron-rebuild -m ./node_modules/better-sqlite3
typescript
// main.ts — 使用 better-sqlite3
import Database from 'better-sqlite3';
import path from 'path';
import { app } from 'electron';

const dbPath = path.join(app.getPath('userData'), 'app.db');
const db = new Database(dbPath);

// 创建表
db.exec(`
  CREATE TABLE IF NOT EXISTS notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);

// 插入
const insert = db.prepare(
  'INSERT INTO notes (title, content) VALUES (?, ?)'
);
insert.run('学习 Electron', 'IPC 通信是 Electron 的核心');

// 查询
const rows = db.prepare('SELECT * FROM notes ORDER BY created_at DESC').all();
console.log('笔记列表:', rows);

// 关闭
db.close();

sharp(图片处理)

原生图片处理库,处理缩略图、格式转换非常高效:

bash
pnpm add sharp
pnpm exec electron-rebuild -m ./node_modules/sharp
typescript
// main.ts — 图片处理
import sharp from 'sharp';
import path from 'path';
import { ipcMain, app } from 'electron';

ipcMain.handle('image:thumbnail', async (_event, inputPath: string, width: number) => {
  const outputPath = path.join(app.getPath('temp'), `thumb_${Date.now()}.jpg`);

  await sharp(inputPath)
    .resize(width, null, { withoutEnlargement: true })
    .jpeg({ quality: 80 })
    .toFile(outputPath);

  return outputPath;
});

ipcMain.handle('image:convert', async (_event, inputPath: string, format: 'png' | 'webp' | 'jpeg') => {
  const outputPath = inputPath.replace(/\.\w+$/, `.${format}`);

  await sharp(inputPath)[format]({ quality: 85 }).toFile(outputPath);
  return outputPath;
});

ipcMain.handle('image:meta', async (_event, imagePath: string) => {
  const meta = await sharp(imagePath).metadata();
  return {
    width: meta.width,
    height: meta.height,
    format: meta.format,
    size: meta.size,
  };
});

node-fetch(HTTP 请求)

Electron 主进程虽然可以用 Node.js 的 fetch(Node 18+),但如果需要更精细的控制,可以使用 node-fetchaxios

bash
pnpm add node-fetch
typescript
// main.ts — 在主进程中使用 node-fetch
import fetch from 'node-fetch';
import { ipcMain } from 'electron';

ipcMain.handle('http:download', async (_event, url: string) => {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);

  const buffer = await response.buffer();
  return buffer.toString('base64');
});

纯 JavaScript 替代方案

如果原生模块的编译过程过于复杂,可以考虑纯 JS 替代:

原生模块JS 替代说明
sqlite3sql.jsWebAssembly 实现的 SQLite,无需编译
sharp@squoosh/lib图片压缩,但功能较少
node-canvasSVG 渲染Electron 渲染进程直接用 Canvas
nodejieba无成熟 JS 替代,可尝试预编译二进制

使用 sql.js(无编译的 SQLite)

bash
pnpm add sql.js
typescript
// main.ts — sql.js 使用(无需 electron-rebuild)
import initSqlJs from 'sql.js';
import fs from 'fs';
import { ipcMain, app } from 'electron';

let db: Awaited<ReturnType<typeof initSqlJs>>['Database'];

ipcMain.handle('db:init', async () => {
  const SQL = await initSqlJs();
  const dbPath = path.join(app.getPath('userData'), 'app.sqlite');

  if (fs.existsSync(dbPath)) {
    const buffer = fs.readFileSync(dbPath);
    db = new SQL.Database(buffer);
  } else {
    db = new SQL.Database();
  }
  return { success: true };
});

ipcMain.handle('db:query', async (_event, sql: string) => {
  try {
    const results = db.exec(sql);
    return { columns: results[0]?.columns, rows: results[0]?.values ?? [] };
  } catch (error) {
    return { error: (error as Error).message };
  }
});

ipcMain.handle('db:save', async () => {
  const data = db.export();
  const buffer = Buffer.from(data);
  fs.writeFileSync(path.join(app.getPath('userData'), 'app.sqlite'), buffer);
});

注意:sql.js 在 WebAssembly 模式下运行,数据存储需要手动保存到文件系统。

Worker Threads 多线程

主进程中执行 CPU 密集型任务(如大文件处理、JSON 解析)会阻塞事件循环,导致 UI 卡顿。Worker Threads 可以在后台线程执行这些任务:

typescript
// main.ts — Worker Threads 示例
import { Worker } from 'worker_threads';
import path from 'path';
import { ipcMain } from 'electron';

// 创建 Worker 执行后台任务
function runWorker(data: unknown) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(path.join(__dirname, 'worker.js'), {
      workerData: data,
    });

    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker 退出码: ${code}`));
    });
  });
}

// worker.js — Worker 线程入口
import { workerData, parentPort } from 'worker_threads';

const result = heavyComputation(workerData);
parentPort?.postMessage(result);

function heavyComputation(data: unknown) {
  // 这里是 CPU 密集型计算,不会阻塞主线程
  // ...
  return result;
}
typescript
// main.ts — IPC 处理器
ipcMain.handle('worker:process', async (_event, data: unknown) => {
  return runWorker(data);
});

electron-rebuild 在 CI/CD 中

GitHub Actions 中的完整构建流程:

yaml
# .github/workflows/build.yml
name: Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - run: pnpm install

      - name: Rebuild native modules
        run: pnpm exec electron-rebuild

      - name: Build app
        run: pnpm make

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: electron-app
          path: out/make

常见问题

编译报错 “node-gyp not found”

bash
# 安装 node-gyp 依赖(macOS 需要 Xcode)
brew install python3 node-gyp

# Windows 需要 Visual Studio Build Tools
# npm install --global --production windows-build-tools

pnpm 安装原生模块失败

json
// package.json
{
  "pnpm": {
    "onlyBuiltDependencies": ["better-sqlite3", "sharp", "sqlite3"]
  }
}

原生模块在打包后找不到

javascript
// forge.config.js — 标记为非 asar 打包
packagerConfig: {
  asar: {
    unpack: '**/node_modules/better-sqlite3/**',
  },
}

小结

  • 原生模块需要与 Electron 内置的 Node.js 版本匹配才能运行
  • electron-rebuild 是重新编译原生模块的标准工具
  • better-sqlite3sharp 是常用的高性能原生模块
  • sql.js(WebAssembly)提供了免编译的 SQLite 替代方案
  • Worker Threads 用于在主进程中进行 CPU 密集型计算而不阻塞 UI
  • 生产构建中注意 asar.unpack 配置,确保原生二进制正确打包

下一篇文章我们将介绍 React / Vue 与 Electron 集成,使用 electron-vite 工具链构建现代化前端 + Electron 应用。

#electron #native-modules #node-gyp #electron-rebuild #sqlite3

评论

A

Written by

AI-Writer

Related Articles

electron
#3

IPC 通信机制详解

深入理解 Electron 的 IPC 模块:invoke/handle 双向异步模式、ipcRenderer.on 事件监听、channel 命名规范、进程间数据传输规则

Read More
electron
#9

Electron 应用打包与分发

使用 electron-builder 配置多平台打包(Windows NSIS / macOS DMG / Linux AppImage),设置自动更新(electron-updater),配置代码签名(Authenticode / Apple Developer),搭建 GitHub Actions CI 流水线

Read More