Electron Forge由官方推薦和維護轻专,結構優(yōu)美,原本以為用起來會很順利察蹲,沒想到還是有坑请垛。
打包方案:
- MAC:
@electron-forge/maker-dmg
DMG 包供用戶安裝 - WINDOWS:
-
框架默認,但它安裝時不能選安裝路徑@electron-forge/maker-squirrel
-
官方推薦之一洽议,打出 MSI 鏡像包宗收。最大的坑就在這里,它的自動更新幾乎不可用绞铃,issues 也沒人回復镜雨,白白花費了很多時間@electron-forge/maker-wix
-
@felixrieseberg/electron-forge-maker-nsis
最后還是換回了electron-builder的 NSIS 方案
-
以下記錄使用的詳細配置
// forge.config.ts
module.exports = {
packagerConfig: {
name: 'APP_NAME',
// 不加擴展名,MAC 會自動查找 .icns儿捧、WIN 使用 .ico
icon: './icon/icon',
// 最終包不使用的代碼,不要打入 asar
ignore: [/\.yarn/, /src\/render/],
appBundleId: `com.xxx.xxx`,
appCopyright: `Copyright ? 2023 ${packageJson.author}`
},
...
packageJson 中的 dependencies 引用也會被打入 asar 中挑宠,非主進程使用的包不要放入 dependencies 可以有效減小包大小
@electron-forge/maker-dmg
// forge.config.ts
const RELEASE_APP_DIR = path.join(__dirname, `./out/${APP_NAME}-${process.platform}-${ARCH}/${APP_NAME}.app`)
makers: [
{
name: '@electron-forge/maker-dmg',
config: {
icon: './icon/icon.icns',
background: './icon/background.png',
format: 'ULFO',
contents: [
{ x: 192, y: 244, type: 'file', path: RELEASE_APP_DIR },
{ x: 448, y: 244, type: 'link', path: '/Applications' },
{ x: 192, y: 700, type: 'position', path: '.background' },
{ x: 292, y: 700, type: 'position', path: '.VolumeIcon.icns' },
{ x: 392, y: 700, type: 'position', path: '.DS_Store' },
{ x: 492, y: 700, type: 'position', path: '.Trashes' }
]
}
},
@electron-forge/maker-squirrel
for exe
@electron-forge/maker-squirrel
// forge.config.ts
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
authors: packageJson.author,
description: packageJson.description,
copyright: packageJson.author,
iconUrl: 'https://www.xxxx.com/favicon.ico', // http url only
setupIcon: path.join(__dirname, `icon/icon.ico`),
skipUpdateIcon: true
// certificateFile: './cert.pfx',
// certificatePassword: process.env.CERTIFICATE_PASSWORD
}
},
@electron-forge/maker-wix
for msi
@electron-forge/maker-wix
// forge.config.ts
makers: [
{
name: '@electron-forge/maker-wix',
config: {
language: '2052" Codepage="utf-8',
cultures: 'zh-CN,en-US',
icon: path.join(__dirname, `./icon/icon.ico`),
shortName: 'APP_EN_NAME',
manufacturer: packageJson.author,
appUserModelId: `com.xxx.xxx`,
upgradeCode: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
features: {
autoUpdate: true, // 無法使用
autoLaunch: false
},
ui: {
template: wixUiTemplate
}
}
const wixUiTemplate = wixUiTemplate = ` <UI Id="UserInterface">
<Property Id="WixUI_Mode" Value="InstallDir" />
<TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
<TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
<TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
<Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
<DialogRef Id="ErrorDlg" />
<DialogRef Id="FatalError" />
<DialogRef Id="FilesInUse" />
<DialogRef Id="MsiRMFilesInUse" />
<DialogRef Id="PrepareDlg" />
<DialogRef Id="ProgressDlg" />
<DialogRef Id="ResumeDlg" />
<DialogRef Id="UserExit" />
<Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">NOT Installed</Publish>
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">Installed AND PATCH</Publish>
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1">NOT Installed</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed AND NOT PATCH</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">Installed AND PATCH</Publish>
<Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish>
</UI>
<Property Id="WIXUI_INSTALLDIR" Value="APPLICATIONROOTDIRECTORY" />
<UIRef Id="WixUI_Common" />`
@felixrieseberg/electron-forge-maker-nsis
for exe
// forge.config.ts
makers: [
{
name: '@felixrieseberg/electron-forge-maker-nsis',
config: {
// codesigning: {
// certificateFile?: string;
// certificatePassword?: string;
// },
updater: {}
}
// package.json
"build": {
"appId": "com.xxx.xxx",
"productName": "應用名稱",
"nsis": {
"oneClick": false,
"allowElevation": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"allowToChangeInstallationDirectory": true,
"perMachine": true,
"deleteAppDataOnUninstall": true,
"installerIcon": "icon/icon.ico",
"installerHeaderIcon": "icon/icon.ico",
"guid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
"publish": {
"provider": "generic",
"url": "",
"channel": "latest"
}
},
MAC 簽名與公證
坑 2:根據官網配置菲盾,始終無法成功公證,最后使用codesign
與xcrun notarytool
指令手動簽名與公證
// forge.config.ts
hooks: {
preMake: async () => {
if (process.platform == 'darwin') {
await makeMacProfile()
await signMac('/path/to/xxx.app', 'APP_NAME')
}
},
postMake: async () => {
if (process.platform == 'darwin') {
await notarizeMac('/path/to/xxx.dmg')
}
}
}
簽名
將證書存入鑰匙串參考此文 “代碼簽名”部分 1各淀、2
const NEED_SIGN_FW: string[] = [
'Contents/Frameworks/Electron" "Framework.framework/Versions/A/Libraries/libEGL.dylib',
'Contents/Frameworks/Electron" "Framework.framework/Versions/A/Libraries/libffmpeg.dylib',
'Contents/Frameworks/Electron" "Framework.framework/Versions/A/Libraries/libGLESv2.dylib',
'Contents/Frameworks/Electron" "Framework.framework/Versions/A/Libraries/libvk_swiftshader.dylib',
'Contents/Frameworks/Squirrel.framework/Versions/A/Resources/ShipIt'
]
const ETM_DIR = path.join(__dirname, `./entitlements.mac.plist`)
function signMac(appPath: string, APP_NAME: string) {
const caName = 'Developer ID Application: CompanyName (xxxxxxxx)'
const needSignDirs: string[] = [appPath]
NEED_SIGN_FW.forEach((subdir) => {
needSignDirs.push(`${appPath}/${subdir}`)
})
return execPromise(`security find-identity -v -p codesigning`)
.then(({ stdout, stderr }) => {
return stdout.includes(caName)
})
.then((hasCertificate) => {
if (!hasCertificate) throw new Error('鑰匙串中沒有需要的證書')
return execPromise(`xattr -cr ${appPath}`)
})
.then(() => {
const signList: Promise<{ stdout: string; stderr: string }>[] = []
needSignDirs.forEach((dir) => {
signList.push(
execPromise(
`codesign --force --deep --timestamp --options runtime --entitlements ${ETM_DIR} --sign "${caName}" --verbose=2 -v ${dir}`
)
)
})
return Promise.all(signList)
})
.then((resList) => {
console.info('【簽名完成】', resList)
return resList
})
// 查看應用的簽名: codesign -d -v -r - /path/to/xxx.app
}
function execPromise(command: string): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
exec(command, (err, stdout, stderr) => {
if (err) return reject(err)
else resolve({ stdout, stderr })
})
})
}
<!-- entitlements.mac.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
公證
export function makeMacProfile() {
const conf = `--apple-id "${process.env.NOTARY_APP_ID}" --team-id "${process.env.NOTARY_TEAM_ID}" --password "${process.env.NOTARY_PASSWORD}"`
return execPromise(`xcrun notarytool store-credentials HAHAHA ${conf}`)
}
function notarizeMac(dmgPath: string) {
console.info(`【開始公證】: ${dmgPath}`)
return execPromise(`xcrun notarytool submit ${dmgPath} --keychain-profile "HAHAHA" --wait`).then(
({ stdout, stderr }) => {
const res = stdout || stderr
if (res.includes('Invalid')) {
console.info('【公證失敗】', res)
// @ts-ignore
return logNotarytoolErr(res.match(/id: .{36}/)[0]?.substr(4))
}
console.info('【公證完成】', stdout, stderr)
return res
}
)
// xcrun notarytool log "96e8072f-4a0c-443d-b2c3-076b39376817" --keychain-profile "OneApps"
}
function logNotarytoolErr(id: string) {
return execPromise(`xcrun notarytool log "${id}" --keychain-profile "OneApps"`).then(({ stdout, stderr }) => {
const res = stdout || stderr
console.info('【公證失敗原因】', res)
return Promise.reject(res)
})
}
自動更新
MAC 自更新依賴@electron-forge/maker-zip
// forge.config.ts
makers: [
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
config: {
macUpdateManifestBaseUrl: 'https://XX.oss.aliyuncs.com/apps',
macUpdateReleaseNotes: '添加了自動更新功能'
}
}
打包后出現RELEASES.json
懒鉴、XXXX-${platform}-${arch}-${version}.zip
,后續(xù)自動更新依賴此文件。DMG 包供第一次安裝使用临谱。
import { app, autoUpdater, dialog } from 'electron'
autoUpdater.on('error', (message) => {
console.error('自動更新', message)
})
autoUpdater.on('update-available', () => {
console.info('自動更新 有新版本')
})
autoUpdater.on('update-not-available', () => {
console.info('自動更新 沒有新版本')
})
autoUpdater.on('update-downloaded', () => {
console.info('自動更新 新版本下載完成')
autoUpdater.quitAndInstall()
})
autoUpdater.setFeedURL({ url: 'https://XX.oss.aliyuncs.com/apps/RELEASES.json', serverType: 'json' })
autoUpdater.checkForUpdates()
WIN 自更新依賴electron-updater
隸屬于 electron-builder
打包后出現latest.yml
璃俗、XXX Setup ${version}.exe.blockmap
、XXX Setup ${version}.exe
import { autoUpdater as winAutoUpdater } from 'electron-updater'
winAutoUpdater.on('error', (message) => {
console.error('自動更新', message)
})
winAutoUpdater.on('update-available', () => {
console.info('自動更新 有新版本')
})
winAutoUpdater.on('update-not-available', () => {
console.info('自動更新 沒有新版本')
})
winAutoUpdater.on('update-downloaded', () => {
console.info('自動更新 新版本下載完成')
winAutoUpdater.quitAndInstall()
})
winAutoUpdater.setFeedURL('https://XX.oss.aliyuncs.com/apps')
winAutoUpdater.checkForUpdates()