DevToolBox免费
博客

Electron 完全指南:使用 Web 技术构建跨平台桌面应用 (2026)

19 分钟阅读作者 DevToolBox Team

Electron 是一个开源框架,允许你使用 HTML、CSS 和 JavaScript 构建跨平台桌面应用程序。由 GitHub 创建,现由 OpenJS 基金会维护,Electron 将 Chromium 渲染引擎与 Node.js 运行时结合,为 Web 开发者提供对原生操作系统 API 的完整访问。Visual Studio Code、Slack、Discord、Figma 和 Notion 都是使用 Electron 构建的,证明了 Web 技术可以在 Windows、macOS 和 Linux 上提供生产级桌面体验。

TL;DR

Electron 支持使用 Web 技术(HTML、CSS、JS)构建跨平台桌面应用。它使用多进程架构,主进程访问原生 OS,渲染进程负责 UI。现代 Electron 强制使用上下文隔离和预加载脚本以确保安全。electron-builder 和 electron-forge 处理打包和分发。虽然 Tauri 提供更小的二进制文件,但 Electron 拥有成熟的生态系统、经过验证的可扩展性(VS Code、Slack、Discord)和完整的 Node.js 访问。

Key Takeaways
  • Electron 结合 Chromium 和 Node.js,允许 Web 开发者从单一代码库构建 Windows、macOS 和 Linux 原生桌面应用。
  • 多进程架构将主进程(Node.js、原生 API)与渲染进程(Web UI)分离,提高安全性和稳定性。
  • 上下文隔离和预加载脚本是强制性安全模式,防止渲染进程直接访问 Node.js API。
  • IPC(进程间通信)通过 ipcMain 和 ipcRenderer 是主进程和渲染进程之间安全通信的方式。
  • electron-builder 和 electron-forge 通过单一配置处理代码签名、自动更新和跨平台打包。
  • Electron 应用可以集成 React、Vue 或 Svelte 作为 UI 层,同时完全访问文件系统、通知、托盘和菜单等原生 API。

什么是 Electron,它是如何工作的?

Electron 是一个使用 Web 技术构建桌面应用的框架。底层捆绑 Chromium 浏览器渲染 UI,Node.js 运行时与操作系统交互。启动 Electron 应用时,会启动一个主进程来创建浏览器窗口,每个窗口在自己的渲染进程中运行。

这种架构意味着你可以使用任何 Web 框架(React、Vue、Svelte、Angular)构建 UI,使用任何 npm 包处理后端逻辑。Electron 负责 Web 代码和操作系统之间的连接。

// main.js - Electron entry point
const { app, BrowserWindow } = require("electron");
const path = require("path");

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
      contextIsolation: true,
      sandbox: true,
    },
  });

  win.loadFile("index.html");
}

app.whenReady().then(createWindow);

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") app.quit();
});

app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

Electron 架构:主进程、渲染进程和预加载

Electron 使用受 Chromium 启发的多进程架构。理解三种进程类型对构建安全、高性能的 Electron 应用至关重要。

主进程

主进程是应用的入口点,运行 Node.js 并完全访问操作系统。每个应用只有一个主进程,负责创建和管理 BrowserWindow 实例、处理原生 OS 交互和管理应用生命周期。

渲染进程

每个 BrowserWindow 在自己的渲染进程中运行,本质上是加载 HTML、CSS 和 JavaScript 的 Chromium 浏览器标签。渲染进程默认沙箱化,不能直接访问 Node.js API。

预加载脚本

预加载脚本是主进程和渲染进程之间的桥梁。它们在渲染上下文中运行,但可以访问有限的 Node.js 和 Electron API。通过 contextBridge 选择性地向渲染进程暴露安全 API。

// preload.js - Bridge between main and renderer
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
  // Expose specific functions, not the entire ipcRenderer
  openFile: () => ipcRenderer.invoke("dialog:openFile"),
  saveFile: (content) => ipcRenderer.invoke("dialog:saveFile", content),
  onUpdateAvailable: (callback) => {
    ipcRenderer.on("update-available", (_event, info) => callback(info));
  },
  getAppVersion: () => ipcRenderer.invoke("app:getVersion"),
});

