人人都会AI编程

15.4 代码规范与类型支持:ESLint、TypeScript 整合

更新时间:2026-07-11

在 Electron 项目长期维护的过程中,代码规范和类型安全会直接决定团队的开发效率、重构信心以及新成员的上手速度。这一节不讲空泛的理论,而是聚焦两个工具在真实 Electron 工程中的落地:ESLint 保证代码风格和潜在错误检测,TypeScript 为主进程、预加载脚本和渲染进程提供统一的类型约束

15.4.1 为什么要在一个项目里同时用 ESLint 和 TypeScript

很多开发者会有一个误解:既然用了 TypeScript,编译阶段就能发现很多类型错误,是不是可以不用 ESLint 了?实际上,两者的职责并不重叠:

  • TypeScript 负责类型检查,确保你传给函数的是正确的参数、对象上不会出现不存在的属性。
  • ESLint 负责代码风格与逻辑规则,例如禁止使用 var、要求使用 const、检测可能的空值调用、统一导入顺序、避免使用已废弃的 API 等。

在 Electron 项目中,同一个仓库里通常包含多种运行环境:主进程运行在 Node.js 环境下,渲染进程运行在浏览器环境下,预加载脚本则处于一种受限的 Node.js 环境。这三者的全局变量、可用 API 各不相同。ESLint 的 env 配置和 TypeScript 的 tsconfig 可以分别为不同目录指定正确的上下文,避免出现误报或漏报。

15.4.2 在 Electron 项目中配置 ESLint

基础安装与初始化

npm install -D eslint @eslint/js
# 如果使用 TypeScript,还需要
npm install -D typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin

然后创建 eslint.config.js(扁平配置风格,ESLint 9+ 推荐)或 .eslintrc.cjs(传统风格)。这里以扁平配置为例:

// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  // 全局忽略模式
  { ignores: ['dist/', 'out/', 'node_modules/'] },
  // 基础 JS 规则
  js.configs.recommended,
  // 主进程与预加载脚本:Node.js 环境
  {
    files: ['electron/**/*.ts', 'preload/**/*.ts'],
    languageOptions: {
      globals: {
        ...require('globals').node,  // Node.js 全局变量
      },
    },
    rules: {
      'no-console': 'off',           // 主进程允许 console
      '@typescript-eslint/no-var-requires': 'off', // 动态 require 有时不可避免
    },
  },
  // 渲染进程:浏览器环境
  {
    files: ['src/**/*.ts', 'src/**/*.tsx'],
    languageOptions: {
      globals: {
        ...require('globals').browser, // 浏览器全局变量
      },
    },
    rules: {
      'no-console': 'warn',          // 渲染进程尽量用 logging 工具
    },
  },
  // TypeScript 推荐规则
  ...tseslint.configs.recommended,
);

如果你的项目还在用 .eslintrc,核心思路相同:为 electron/src/ 分别配置 overrides,并指定不同的 env

真实项目中常见的规则调整

Electron 开发中有几个特殊场景需要单独配置:

  • 主进程里常常使用 ipcMain.handleapp.whenReady() 等 Electron API,这些在 ESLint 看来可能是未定义的全局变量。需要安装 eslint-plugin-electron 插件,它提供了 electron 环境配置,能正确识别 appBrowserWindowipcMain 等全局对象。安装后在 languageOptions.globals 中引入,或者使用插件内置的环境配置。
  • contextBridge.exposeInMainWorld 注入到 window 上的自定义属性,在 TypeScript 中需要声明全局类型(见 15.4.3),在 ESLint 层面可以用 globals 手动声明,避免 no-undef 报错。
  • 如果渲染进程使用了 JSX 或 React,记得安装 eslint-plugin-react 或对应的 Vue 插件,并开启推荐规则。

一个实用的命令:在 package.jsonscripts 中添加:

{
  "lint": "eslint . --ext .ts,.tsx,.js,.jsx",
  "lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix"
}

15.4.3 TypeScript 在 Electron 项目中的整合

Electron 项目天然适合 TypeScript,因为它需要频繁地在主进程和渲染进程之间传递结构化的数据(IPC 消息、配置对象、文件信息等)。类型定义能够充当两个进程之间的“契约文档”,避免因字段拼写错误导致的难以排查的 bug。

多项目结构与 tsconfig 策略

由于主进程、预加载脚本和渲染进程的目标环境不同,通常会为它们准备不同的 tsconfig 文件:

project/
├── tsconfig.json           // 根配置,仅用于引用
├── tsconfig.main.json      // 主进程 + 预加载
├── tsconfig.renderer.json  // 渲染进程
└── src/                    // 渲染进程代码
└── electron/               // 主进程代码
└── preload/                // 预加载脚本

示例 tsconfig.main.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",          // 主进程通常用 CommonJS(或 ESM 用 module: NodeNext)
    "lib": ["ES2022"],
    "outDir": "dist/main",
    "rootDir": "electron",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["node"],
    "resolveJsonModule": true
  },
  "include": ["electron/**/*.ts", "preload/**/*.ts"]
}

示例 tsconfig.renderer.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "outDir": "dist/renderer",
    "rootDir": "src",
    "strict": true,
    "jsx": "preserve",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": ["vite/client"],       // 或 webpack 的环境类型
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true
  },
  "include": ["src/**/*.ts", "src/**/*.tsx"]
}

