{T}

应用快速检索

本文档介绍如何实现跨平台的应用快速检索功能,涵盖 macOS、Windows 和 Linux 三个平台的应用发现与图标提取技术。

Electron 实现系统级别的应用检索,需要针对不同平台进行定制化开发。

实现原理

应用快速检索的核心在于 发现解析。需要在各个操作系统中找到已安装应用程序的元数据,包括应用名称、可执行文件路径、图标等信息。由于不同操作系统的软件安装管理机制各异,因此需要采用不同的策略:

  • macOS: 利用系统自带的 system_profiler 工具,它可以输出详细的 XML 格式的软件信息。通过 child_process 执行该命令,然后解析返回的 XML 数据来获取应用列表。图标则通过解析应用包内的 Info.plist 文件来定位并转换
  • Windows: 主要依赖两种方式:
    1. 注册表: 读取 Uninstall 相关的注册表项,这是最直接的方式,但存在数据不一致和残留项的问题
    2. 快捷方式: 遍历“开始菜单”等目录下的快捷方式(.lnk 文件),通过解析这些快捷方式获取应用信息。这种方式更为可靠
  • Linux: 通常通过解析 .desktop 文件来发现应用。这些文件遵循 freedesktop.org 标准,包含了应用的名称、执行命令、图标等元数据

获取到应用信息后,为了在界面上统一展示,还需要将各平台不同格式的图标(如 macOS 的 .icns,Windows 的 .ico 或内嵌在 .exe 中的图标)转换为通用的 .pngbase64 格式

平台具体实现

macOS

获取应用列表

在 macOS 中使用 system_profiler 命令行工具来获取已安装的应用信息。system_profiler 是功能强大的工具,可以提供关于系统硬件、软件的详尽报告。通过指定 SPApplicationsDataType 数据类型和 -xml 输出格式,可以得到一个包含所有已安装应用信息的 XML 文档

常用参数:

  • -xml: 以 XML 格式输出
  • -detailLevel mini: 提供最小化的信息,以提高效率
  • SPApplicationsDataType: 指定获取应用相关数据

示例命令:

shell
/usr/sbin/system_profiler -xml -detailLevel mini SPApplicationsDataType

输出的 XML 片段示例:

xml
<dict>
  <key>_name</key>
  <string>钉钉</string>
  <key>path</key>
  <string>/Applications/DingTalk.app</string>
  <key>version</key>
  <string>7.1.6</string>
  <!-- ... 其他信息 ... -->
</dict>

实现步骤:

  1. 使用 child_process.spawn 执行 system_profiler 命令
  2. 监听 stdout,将输出的 XML 数据拼接完整
  3. 使用 plist 库解析 XML 字符串,提取应用列表

代码实现:

javascript
import { spawn } from 'child_process';
import plist from 'plist';

export default function getApps(resolve, reject) {
  let resultBuffer = new Buffer.from([]);
  // 通过 spawn 调用 system_profiler 脚本
  const profileInstalledApps = spawn('/usr/sbin/system_profiler', [
    '-xml',
    '-detailLevel',
    'mini',
    'SPApplicationsDataType',
  ]);
  
  // 监听返回结果,写入 resultBuffer
  profileInstalledApps.stdout.on('data', (chunckBuffer) => {
    resultBuffer = Buffer.concat([resultBuffer, chunckBuffer]);
  });

  // 监听退出事件
  profileInstalledApps.on('exit', (exitCode) => {
    if (exitCode !== 0) {
      reject([]);
      return;
    }

    try {
      // 解析 XML 文档
      const [installedApps] = plist.parse(resultBuffer.toString());
      // 返回结果
      return resolve(installedApps._items);
    } catch (err) {
      reject(err);
    }
  });
  // 出错后抛出
  profileInstalledApps.on('error', (err) => {
    reject(err);
  });
}

源码参考: getApps.ts

获取应用图标

system_profiler 的输出不包含图标信息,因此需要手动提取。macOS 应用的图标信息通常定义在应用包(.app)的 Contents/Info.plist 文件中

实现步骤:

  1. 定位 Info.plist: 根据应用路径找到 Contents/Info.plist 文件
  2. 解析 Info.plist: 读取并解析该文件,找到 CFBundleIconFile 字段,它定义了图标文件的名称(不含扩展名)
  3. 定位图标文件: 图标文件通常位于 Contents/Resources/ 目录下,常见的格式为 .icns.tiff
  4. 格式转换: 由于 Electron 无法直接渲染 .icns 文件,使用 macOS 自带的 sips (Scriptable Image Processing System) 命令将其转换为 .png 格式