// renderer.js - Uses the exposed API
// window.electronAPI.openFile();
// window.electronAPI.saveFile("Hello World");

Electron vs Tauri:详细对比

Tauri 是 Electron 最常见的替代方案。虽然两者都使用 Web UI 构建跨平台桌面应用,但在后端运行时和系统集成方面采取了根本不同的方法。

特性ElectronTauri
后端运行时Node.js(完整 npm 生态)Rust(编译二进制)
渲染引擎捆绑 Chromium系统 WebView (WRY)
二进制大小~150-200 MB~3-10 MB
内存使用~100-300 MB 基线~30-80 MB 基线
后端语言JavaScript / TypeScriptRust
生态系统成熟,大量 npm 库支持成长中,Rust crate 生态
安全模型进程隔离 + 上下文隔离Rust 安全性 + 许可白名单
知名应用VS Code, Slack, Discord, Figma新兴采用

BrowserWindow API

BrowserWindow 类是创建应用窗口的主要方式。每个窗口是一个完整的 Chromium 浏览器实例,可配置窗口尺寸、框架样式、安全设置等。

const { BrowserWindow, screen } = require("electron");

// Create a frameless, transparent window
const overlay = new BrowserWindow({
  width: 400,
  height: 300,
  frame: false,
  transparent: true,
  alwaysOnTop: true,
  webPreferences: {
    preload: path.join(__dirname, "preload.js"),
    contextIsolation: true,
    sandbox: true,
  },
});

// Create a window that remembers its position
const mainWin = new BrowserWindow({
  width: 1200,
  height: 800,
  minWidth: 800,
  minHeight: 600,
  show: false, // Prevent white flash
  backgroundColor: "#1e1e1e",
  titleBarStyle: "hiddenInset", // macOS traffic lights
  webPreferences: {
    preload: path.join(__dirname, "preload.js"),
    contextIsolation: true,
    sandbox: true,
  },
});

// Show window when ready to avoid white flash
mainWin.once("ready-to-show", () => {
  mainWin.show();
});

IPC 通信:ipcMain 和 ipcRenderer

进程间通信(IPC)是主进程和渲染进程之间交换数据的机制。由于渲染进程被沙箱化,IPC 是从 UI 触发原生操作的唯一安全方式。

单向消息(渲染进程到主进程)

在预加载脚本中使用 ipcRenderer.send(),在主进程中使用 ipcMain.on() 发送即发即忘消息。

// preload.js - One-way message
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("electronAPI", {
  sendNotification: (title, body) => {
    ipcRenderer.send("show-notification", { title, body });
  },
});

// main.js - Handle one-way message
const { ipcMain, Notification } = require("electron");

ipcMain.on("show-notification", (_event, { title, body }) => {
  new Notification({ title, body }).show();
});

双向消息(Invoke/Handle)

使用 ipcRenderer.invoke() 和 ipcMain.handle() 实现请求-响应模式,这是大多数 IPC 通信的推荐方式。

// preload.js - Two-way invoke/handle
contextBridge.exposeInMainWorld("electronAPI", {
  readFile: (filePath) => ipcRenderer.invoke("fs:readFile", filePath),
  writeFile: (filePath, data) => ipcRenderer.invoke("fs:writeFile", filePath, data),
  listDir: (dirPath) => ipcRenderer.invoke("fs:listDir", dirPath),
});

// main.js - Handle invoke requests
const fs = require("fs/promises");

ipcMain.handle("fs:readFile", async (_event, filePath) => {
  // Validate the path before reading
  if (!filePath.startsWith(app.getPath("userData"))) {
    throw new Error("Access denied: path outside user data");
  }
  return fs.readFile(filePath, "utf-8");
});

ipcMain.handle("fs:writeFile", async (_event, filePath, data) => {
  if (!filePath.startsWith(app.getPath("userData"))) {
    throw new Error("Access denied: path outside user data");
  }
  await fs.writeFile(filePath, data, "utf-8");
  return { success: true };
});

// renderer.js - Using the API
// const content = await window.electronAPI.readFile("/path/to/file");
// await window.electronAPI.writeFile("/path/to/file", "new content");

