Electron-builder 是如何打包 Windows 应用的?

简介: 本文首发于微信公众号“前端徐徐”,作者徐徐深入解析了 electron-builder 在 Windows 平台上的打包流程。文章详细介绍了 `winPackager.ts`、`AppxTarget.ts`、`MsiTarget.ts` 和 `NsisTarget.ts` 等核心文件,涵盖了目标创建、图标处理、代码签名、资源编辑、应用签名、性能优化等内容,并分别讲解了 AppX/MSIX、MSI 和 NSIS 安装程序的生成过程。通过这些内容,读者可以更好地理解和使用 electron-builder 进行 Windows 应用的打包和发布。

本文首发微信公众号:前端徐徐。

大家好,我是徐徐。今天我们聊聊 electron-builder 中 windows 是如何打包的。

前言

 electron-builder  中 windows 的打包其实也是很复杂的,因为光是使用这个工具去打包就会遇到很多问题,更别说去探究里面的源码了解其原理了。但是之前已经写了 electron-builder 中 macOS 的源码和原理解读,然后为了彻底了解所有平台的打包逻辑,我又开始阅读 windows 的构建源码,这里跟大家分享一下我的一些理解。

涉及的核心源码路径

  • winPackager.ts:Windows 平台打包的核心文件

https://github1s.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/winPackager.ts

  • AppxTarget.ts:用于创建 AppX/MSIX 包

https://github1s.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/targets/AppxTarget.ts

  • MsiTarget.ts:用于创建 MSI 安装包

https://github1s.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/targets/MsiTarget.ts

  • NsisTarget.ts:创建 NSIS 安装程序

https://github1s.com/electron-userland/electron-builder/blob/master/packages/app-builder-lib/src/targets/nsis/NsisTarget.ts

Windows 平台打包的核心流程

我们可以通过 Windows 平台打包的核心文件来分析一下核心的流程。WinPackager 类是 electron-builder 中处理 Windows 平台打包的核心类。它继承自 PlatformPackager,专门处理 Windows 相关的打包逻辑。

export class WinPackager extends PlatformPackager<WindowsConfiguration> {
  // ...
}

主要有以下几个核心的功能流程:

目标创建

createTargets 方法根据用户配置创建不同的打包目标:

createTargets(targets: Array<string>,mapper: (name: string, factory: (outDir: string) => Target) => void): void {
  // ...
  for (const name of targets) {
    if (name === "nsis" || name === "portable") {
      mapper(name, outDir => new NsisTarget(this, outDir, name, getHelper()))
    } else if (name === "nsis-web") {
      mapper(name, outDir => new WebInstallerTarget(this, path.join(outDir, name), name, new AppPackageHelper(getCopyElevateHelper())))
    }
    // ... 其他目标类型
  }
}

这里支持多种打包格式,如 NSIS、AppX、MSI 等,每种格式对应一个特定的 Target 类,这一部分是非常关键和核心的逻辑,在后面会对每种包的生成做一个详细的讲解。

图标处理

图标处理使用懒加载方式:

_iconPath = new Lazy(() => this.getOrConvertIcon("ico"))

这确保了只在需要时才进行图标转换,优化了性能。

代码签名

签名过程主要通过 signdoSign 方法实现:

async sign(file: string): Promise<boolean> {
  const signOptions: WindowsSignOptions = {
    path: file,
    options: this.platformSpecificBuildOptions,
  }
  const didSignSuccessfully = await this.doSign(signOptions)
  // ...
}
private async doSign(options: WindowsSignOptions) {
  return retry(
    () => signWindows(options, this),
    3,
    500,
    500,
    0,
    // ... 错误处理逻辑
  )
}

这里使用了重试机制,提高了签名的可靠性。

资源编辑

signAndEditResources 方法处理可执行文件的资源编辑:

async signAndEditResources(file: string, arch: Arch, outDir: string, internalName?: string | null, requestedExecutionLevel?: RequestedExecutionLevel | null) {
  // ... 准备参数
  if (process.platform === "win32" || process.platform === "darwin") {
    await executeAppBuilder(["rcedit", "--args", JSON.stringify(args)])
  } else if (this.info.framework.name === "electron") {
    // 使用 Wine 在非 Windows 平台上运行 rcedit
    await execWine(path.join(vendorPath, "rcedit-ia32.exe"), path.join(vendorPath, "rcedit-x64.exe"), args)
  }
  // ... 签名和缓存处理
}

这段代码展示了如何跨平台编辑 Windows 可执行文件的资源。

应用签名

signApp 方法处理整个应用的签名过程:

protected async signApp(packContext: AfterPackContext, isAsar: boolean): Promise<boolean> {
  // ... 签名主可执行文件
  if (!isAsar) {
    return true
  }
  // 处理 asar 打包情况下的文件签名
  const filesToSign = await Promise.all([filesPromise(["resources", "app.asar.unpacked"]), filesPromise(["swiftshader"])])
  await BluebirdPromise.map(filesToSign.flat(1), file => this.sign(file), { concurrency: 4 })
  // ...
}

这里特别处理了 asar 打包的情况,确保所有需要的文件都被正确签名。

性能优化

代码中使用了几种性能优化技术:

  • 懒加载: 如 _iconPathvm 的处理
  • 缓存: 使用 BuildCacheManager 缓存构建结果
  • 并发: 使用 BluebirdPromise.map 并行处理文件签名

创建 AppX/MSIX 包

AppX/MSIX 是 Windows 应用程序的现代打包格式,主要用于 Windows Store 分发和企业部署。AppxTarget 类负责将 Electron 应用打包成 AppX/MSIX 格式。以下是这个过程的主要步骤:

初始化和验证

constructor(private readonly packager: WinPackager, readonly outDir: string) {
  super("appx")
  if (process.platform !== "darwin" && (process.platform !== "win32" || isOldWin6())) {
    throw new Error("AppX is supported only on Windows 10 or Windows Server 2012 R2 (version number 6.3+)")
  }
}

这段代码确保 AppX 打包只在支持的系统上进行。

构建过程 (build 方法):

async build(appOutDir: string, arch: Arch): Promise<any> {
  // ... (设置输出路径等)
  const stageDir = await createStageDir(this, packager, arch)
  // ... (准备映射文件)
}

build 方法是整个打包过程的核心,它协调了所有必要的步骤。

资源处理

const assetInfo = await AppXTarget.computeUserAssets(vm, vendorPath, userAssetDir)

这部分处理应用程序的资源文件,包括图标和其他视觉元素。

生成 AppX 清单

await this.writeManifest(manifestFile, arch, await this.computePublisherName(), userAssets)

writeManifest  方法生成 AppxManifest.xml 文件,这是 AppX 包的核心配置文件,定义了应用的身份、能力和资源。

创建资源索引 (resources.pri)

if (isScaledAssetsProvided(userAssets)) {
  // ... (使用 makepri.exe 创建 resources.pri)
}

如果提供了缩放资源,会使用 makepri.exe 工具创建资源索引文件。

打包 AppX

await vm.exec(vm.toVmFile(path.join(vendorPath, "windows-10", signToolArch, "makeappx.exe")), makeAppXArgs)

使用 makeappx.exe 工具将所有文件打包成 .appx 或 .msix 文件。

签名

await packager.sign(artifactPath)

对生成的 AppX 包进行数字签名,这是分发和安装 AppX 包的必要步骤。

清理和完成

await stageDir.cleanup()
await packager.info.callArtifactBuildCompleted({
  // ...
})

清理临时文件并通知打包过程完成。