sips 是 macOS 系统中的一个命令行工具,用于对图像进行处理和操作。该命令的全称是 "Scriptable Image Processing System",它允许用户通过命令行对图像文件进行各种操作,包括格式转换、大小调整、颜色管理等

sips 命令示例:

shell
sips -s format png '/path/to/icon.icns' --out '/path/to/output.png' --resampleHeightWidth 64 64

代码实现:

javascript
import path from 'path';
import fs from 'fs';
import { exec } from 'child_process';

import plist from 'plist';

const getIconFile = (appFileInput) => {
  return new Promise((resolve, reject) => {
    // 根据 app 路径,获取 Info.plist 路径
    const plistPath = path.join(appFileInput, 'Contents', 'Info.plist');
    // 解析 plist 文件
    plist.readFile(plistPath, (err, data) => {
      // 如果不存在 CFBundleIconFile 则返回 macOS 系统默认图标
      if (err || !data.CFBundleIconFile) {
        return resolve(
          '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns'
        );
      }
      // 获取 icns 图标路径
      const iconFile = path.join(
        appFileInput,
        'Contents',
        'Resources',
        data.CFBundleIconFile
      );
      
      // 依次通过 文件名、.icns、.tiff 来寻找 app icon
      const iconFiles = [iconFile, iconFile + '.icns', iconFile + '.tiff'];
      const existedIcon = iconFiles.find((iconFile) => {
        return fs.existsSync(iconFile);
      });
      // 找不到也返回 macOS 系统默认图标
      resolve(
        existedIcon ||
          '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns'
      );
    });
  });
};

const tiffToPng = (iconFile, pngFileOutput) => {
  return new Promise((resolve, reject) => {
    // tiff、icns 图标转 png
    exec(
      `sips -s format png '${iconFile}' --out '${pngFileOutput}' --resampleHeightWidth 64 64`,
      (error) => {
        error ? reject(error) : resolve(null);
      }
    );
  });
};

// 传入 app 路径,返回对应 app 图片路径
const app2png = (appFileInput, pngFileOutput) => {
  return getIconFile(appFileInput).then((iconFile) => {
    return tiffToPng(iconFile, pngFileOutput);
  });
};

export default app2png;

源码参考: app2png.ts

Windows

在 Windows 平台上,应用信息的获取方式与 macOS 和 Linux 截然不同。主要有两种途径:读取注册表和遍历“开始菜单”的快捷方式。

获取应用列表

1. 注册表方式 (不推荐)

通过查询 Uninstall 相关的注册表项,可以找到大部分已安装的程序信息

  • 注册表路径:
    • HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall
    • HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall

尽管这种方法直接,但存在明显缺陷:

  • 数据不一致: 并非所有应用都遵循此规范,导致信息缺失
  • 残留项: 应用卸载不干净会导致注册表残留,从而抓取到无效应用
  • 格式不统一: DisplayIconDisplayName 等关键字段可能缺失或格式错误

2. 快捷方式方式 (推荐)

遍历“开始菜单”中的快捷方式 (.lnk 文件) 是一种更可靠的方法。这些快捷方式通常指向应用的主可执行文件

  • 常见目录:
    • 系统级: C:\ProgramData\Microsoft\Windows\Start Menu\Programs
    • 用户级: %APPDATA%\Microsoft\Windows\Start Menu\Programs

实现步骤:

  1. 确定搜索目录: 整合系统级和用户级的“开始菜单”程序目录
  2. 递归遍历: 递归扫描目录下的所有文件和文件夹
  3. 解析快捷方式: 对 .lnk 文件,使用 Electron 的 shell.readShortcutLink() 方法解析出目标文件路径 (target)
  4. 过滤无效应用: 排除卸载程序、说明文档等非应用目标
  5. 收集应用信息: 整理出应用名称、可执行文件路径等信息

代码实现:

javascript
import { shell } from "electron"
import fs from "fs/promises"
import path from "path"
import os from "os"