预加载脚本和上下文隔离

上下文隔离是自 Electron 12 以来默认启用的安全特性。它确保预加载脚本和 Electron 内部逻辑在与渲染页面独立的 JavaScript 上下文中运行。

contextBridge 模块提供安全的方式将预加载脚本中的特定 API 暴露给渲染进程。只有通过 contextBridge.exposeInMainWorld() 显式暴露的函数才可用。

// preload.js - Secure API exposure with contextBridge
const { contextBridge, ipcRenderer } = require("electron");

// GOOD: Expose specific functions with validation
contextBridge.exposeInMainWorld("api", {
  getSettings: () => ipcRenderer.invoke("get-settings"),
  saveSettings: (settings) => {
    // Validate data before sending to main process
    if (typeof settings !== "object") throw new Error("Invalid settings");
    return ipcRenderer.invoke("save-settings", settings);
  },
  onThemeChange: (callback) => {
    const handler = (_event, theme) => callback(theme);
    ipcRenderer.on("theme-changed", handler);
    // Return cleanup function
    return () => ipcRenderer.removeListener("theme-changed", handler);
  },
});

// BAD: Never expose the entire ipcRenderer
// contextBridge.exposeInMainWorld("ipc", ipcRenderer); // DANGEROUS!

自动更新

Electron 通过 autoUpdater 模块和流行的 electron-updater 包提供内置的自动更新功能,无需用户手动下载和安装更新。

// main.js - Auto-update with electron-updater
const { autoUpdater } = require("electron-updater");

autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;

app.whenReady().then(() => {
  autoUpdater.checkForUpdates();
});

autoUpdater.on("update-available", (info) => {
  // Notify renderer about the update
  mainWindow.webContents.send("update-available", info.version);
});

autoUpdater.on("download-progress", (progress) => {
  mainWindow.webContents.send("download-progress", progress.percent);
});

autoUpdater.on("update-downloaded", () => {
  mainWindow.webContents.send("update-ready");
});

// IPC handler to trigger download and install
ipcMain.handle("download-update", () => autoUpdater.downloadUpdate());
ipcMain.handle("install-update", () => autoUpdater.quitAndInstall());

打包和分发

打包 Electron 应用涉及将应用代码与 Electron 运行时捆绑为特定平台的安装程序。两个主要工具是 electron-builder 和 electron-forge。

electron-builder

electron-builder 是最流行的打包工具,支持创建 Windows(NSIS、MSI)、macOS(DMG、PKG)和 Linux(AppImage、DEB、RPM)安装程序,还处理代码签名和公证。

// package.json - electron-builder configuration
{
  "name": "my-electron-app",
  "version": "1.0.0",
  "main": "dist/main.js",
  "build": {
    "appId": "com.example.myapp",
    "productName": "My App",
    "directories": { "output": "release" },
    "mac": {
      "category": "public.app-category.developer-tools",
      "target": ["dmg", "zip"],
      "hardenedRuntime": true,
      "notarize": true
    },
    "win": {
      "target": ["nsis"],
      "signingHashAlgorithms": ["sha256"]
    },
    "linux": {
      "target": ["AppImage", "deb"],
      "category": "Development"
    },
    "publish": {
      "provider": "github",
      "owner": "your-username",
      "repo": "your-repo"
    }
  },
  "scripts": {
    "build": "tsc && electron-builder",
    "build:mac": "tsc && electron-builder --mac",
    "build:win": "tsc && electron-builder --win",
    "build:linux": "tsc && electron-builder --linux"
  }
}

electron-forge

electron-forge 是 Electron 团队维护的官方打包工具,提供更有主见的工作流,内置支持 webpack、Vite 和 TypeScript 脚手架。

# Create a new Electron Forge project
npm init electron-app@latest my-app -- --template=vite-typescript

# Build and package
npm run make

# Publish to GitHub Releases
npm run publish

原生 API:对话框、菜单、托盘和通知

Electron 提供对普通浏览器中不可用的原生操作系统功能的访问,让你的应用在每个平台上都像原生应用。

对话框(文件选择器)

