你不知道的Electron (二)-了解Electron打包
本文作者:IMWeb laynechen
我们知道 Electron 提供了一个类似浏览器,但有更多权限的环境来运行我们的网页,那么 Electron 是怎么做到将我们的网页代码打包成一个可执行程序的呢?
这篇文章主要介绍如何打包 Electron 应用,以及分析
electron-builder
是如何对我们的应用进行打包的。
如何打包
Electron 目前有两种打包工具: electron-userland/electron-builder 和 electron-userland/electron-packager 。
使用 electron-builder 打包
安装依赖:
yarn add electron-builder --dev
npm i electron-builder --save-dev
打包:
-
在项目的
package.json
文件中定义name
、description
、version
和author
信息。 -
在项目的
package.json
文件中定义build
字段:
"build": {
"appId": "your.id",
"mac": {
"category": "your.app.category.type"
}
( 全部选项 )
-
添加
scripts
到package.json
中
"scripts": {
"pack": "electron-builder --dir",
"dist": "electron-builder"
}
- 打包
生成 package 目录但是没有打包为一个文件
npm run pack
生成一个 exe 或者 dmg 文件
npm run dist
- 指定平台和架构
# windows 64bit
electron-builder --win --x64
# windows and mac 32bit
electron-builder --win --mac --ia32
详细参数: Command Line Interface (CLI)
使用 electron-packager 打包
安装依赖:
npm i electron-packager --save-dev
打包:
electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> [optional flags...]
最简单的就是直接运行
electron-packager .
打包。
默认情况下,
appname
为 当前项目下的
package.json
文件中的
productName
或者
name
字段的值;
platform
和
arch
则与主机一致,在
Windows 64位
下打包就是 Windows 64 位的版本。
具体每个字段的值可以看 electron-packager/usage.txt
注:
OS X 下打包 Windows 的应用需要安装
Wine
才行,
electron-packager
需要使用
node-rcedit
编辑
Electron.exe
文件。
Building an Electron app for the Windows target platform requires editing the Electron.exe file. Currently, Electron Packager uses node-rcedit to accomplish this. A Windows executable is bundled in that Node package and needs to be run in order for this functionality to work, so on non-Windows host platforms, Wine 1.6 or later needs to be installed. On OS X, it is installable via Homebrew.
electron-builder 打包分析
文件大小分析
因为要达到跨平台的目的,每个 Electron 应用都包含了整个 V8 引擎和 Chromium 内核,以致于一个空的 Electron 项目,使用
electron-builder --dir
打包后没有压缩的项目文件夹,大小也已经到了 121.1 MB。如果使用
electron-builder
进行打包,安装程序的大小为 36MB,这个大小是可以接受。
但是上面是一个空的项目,那么一个实际的项目打包之后有多大呢?一个安装了 30+ 个依赖的项目,未生成安装包前,项目文件夹的大小是 230+ MB,生成安装程序后是 56.3 MB,生成安装程序之后的大小还是可以接受的,比空项目也只大了 20MB 左右。
但其实大了 20MB 也是不太科学的,本身项目并没有什么大的资源文件,如果只是代码的话不打包的大小应该也只有 10MB 以下。那么是什么让项目的大小大了接近 100MB?
打包后的项目结构
我们看下打包后的项目结构 (electron-builder --dir)
加上
--dir
参数,不将整个应用打包成安装文件,来查看一个应用的目录结构:
.
├── locales
│ ├── am.pak
│ └── ... 一堆的 pak 文件
├── resources
│ ├── app.asar (空项目只有 2KB,一个实际项目有 130MB+)
│ └── electron.asar (大小在 250KB 左右)
├── electron.exe (67.5MB)
└── ...
这里忽略了很多的文件,我们主要看
electron.exe
文件和
resources
文件夹。因此实际项目和空项目多的东西应该就是在 app.asar 上面了。
app.asar
在
dist/win-unpacked/resources/
下生成了
app.asar
文件,这是一个用 asar 压缩后的文件。我们可以解压看下里面是什么:
# 安装 asar
npm install -g asar
# 解压到 ./app 文件夹下
asar extarct app.asar ./app
解压目录如下:
.
├── CHANGELOG.md
├── README.md
├── core
├── electron
├── icon
├── node_modules
├── package.json
├── test
├── view
└── webpack.config.js
看到这个目录会不会很熟悉?~实际上是把我们的整个项目的内容都打包进来了。当然对
node_modules
文件夹有特殊处理,这里只打包了
production dependencies
,即在
package.json
的
dependencies
中定义的依赖。
空的项目和一个实际项目的大小差距就出在依赖这里了。
electron.asar
我们再来看下 electron.asar 打包了什么东西:
asar extract electron.asar ./electron
├── browser
│ ├── api
│ ├── chrome-extension.js
│ ├── desktop-capturer.js
│ ├── guest-view-manager.js
│ ├── guest-window-manager.js
│ ├── init.js
│ ├── objects-registry.js
│ └── rpc-server.js
├── common
│ ├── api
│ ├── atom-binding-setup.js
│ ├── init.js
│ ├── parse-features-string.js
│ └── reset-search-paths.js
├── renderer
│ ├── api
│ ├── chrome-api.js
│ ├── content-scripts-injector.js
│ ├── extensions
│ ├── init.js
│ ├── inspector.js
│ ├── override.js
│ ├── web-view
│ └── window-setup.js
└── worker
└── init.js
Electron 相关的源代码被压缩到了 electron.asar 文件中。
打包分析
electron-builder 打包时输出的信息
打包的时候我们可以看到 控制台输出了如下信息:
• electron-builder version=20.15.1
• loaded configuration file=package.json ("build" field)
• writing effective config file=dist/electron-builder-effective-config.yaml
• rebuilding native production dependencies platform=win32 arch=x64
• packaging platform=win32 arch=x64 electron=1.8.7 appOutDir=dist/win-unpacked
如果还要打包程序的话,还有以下打印信息:
• building target=nsis file=dist/xxx.exe archs=x64 oneClick=true
• building block map blockMapFile=dist/xxx.exe.blockmap
大致可以知道打包主要做了以下事情:
- 重新安装依赖
- 打包
从这里知道的信息还是比较有限,所以还是得看下从输入
electron-builder
到生成安装程序中间经历了什么。
"bin"
我们从安装的
electron-builder
依赖的
packager.json
文件定义的 "bin" 字段信息可以看到它执行了
./out/cli/cli.js
这个文件。
"bin": {
"electron-builder": "./out/cli/cli.js",
"build": "./out/cli/cli.js",
"install-app-deps": "./out/cli/install-app-deps.js"
}
./out
目录下的文件是已经经过
babel
转译之后的,我们可以去下载
electron-builder
源码来分析。
"packages/electron-builder/src/cli/cli.ts"
从源码中我们不难定位到
packages/electron-builder/src/cli/cli.ts
这个文件就是命令的入口文件。从入口文件往下分析:
-
packages/electron-builder/src/builder.ts
cli.ts
文件中 import 了上一层目录的
builder.ts
文件导出的
build
方法。
build
方法中创建了一个
Packager
对象,然后又调用了
packages/electron-builder-lib
导出的
build
方法。
cli.ts
中的
build
方法:
export function build(rawOptions?: CliOptions): Promise<Array<string>> {
const buildOptions = normalizeOptions(rawOptions || {})
const packager = new Packager(buildOptions)
let electronDownloader: any = null
packager.electronDownloader = options => {
if (electronDownloader == null) {
electronDownloader = BluebirdPromise.promisify(require("electron-download-tf"))
return electronDownloader(options)
return _build(buildOptions, packager)
}
-
packages/electron-builder-lib/index.ts
export async function build(options: PackagerOptions & PublishOptions, packager: Packager = new Packager(options)): Promise<Array<string>> {
return await executeFinally(packager.build().then(() => Array.from(artifactPaths)), errorOccurred => {
}
build
方法中调用了
packager
的
build
方法。
-
packages/electron-builder-lib/packager.ts
build 方法对一些信息进行处理后又调用了
_build
方法:
async build(): Promise<BuildResult> {
return await this._build(configuration, this._metadata, this._devMetadata)
}
_build
方法继续调用了私有方法
doBuild
:
async _build(configuration: Configuration, metadata: Metadata, devMetadata: Metadata | null, repositoryInfo?: SourceRepositoryInfo): Promise<BuildResult> {
return {
outDir,
platformToTargets: await executeFinally(this.doBuild(outDir), async () => {
if (this.debugLogger.enabled) {
await this.debugLogger.save(path.join(outDir, "electron-builder-debug.yml"))
await this.tempDirManager.cleanup()
}
doBuild
中负责了要创建哪些平台的安装包、以及如何去打包:
private async doBuild(outDir: string): Promise<Map<Platform, Map<string, Target>>> {
for (const [platform, archToType] of this.options.targets!) {
const packager = this.createHelper(platform)
for (const [arch, targetNames] of computeArchToTargetNamesMap(archToType, packager.platformSpecificBuildOptions, platform)) {
await this.installAppDependencies(platform, arch)
const targetList = createTargets(nameToTarget, targetNames.length === 0 ? packager.defaultTarget : targetNames, outDir, packager)
await createOutDirIfNeed(targetList, createdOutDirs)
await packager.pack(outDir, arch, targetList, taskManager)
return platformToTarget
}
createHelper
实际上就是根据平台去创建相对应的
Packager
对象,另外根据不同架构去安装应用的依赖,最后调用
pack
方法打包。
后面分析下打包 Windows 平台的
WinPackager
WinPackager
实际上
WinPackager
是继承于
PlatformPackager
类,
pack
方法也是在这个父类里面定义的:
async pack(outDir: string, arch: Arch, targets: Array<Target>, taskManager: AsyncTaskManager): Promise<any> {
const appOutDir = this.computeAppOutDir(outDir, arch)
await this.doPack(outDir, appOutDir, this.platform.nodeName, arch, this.platformSpecificBuildOptions, targets)
this.packageInDistributableFormat(appOutDir, arch, targets, taskManager)
}
这个方法里面又是调用了另一个方法
doPack
:
protected async doPack(outDir: string, appOutDir: string, platformName: string, arch: Arch, platformSpecificBuildOptions: DC, targets: Array<Target>) {
const computeParsedPatterns = (patterns: Array<FileMatcher> | null) => {
if (patterns != null) {
for (const pattern of patterns) {
pattern.computeParsedPatterns(excludePatterns, this.info.projectDir)
const getFileMatchersOptions: GetFileMatchersOptions = {
macroExpander,
customBuildOptions: platformSpecificBuildOptions,
outDir,
const extraResourceMatchers = this.getExtraFileMatchers(true, appOutDir, getFileMatchersOptions)
computeParsedPatterns(extraResourceMatchers)
const extraFileMatchers = this.getExtraFileMatchers(false, appOutDir, getFileMatchersOptions)
computeParsedPatterns(extraFileMatchers)
const packContext: AfterPackContext = {
appOutDir, outDir, arch, targets,
packager: this,
electronPlatformName: platformName,
const taskManager = new AsyncTaskManager(this.info.cancellationToken)
const asarOptions = await this.computeAsarOptions(platformSpecificBuildOptions)
const resourcesPath = this.platform === Platform.MAC ? path.join(appOutDir, framework.distMacOsAppName, "Contents", "Resources") : (isElectronBased(framework) ? path.join(appOutDir, "resources") : appOutDir)
this.copyAppFiles(taskManager, asarOptions, resourcesPath, path.join(resourcesPath, "app"), outDir, platformSpecificBuildOptions, excludePatterns, macroExpander)
await taskManager.awaitTasks()
const beforeCopyExtraFiles = this.info.framework.beforeCopyExtraFiles
if (beforeCopyExtraFiles != null) {
await beforeCopyExtraFiles(this, appOutDir, asarOptions == null ? null : await computeData(resourcesPath, asarOptions.externalAllowed ? {externalAllowed: true} : null))
await BluebirdPromise.each([extraResourceMatchers, extraFileMatchers], it => copyFiles(it))
await this.info.afterPack(packContext)
await this.sanityCheckPackage(appOutDir, asarOptions != null)
await this.signApp(packContext)
await this.info.afterSign(packContext)
}
这里我们知道了,
app.asar
文件就是在这个方法中生成的。
在打包的时候,是通过
Matcher
来实现选择性的打包哪些文件。从
FileMatcher
中可以看到相关定义:
export const excludedNames = ".git,.hg,.svn,CVS,RCS,SCCS," +