electron-builder 解析:你了解其背后的构建原理吗?

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 本文首发于微信公众号“前端徐徐”,详细解析了 electron-builder 的工作原理。electron-builder 是一个专为整合前端项目与 Electron 应用的打包工具,负责管理依赖、生成配置文件及多平台构建。文章介绍了前端项目的构建流程、配置信息收集、依赖处理、asar 打包、附加资源准备、Electron 打包、代码签名、资源压缩、卸载程序生成、安装程序生成及最终安装包输出等环节。通过剖析 electron-builder 的原理,帮助开发者更好地理解和掌握跨端桌面应用的构建流程。

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

大家好,我是徐徐。今天我们讲讲 electron-builder 的原理。

前言

我们前面浅析了 Electron 的相关原理,这一节我们来讲讲 electron-builder 的原理。为什么要讲它呢,因为它的原理基本上包含了业界跨端的桌面端开发框架的打包构建原理,搞懂了这款工具的原理,其他的类似的跨端开发框架的构建思路都差不多,比如 NW.js 之类的,都差不多。而且它在构建 electron 应用这一块是非常成熟和优秀的,如果你想无痛构建 electron 应用,它是首选。下面我们就来看看它的整体原理与构建流程。

原理概览

electron-builder 是一个专注于将 Electron 应用与前端项目整合的打包工具,它负责管理依赖、生成配置文件、打包资源,并支持多平台构建。首先,在开始构建应用之前,需要确保前端项目经过构建,生成正确的输出文件和 package.json。只有这样,electron-builder才会进行后面的操作,比如收集配置信息并处理依赖,特别是原生模块的编译。然后,通过生成 ASAR 包、创建安装和卸载程序,最终输出完整的安装包。下面是每一部分的详细讲解。

前端项目构建

electron-builder 是不会构建你的前端项目的,它只负责你的前端项目和 electron 的整合,所以你需要自己完成以下步骤:

  • 使用现代前端框架(如React, Vue, Angular)开发应用
  • 配置webpack, Rollup或Vite进行打包
  • 优化构建过程,如代码分割、懒加载、Tree Shaking等
  • 输出构建结果到指定目录,通常是 distbuild
  • 确保所有静态资源(图片、字体等)都被正确处理和复制

上面这些步骤跟你打包一个普通的前端应用差别不是太大,但是还是有一些细微的差别,比如:

  • 渲染进程和主进程的代码分离
  • 依赖管理需要正确,可能需要处理特定的原生模块依赖
  • 网络请求需要设置绝对路径,去除 nginx 的配置

上面的步骤做好了,才会真正的走到 electron 的构建世界。

准备package.json

这里可能大家会有疑问,为什么会需要准备 package.json 呢,项目里面肯定有 package.json,这里说的 package.json 可不是项目里面的 package.json ,而是 electron-builder 打包需要的 package.json,你可以理解为现在需要创建一个项目,这个项目专门是为了打包 electron 应用而构建的项目,项目里面需要前端构建的文件和一个 package.json,然后用这个项目打包。大概是如下情形:

  • 主项目package.json:
{
  "name": "your-app",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "build": "vite build && electron-builder"
  },
  "devDependencies": {
    "electron": "^13.0.0",
    "electron-builder": "^22.11.7",
    "vite": "^2.5.0"
  }
}
  • 输出目录的 dist package.json:
{
  "name": "your-app",
  "version": "1.0.0",
  "main": "main.js",
  "dependencies": {
    "sqlite3": "^5.0.2"  // 仅包含原生模块
  }
}

当然,我们在实际开发的过程中肯定是不会单独搞两个项目的,打包构建前端项目和构建 electron 应用都是在一个项目中完成的,上面提到的只是为了让大家理解 electron-builder 如何开始工作的,以及 package.json 对它的重要性。这个地方会有相应的解释:https://www.electron.build/tutorials/two-package-structure

收集配置信息

要打包成自己想要的应用,肯定需要一些配置信息,有了这些配置信息 electron-builder 才会正常的运行并获取相应的配置去构建应用。

它支持多种方式的配置信息:

  • 第一种是从 package.json 的 "build" 字段读取配置:
"build": {
  "appId": "com.example.app",
  "productName": "Your App",
  "mac": {
    "category": "public.app-category.productivity"
  },
  "win": {
    "target": ["nsis", "portable"]
  },
  "linux": {
    "target": ["AppImage", "deb"]
  },
  "extraResources": [
    {"from": "assets/", "to": "assets/"}
  ]
}
  • 或从electron-builder.yml 或者 electron-builder.json 读取配置