dialog 模块提供显示原生系统对话框的方法,包括文件打开、文件保存、消息框和错误对话框。

// main.js - File dialog examples
const { dialog } = require("electron");

ipcMain.handle("dialog:openFile", async () => {
  const result = await dialog.showOpenDialog(mainWindow, {
    properties: ["openFile", "multiSelections"],
    filters: [
      { name: "Documents", extensions: ["txt", "md", "json"] },
      { name: "Images", extensions: ["png", "jpg", "gif"] },
      { name: "All Files", extensions: ["*"] },
    ],
  });
  return result.canceled ? null : result.filePaths;
});

ipcMain.handle("dialog:saveFile", async (_event, content) => {
  const result = await dialog.showSaveDialog(mainWindow, {
    defaultPath: "untitled.txt",
    filters: [{ name: "Text", extensions: ["txt"] }],
  });
  if (!result.canceled) {
    await fs.writeFile(result.filePath, content);
    return result.filePath;
  }
  return null;
});

应用菜单和上下文菜单

Electron 支持应用菜单(窗口或屏幕顶部的菜单栏)和上下文菜单(右键菜单),通过 MenuItem 模板构建。

const { Menu, MenuItem } = require("electron");

const template = [
  {
    label: "File",
    submenu: [
      { label: "New File", accelerator: "CmdOrCtrl+N", click: () => createNewFile() },
      { label: "Open...", accelerator: "CmdOrCtrl+O", click: () => openFile() },
      { label: "Save", accelerator: "CmdOrCtrl+S", click: () => saveFile() },
      { type: "separator" },
      { role: "quit" },
    ],
  },
  {
    label: "Edit",
    submenu: [
      { role: "undo" },
      { role: "redo" },
      { type: "separator" },
      { role: "cut" },
      { role: "copy" },
      { role: "paste" },
      { role: "selectAll" },
    ],
  },
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

系统托盘

Tray 模块允许在系统托盘(macOS 上的菜单栏)中创建图标和上下文菜单,常用于没有可见窗口的后台应用。

const { Tray, Menu, nativeImage } = require("electron");

let tray = null;

app.whenReady().then(() => {
  const icon = nativeImage.createFromPath("assets/tray-icon.png");
  tray = new Tray(icon.resize({ width: 16, height: 16 }));

  const contextMenu = Menu.buildFromTemplate([
    { label: "Show App", click: () => mainWindow.show() },
    { label: "Status: Running", enabled: false },
    { type: "separator" },
    { label: "Quit", click: () => app.quit() },
  ]);

  tray.setToolTip("My Electron App");
  tray.setContextMenu(contextMenu);
  tray.on("click", () => mainWindow.show());
});

通知

Electron 通过 Notification 类支持原生 OS 通知,可包含标题、正文、图标和操作按钮。

const { Notification } = require("electron");

function showNotification(title, body) {
  const notification = new Notification({
    title: title,
    body: body,
    icon: "assets/icon.png",
    silent: false,
  });

  notification.on("click", () => {
    mainWindow.show();
    mainWindow.focus();
  });

  notification.show();
}

安全最佳实践

安全在 Electron 中至关重要,因为应用同时访问 Web 和操作系统。遵循这些最佳实践可以保护用户免受常见攻击。

  • 始终为所有 BrowserWindow 启用上下文隔离(contextIsolation: true)和沙箱模式(sandbox: true)。
  • 永远不要禁用 webSecurity,保持启用可防止跨源攻击。
  • 使用 contextBridge.exposeInMainWorld() 只向渲染进程暴露特定的、经过验证的函数。
  • 在主进程中验证所有 IPC 消息参数,永远不信任来自渲染进程的数据。
  • 在所有渲染进程中禁用 nodeIntegration,改用预加载脚本。
  • 设置严格的内容安全策略(CSP)限制脚本来源并防止内联脚本。
  • 永远不要在启用 node integration 或没有适当 CSP 的 BrowserWindow 中加载远程内容。
  • 使用 session.defaultSession.webRequest 过滤和阻止可疑网络请求。
// Secure BrowserWindow configuration
const win = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,    // Required: isolate preload from page
    sandbox: true,             // Required: sandbox renderer process
    nodeIntegration: false,    // Required: no Node in renderer
    webSecurity: true,         // Required: enforce same-origin
    allowRunningInsecureContent: false,
    preload: path.join(__dirname, "preload.js"),
  },
});

