今日谈:electron集成appex

在 Electron 应用中集成 macOS 访达扩展 (FinderSync) 的终极指南

本文档总结了将一个原生 macOS 访达扩展(.appex)成功集成到 Electron 应用中所需的所有步骤、配置和关键代码。我们从最初的签名、公证失败,到最终实现"首次启动时引导用户开启"的良好体验,涵盖了整个流程。

第一步:Xcode 项目配置 (你的 .appex 扩展)

这是所有工作的基础。在你的原生扩展的 Xcode 项目中,必须确保以下配置正确:

  1. Bundle ID: 为你的主应用和扩展设置清晰的 Bundle ID。

    • 主应用 (Electron) : 例如 com.yourcompany.yourapp
    • 扩展 (.appex) : 应该是主应用的子级,例如 com.yourcompany.yourapp.FinderExtension
  2. Hardened Runtime (强化运行时) : 必须开启 。这是苹果公证的强制要求。在 Signing & Capabilities 选项卡中勾选它。

  3. App Groups: 如果你的主应用和扩展需要共享数据,请配置 App Groups。

  4. 【关键经验】Info.plist 的配置:

    • 最初的尝试 : 我们曾在扩展的 Info.plist 中添加了 NSExtensionContainingAppBundleIdentifier 键来明确声明父应用。
    • 对比"豆包"后的经验 : 我们发现,对于 electron-builder 这种第三方打包工具,这个"显式声明"有时会与签名过程中的"隐式关系"(通过文件结构和统一签名建立)产生冲突。
    • 最终方案 : 从扩展的 Info.plist 中移除 NSExtensionContainingAppBundleIdentifier 。我们完全依赖于文件结构(.appexContents/Plugins 目录下)和后续的统一签名来建立父子关系,这种方式更稳健,不易出错。

第二步:electron-builder.yml 核心配置

这是指导 electron-builder 如何打包你的应用的"总指挥部"。

yaml 复制代码
# ... 其他配置

mac:
  identity: "你的开发者签名身份 (Your Developer ID)"
  hardenedRuntime: true
  
  # 为主应用和扩展分别提供授权文件
  entitlements: "build/entitlements.mac.app.plist"
  entitlementsInherit: "build/entitlements.mac.extension.plist"//这个是影响所有子组件的,安情况而定,建议和主应用一样,
  
  # 将编译好的 .appex 复制到打包后的应用中
  extraFiles:
    - from: "build/YourAppExtension.appex" # 替换成你实际的.appex文件名
      to: "Plugins/YourAppExtension.appex"
      filter: ["**/*"]

  # 【重要】将我们编译的原生插件也打包进去
  extraResources:
    # 路径需要根据你的项目结构调整
    - from: "native_addon/build/Release/addon.node"
      to: "native/addon.node"

# ... 其他配置

配套的授权文件:

  • build/entitlements.mac.app.plist (给主应用,不带沙箱)

    xml 复制代码
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>com.apple.security.cs.allow-jit</key><true/>
        <key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
        <key>com.apple.security.cs.disable-library-validation</key><true/>
        <!-- 其他主应用需要的权限... -->
    </dict>
    </plist>
  • build/entitlements.mac.extension.plist (给扩展,必须带沙箱)

    xml 复制代码
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>com.apple.security.app-sandbox</key><true/>
        <!-- 如果需要,加入 App Groups -->
        <key>com.apple.security.application-groups</key>
        <array>
            <string>group.com.yourcompany.yourapp</string> <!-- 替换成你的 App Group ID -->
        </array>
    </dict>
    </plist>

第三步:afterSign 钩子脚本 (签名与公证)

这是整个流程中最关键、最精妙的部分。它解决了所有签名和公证问题。

核心逻辑:

  1. electron-builder 完成了第一次签名。
  2. 我们的脚本介入,重签了扩展,但这破坏了主应用的签名。
  3. 所以,我们再次重签整个主应用,使其签名与内部所有文件(包括被我们修改过的扩展)重新匹配。
  4. 最后,将这个签名完美的应用提交公证。

build/notarize.js (最终版)

ini 复制代码
const { execSync } = require('child_process');
const { notarize } = require('@electron/notarize');
// ... 其他 require