当 electron-builder 读取到用户的配置之后,它会合并默认配置和用户配置,最后组成一个完整的打包配置信息。

源码路径在这里:

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

export async function getConfig(
  projectDir: string,
  configPath: string | null,
  configFromOptions: Configuration | null | undefined,
  packageMetadata: Lazy<{ [key: string]: any } | null> = new Lazy(() => orNullIfFileNotExist(readJson(path.join(projectDir, "package.json"))))
): Promise<Configuration> {
  const configRequest: ReadConfigRequest = { packageKey: "build", configFilename: "electron-builder", projectDir, packageMetadata }
  // 省略一些代码
  return doMergeConfigs([...parentConfigs, config])
}

在这个方法中,会收集所有的配置信息然后经过处理得到最终的打包配置信息。

依赖处理

依赖处理是非常重要的一个环境,他可能决定你的应用是否打包成功,这一个步骤大概分成以下几个小的步骤:

  • 分析package.json中的dependencies
  • 使用npm或yarn安装这些依赖
  • 对于原生模块(如sqlite3),使用electron-rebuild重新编译:
./node_modules/.bin/electron-rebuild
  • 确保所有依赖与Electron版本兼容

这一步其实最最重要的就是安装依赖,不管是 electron 本身的包还是需要重新编译的包,都会在这一步完成。

源码路径在这里:

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

public async installAppDependencies(platform: Platform, arch: Arch): Promise<any> {
    if (this.options.prepackaged != null || !this.framework.isNpmRebuildRequired) {
      return
    }
    const frameworkInfo = { version: this.framework.version, useCustomDist: true }
    const config = this.config
    if (config.nodeGypRebuild === true) {
      await nodeGypRebuild(platform.nodeName, Arch[arch], frameworkInfo)
    }
    if (config.npmRebuild === false) {
      log.info({ reason: "npmRebuild is set to false" }, "skipped dependencies rebuild")
      return
    }
    const beforeBuild = await resolveFunction(this.appInfo.type, config.beforeBuild, "beforeBuild")
    if (beforeBuild != null) {
      const performDependenciesInstallOrRebuild = await beforeBuild({
        appDir: this.appDir,
        electronVersion: this.config.electronVersion!,
        platform,
        arch: Arch[arch],
      })
      // If beforeBuild resolves to false, it means that handling node_modules is done outside of electron-builder.
      this._nodeModulesHandledExternally = !performDependenciesInstallOrRebuild
      if (!performDependenciesInstallOrRebuild) {
        return
      }
    }
    if (config.buildDependenciesFromSource === true && platform.nodeName !== process.platform) {
      log.info({ reason: "platform is different and buildDependenciesFromSource is set to true" }, "skipped dependencies rebuild")
    } else {
      await installOrRebuild(config, this.appDir, {
        frameworkInfo,
        platform: platform.nodeName,
        arch: Arch[arch],
        productionDeps: this.getNodeDependencyInfo(null, false) as Lazy<Array<NodeModuleDirInfo>>,
      })
    }
  }

这个方法主要用于确保在不同的平台和架构上,应用的依赖项能够正确地安装和重建,以支持 Electron 应用的构建过程。  

生成asar

这个是 electron-builder 提供的能力,可以对源码进行一定的加密保护。默认情况下,它会将应用文件打包成app.asar,当然你也可以通过配置禁用 asar 打包:

"build": {
  "asar": false
}

asar 打包可以提高加载速度,但可能影响某些原生模块的使用,这个在处理的时候需要特别注意,另外就是打包之后的路径问题处理。

准备附加资源

除了 Electron 相关的文件和 app.asar 文件外,有的时候还有一些不需要进入构建的附加资源,这些文件根据extraResources 配置复制额外文件,然后会一并打入到整个应用程序里,它们可以通过 process.resourcesPath 访问。

Electron打包

经过上面的前端项目的构建,依赖的下载,附加资源等很多资源的准备,到这一步就开始最最核心的打包工作了,大概的步骤如下:

  • 首先检查缓存中是否有指定版本的 Electron
  • 如果没有,从官方源下载 Electron
  • 将 Electron 文件复制到临时目录,如 win-unpacked/
  • 将应用资源复制到 Electron 结构中的正确位置
  • 然后根据配置分端组装构建不同的目标文件

源码路径在这里:

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