// Set Content Security Policy
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
  callback({
    responseHeaders: {
      ...details.responseHeaders,
      "Content-Security-Policy": [
        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
      ],
    },
  });
});

性能优化

Electron 应用可能消耗大量内存和 CPU。这些技术有助于减少资源使用并改善启动时间。

  • 延迟加载重型模块,将非关键初始化推迟到窗口可见之后。
  • 使用 BrowserWindow show: false,仅在 ready-to-show 事件后显示窗口以避免白屏闪烁。
  • 最小化 BrowserWindow 实例数量,每个窗口创建一个新的 Chromium 渲染进程。
  • 使用 V8 快照减少启动时间,通过预编译 JavaScript。
  • 将 CPU 密集型工作卸载到 worker 线程或子进程以保持主进程响应。
  • 使用 Chrome DevTools 分析内存使用,修复分离的 DOM 节点和未清理的事件监听器导致的内存泄漏。
  • 使用 Vite 或 webpack 打包渲染进程代码并启用 tree-shaking 以减少包大小。
// Performance: Lazy-load heavy modules
app.whenReady().then(async () => {
  // Show window immediately with skeleton UI
  const win = new BrowserWindow({ show: false, /* ... */ });
  win.loadFile("index.html");
  win.once("ready-to-show", () => win.show());

  // Defer heavy initialization
  setTimeout(async () => {
    const db = await import("./database.js");
    await db.initialize();
    const analytics = await import("./analytics.js");
    analytics.track("app-launched");
  }, 1000);
});

// Offload CPU-intensive work to a worker thread
const { Worker } = require("worker_threads");

function processLargeFile(filePath) {
  return new Promise((resolve, reject) => {
    const worker = new Worker("./file-processor.js", {
      workerData: { filePath },
    });
    worker.on("message", resolve);
    worker.on("error", reject);
  });
}

集成 React、Vue 和 Svelte

Electron 的渲染进程与框架无关,你可以使用任何 Web 框架构建 UI。最流行的选择是 React、Vue 和 Svelte。

React 与 Electron

使用 electron-vite 或带 Vite 模板的 create-electron-app 脚手架创建基于 React 的 Electron 应用。

# Scaffold React + Electron with electron-vite
npm create electron-vite@latest my-react-app -- --template react-ts
cd my-react-app
npm install
npm run dev

# Project structure:
# src/
#   main/       - Electron main process
#   preload/    - Preload scripts
#   renderer/   - React app (Vite-powered)
#     App.tsx
#     main.tsx

Vue 与 Electron

electron-vite 也支持 Vue。或者使用 Vue CLI 配合 electron-builder 插件进行更传统的设置。

# Scaffold Vue + Electron with electron-vite
npm create electron-vite@latest my-vue-app -- --template vue-ts

# Or using Vue CLI plugin
vue create my-vue-app
cd my-vue-app
vue add electron-builder

Svelte 与 Electron

Svelte 的编译输出和小包大小与 Electron 配合良好。使用带 Svelte 模板的 electron-vite 可快速搭建。

# Scaffold Svelte + Electron with electron-vite
npm create electron-vite@latest my-svelte-app -- --template svelte-ts
cd my-svelte-app
npm install
npm run dev

测试 Electron 应用

测试 Electron 应用需要不同的策略来进行单元测试、集成测试和端到端测试。最常用的工具是 Playwright(支持 Electron)和 Vitest 进行单元测试。

// e2e test with Playwright for Electron
const { _electron: electron } = require("@playwright/test");
const { test, expect } = require("@playwright/test");