module.exports = async function (context) {
  const { appOutDir, packager } = context;
  const appName = packager.appInfo.productFilename;
  const appPath = path.join(appOutDir, `${appName}.app`);
  const identity = packager.platformSpecificBuildOptions.identity;

  // 步骤1: 重签 App Extension
  console.log('▶ Step 1: Re-signing App Extension with correct entitlements...');
  const appexPath = path.join(appPath, 'Contents/Plugins/YourAppExtension.appex'); // 替换成你实际的.appex文件名
  const extensionEntitlementsPath = path.resolve(__dirname, 'entitlements.mac.extension.plist');
  const cmdExtension = `codesign --sign "${identity}" --force --options runtime --entitlements "${extensionEntitlementsPath}" "${appexPath}"`;
  execSync(cmdExtension);

  // 步骤2: 重签整个主应用以修复签名链
  console.log('▶ Step 2: Re-signing the main application...');
  const appEntitlementsPath = path.resolve(__dirname, 'entitlements.mac.app.plist');
  const cmdApp = `codesign --sign "${identity}" --force --options runtime --entitlements "${appEntitlementsPath}" "${appPath}"`;
  execSync(cmdApp);

  // 步骤3: 公证
  console.log('▶ Step 3: Notarizing the application...');
  await notarize({
    appBundleId: packager.appInfo.id,
    appPath: appPath,
    appleId: process.env.APPLE_ID,
    appleIdPassword: process.env.APPLE_ID_PASS,
    teamId: process.env.APPLE_TEAM_ID,
    tool: 'notarytool'
  });
};

第四步:实现"首次启动时提示一次" (原生插件)

核心经验 : 我们无法,也不应该去尝试"静默开启"扩展。这是macOS的安全底线。所有专业应用(包括豆包)的"自动开启"体验,都是通过以最快、最友好的方式引导用户完成授权来实现的。

我们通过创建一个原生Node.js插件来调用macOS的API。

  1. 文件结构 : 在项目根目录下创建 native_addon 文件夹,包含以下文件。

  2. FinderExtensionManager.h: 定义接口。

  3. FinderExtensionManager.m (最终版) : 使用动态调用,避免SDK版本问题,非常稳健。

    ini 复制代码
    #import "FinderExtensionManager.h"
    #import <FinderSync/FinderSync.h>
    
    @implementation FinderExtensionManager
    + (void)showPanel {
        // 【重要】将这里的 Bundle ID 替换成你自己的 .appex 扩展的真实 ID
        NSString *extensionBundleIdentifier = @"com.yourcompany.yourapp.FinderExtension"; 
        FIFinderSyncController *controller = [FIFinderSyncController defaultController];
        SEL selector = NSSelectorFromString(@"showPanelForEnablingExtension:");
        if ([controller respondsToSelector:selector]) {
            #pragma clang diagnostic push
            #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [controller performSelector:selector withObject:extensionBundleIdentifier];
            #pragma clang diagnostic pop
        }
    }
    @end
  4. addon.mm : 使用 Objective-C++ (.mm) 作为桥梁,连接C++和Objective-C。

  5. binding.gyp : 编译脚本,确保 sources 中包含 addon.mm

第五步:在 Electron 主进程中调用插件

这是将所有努力呈现给用户的最后一步。

  1. 安装依赖 : npm install electron-store node-addon-apinpm install --save-dev node-gyp

  2. 主进程代码 (例如 main.js) :

    • 智能加载 : 创建一个函数,使用 app.isPackaged 判断环境,从不同路径加载 .node 文件,并用 try...catch 包裹,防止应用崩溃。
    • 首次运行逻辑 : 使用 electron-store 检查一个一次性标记(如 extensionPrompted)。
    • 触发调用 : 在主窗口的 did-finish-load 事件中,检查标记并调用原生插件。

这个最终的方案,集合了我们今天所有的探索和经验,它安全、健壮、专业,并且能为用户提供与顶尖应用相媲美的流畅体验。

相关推荐
平凡之大路2 小时前
【企业架构】TOGAF概念之二
架构·togaf·企业架构
秋千码途3 小时前
小架构step系列26:Spring提供的validator
java·spring·架构
西陵4 小时前
Nx带来极致的前端开发体验——借助playground开发提效
前端·javascript·架构
Edingbrugh.南空4 小时前
Aerospike架构深度解析:打造web级分布式应用的理想数据库
数据库·架构
人生都在赌5 小时前
从拒绝Copilot到拥抱GPT-5 Agent:一个Team Leader的效能革命
人工智能·架构·devops
小裕哥略帅5 小时前
架构师--基于常见组件的微服务场景实战
微服务·云原生·架构
苦逼前端画 K 线8 小时前
期货交易系统界面的技术架构与功能实现解析
架构·期货配资软件·大宗交易软件·子母账户系统·文华财经·倚天
DemonAvenger8 小时前
HTTP/2在Go中的实现与优化
网络协议·架构·go
范纹杉想快点毕业1 天前
ZYNQ芯片,SPI驱动开发自学全解析个人笔记【FPGA】【赛灵思
stm32·单片机·嵌入式硬件·mcu·架构·51单片机·proteus