Skip to content

开发最佳实践

项目结构规范

模块划分

packages/main/src/
├── apps/          # 应用层 - 应用初始化和配置
├── core/          # 核心层 - 窗口、托盘等核心功能
├── services/      # 服务层 - 业务逻辑封装
├── handlers/      # 处理器层 - 事件和请求处理
├── ipc-handlers/  # IPC 层 - 进程间通信
├── plugins/       # 插件层 - 插件系统
├── config/        # 配置层 - 配置读取和管理
├── utils/         # 工具层 - 通用工具函数
└── constants/     # 常量层 - 常量定义

分层原则

  • 单向依赖: 上层可以依赖下层,下层不能依赖上层
  • 职责单一: 每个模块只负责一个功能领域
  • 高内聚低耦合: 模块内部紧密相关,模块之间松散耦合
应用层 (apps/)

核心层 (core/) → 处理器层 (handlers/)
   ↓                   ↓
服务层 (services/) ← IPC层 (ipc-handlers/)

工具层 (utils/) + 配置层 (config/)

常量层 (constants/)

代码规范

命名规范

文件命名

javascript
// ✅ 推荐:kebab-case
machine - service.js;
floating - ball - ipc - handler.js;
update - handler.js;

// ❌ 避免:camelCase 或 PascalCase
machineService.js;
FloatingBallIpcHandler.js;

类命名

javascript
// ✅ 推荐:PascalCase
class MachineService {}
class FloatingBallService {}
class UpdateHandler {}

// ❌ 避免:camelCase
class machineService {}
class floatingBallService {}

变量和函数命名

javascript
// ✅ 推荐:camelCase
const machineInfo = {};
function getMachineInfo() {}

// 常量:UPPER_SNAKE_CASE
const MAX_RETRY_COUNT = 3;
const API_TIMEOUT = 30000;

// 私有属性/方法:下划线前缀
class MyClass {
  _privateMethod() {}
  _privateProperty = null;
}

代码组织

模块导出

javascript
// ✅ 推荐:导出类或对象
class MachineService {
  static async initialize() {}
  static getMachineInfo() {}
}

module.exports = MachineService;

// ✅ 或导出多个函数
module.exports = {
  initialize,
  getMachineInfo,
};

// ❌ 避免:混合导出
module.exports = MachineService;
module.exports.extraFunction = () => {};

文件结构

javascript
// 1. 依赖导入
const { app } = require('electron');
const logger = require('../utils/logger');

// 2. 常量定义
const MAX_RETRY = 3;
const TIMEOUT = 5000;

// 3. 类或函数定义
class MyService {
  // 3.1 静态属性
  static instance = null;

  // 3.2 实例属性
  constructor() {
    this.data = {};
  }

  // 3.3 静态方法
  static getInstance() {
    if (!this.instance) {
      this.instance = new MyService();
    }
    return this.instance;
  }

  // 3.4 公共方法
  publicMethod() {}

  // 3.5 私有方法
  _privateMethod() {}
}

// 4. 模块导出
module.exports = MyService;

异步编程

使用 async/await

javascript
// ✅ 推荐:async/await
async function loadData() {
  try {
    const data = await fetchData();
    const processed = await processData(data);
    return processed;
  } catch (error) {
    logger.error('加载数据失败:', error);
    throw error;
  }
}

// ❌ 避免:Promise 链
function loadData() {
  return fetchData()
    .then((data) => processData(data))
    .then((processed) => {
      return processed;
    })
    .catch((error) => {
      logger.error('加载数据失败:', error);
      throw error;
    });
}

并行执行

javascript
// ✅ 推荐:使用 Promise.all
async function initializeServices() {
  await Promise.all([
    MachineService.initialize(),
    PluginService.initialize(),
    LoggerClean.getInstance().initialize(),
  ]);
}

// ❌ 避免:串行执行(除非有依赖关系)
async function initializeServices() {
  await MachineService.initialize();
  await PluginService.initialize();
  await LoggerClean.getInstance().initialize();
}

错误处理

javascript
// ✅ 推荐:细粒度错误处理
async function doSomething() {
  try {
    const result = await riskyOperation();
    return result;
  } catch (error) {
    if (error.code === 'ENOENT') {
      logger.warn('文件不存在,使用默认值');
      return defaultValue;
    }
    logger.error('操作失败:', error);
    throw new Error(`操作失败: ${error.message}`);
  }
}

// ❌ 避免:吞掉所有错误
async function doSomething() {
  try {
    return await riskyOperation();
  } catch (error) {
    return null; // 错误被隐藏了
  }
}

IPC 通信

主进程注册

javascript
// ✅ 推荐:统一管理 IPC 处理器
// ipc-handlers/my-handler.js
const { ipcMain } = require('electron');
const logger = require('../utils/logger');