async build(repositoryInfo?: SourceRepositoryInfo): Promise<BuildResult> {
    await this.validateConfig()
    if (repositoryInfo != null) {
      this._repositoryInfo.value = Promise.resolve(repositoryInfo)
    }
    this._appInfo = new AppInfo(this, null)
    this._framework = await createFrameworkInfo(this.config, this)
    const commonOutDirWithoutPossibleOsMacro = path.resolve(
      this.projectDir,
      expandMacro(this.config.directories!.output!, null, this._appInfo, {
        os: "",
      })
    )
    if (!isCI && (process.stdout as any).isTTY) {
      const effectiveConfigFile = path.join(commonOutDirWithoutPossibleOsMacro, "builder-effective-config.yaml")
      log.info({ file: log.filePath(effectiveConfigFile) }, "writing effective config")
      await outputFile(effectiveConfigFile, getSafeEffectiveConfig(this.config))
    }
    // because artifact event maybe dispatched several times for different publish providers
    const artifactPaths = new Set<string>()
    this.artifactCreated(event => {
      if (event.file != null) {
        artifactPaths.add(event.file)
      }
    })
    this.disposeOnBuildFinish(() => this.tempDirManager.cleanup())
    const platformToTargets = await executeFinally(this.doBuild(), async () => {
      if (this.debugLogger.isEnabled) {
        await this.debugLogger.save(path.join(commonOutDirWithoutPossibleOsMacro, "builder-debug.yml"))
      }
      const toDispose = this.toDispose.slice()
      this.toDispose.length = 0
      for (const disposer of toDispose) {
        await disposer().catch((e: any) => {
          log.warn({ error: e }, "cannot dispose")
        })
      }
    })
    return {
      outDir: commonOutDirWithoutPossibleOsMacro,
      artifactPaths: Array.from(artifactPaths),
      platformToTargets,
      configuration: this.config,
    }
  }

这段代码定义了一个异步的 electron 的 build 方法,主要流程包括验证配置、处理应用信息、创建框架信息和设置输出目录。它会记录有效配置到文件中,并管理构建生成的工件。最后,它调用 doBuild 方法进行实际构建,并在完成后返回构建结果,包括输出目录、工件路径和目标平台信息。  

private async doBuild(): Promise<Map<Platform, Map<string, Target>>> {
    const taskManager = new AsyncTaskManager(this.cancellationToken)
    const syncTargetsIfAny = [] as Target[]
    const platformToTarget = new Map<Platform, Map<string, Target>>()
    const createdOutDirs = new Set<string>()
    for (const [platform, archToType] of this.options.targets!) {
      if (this.cancellationToken.cancelled) {
        break
      }
      if (platform === Platform.MAC && process.platform === Platform.WINDOWS.nodeName) {
        throw new InvalidConfigurationError("Build for macOS is supported only on macOS, please see https://electron.build/multi-platform-build")
      }
      const packager = await this.createHelper(platform)
      const nameToTarget: Map<string, Target> = new Map()
      platformToTarget.set(platform, nameToTarget)
      for (const [arch, targetNames] of computeArchToTargetNamesMap(archToType, packager, platform)) {
        if (this.cancellationToken.cancelled) {
          break
        }
        // support os and arch macro in output value
        const outDir = path.resolve(this.projectDir, packager.expandMacro(this.config.directories!.output!, Arch[arch]))
        const targetList = createTargets(nameToTarget, targetNames.length === 0 ? packager.defaultTarget : targetNames, outDir, packager)
        await createOutDirIfNeed(targetList, createdOutDirs)
        await packager.pack(outDir, arch, targetList, taskManager)
      }
      if (this.cancellationToken.cancelled) {
        break
      }
      for (const target of nameToTarget.values()) {
        if (target.isAsyncSupported) {
          taskManager.addTask(target.finishBuild())
        } else {
          syncTargetsIfAny.push(target)
        }
      }
    }
    await taskManager.awaitTasks()
    for (const target of syncTargetsIfAny) {
      await target.finishBuild()
    }
    return platformToTarget
  }

上面的代码实现了一个跨平台的构建过程,通过管理不同平台和架构的构建任务,使用异步任务管理器提高效率。它动态创建输出目录,处理宏替换,确保兼容性,并最终返回包含所有构建目标的映射,形成灵活可扩展的构建框架。  每个平台都有一个 packager 文件,可以在对应的 packager 下看每个平台的打包构建过程。

代码签名