async function getWindowsApps() {
  const appList = []
  const startMenuPaths = [
    path.join("C:", "ProgramData", "Microsoft", "Windows", "Start Menu", "Programs"),
    path.join(
      os.homedir(),
      "AppData",
      "Roaming",
      "Microsoft",
      "Windows",
      "Start Menu",
      "Programs"
    )
  ]

  async function findShortcuts(directory) {
    try {
      const files = await fs.readdir(directory, { withFileTypes: true })
      for (const file of files) {
        const fullPath = path.join(directory, file.name)
        if (file.isDirectory()) {
          await findShortcuts(fullPath)
        } else if (path.extname(fullPath).toLowerCase() === ".lnk") {
          try {
            const shortcutDetails = shell.readShortcutLink(fullPath)
            const targetPath = shortcutDetails.target

            // 过滤无效或卸载程序
            if (!targetPath || targetPath.toLowerCase().includes("uninstall")) {
              continue
            }

            appList.push({
              name: path.basename(targetPath, path.extname(targetPath)),
              path: targetPath,
              icon: "" // Icon path will be resolved later
            })
          } catch (e) {
            // Ignore shortcuts that can't be read
          }
        }
      }
    } catch (e) {
      // Ignore directories that can't be read
    }
  }

  for (const menuPath of startMenuPaths) {
    await findShortcuts(menuPath)
  }

  // 去重
  return [...new Map(appList.map((app) => [app.path, app])).values()]
}

源码参考: win.ts

获取应用图标

Windows 应用的图标通常内嵌在 .exe.dll 文件中,或以 .ico 文件的形式存在。需要借助原生模块来提取这些图标

实现步骤:

  1. 使用原生模块: 借助 extract-file-icon 这样的 C++ 插件,可以直接从可执行文件中提取图标数据
  2. 提取图标: 调用模块方法,传入应用的可执行文件路径和所需图标大小
  3. 缓存图标: 为了性能,首次提取后应将图标(如转换为 PNG)保存到临时目录。后续直接从缓存加载,避免重复提取

代码实现:

javascript
import fs from "fs/promises"
import path from "path"
import os from "os"
import fileIcon from "extract-file-icon"

// 确保图标缓存目录存在
const ICON_CACHE_DIR = path.join(os.tmpdir(), "app-icon-cache")

async function ensureCacheDir() {
  try {
    await fs.access(ICON_CACHE_DIR)
  } catch {
    await fs.mkdir(ICON_CACHE_DIR)
  }
}

// 获取并缓存应用图标
async function getWindowsAppIcon(appPath, appName) {
  await ensureCacheDir()
  const iconPath = path.join(ICON_CACHE_DIR, `${appName}.png`)

  try {
    // 检查缓存
    await fs.access(iconPath)
    return iconPath
  } catch {
    // 缓存未命中,提取图标
    try {
      const buffer = fileIcon(appPath, 64) // size: 64
      await fs.writeFile(iconPath, buffer)
      return iconPath
    } catch (e) {
      console.error(`Failed to extract icon for ${appName}:`, e)
      return null // or return a path to a default icon
    }
  }
}

注意: extract-file-icon 是原生依赖,需要根据 Electron 版本进行正确的编译 (rebuild)

Linux 实现

在 Linux 系统中,应用发现通常通过解析 .desktop 文件来完成。这些文件是遵循 freedesktop.org 标准的桌面条目,充当了应用的快捷方式和元数据中心

获取应用列表

.desktop 文件通常分布在以下目录中:

  • 系统级目录:
    • /usr/share/applications
    • /usr/local/share/applications
  • 用户级目录:
    • ~/.local/share/applications

这些文件采用 INI 格式,包含了 [Desktop Entry] 部分,其中定义了应用的各种属性。

关键字段:

  • Name: 应用名称。
  • Exec: 启动应用的命令。
  • Icon: 图标的名称或路径。
  • NoDisplay: 如果为 true,则表示该应用不应显示在菜单中。
  • Terminal: 是否在终端中运行。

实现步骤:

  1. 扫描目录: 遍历上述标准目录,收集所有 .desktop 文件。
  2. 解析文件: 读取文件内容,并解析 [Desktop Entry] 下的键值对。
  3. 过滤应用: 忽略 NoDisplay=true 的条目,并对 Exec 命令进行清洗,去除参数(如 %U, %F 等)。
  4. 收集信息: 提取 Name, Exec, Icon 等关键信息,形成应用列表。

代码实现:

javascript
import fs from "fs/promises"
import path from "path"
import os from "os"