function registerMyHandlers() {
  // 使用 handle 处理需要返回值的请求
  ipcMain.handle('my-action', async (event, params) => {
    try {
      logger.info('收到请求:', params);
      const result = await doSomething(params);
      return { success: true, data: result };
    } catch (error) {
      logger.error('处理失败:', error);
      return { success: false, error: error.message };
    }
  });

  // 使用 on 处理不需要返回值的事件
  ipcMain.on('my-event', (event, data) => {
    logger.info('收到事件:', data);
    handleEvent(data);
  });

  logger.info('MyHandler IPC 注册成功');
}

module.exports = { registerMyHandlers };

渲染进程调用

javascript
// ✅ 推荐:使用预加载脚本暴露的 API
// 渲染进程
const result = await window.electron.invoke('my-action', params);

window.electron.send('my-event', data);

window.electron.on('notification', (data) => {
  console.log('收到通知:', data);
});

// ❌ 避免:直接使用 ipcRenderer(不安全)
const { ipcRenderer } = require('electron');
ipcRenderer.send('my-event', data);

预加载脚本

javascript
// ✅ 推荐:精确控制暴露的 API
// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electron', {
  // 双向通信(有返回值)
  invoke: (channel, data) => {
    const validChannels = ['my-action', 'another-action'];
    if (validChannels.includes(channel)) {
      return ipcRenderer.invoke(channel, data);
    }
  },

  // 单向通信(无返回值)
  send: (channel, data) => {
    const validChannels = ['my-event'];
    if (validChannels.includes(channel)) {
      ipcRenderer.send(channel, data);
    }
  },

  // 监听事件
  on: (channel, callback) => {
    const validChannels = ['notification'];
    if (validChannels.includes(channel)) {
      ipcRenderer.on(channel, (event, ...args) => callback(...args));
    }
  },
});

日志记录

日志级别

javascript
// ✅ 推荐:使用合适的日志级别
logger.info('正常信息'); // 一般信息
logger.warn('警告信息'); // 警告,不影响运行
logger.error('错误信息'); // 错误,需要关注
logger.debug('调试信息'); // 调试信息(生产环境不输出)

// ❌ 避免:全部使用 console.log
console.log('这是什么级别的日志?');

结构化日志

javascript
// ✅ 推荐:提供上下文信息
logger.info('用户登录', {
  userId: 123,
  username: 'user@example.com',
  ip: '192.168.1.1',
  timestamp: new Date().toISOString(),
});

logger.error('API 请求失败', {
  url: 'https://api.example.com/data',
  method: 'POST',
  status: 500,
  error: error.message,
  stack: error.stack,
});

// ❌ 避免:信息不完整
logger.info('用户登录');
logger.error('请求失败', error);

敏感信息保护

javascript
// ✅ 推荐:过滤敏感信息
function sanitizeData(data) {
  const sanitized = { ...data };
  delete sanitized.password;
  delete sanitized.token;
  delete sanitized.creditCard;
  return sanitized;
}

logger.info('用户数据', sanitizeData(userData));

// ❌ 避免:记录敏感信息
logger.info('用户数据', userData); // 可能包含密码

错误处理

自定义错误类

javascript
// ✅ 推荐:定义业务错误类
class AppError extends Error {
  constructor(message, code, details = {}) {
    super(message);
    this.name = 'AppError';
    this.code = code;
    this.details = details;
  }
}

class NetworkError extends AppError {
  constructor(message, details) {
    super(message, 'NETWORK_ERROR', details);
    this.name = 'NetworkError';
  }
}

// 使用
throw new NetworkError('API 请求失败', {
  url: 'https://api.example.com',
  status: 500,
});

全局错误捕获

javascript
// ✅ 推荐:捕获未处理的异常
process.on('uncaughtException', (error) => {
  logger.error('未捕获的异常:', error);
  // 记录错误但不退出
  // 可选:重启应用或提示用户
});

process.on('unhandledRejection', (reason, promise) => {
  logger.error('未处理的 Promise 拒绝:', reason);
});

// 渲染进程错误
window.addEventListener('error', (event) => {
  logger.error('渲染进程错误:', event.error);
});

window.addEventListener('unhandledrejection', (event) => {
  logger.error('未处理的 Promise 拒绝:', event.reason);
});

性能优化

懒加载

javascript
// ✅ 推荐:按需加载大模块
class PluginService {
  static _heavyModule = null;

  static async getHeavyModule() {
    if (!this._heavyModule) {
      this._heavyModule = require('./heavy-module');
      await this._heavyModule.initialize();
    }
    return this._heavyModule;
  }
}

// ❌ 避免:启动时加载所有模块
const heavyModule = require('./heavy-module'); // 立即加载

资源清理

javascript
// ✅ 推荐:及时清理资源
class MyService {
  constructor() {
    this.timers = [];
    this.listeners = [];
  }