test("app launches and shows title", async () => {
  const app = await electron.launch({ args: ["."] });
  const window = await app.firstWindow();

  // Wait for the page to load
  await window.waitForLoadState("domcontentloaded");

  // Check the window title
  const title = await window.title();
  expect(title).toBe("My Electron App");

  // Interact with the UI
  await window.click("button#open-file");
  await expect(window.locator("#file-content")).toBeVisible();

  // Check main process evaluations
  const appPath = await app.evaluate(async ({ app }) => {
    return app.getAppPath();
  });
  expect(appPath).toBeTruthy();

  await app.close();
});

使用 Electron 构建的真实应用

Electron 驱动着世界上最广泛使用的一些桌面应用。这些例子证明 Electron 可以处理复杂的、性能敏感的场景。

  • VS Code - 最流行的代码编辑器,数百万开发者使用。证明 Electron 可以提供高性能文本编辑、扩展系统和集成终端功能。
  • Slack - 数百万团队使用的企业消息平台。在 Windows、macOS 和 Linux 上处理实时消息、文件共享和视频通话。
  • Discord - 服务数亿用户的游戏和社区聊天平台。支持语音聊天、视频流和富媒体嵌入。
  • Figma Desktop - 专业设计工具,使用 Electron 作为桌面封装,核心渲染引擎运行在 WebAssembly 上以获得接近原生的性能。
  • Notion - 笔记、数据库和项目管理的一体化工作空间。展示了 Electron 中复杂的富文本编辑和实时协作。
  • GitHub Desktop - 简化版本控制工作流的 Git 客户端。使用 React 的干净 Electron 架构典范。

常见问题

Electron 用来做什么?

Electron 用于使用 Web 技术构建跨平台桌面应用。适合希望复用 Web 开发技能构建桌面应用的团队。常见用例包括代码编辑器(VS Code)、消息应用(Slack、Discord)、生产力工具(Notion)和开发者工具。

2026 年 Electron 还是好的选择吗?

是的。尽管有 Tauri 的竞争,Electron 仍然是最成熟、使用最广泛的跨平台桌面应用框架。拥有最大的生态系统、广泛的文档和经过验证的生产记录。

为什么 Electron 应用这么大?

Electron 应用捆绑了完整的 Chromium 浏览器和 Node.js 运行时,大约增加 150-200 MB。这确保跨平台一致渲染,但导致下载体积较大。

如何减少 Electron 应用的内存使用?

减少 BrowserWindow 实例数量、延迟加载模块、使用 web worker 处理密集计算、使用 Chrome DevTools 查找内存泄漏、升级到最新 Electron 版本。

Electron 安全吗?

遵循最佳实践时 Electron 可以非常安全。启用上下文隔离和沙箱模式,使用预加载脚本代替 nodeIntegration,验证所有 IPC 消息,设置严格的 CSP,保持 Electron 更新。

可以在 Electron 中使用 TypeScript 吗?

可以。Electron 有出色的 TypeScript 支持。electron-vite 和 electron-forge 包含 TypeScript 模板,Electron 自带类型定义,IPC 通信可以完全类型化。

Electron 自动更新是如何工作的?

Electron 自动更新使用 autoUpdater 模块或 electron-updater 包。应用检查远程服务器的新版本,后台下载更新,提示用户重启。可以使用 GitHub Releases、S3 或自定义服务器托管更新。

应该选择 Electron 还是 Tauri?

如果需要完整的 Node.js 和 npm 生态访问、有现有 Web 应用要封装、或需要经过验证的大规模稳定性,选择 Electron。如果二进制大小和内存使用至关重要、团队熟悉 Rust、或构建轻量工具,选择 Tauri。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON FormatterJSJavaScript MinifierTSJSON to TypeScript

相关文章

Tauri 完全指南:使用 Rust 构建轻量跨平台桌面应用 (2026)

完整的 Tauri 指南,涵盖 Rust 后端、Web 前端、Tauri 2.0、Commands & Invoke、事件系统、插件、安全模型、自动更新、移动端支持和与 Electron 对比。

React 设计模式指南:复合组件、自定义 Hook、HOC、Render Props 与状态机

完整的 React 设计模式指南,涵盖复合组件、render props、自定义 hooks、高阶组件、Provider 模式、状态机、受控与非受控、组合模式、观察者模式、错误边界和模块模式。

Playwright E2E 测试完全指南

学习 Playwright 端到端测试。