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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 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 生成,还是代码签名与安装程序的创建,这些环节都直接影响到应用的最终质量和用户体验。那么,你是否已经掌握了这些关键要素,能够在实际开发中灵活运用呢?  

相关文章
|
8天前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
8天前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
8天前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
5天前
|
前端开发 Java 应用服务中间件
21张图解析Tomcat运行原理与架构全貌
【10月更文挑战第2天】本文通过21张图详细解析了Tomcat的运行原理与架构。Tomcat作为Java Web开发中最流行的Web服务器之一,其架构设计精妙。文章首先介绍了Tomcat的基本组件:Connector(连接器)负责网络通信,Container(容器)处理业务逻辑。连接器内部包括EndPoint、Processor和Adapter等组件,分别处理通信、协议解析和请求封装。容器采用多级结构(Engine、Host、Context、Wrapper),并通过Mapper组件进行请求路由。文章还探讨了Tomcat的生命周期管理、启动与停止机制,并通过源码分析展示了请求处理流程。
|
8天前
|
搜索推荐 Shell
解析排序算法:十大排序方法的工作原理与性能比较
解析排序算法:十大排序方法的工作原理与性能比较
27 9
|
10天前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
【10月更文挑战第1天】Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
47 9
|
8天前
|
搜索推荐 C++
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理(一)
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理
|
8天前
|
搜索推荐 索引
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理(二)
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理
|
16天前
|
存储 缓存 关系型数据库
redo log 原理解析
redo log 原理解析
26 0
redo log 原理解析
|
2天前
|
SQL 分布式计算 大数据
大数据-97 Spark 集群 SparkSQL 原理详细解析 Broadcast Shuffle SQL解析过程(一)
大数据-97 Spark 集群 SparkSQL 原理详细解析 Broadcast Shuffle SQL解析过程(一)
11 0

推荐镜像

更多