// 解析 .desktop 文件
async function parseDesktopFile(filePath) {
  try {
    const content = await fs.readFile(filePath, "utf-8")
    const lines = content.split("\n")
    const entry = {}
    let inDesktopEntry = false

    for (const line of lines) {
      if (line.trim() === "[Desktop Entry]") {
        inDesktopEntry = true
        continue
      }
      if (inDesktopEntry && line.includes("=")) {
        const [key, ...valueParts] = line.split("=")
        entry[key.trim()] = valueParts.join("=").trim()
      }
    }

    // 过滤无效条目
    if (entry.NoDisplay === "true" || !entry.Name || !entry.Exec) {
      return null
    }

    return {
      name: entry.Name,
      exec: entry.Exec.replace(/%[fFuUdDnNickvm]/g, "").trim(),
      icon: entry.Icon || entry.Name.toLowerCase()
    }
  } catch {
    return null
  }
}

// 获取 Linux 应用
async function getLinuxApps() {
  const appDirs = [
    "/usr/share/applications",
    "/usr/local/share/applications",
    path.join(os.homedir(), ".local", "share", "applications")
  ]

  const apps = []
  for (const dir of appDirs) {
    try {
      const files = await fs.readdir(dir)
      for (const file of files) {
        if (file.endsWith(".desktop")) {
          const appInfo = await parseDesktopFile(path.join(dir, file))
          if (appInfo) {
            apps.push(appInfo)
          }
        }
      }
    } catch {
      // Ignore directories that don't exist
    }
  }

  // 去重
  return [...new Map(apps.map((app) => [app.name, app])).values()]
}

获取应用图标

