likes
comments
collection
share

详解 Electron 打包

作者站长头像
站长
· 阅读数 81

Electron 可以使用多种工具进行打包,新手很容易眼花缭乱,不知道如何选择。其实 Electron 打包分为两大阵营:

  • 社区提供的 Electron Builder:这是目前最流行的打包工具,配置简单,开箱即用,覆盖 Windows、macOS 和 Linux 平台,并支持多种打包格式,集成了自动更新和代码签名的功能。
  • 官方提供的 Electron PackagerElectron Forge:这两个经常是配合在一起使用,因为 Electron Packager 只是将应用打包成可执行程序,而 Electron Forge 会继续将其打包成安装程序。

这两大阵营之间彼此互相竞争,背地里还有一些小动作,且听我慢慢道来。首先是官方教程中的打包章节只字未提 Electron Builder,直接上来就推荐 Electron Forge:

详解 Electron 打包

当你按照文档安装了 @electron-forge/cli ,并使用 import 脚本安装依赖之后,它会偷偷的将你项目中集成的 electorn-builder 给删掉,理由很简单,就一句话:

provides mostly equivalent functionality

意思是 Electron Forge 已经提供了大部分相同的功能了,就不需要 Electron Builder 了,有种一山不容二虎,有你没我,非此即彼的意思。

当然,Electron Builder 也不甘示弱啊,既然你不仁就别怪我不义,直接在 GitHub 仓库把 Electron Forge 的功能集成进去了,告诉开发者别用 forge 了,我 Electron Builder 也可以打 forge 的包。

详解 Electron 打包

两大阵营针锋相对,逼着开发者站队,这也太难了!不过别慌,正所谓「本领在手,说走就走」,只要掌握了它们的打包原理,可以随时切换,谁好用就用谁,接下来就为大家详细讲解它们的使用方法和底层原理。

社区阵营

Electron Builder 不愧是 Electron 界的当红炸子鸡,真的是 all-in-one 的打包工具,大部分的打包逻辑都是自己写的,要知道打包流程是非常复杂的,Mac、Windows 和 Linux 的包格式完全不一样,涉及到方方面面的东西全给封装掉了,真的践行了:把复杂留给自己,简单交给别人。开发者只需在 package.json 中添加 build 字段,然后增加一些配置即可快速打包,下面是常用的顶级配置项:

  • appId:应用 id
  • productName:应用名
  • electronVersion:打包使用的 Electron 版本
  • directories:输入输出目录相关的配置项
  • files:用于指定哪些文件和文件夹应该被打包到最终的应用程序中
  • mac:macOS 系统下的专属配置
  • win:Windows 系统下的专属配置
  • linux:Linux 系统下的专属配置

示例配置如下:

"build": {
  "productName": "electron-desktop",
  "appId": "com.keliq.electron-desktop",
  "directories": {
    "output": "builder-dist"
  },
  "npmRebuild": false,
  "files": [
    "build/**/*",
    "public/**/*",
    "!node_modules/**/*"
  ],
  "extraResources": [
    "package_resources"
  ],
  "extraFiles": [
    {
      "from": "build/extra",
      "to": "extraFiles",
      "filter": "*.dll"
    }
  ]
}

大部分的属性语义非常明确,一眼就知道是什么意思,这里重点讲以下三个属性:

  • files:用于指定哪些文件应该被打包到最终的应用程序中。
  • extraResources:用于指定哪些文件被打包到应用程序中的资源文件夹中,应用程序代码可以用相对路径进行访问。
  • extraFiles:用于指定哪些文件被打包到应用程序安装目录中的文件。

extraResources 和 extraFiles 都是在构建过程中指定需要包含的文件或资源,但它们的作用略有不同。前者将资源打包到应用程序中的资源文件夹中,后者则是直接复制到应用程序的安装目录中。

  • 资源目录:macOS 下是 Contents/Resources 目录,Windows 和 Linux 下是 resources 目录
  • 安装目录:macOS 下是 Contents 目录,Windows 和 Linux 下是根目录

files 配置使用的是 glob 语法,Electron Builder 在其基础上做了一些增强,支持 File Macros 语法。通过这种方式可以快速指定哪些文件会被打包进去,过滤掉那些不想要的文件。默认的配置如下:

[
  "**/*",
  "!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
  "!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
  "!**/node_modules/*.d.ts",
  "!**/node_modules/.bin",
  "!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}",
  "!.editorconfig",
  "!**/._*",
  "!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
  "!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
  "!**/{appveyor.yml,.travis.yml,circle.yml}",
  "!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}"
]