这样做的好处是:主进程可以使用 Node.js 的模块解析策略,渲染进程则可以使用现代打包工具(Vite/Webpack)的模块解析,两者互不干扰。

打通主进程与渲染进程的类型通信

这是 TypeScript 在 Electron 中最有价值的部分。通常会定义一个共享的类型文件(如 shared/ipc-types.ts):

// shared/ipc-types.ts
export interface FileInfo {
  name: string;
  path: string;
  size: number;
}

export interface IElectronAPI {
  openFile: () => Promise<FileInfo | null>;
  saveFile: (content: string) => Promise<void>;
  onFileSaved: (callback: (path: string) => void) => void;
}

然后在主进程的 IPC handler 里实现这些接口:

// electron/main.ts
import { ipcMain, dialog } from 'electron';
import type { FileInfo } from '../shared/ipc-types';

ipcMain.handle('open-file', async (): Promise<FileInfo | null> => {
  const result = await dialog.showOpenDialog({ /* ... */ });
  // ...返回 FileInfo 或 null
});

在预加载脚本中,使用 contextBridge 暴露 API 时强制类型:

// preload/index.ts
import { contextBridge, ipcRenderer } from 'electron';
import type { IElectronAPI, FileInfo } from '../shared/ipc-types';

const api: IElectronAPI = {
  openFile: () => ipcRenderer.invoke('open-file'),
  saveFile: (content) => ipcRenderer.invoke('save-file', content),
  onFileSaved: (callback) => ipcRenderer.on('file-saved', (_event, path) => callback(path)),
};

contextBridge.exposeInMainWorld('electronAPI', api);

渲染进程中,通过类型声明文件让 TypeScript 知道 window.electronAPI 的存在:

// src/types/electron.d.ts
import type { IElectronAPI } from '../../shared/ipc-types';

declare global {
  interface Window {
    electronAPI: IElectronAPI;
  }
}

此后,在任何组件中使用 window.electronAPI.openFile() 时,TypeScript 都会提供完整的类型提示和检查,避免把 file.path 误写成 file.paht 这种低级错误。

真实项目中的细节

  • 使用 electron-builder 打包时,通常会让主进程和预加载脚本先被 TypeScript 编译到 dist/ 目录,再在 electron-builder.yml 中指定入口文件为编译后的 .js 文件。
  • 如果项目使用了 TypeScript 的 path alias(如 @shared/*),记得在主进程的 tsconfig 中配置好 pathsbaseUrl,并在编译时使用 tsc-alias 或类似工具将路径替换为实际相对路径,否则打包后运行会报模块找不到。
  • 为了保持类型严格性,建议在根 tsconfig.json 中开启 strict: true,并逐步消灭 any。一个良好的习惯是为主进程的 IPC handler 返回值和参数都显式标注类型,这样即便几个月后回来看代码,也能立刻知道每个通道传的是什么数据。

15.4.4 ESLint 与 TypeScript 的协同工作流

将两者无缝衔接的关键配置:

  1. 使用 typescript-eslint 作为解析器,这样 ESLint 在检查 .ts 文件时能够理解 TypeScript 的语法和类型信息。
  2. eslint.config.js 中启用 tseslint.configs.recommended,它会替代原生的 ESLint 规则,避免与 TypeScript 编译器规则冲突。
  3. 把类型检查的任务交给 TypeScript 编译器(tsc --noEmit),ESLint 只负责代码风格和逻辑规则,这样运行速度更快(ESLint 不需要执行繁重的类型推导)。

日常开发中,可以在 package.json 中添加检查脚本,组合使用:

{
  "lint": "eslint . --ext .ts,.tsx,.js,.jsx",
  "typecheck": "tsc --noEmit -p tsconfig.main.json && tsc --noEmit -p tsconfig.renderer.json",
  "validate": "npm run lint && npm run typecheck"
}

配合 Git hooks(如 Husky + lint-staged),可以在每次提交前自动修复格式并检查类型,确保仓库中的代码始终处于健康状态。

15.4.5 避坑指南

  • 主进程 import 路径问题

如果渲染进程使用 Vite 等打包工具,可以用 @/ 别名,但主进程直接运行在 Node.js,不经过打包(除非你也用工具打包)。因此主进程中的相对导入一定要写对,或者用 tsconfigpaths + tsc-alias 处理。

  • contextBridge 暴露的对象引用不响应更新

这是 Vue/React 响应式系统容易踩的坑。window.electronAPI 是静态对象,如果把它赋值给响应式状态,它不会自动追踪底层 IPC 事件。正确做法是在组件中调用 API 方法,并手动更新本地状态。

  • 类型文件版本差异

Electron 的类型定义(@types/electron)已弃用,现在 Electron 自带了类型声明。在项目中只需安装 electron 即可获得类型支持。如果还在使用旧包,请移除 @types/electron,避免类型冲突。

  • ESLint 检查 dist 目录

一定要在 ESLint 配置中忽略编译输出目录(如 dist/out/),否则可能因为检查大量自动生成的 JS 文件而严重拖慢速度。


通过 ESLint 与 TypeScript 的扎实整合,你的 Electron 项目将拥有两个层面的质量护栏:编译时就能发现数据类型错误,提交前自动纠正风格违规。这不仅是代码规范的建设,更是对项目长期可维护性的投资——当项目从一个人变成三个人,从三个窗口变成十个窗口时,你会感谢这套基础设施的存在。