你不知道的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," +
"__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore," +
".idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci," +
".yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log," +
"appveyor.yml,.travis.yml,circle.yml,.nyc_output"
?
export const excludedExts = "iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts"
electron.exe
我們運行的 electron.exe
可執行程序,實際上是早就已經編譯好的文件。他的功能就是載入 resources/app.asar
文件中的內容,包括入口文件的位置,也是從 app.asar
中打包的 package.json
的 main
欄位來獲取載入。
打包工具需要做的事情只是把這個 electron.exe
文件修改下圖標、作者、版本等信息即可。
總結
上面簡單的對 electron-builder
的打包過程進行了分析。通過分析,我們瞭解了:
- Electron 應用體積的分佈情況:
electron.exe
在 67.5MB 左右,electron.asar 在 250KB 左右,app.asar 則根據實際項目差別會比較大,空的項目在 2KB 左右,測試中的一個實際項目在 130MB 左右。app.asar 大的原因在於實際項目依賴上會比較多,而打包工具在打包時是需要將整個 node_modules
文件夾都打包進來的,因此體積上會大很多。
- 可執行文件是怎麼來的
通過實現一個通用的可執行程序,這個程序做的事情是將 resources/app.asar
作為項目根目錄,運行 app.asar/package.json
中 main
指定文件作為入口文件。不同的應用程序只需要重新打包好相應的 app.asar
即可。最後對這個可執行程序的圖標等信息進行修改就可以得到我們的應用程序了~
- 打包可能存在的問題
electron-builder
打包雖然幫我們把一些文件過濾掉不進行打包,但是我們的項目源碼是沒有經過任何處理的被打包了進去。
【參考資料】
- electron-userland/electron-builder
- electron-userland/electron-packager
推薦閱讀: