详解 Electron 打包
Electron 可以使用多种工具进行打包,新手很容易眼花缭乱,不知道如何选择。其实 Electron 打包分为两大阵营:
- 社区提供的 Electron Builder:这是目前最流行的打包工具,配置简单,开箱即用,覆盖 Windows、macOS 和 Linux 平台,并支持多种打包格式,集成了自动更新和代码签名的功能。
- 官方提供的 Electron Packager 和 Electron Forge:这两个经常是配合在一起使用,因为 Electron Packager 只是将应用打包成可执行程序,而 Electron Forge 会继续将其打包成安装程序。
这两大阵营之间彼此互相竞争,背地里还有一些小动作,且听我慢慢道来。首先是官方教程中的打包章节只字未提 Electron Builder,直接上来就推荐 Electron Forge:
当你按照文档安装了 @electron-forge/cli
,并使用 import 脚本安装依赖之后,它会偷偷的将你项目中集成的 electorn-builder
给删掉,理由很简单,就一句话:
provides mostly equivalent functionality
意思是 Electron Forge 已经提供了大部分相同的功能了,就不需要 Electron Builder 了,有种一山不容二虎,有你没我,非此即彼的意思。
当然,Electron Builder 也不甘示弱啊,既然你不仁就别怪我不义,直接在 GitHub 仓库把 Electron Forge 的功能集成进去了,告诉开发者别用 forge 了,我 Electron Builder 也可以打 forge 的包。
两大阵营针锋相对,逼着开发者站队,这也太难了!不过别慌,正所谓「本领在手,说走就走」,只要掌握了它们的打包原理,可以随时切换,谁好用就用谁,接下来就为大家详细讲解它们的使用方法和底层原理。
社区阵营
Electron Builder 不愧是 Electron 界的当红炸子鸡,真的是 all-in-one 的打包工具,大部分的打包逻辑都是自己写的,要知道打包流程是非常复杂的,Mac、Windows 和 Linux 的包格式完全不一样,涉及到方方面面的东西全给封装掉了,真的践行了:把复杂留给自己,简单交给别人。开发者只需在 package.json 中添加 build 字段,然后增加一些配置即可快速打包,下面是常用的顶级配置项:
appId
:应用 idproductName
:应用名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.json
和 node_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 把它们抽象为三个阶段:
- package(打包)
- make(制作)
- publish(发布)
在这三个环节当中,如果开发者想深度介入构建流程,也提供了两种参与方式:
- 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
中:
所以当在 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 的路径,例如:
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
);
}
配置优先级的顺序是:
.npmrc
中的最高(自动添加npm_config
前缀放到环境变量中)package.json
中的其次(自动添加npm_package_config
前缀放到环境变量中)- 然后是以
ELECTRON
开头的环境变量 - 最后才是通过命令行传进去的参数
上面的方法都是从不同镜像下载官方编译好的 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
:上传到 Bitbucketpublisher-electron-release-server
:上传到 Electron Release Serverpublisher-github
:上传到 githubpublisher-nucleus
:上传到 nucleus 服务器publisher-s3
:上传到 Amazon S3publisher-snapcraft
:上传到 snap 市场
当然,这些平台可能不够用,例如用户想上传到阿里云 oss 或者七牛云的话,就需要自己写 publisher,也很简单,感兴趣的可参考官方文档。
plugins(插件)
目前官方提供了以下四个插件:
plugin-webpack
:用于集成 webpackplugin-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 Builder 不好,恰恰相反,它目前是功能最丰富、使用最多的打包方案,Electron 官方团队没有把它作为官方推荐的方案的原因,笔者猜测可能跟设计架构和维护成本有关系,因为 Electron Builder 是 all-in-one 开箱即用的,虽然小白上手速度快,但是牺牲了灵活性和便捷性,官方出的一些新特性(例如打 universal 包等)不能第一时间集成进去,而且开发者想要定制打包流程也很困难,从长远来看,Electron 团队可能会付出更多时间和精力,而 Electron Forge 是完全插件化的,修改打包流程非常简单,易于后期维护和打造插件生态。所以,笔者的结论是:短期支持 Electron Builder,长期看好 Electron Forge。
转载自:https://juejin.cn/post/7250085815430430781