经过上面最核心的步骤之后就开始进行代码签名的步骤了,在这个阶段会使用指定的证书对可执行文件进行签名,Windows 上使用 signtool,macOS上使用 codesign,签名的目的是增加用户信任,减少安全警告,当然你也可以配置不签名,不签名的应该会被系统判断为危险应用,所以商用软件都是有正规签名的。

压缩资源

压缩资源也是一个非常重要的工作, electron-builder 也把这项工作包含进去了。它会使用压缩工具将应用文件压缩成单个包,高压缩比可以显著减小安装包大小,其压缩文件名格式大概为:appName-version-arch.zip 或者 以 7z 结尾的压缩包。

生成卸载程序

electron-builder 会使用一个名为NSIS的工具生成卸载程序的可执行文件,这个卸载程序记录了 win-ia32-unpacked 目录下所有文件的相对路径。当用户卸载我们的应用时,卸载程序会根据这些相对路径删除我们的文件,同时它也会记录一些安装时使用的注册表信息,在卸载时清除这些注册表信息。如果开发者配置了签名逻辑,则 electron-builder 也会为卸载程序的可执行文件进行签名。

生成安装程序

同样的,electron-builder 会使用 NSIS 创建安装程序,在里面嵌入压缩的应用文件和卸载程序,而且可以配置安装过程,如创建快捷方式、注册文件关联等,如果配置了签名,对安装程序进行签名。

输出最终安装包

最后将生成的安装程序移动到指定的输出目录生成安装包,这里可能进行额外的处理,如重命名、生成校验和等。

结语

通过对 electron-builder 原理的剖析,揭示了跨端桌面应用打包构建的核心流程与细节。无论是依赖处理、ASAR 生成,还是代码签名与安装程序的创建,这些环节都直接影响到应用的最终质量和用户体验。那么,你是否已经掌握了这些关键要素,能够在实际开发中灵活运用呢?  

相关文章
|
18天前
|
运维 持续交付 云计算
深入解析云计算中的微服务架构:原理、优势与实践
深入解析云计算中的微服务架构:原理、优势与实践
52 1
|
2月前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
52 3
|
21天前
|
自然语言处理 算法 Python
再谈递归下降解析器:构建一个简单的算术表达式解析器
本文介绍了递归下降解析器的原理与实现,重点讲解了如何使用Python构建一个简单的算术表达式解析器。通过定义文法、实现词法分析器和解析器类,最终实现了对基本算术表达式的解析与计算功能。
91 52
|
18天前
|
弹性计算 持续交付 API
构建高效后端服务:微服务架构的深度解析与实践
在当今快速发展的软件行业中,构建高效、可扩展且易于维护的后端服务是每个技术团队的追求。本文将深入探讨微服务架构的核心概念、设计原则及其在实际项目中的应用,通过具体案例分析,展示如何利用微服务架构解决传统单体应用面临的挑战,提升系统的灵活性和响应速度。我们将从微服务的拆分策略、通信机制、服务发现、配置管理、以及持续集成/持续部署(CI/CD)等方面进行全面剖析,旨在为读者提供一套实用的微服务实施指南。
|
1月前
|
安全 前端开发 Windows
Windows Electron 应用更新的原理是什么?揭秘 NsisUpdater
本文介绍了 Electron 应用在 Windows 中的更新原理,重点分析了 `NsisUpdater` 类的实现。该类利用 NSIS 脚本,通过初始化、检查更新、下载更新、验证签名和安装更新等步骤,确保应用的更新过程安全可靠。核心功能包括差异下载、签名验证和管理员权限处理,确保更新高效且安全。
33 4
Windows Electron 应用更新的原理是什么?揭秘 NsisUpdater
|
23天前
|
监控 持续交付 数据库
构建高效的后端服务:微服务架构的深度解析
在现代软件开发中,微服务架构已成为提升系统可扩展性、灵活性和维护性的关键。本文深入探讨了微服务架构的核心概念、设计原则和最佳实践,通过案例分析展示了如何在实际项目中有效地实施微服务策略,以及面临的挑战和解决方案。文章旨在为开发者提供一套完整的指导框架,帮助他们构建出更加高效、稳定的后端服务。
|
26天前
|
运维 持续交付 虚拟化
深入解析Docker容器化技术的核心原理
深入解析Docker容器化技术的核心原理
45 1
|
19天前
|
存储 供应链 算法
深入解析区块链技术的核心原理与应用前景
深入解析区块链技术的核心原理与应用前景
43 0
|
1月前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
52 1
|
22天前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
51 0

推荐镜像

更多