  startPolling() {
    const timer = setInterval(() => {
      this.poll();
    }, 5000);
    this.timers.push(timer);
  }

  destroy() {
    // 清理定时器
    this.timers.forEach((timer) => clearInterval(timer));
    this.timers = [];

    // 清理事件监听
    this.listeners.forEach(({ target, event, handler }) => {
      target.removeListener(event, handler);
    });
    this.listeners = [];
  }
}

// 应用退出时清理
app.on('before-quit', () => {
  myService.destroy();
});

缓存策略

javascript
// ✅ 推荐:使用缓存减少重复计算
class DataService {
  static cache = new Map();
  static CACHE_TTL = 5 * 60 * 1000; // 5 分钟

  static async getData(key) {
    const cached = this.cache.get(key);
    if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
      return cached.data;
    }

    const data = await this.fetchData(key);
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
    });
    return data;
  }

  static clearCache() {
    this.cache.clear();
  }
}

安全实践

上下文隔离

javascript
// ✅ 推荐:启用上下文隔离
const win = new BrowserWindow({
  webPreferences: {
    contextIsolation: true, // 隔离上下文
    nodeIntegration: false, // 禁用 Node.js 集成
    sandbox: true, // 启用沙箱
    preload: path.join(__dirname, 'preload.js'),
  },
});

// ❌ 避免:直接暴露 Node.js(不安全)
const win = new BrowserWindow({
  webPreferences: {
    nodeIntegration: true, // 危险!
    contextIsolation: false, // 危险!
  },
});

输入验证

javascript
// ✅ 推荐:验证所有外部输入
ipcMain.handle('save-file', async (event, { path, content }) => {
  // 验证路径
  if (!path || typeof path !== 'string') {
    throw new Error('无效的文件路径');
  }

  // 防止路径遍历攻击
  const normalizedPath = path.normalize(path);
  const basePath = app.getPath('userData');
  if (!normalizedPath.startsWith(basePath)) {
    throw new Error('不允许访问该路径');
  }

  // 验证内容大小
  if (content.length > 10 * 1024 * 1024) {
    // 10MB
    throw new Error('文件内容过大');
  }

  await fs.writeFile(normalizedPath, content);
});

权限控制

javascript
// ✅ 推荐:最小权限原则
const session = require('electron').session;

session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
  const allowedPermissions = ['notifications', 'clipboard-read'];

  if (allowedPermissions.includes(permission)) {
    callback(true);
  } else {
    logger.warn('拒绝权限请求:', permission);
    callback(false);
  }
});

测试实践

单元测试

javascript
// ✅ 推荐:为核心逻辑编写测试
// tests/services/machine-service.test.js
const MachineService = require('../../src/services/machine-service');

describe('MachineService', () => {
  test('应该返回机器信息', async () => {
    await MachineService.initialize();
    const info = MachineService.getMachineInfo();

    expect(info).toHaveProperty('machineId');
    expect(info).toHaveProperty('cpu');
    expect(info).toHaveProperty('memory');
  });

  test('应该处理初始化失败', async () => {
    // 模拟失败场景
    jest.spyOn(MachineService, '_collectInfo').mockRejectedValue(new Error('Failed'));

    await expect(MachineService.initialize()).rejects.toThrow('Failed');
  });
});

E2E 测试

javascript
// ✅ 推荐:使用 Spectron 或 Playwright
const { Application } = require('spectron');

describe('应用启动', () => {
  let app;

  beforeEach(async () => {
    app = new Application({
      path: electronPath,
      args: [path.join(__dirname, '..')],
    });
    await app.start();
  });

  afterEach(async () => {
    if (app && app.isRunning()) {
      await app.stop();
    }
  });

  test('应该显示主窗口', async () => {
    const count = await app.client.getWindowCount();
    expect(count).toBe(1);

    const title = await app.client.getTitle();
    expect(title).toBe('Vue3-Simple-Electron');
  });
});

文档规范

代码注释

javascript
/**
 * 获取机器信息
 *
 * @description 收集系统的 CPU、内存、硬盘等信息
 * @returns {Promise<MachineInfo>} 机器信息对象
 * @throws {Error} 当收集信息失败时抛出异常
 *
 * @example
 * const info = await MachineService.getMachineInfo();
 * console.log(info.cpu.model);
 */
static async getMachineInfo() {
  // 实现...
}

README 编写

markdown
# 模块名称

## 功能说明

简要描述模块的功能和用途。

## 使用方法

```javascript
// 示例代码
const result = await MyModule.doSomething();
```

API 文档

doSomething(params)

  • 参数:

    • params.name (string): 名称
    • params.value (number): 数值
  • 返回值: Promise<Result>

  • 示例:

    javascript
    await MyModule.doSomething({ name: 'test', value: 42 });

注意事项

  • 使用前需要先初始化
  • 线程不安全,避免并发调用

相关文档

基于 MIT 许可发布