.desktop 文件中的 Icon 字段定义图标的名称,而不是直接的路径。Linux 桌面环境遵循 Icon Theme Specification,会根据图标名称在标准主题目录中搜索对应的图标文件(如 .png, .svg

图标搜索路径:

  • /usr/share/icons
  • ~/.local/share/icons
  • /usr/share/pixmaps

实现步骤:

  1. 获取图标名称: 从解析出的应用信息中得到 Icon 字段。
  2. 构建搜索路径: 结合标准图标目录和不同的主题目录(如 hicolor, Adwaita)进行搜索。
  3. 查找图标文件: 遍历搜索路径,找到与图标名称匹配的文件。通常需要检查多种尺寸和文件格式(.png, .svg, .xpm)。
  4. 返回最佳匹配: 选择一个合适的图标路径返回。如果找不到,可以提供一个通用后备图标。

代码实现 (简化版):

javascript
import fs from "fs/promises"
import path from "path"

const ICON_PATHS = [
  "/usr/share/icons/hicolor/scalable/apps",
  "/usr/share/icons/hicolor/128x128/apps",
  "/usr/share/pixmaps"
]

async function findIconPath(iconName) {
  const extensions = [".png", ".svg", ".xpm"]
  for (const dir of ICON_PATHS) {
    for (const ext of extensions) {
      const fullPath = path.join(dir, `${iconName}${ext}`)
      try {
        await fs.access(fullPath)
        return fullPath
      } catch {
        // Not found, continue
      }
    }
  }
  return null // Or a path to a default icon
}

这种简化的实现只覆盖了部分常见路径。一个完整的实现需要更全面地遵循图标主题规范,递归搜索不同尺寸和主题的目录

API 参考

为了方便上层调用,将平台特定的实现封装在统一的 API 后面。该 API 负责根据当前运行的操作系统,调用相应的方法来获取应用列表和图标

数据结构

无论在哪个平台,检索到的应用信息都将统一为以下 AppInfo 格式:

typescript
interface AppInfo {
  /**
   * 应用名称
   */
  name: string
  /**
   * 应用的可执行文件或包路径
   */
  path: string
  /**
   * 应用图标的本地缓存路径 (PNG 格式)
   */
  icon: string
}

主要函数

getApps(): Promise<AppInfo[]>

该函数是应用检索的统一入口点。它会异步地获取当前系统上所有已安装的应用,并以 AppInfo[] 的形式返回

  • 返回值: Promise<AppInfo[]> - 一个解析为 AppInfo 数组的 Promise。

实现逻辑:

该函数内部会通过 process.platform 判断当前操作系统,然后调用对应的平台实现:

  • macOS: 调用 getMacApps()app2png()
  • Windows: 调用 getWindowsApps()getWindowsAppIcon()
  • Linux: 调用 getLinuxApps()findIconPath()

示例:

javascript
import { getApps } from "./app-retriever" // 假设统一的 API 在此文件中

async function loadAndDisplayApps() {
  try {
    console.log("正在检索应用...")
    const apps = await getApps()
    console.log(`共找到 ${apps.length} 个应用:`)
    apps.forEach((app) => {
      console.log(`- 名称: ${app.name}`)
      console.log(`  路径: ${app.path}`)
      console.log(`  图标: ${app.icon}`)
    })
  } catch (error) {
    console.error("检索应用时出错:", error)
  }
}

loadAndDisplayApps()

配置与使用

指导将应用检索功能集成到你的 Electron 项目中。根据目标平台,可能需要安装以下 npm 包:

  • macOS: plist
  • Windows: extract-file-icon (原生模块)
  • Linux: 无需额外依赖
bash
npm install plist extract-file-icon

注意: extract-file-icon 是一个原生 C++ 插件,安装后需要针对你使用的 Electron 版本进行重新编译。通常,electron-rebuild 工具可以简化这个过程。

bash
npm install --save-dev electron-rebuild

# 在安装或更新 Electron 后运行
./node_modules/.bin/electron-rebuild

项目结构建议

建议将所有与应用检索相关的代码组织在一个独立的模块中,例如 /src/main/app-retriever

code
src/
|-- main/
|   |-- app-retriever/
|   |   |-- index.js         # 统一的 API 入口 (getApps)
|   |   |-- macos.js         # macOS 实现
|   |   |-- windows.js       # Windows 实现
|   |   |-- linux.js         # Linux 实现
|   |-- preload.js
|   |-- main.js
|-- renderer/
|   |-- app.js
|   |-- index.html

集成示例

以下示例展示通过 ipc (进程间通信) 在主进程中检索应用列表,并将其发送到渲染进程进行展示

1. 主进程 (main.js)

在主进程中监听一个 ipc 事件,当收到渲染进程的请求时,调用 getApps 函数,并将结果回传

javascript
// src/main/main.js
import { ipcMain } from "electron"
import { getApps } from "./app-retriever" // 引入统一的 API

ipcMain.handle("get-apps", async () => {
  const apps = await getApps()
  return apps
})

2. 预加载脚本 (preload.js)

为了安全地在渲染进程中调用主进程的功能,我们通过 contextBridge 暴露一个接口

javascript
// src/main/preload.js
import { contextBridge, ipcRenderer } from "electron"

contextBridge.exposeInMainWorld("electronAPI", {
  getApps: () => ipcRenderer.invoke("get-apps")
})

3. 渲染进程 (renderer/app.js)

在渲染进程中可以调用 window.electronAPI.getApps() 来获取应用列表,并将其渲染到页面上

javascript
// src/renderer/app.js

document.getElementById("load-apps-btn").addEventListener("click", async () => {
  const appListElement = document.getElementById("app-list")
  appListElement.innerHTML = "<li>正在加载...</li>"

  try {
    const apps = await window.electronAPI.getApps()
    appListElement.innerHTML = apps
      .map(
        (app) => `
      <li>
        <img src="${app.icon}" width="32" height="32" />
        <span>${app.name}</span>
      </li>
    `
      )
      .join("")
  } catch (error) {
    appListElement.innerHTML = `<li>加载失败: ${error.message}</li>`
  }
})

HTML (renderer/index.html)

html
<!DOCTYPE html>
<html>
  <head>
    <title>应用列表</title>
  </head>
  <body>
    <h1>已安装的应用</h1>
    <button id="load-apps-btn">加载应用</button>
    <ul id="app-list"></ul>
    <script src="app.js"></script>
  </body>
</html>

性能优化与最佳实践

应用检索是一个 I/O 密集型操作,如果不进行优化,很容易导致界面卡顿和过度的资源消耗。以下是一些关键的优化策略和最佳实践。

全面的缓存机制

缓存是提升性能最有效的手段。 无论是应用列表还是图标,都应该被缓存。

  • 应用列表缓存: 首次检索后,将完整的应用列表(AppInfo[])以 JSON 格式存储在本地文件系统中(例如,存储在 app.getPath('userData') 目录中)。应用启动时,首先从缓存加载数据,立即呈现给用户,然后可以在后台异步执行一次新的检索,以发现变更。
  • 图标缓存: 图标的提取和转换成本很高。如前文所述,所有平台的实现都应将生成的 PNG 图标存储在一个临时目录中。图标的命名可以基于应用路径的哈希值或应用名称,以确保唯一性。

异步操作与非阻塞

所有文件系统操作、命令执行和图标提取都必须是异步的,以避免阻塞主进程。

  • 使用 async/await: 现代 async/await 语法可以让你用同步的方式编写异步代码,提高可读性。
  • 避免同步 API: 严禁在主流程中使用 fs 的同步方法(如 readFileSync)或 child_process 的同步方法(如 execSync)。

懒加载与分批处理

如果应用数量非常多,一次性加载和渲染所有应用可能会对前端性能造成压力。

  • 图标懒加载: 不要在获取应用列表时立即提取所有图标。而是在应用列表展示在界面上,并且用户滚动到相应位置时,再按需请求该应用的图标。
  • 虚拟列表: 在渲染层,如果列表很长,应使用“虚拟滚动”(Virtual Scrolling)技术。这种技术只渲染视口内可见的列表项,从而极大地减少 DOM 元素的数量,提高渲染性能。

智能的后台刷新

应用列表不是一成不变的,用户随时可能安装或卸载应用。因此,需要一个智能的刷新机制

  • 启动时刷新: 应用启动时,在加载缓存后,立即在后台启动一次静默刷新
  • 定时刷新: 可以设置一个定时器(例如,每隔几小时)在后台自动刷新应用列表。刷新完成后,将新的列表与旧的进行比对,只更新有变化的部分,并通知渲染进程
  • 文件系统监听 (可选): 更高级的实现可以监听应用目录(如 macOS 的 /Applications,Windows 的“开始菜单”目录)的变化。当检测到文件系统事件时,触发一次增量更新。这比定时轮询更高效,但实现也更复杂

7. 常见问题 (FAQ)

本章节提供在开发和集成应用快速检索功能时可能遇到的常见问题及其解决方案。

Q1: 在 macOS 上无法获取应用列表,或返回为空?

A: 这通常与 macOS 的隐私和安全设置有关。

  • 权限检查: 确保你的应用或用于测试的终端(如 VS Code 的内置终端)已被授予“完全磁盘访问权限”或“自动化”权限。前往“系统设置” > “隐私与安全性”进行检查和添加。
  • 命令调试: 尝试在独立的终端中直接运行 system_profiler SPApplicationsDataType -xml 命令,查看是否有错误输出或权限提示。
  • 沙盒环境: 如果你的 Electron 应用开启了沙盒模式,访问系统命令可能会受限,需要正确配置 entitlements 文件。

Q2: Windows 图标提取失败或显示不正确?

A: extract-file-icon 是一个原生 Node.js 模块,问题通常出在编译环节。

  • 编译环境: 确保你的开发环境中安装了 windows-build-tools 或独立的 Visual Studio C++ 构建工具、Python。
  • 使用 electron-rebuild: 每次更新 Electron 版本或 npm install 后,都需要重新编译原生模块以匹配 Electron 的 V8 引擎。运行以下命令:
    bash
    npm install --save-dev electron-rebuild
    npx electron-rebuild
  • 图标尺寸: 检查 getWindowsAppIcon 函数中 size 参数的设置。某些应用可能不提供所有尺寸的图标,尝试使用不同尺寸(如 32, 64)可能会解决问题。

Q3: Linux 系统中找不到某些应用?

A: 默认实现扫描的是标准 .desktop 文件目录。

  • 非标准路径: 部分应用(特别是通过非标准方式安装的,如 AppImage 或手动编译的)可能将其 .desktop 文件放在其他位置。你可以在配置中增加额外的扫描目录。
  • 环境变量: 确认 XDG_DATA_DIRS 环境变量是否包含所有相关的应用目录。我们的脚本会尝试读取此变量以获得更全面的搜索路径。

Q4: 应用启动或搜索时感觉卡顿,如何优化?

A: 卡顿通常源于同步的 I/O 操作或一次性渲染大量数据。

  • 检查缓存: 确认“性能优化”章节中提到的缓存机制是否已正确实现。首次启动后,应用列表和图标应从缓存加载,而不是每次都重新扫描。
  • 异步处理: 确保所有文件系统访问、命令执行和图标处理都在非 UI 阻塞的异步函数中完成。
  • UI 渲染: 如果应用数量非常多(超过数百个),在前端使用虚拟滚动(Virtual Scrolling)或懒加载列表是避免渲染卡顿的关键。

Q5: 如何处理原生依赖(如 extract-file-icon)的打包问题?

A: 使用 electron-builder 等打包工具时,需要确保原生模块被正确地为目标平台和架构重新编译。

  • electron-builder 配置: 在 package.jsonbuild 配置中,可以设置 npmRebuild: true,但这有时不够可靠。
  • 推荐流程: 在执行打包命令前,手动运行 electron-rebuild。一个可靠的构建脚本可能是这样的:
    json
    "scripts": {
      "rebuild": "electron-rebuild",
      "package": "npm run rebuild && electron-builder"
    }
    这样可以确保所有原生依赖在打包前都已是正确的状态