感叹号开头的是要被忽略的文件,Electron Builder 非常贴心的把无关的文件全部排除在外了,但是这里有个坑:

package.json and **/node_modules/**/* (only production dependencies will be copied) is added to your custom in any case.

也就是说 package.jsonnode_modules 目录会被自动加到 files 属性里面,而我们项目在打包的时候一般会通过构建工具(例如 webpack 或 rollup 等)生成了目标产物,node_modules 里面的那些依赖项已经被打进去了,完全没必要再打包进去,所以要手动把 node_modules 目录给忽略掉:

"files": [
  "build/**/*",
  "public/**/*",
  "!**/node_modules/**/*"
],

否则一不小心,你的 asar 包可能就大几百 M 了。以 antd 为例,组件库依赖一般都会放到 dependencies 下面,但是在经过 webpack 构建完成之后,引用到的组件已经被打到 dist 目录下了,结果 Electron Builder 还会把 antd 打到 asar 里面,要知道一个 antd 有 454M 啊!

$ du -hs * | sort -h
908K	axios
4.6M	react-dom
4.9M	lodash
 27M	@ant-design
454M	antd

这个错误,很多新手和老手都会犯,所以我建议使用 Electron Builder 的朋友,打包完成之后看看 asar 文件里面的内容是否符合预期,至于如何分析 asar 文件可以参考笔者这篇文章

除了上面的通用配置项之外,打不同系统的包时,还有专属的配置项,例如下面指定了 Windows 下的配置:

"build": {
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": [
          "ia32"
        ]
      }
    ],
    "icon": "public/icon/icons/win/icon.ico"
  },
  "nsis": {
    "oneClick": false,
    "perMachine": false,
    "allowElevation": true,
    "allowToChangeInstallationDirectory": true
  },
}

具体的配置项不在赘述,使用时可以查阅官方文档。除了配置项之外,Electron Builder 还提供了一些构建时的钩子函数,允许开发者插入自定义逻辑,常见的钩子有:

  • beforePack:在打包可执行前执行
  • afterPack:打可执行包后执行(在签名和打安装包之前)
  • afterSign:在签名之后执行
  • artifactBuildStarted:打安装包开始时执行
  • artifactBuildCompleted:安装包结束时执行
  • afterAllArtifactBuild:所有安装包都打完时执行

由于在 package.json 中无法写 js 函数,可以指定一个 js 文件来执行钩子,注意这些文件必须默认导出一个函数:

"build": {
  "beforePack": "hooks/beforePack.js",
  "afterPack": "hooks/afterPack.js",
  "afterSign": "hooks/afterSign.js",
  "artifactBuildStarted": "hooks/artifactBuildStarted.js",
  "artifactBuildCompleted": "hooks/artifactBuildCompleted.js",
  "afterAllArtifactBuild": "hooks/afterAllArtifactBuild.js"
},	

另外需要注意,Electron Builder 在打包的时候,会自动去钥匙串寻找可用证书,然后发起签名,如果想禁用这种行为,可以设置下面的环境变量:

"scripts": {
  "pack-by-builder": "CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder",
},

打包输出的时候就会有一项 skipped macOS application code signing

$ npm run pack-by-builder        

> electron-desktop@1.0.0 pack-by-builder
> CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder

  • electron-builder  version=23.6.0 os=21.6.0
  • loaded configuration  file=package.json ("build" field)
  • writing effective config  file=dist/builder-effective-config.yaml
  • packaging       platform=darwin arch=x64 electron=23.1.4 appOutDir=dist/mac
  • skipped macOS application code signing  reason=, see https://electron.build/code-signing CSC_IDENTITY_AUTO_DISCOVERY=false
  • building        target=macOS zip arch=x64 file=dist/electron-desktop-1.0.0-mac.zip
  • building        target=DMG arch=x64 file=dist/electron-desktop-1.0.0.dmg
  • building block map  blockMapFile=dist/electron-desktop-1.0.0.dmg.blockmap

官方阵营

接下来再来说说官方推荐的 Electron Forge,首运行以下命令:

$ yarn add --dev @electron-forge/cli
$ yarn electron-forge import 

这个时候会自动安装其他的依赖:

"devDependencies": {
  "@electron-forge/cli": "^6.0.5",
  "@electron-forge/maker-deb": "^6.0.5",
  "@electron-forge/maker-rpm": "^6.0.5",
  "@electron-forge/maker-squirrel": "^6.0.5",
  "@electron-forge/maker-zip": "^6.0.5",
},

Electron Forge 自身不提供任何具体的功能,它就是一个管理框架,把打包的各个环节串联起来,最终输出用户想要的产物。这从源码结构上就可以看出来:

src
├── electron-forge-import.ts
├── electron-forge-init.ts
├── electron-forge-make.ts
├── electron-forge-package.ts
├── electron-forge-publish.ts
├── electron-forge-start.ts
└── electron-forge.ts

以打 macOS 平台的包为例,会经历以下几个步骤:

  • electron-rebuild 来重新构建依赖
  • electron-packager 进行打可执行包
  • @electron/osx-sign 进行签名
  • electron-installer-dmg 打 dmg 安装包
  • @electron/universal 打 universal 包

而在 Windows 系统上又会是另外一套流程了,虽然具体构建流程不同,但是 Electron Forge 把它们抽象为三个阶段:

  1. package(打包)
  2. make(制作)
  3. publish(发布)

详解 Electron 打包

在这三个环节当中,如果开发者想深度介入构建流程,也提供了两种参与方式:

  • plugins(插件)
  • hooks(钩子)

从 Electron Forge 的配置项可以看出来,都是围绕上面的流程来的:

module.exports = {
  packagerConfig: { ... },
  rebuildConfig: { ... },
  makers: [ ... ],
  publishers: [ ... ],
  plugins: [ ... ],
  hooks: { ... },
  buildIdentifier: 'my-build'
}

package(打包)

配置文件中的 packagerConfig 属性就是传递给 Electron Packager 的打包参数,但是注意有个坑,部分属性是无法覆盖的:

  • dir
  • arch
  • platform
  • out
  • electronVersion

不能覆盖 Electron Packager 的 dir 目录是比较坑的一点,网上有很多的讨论:

但实际上,dir 目录是可以指定的,在 package 阶段有个可选的 [cwd] 选项:

$ npx electron-forge package --help           
✔ Checking your system
Usage: electron-forge-package [options] [cwd]

假如构建产物在 dest 目录下,可以这么写:

"scripts": {
  "pack-by-forge": "electron-forge package dest"
}

但是要注意,dest 目录下一定要有 package.json 文件,并且有 config.forge 配置字段,或者存在 @electron-forge/cli 依赖,否则会继续往上级目录中寻找,这块逻辑在 forge 源码文件 api/core/src/util/resolve-dir.ts 中:

详解 Electron 打包

所以当在 dest 目录下按照上述要求放了 package.json 之后,再打包就能看到 forge 找到了正确的打包目录了:

$ DEBUG=electron-forge:* npm run pack-by-forge

> electron-desktop@1.0.0 pack-by-forge
> electron-forge package dest

⠋ Checking your system
✔ Checking your system
  electron-forge:packager electron-packager options { dir: '/Users/keliq/electron-desktop/dest', interactive: true } +0ms
[STARTED] Preparing to package application
  electron-forge:project-resolver searching for project in: /Users/keliq/electron-desktop/dest +0ms
  electron-forge:electron-version Looking for a lock file to indicate the root of the repo +0ms
  electron-forge:electron-version Found lock file: /Users/keliq/electron-desktop/yarn.lock +3ms
  electron-forge:project-resolver package.json with forge dependency found in /Users/keliq/electron-desktop/dest/package.json +6ms
[SUCCESS] Preparing to package application

不过需要注意,构建产物的目录也会变成 dest/out ,而且暂时不支持自定义 out 目录,不过社区有个哥们提交了一个 pull request,都 1 年多了,目前没得到 merge,估计是没戏了。

关于 Electron Packager 的使用方法,在此也做一个详细的介绍,全局安装之后,可以通过下面的命令直接打包:

$ electron-packager dest "MyApp" \
  --platform=darwin \
  --arch=x64 \
  --download.cacheRoot="$HOME/electron-cache" \
  --download.mirrorOptions.mirror="http://npm.taobao.org/mirrors/electron/" \
  --electron-version=20.0.0 \
  --icon=./icon.icns \
  --ignore=".gitignore" \
  --ignore="resources" \
  --overwrite
  • platform:目标操作系统
  • arch:软件架构
  • download:镜像下载配置
    • cacheRoot:electron 文件缓存目录
    • mirrorOptions:electron 镜像下载地址
  • electron-version:使用的 electron 版本
  • icon:图标文件
  • ignore:忽略的文件或目录
  • overwrite:如果文件存在则覆盖

具体的参数可以参考官方文档,里面有很多选项,其中 download 里面的 mirrorOptions 比较重要,有三个选项:

  • mirror:前缀路径
  • customDir:版本号路径
  • customFilename:文件路径

它们组成了完整的镜像地址:

详解 Electron 打包

也就是说,用户可以通过下面三个选项配置下载 Electron 的路径,例如:

mirrorOptions: {
  mirror: 'https://mirror.example.com/electron/',
  customDir: 'version-{{ version }}',
  customFilename: 'unofficial-electron-linux.zip'
}

那么最终的下载路径是:${mirror}/${customDir}/${customFilename},例如 20.0.0 版本的下载地址是:

而官方默认的 mirror 是 GitHub 的:

由于众所周知的原因,访问非常慢,所以一般都会用 Electron 的淘宝镜像,其 mirror 路径是:

如果我们选择了淘宝镜像的话,有个坑在 customDir 那里,因为不同版本的下载路径格式不一致,高版本例如 20.2.0 的下载地址是:

而有些低版本,例如 5.0.13 的下载地址是:

注意 customDir 的位置,一个带 v 一个不带 v,所以要根据具体的 electron 版本号来配置,例如:

  • 20.2.0 版本的 customDir 要写成 v{{ version }}
  • 5.0.13 版本的 customDir 要写成 {{ version }}

如果你设置了选项,但是发现不起作用,那是因为镜像配置存在优先级,从 @electron/get 的源码中可以看到:

function mirrorVar(name, options, defaultValue) {
  // 驼峰转蛇形
  const snakeName = name.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}_${b}`).toLowerCase();
  return (
    // .npmrc
    process.env[`npm_config_electron_${name.toLowerCase()}`] ||
    process.env[`NPM_CONFIG_ELECTRON_${snakeName.toUpperCase()}`] ||
    process.env[`npm_config_electron_${snakeName}`] ||
    // package.json
    process.env[`npm_package_config_electron_${name}`] ||
    process.env[`npm_package_config_electron_${snakeName.toLowerCase()}`] ||
    // env
    process.env[`ELECTRON_${snakeName.toUpperCase()}`] ||
    options[name] ||
    defaultValue
  );
}

配置优先级的顺序是:

  1. .npmrc 中的最高(自动添加 npm_config前缀放到环境变量中)
  2. package.json中的其次(自动添加 npm_package_config前缀放到环境变量中)
  3. 然后是以 ELECTRON开头的环境变量
  4. 最后才是通过命令行传进去的参数

上面的方法都是从不同镜像下载官方编译好的 Electron,假如你对 Electron 做了定制的话,可以把 Electron 的 zip 包放到指定目录下面,然后在打包的时候指定 electronZipDir:

$ electron-packager dist MyApp \
  --platform=darwin \
  --arch=x64 \
	--electronZipDir=electron-zip \
	--electron-version=20.0.0 \
	--overwrite

如果你想在 Mac 上打 Windows 包,需要安装 wine 环境,下载地址:

由于官网已经长时间不更新了,可以直接下载 GitHub 上的 Wine Devel 包进行安装,双击打开后会进入命令行,可以输入指令来运行 exe 程序:

################################################################################
#                           Wine Is Not an Emulator                            #
################################################################################

 Welcome to wine-6.23.

 In order to start a program:
   .exe: wine64 program.exe
   .msi: wine64 msiexec /i program.msi

 If you want to configure wine:
   wine64 winecfg

 To get information about app compatibility:
   appdb Program Name

make(制作)

打出可执行包之后,想制作成什么呢?这依赖于目标平台,目前官方提供了以下的 maker 来制作不同平台的产物:

  • maker-appx:生成 .appx 格式安装包,用于上架 Windows 软件市场
  • maker-deb:生成 .deb 格式安装包,用于 Debian Linux 系统
  • maker-dmg:生成 .dmg 格式安装包,用于 macOS 系统
  • maker-flatpak:生成 .flatpak 格式文件,用于 Linux 的沙箱环境
  • maker-pkg:生成 .pkg 格式文件,用于 macOS 系统
  • maker-rpm:生成 .rpm 格式文件,用于 Fedora 和 RedHat Linux 系统
  • maker-snap:生成 .snap 格式文件,用于
  • maker-squirrel:使用 Squirrel.Windows 框架生成 exe 安装文件和 nupkg 升级文件
  • maker-wix:生成 .msi 格式文件
  • maker-zip:生成 .zip 压缩包,包含生成的目标产物

官方提供的已经足够用了,覆盖了各大主流平台,很少遇到有超出这些平台外的场景。

publish(发布)

产物制作完毕,接下来就是发布环节了,Forge 也非常贴心的集成了常用的发布平台:

  • publisher-bitbucket:上传到 Bitbucket
  • publisher-electron-release-server:上传到 Electron Release Server
  • publisher-github:上传到 github
  • publisher-nucleus:上传到 nucleus 服务器
  • publisher-s3:上传到 Amazon S3
  • publisher-snapcraft:上传到 snap 市场

当然,这些平台可能不够用,例如用户想上传到阿里云 oss 或者七牛云的话,就需要自己写 publisher,也很简单,感兴趣的可参考官方文档。

plugins(插件)

目前官方提供了以下四个插件:

  • plugin-webpack:用于集成 webpack
  • plugin-electronegativity:用于检查 Electron 的安全性
  • plugin-auto-unpack-natives:自动将可执行文件放到 unpacked 目录
  • plugin-local-electron:集成本地定制的 Electron

这些插件的作用就是侵入 Electron-Forge 的构建流水线,完成自己想要的功能,其中 plugin-webpack 非常强大,把使用 webpack-dev-server 本地开发和 webpack 打包流程全部集成进来了。

hook(钩子)

hook 允许开发者在构建的阶段执行自定义的逻辑,hook 必须是一个返回 promise 的异步函数,传递给 hook 的第一个参数是 configuration 配置项,剩下的参数取决于 hook 的类型,例如:

module.exports = {
  hooks: {
    generateAssets: async (forgeConfig, platform, arch) => {
      console.log('We should generate some assets here');
    }
  }
}

常见的 hook 有:

  • generateAssets:在 start 和 package 执行之前,用于生成一些静态资源文件
  • postStart:在 start 之后,
  • prePackage:在 package 之前
  • packageAfterCopy:在 electron-packager 的 afterCopy hook 里面调用的
  • packageAfterPrune:在 electron-packager 的 afterPrune hook 里面调用
  • packageAfterExtract:在 electron-packager 的 afterExact hook 里面调用的
  • postPackage:在 package 完整之后调用
  • preMake:在 make 阶段之前调用
  • postMake:在 make 阶段结束后调用
  • readPackageJson:读取 package.json 是调用

我的看法

其实笔者也很难抉择,于是就问了 ChatGPT :说说你对 Electron 有多种打包方案的看法。它给的答案还是蛮中肯的:

作为一个 AI 语言模型,我没有个人观点和看法,但是我可以为您介绍一下 Electron 的多种打包方案。Electron 作为一个跨平台的桌面应用开发框架,有多种打包方案,如使用 Electron Forge、Electron Packager、Electron Builder 等等。每种打包方案都有其优缺点,选择合适的打包方案可以提高开发效率和打包产物的质量。

  • Electron Forge 是 Electron 官方推荐的打包方案,它提供了完善的 CLI 和插件生态,可以轻松地创建新项目、添加插件、构建和发布应用。但是,它的配置可能比较复杂,需要一定的学习成本。
  • Electron Packager 可以将应用程序打包成可执行文件,适用于快速打包应用程序,并且可以适配各种平台,但是它的打包速度较慢,且存在一些兼容性问题。
  • Electron Builder 则是综合了 Electron Forge 和 Electron Packager 的优点,提供了可视化的配置界面和丰富的打包选项,同时也支持多平台打包。它的使用较为简单,但是在某些情况下需要手动配置,比如在Windows平台上需要配置签名证书。

总之,选择合适的打包方案需要综合考虑开发需求、打包效率、打包质量等多个因素,而不是单纯地追求一种方案的短期效果。

多套打包方案不仅会让开发者感到困惑,也不利于后续团队发展,Electron 团队其实已经意识到这个问题了,估计也在内部讨论过最终把 Electron Forge 作为官方支持的打包方案。

详解 Electron 打包

其实并非 Electron Builder 不好,恰恰相反,它目前是功能最丰富、使用最多的打包方案,Electron 官方团队没有把它作为官方推荐的方案的原因,笔者猜测可能跟设计架构和维护成本有关系,因为 Electron Builder 是 all-in-one 开箱即用的,虽然小白上手速度快,但是牺牲了灵活性和便捷性,官方出的一些新特性(例如打 universal 包等)不能第一时间集成进去,而且开发者想要定制打包流程也很困难,从长远来看,Electron 团队可能会付出更多时间和精力,而 Electron Forge 是完全插件化的,修改打包流程非常简单,易于后期维护和打造插件生态。所以,笔者的结论是:短期支持 Electron Builder,长期看好 Electron Forge

转载自:https://juejin.cn/post/7250085815430430781
评论
请登录