关键点:

  1. AppX 清单 (AppxManifest.xml) 是整个包的核心,定义了应用的元数据、能力和资源。
  2. 资源处理非常重要,包括图标、启动画面等。
  3. makeappx.exe 工具用于实际创建 AppX 包。
  4. 数字签名是 AppX 分发的必要步骤。
  5. 整个过程支持自定义和扩展,如自定义扩展、文件关联等。

AppX/MSIX 包的优势:

  • 安全性更高,所有内容都经过签名验证。
  • 支持自动更新。
  • 可以利用 Windows 现代特性,如实时磁贴。
  • 更好的应用隔离和清理卸载。

原生参考文档:https://learn.microsoft.com/en-us/windows/msix/package/create-app-package-with-makeappx-tool#mapping-files

创建 MSI 安装程序

MSI(Microsoft Installer)是 Windows 平台上常用的安装包格式,它提供了一种标准化的方式来安装、维护和删除软件。MSI 文件本质上是一个包含安装信息和文件的数据库。

在这个 MsiTarget 类中,MSI 生成的主要步骤如下:

准备阶段

async build(appOutDir: string, arch: Arch) {
  const stageDir = await createStageDir(this, packager, arch)
  // ...
}

这里创建了一个临时目录来存放生成过程中的文件。

生成 WiX 项目文件

const projectFile = stageDir.getTempFile("project.wxs")
await writeFile(projectFile, await this.writeManifest(appOutDir, wixArch, commonOptions))

WiX(Windows Installer XML)是用来创建 MSI 安装程序的工具集。这一步生成了 WiX 需要的 XML 格式的项目文件。

编译 WiX 项目

await vm.exec(vm.toVmFile(path.join(vendorPath, "candle.exe")), candleArgs, {
  cwd: stageDir.dir,
})

使用 WiX 的 candle.exe 工具编译项目文件。

链接并生成 MSI

await this.light(objectFiles, vm, artifactPath, appOutDir, vendorPath, stageDir.dir)

使用 WiX 的 light.exe 工具链接编译后的对象文件,生成最终的 MSI 文件。

签名(可选)

await packager.sign(artifactPath)

对生成的 MSI 文件进行数字签名。

生成原理的核心在于 writeManifest 方法,它定义了 MSI 的结构和内容:

protected async writeManifest(appOutDir: string, wixArch: Arch, commonOptions: FinalCommonWindowsInstallerOptions) {
  const { files, dirs } = await this.computeFileDeclaration(appOutDir)
  // ...
  return (await this.projectTemplate.value)({
    // ... 各种选项
    dirs,
    files,
  })
}

这个方法生成了 WiX 项目文件,定义了安装程序的各个方面,包括:

  • 文件和目录结构
  • 快捷方式
  • 文件关联
  • 注册表项
  • 安装和卸载逻辑

computeFileDeclaration 方法详细定义了文件和目录结构,包括如何处理主可执行文件、创建快捷方式等。

总结一下MSI 程序的优势,大概有如下几个:

  1. 标准化:使用统一的安装、更新和卸载流程。
  2. 回滚能力:安装失败时可以回滚到之前的状态。
  3. 特权分离:支持普通用户安装和管理员安装。
  4. 广泛支持:Windows 原生支持,兼容性好。

总的来说,这个类通过生成 WiX 项目文件,然后使用 WiX 工具集编译和链接,最终创建出一个完整的 MSI 安装程序。这个过程封装了许多复杂的细节,使得创建专业级的 Windows 安装程序变得相对简单,因为 MSI 文件的创作其实里面包含了非常多的东西,不是一篇两篇就能讲完的,更多可参考下面的文档。

原生参考文档:https://learn.microsoft.com/en-us/windows/win32/msi/windows-installer-portal

创建 NSIS 安装程序

NSIS(Nullsoft Scriptable Install System)是一个用于创建 Windows 安装程序的开源系统。它最初由 Nullsoft 公司开发,该公司以创建 Winamp 播放器而闻名。   在 electron-builder 项目中,NSIS 被用来为 Windows 平台生成安装程序。它允许开发者通过配置选项来自定义安装程序,而无需直接编写 NSIS 脚本,大大简化了创建专业安装程序的过程。  

在 NsisTarget 中有一下几个核心的关键步骤:

应用程序打包

首先,NsisTarget 类会将应用程序文件打包成一个压缩文件:

async buildAppPackage(appOutDir: string, arch: Arch): Promise<PackageFileInfo> {
  const format = !isBuildDifferentialAware && options.useZip ? "zip" : "7z"
  const archiveFile = path.join(this.outDir, `${packager.appInfo.sanitizedName}-${packager.appInfo.version}-${Arch[arch]}.nsis.${format}`)
  
  await archive(format, archiveFile, appOutDir, archiveOptions)
  // ...
}

这里将应用程序文件打包成 7z 或 zip 格式,这是后续 NSIS 脚本将要使用的主要内容。

生成 NSIS 脚本

接下来,生成 NSIS 脚本。这个过程主要通过 computeFinalScript 方法完成:

private async computeFinalScript(originalScript: string, isInstaller: boolean, archs: Map<Arch, string>): Promise<string> {
  const scriptGenerator = new NsisScriptGenerator()
  // ...生成各种脚本内容
  return scriptGenerator.build() + originalScript
}

这个方法生成了完整的 NSIS 脚本,包括文件关联、预压缩文件处理等。

配置 NSIS 定义和命令

通过 configureDefinesconfigureDefinesForAllTypeOfInstaller 方法,设置各种 NSIS 定义:

protected configureDefines(oneClick: boolean, defines: Defines): Promise<any> {
  // ...设置各种定义
}
private configureDefinesForAllTypeOfInstaller(defines: Defines): void {
  // ...设置通用定义
}

这些定义控制了 NSIS 脚本的行为,如安装目录、快捷方式创建等。

执行 NSIS 编译

最后,使用 executeMakensis 方法调用 NSIS 编译器:

private async executeMakensis(defines: Defines, commands: Commands, script: string): Promise<void> {
  const args: Array<string> = []
  // ...准备参数
  
  await spawnAndWrite(command, args, script, {
    env: { ...process.env, NSISDIR: nsisPath },
    cwd: nsisTemplatesDir,
  })
}

这个方法将生成的脚本、定义和命令传递给 NSIS 编译器 (makensis),编译器然后生成最终的安装程序。

特殊文件处理

对于预压缩的文件,有特殊处理:

async function generateForPreCompressed(preCompressedFileExtensions: Array<string>, dir: string, arch: Arch, scriptGenerator: NsisScriptGenerator): Promise<void> {
  // ...处理预压缩文件
}

这确保了某些已压缩的文件(如 .asar 文件)能够正确地包含在安装程序中。

多架构支持

源码支持为不同架构生成安装程序:

for (const archs of doBuildArchs) {
  await this.buildInstaller(archs)
}

这允许生成适用于不同 CPU 架构的安装程序。
NSIS 打包这个过程高度可配置,允许定制安装程序的各个方面,如文件关联、快捷方式、多语言支持等。通过抽象和模块化,这个实现使得生成复杂的 Windows 安装程序变得相对简单和灵活。

结语

通过深入探讨 electron-builder 的 Windows 打包过程,我们看到了其背后的复杂性和精巧设计。从 winPackager.ts 的整体协调,到 AppxTarget.ts、MsiTarget.ts 和 NsisTarget.ts 等不同目标的具体实现,每一部分都扮演着重要角色。理解这些打包机制不仅有助于解决在使用 electron-builder 时可能遇到的问题,还能让我们在设计跨平台应用时做出更明智的决策,希望这篇文章可以帮助到你

相关文章
|
2月前
|
资源调度 运维 JavaScript
使用electron创建桌面应用及常见打包错误解决
使用electron创建桌面应用及常见打包错误解决
264 3
|
26天前
|
安全 前端开发 Windows
Windows Electron 应用更新的原理是什么?揭秘 NsisUpdater
本文介绍了 Electron 应用在 Windows 中的更新原理,重点分析了 `NsisUpdater` 类的实现。该类利用 NSIS 脚本,通过初始化、检查更新、下载更新、验证签名和安装更新等步骤,确保应用的更新过程安全可靠。核心功能包括差异下载、签名验证和管理员权限处理,确保更新高效且安全。
27 4
Windows Electron 应用更新的原理是什么?揭秘 NsisUpdater
|
2月前
|
前端开发 Unix Linux
揭秘 Electron 的 Linux 打包过程:你知道背后发生了什么吗?
本文详细介绍了 `electron-builder` 在 Linux 平台上如何打包 Electron 应用程序,涵盖了 AppImage、Flatpak、Snap 等多种格式的打包原理和具体实现。文章从初始化 `LinuxPackager` 到创建各种目标格式的包,详细解析了每个步骤的代码逻辑和关键方法,帮助开发者更好地理解和使用 `electron-builder` 进行 Linux 应用的打包。
104 2
揭秘 Electron 的 Linux 打包过程:你知道背后发生了什么吗?
|
2月前
|
安全 前端开发 iOS开发
揭秘 electron-builder:macOS 应用打包背后到底发生了什么?
本文详细介绍了 Electron 应用在 macOS 平台上的打包流程,涵盖配置文件、打包步骤、签名及 notarization 等关键环节。通过剖析 `electron-builder` 的源码,展示了如何处理多架构应用、执行签名,并解决常见问题。适合希望深入了解 macOS 打包细节的开发者。
66 2
|
2月前
|
监控 前端开发 安全
谈谈我做 Electron 应用的这一两年
本文首发于微信公众号“前端徐徐”,作者徐徐分享了过去一两年间开发Electron桌面应用的经验与心得。文章详细介绍了从项目启动、技术选型到具体实施的过程,并探讨了桌面端开发面临的挑战及解决方案,如软件更新、任务队列设计、性能优化等。此外,还列举了一些特殊需求的实现方法,如静默安装、进程禁用等。作者认为,尽管桌面端开发有其独特性,但通过不断探索与实践,仍能显著提升用户体验和技术水平。
155 0
谈谈我做 Electron 应用的这一两年
|
2月前
|
数据可视化 程序员 C#
C#中windows应用窗体程序的输入输出方法实例
C#中windows应用窗体程序的输入输出方法实例
48 0
|
1月前
|
JSON JavaScript 前端开发
开发桌面程序-Electron入门
【10月更文挑战第16天】Electron 是一个使用 JavaScript、HTML 和 CSS 构建跨平台桌面应用的框架,嵌入了 Chromium 和 Node.js。本文介绍了如何搭建 Electron 开发环境,包括安装 Node.js、创建项目、配置 main.js 和打包应用。通过简单的步骤,你可以快速创建并运行一个基本的 Electron 应用程序。
开发桌面程序-Electron入门
|
4月前
|
JavaScript 开发工具
Electron 开发过程中主进程的无法看到 console.log 输出怎么办
Electron 开发过程中主进程的无法看到 console.log 输出怎么办
|
5月前
|
前端开发
PC端01,桌面端,electron的开发,electron的开发的系列课程,软件开发必备流程,electron的讲解,electron的开发,vitepress博主的gitee链接,PC端效率软件
PC端01,桌面端,electron的开发,electron的开发的系列课程,软件开发必备流程,electron的讲解,electron的开发,vitepress博主的gitee链接,PC端效率软件
PC端01,桌面端,electron的开发,electron的开发的系列课程,软件开发必备流程,electron的讲解,electron的开发,vitepress博主的gitee链接,PC端效率软件
|
7月前
|
移动开发 开发框架 JavaScript
Vue3 Vite electron 开发桌面程序
Vue3 Vite electron 开发桌面程序
343 0