如何在 App Store 釋出 Electron 應用程式與 DMG 安裝包


摘要

本文探討了如何在 App Store 中成功釋出 Electron 應用程式與 DMG 安裝包,並提供實用策略以提升通過率。 歸納要點:

  • 深入分析 Apple 的 Electron 應用程式審核標準,了解近期的趨勢變化及如何撰寫更有效的審核說明文件。
  • 探討硬體加速與效能最佳化技巧,包括使用 Metal 提升圖形渲染速度,以及 V8 引擎的調校和資源預載技術。
  • 強調 DMG 安裝包的安全性,涵蓋程式碼簽章、Gatekeeper 機制,以及如何結合 Notarization 確保應用程式可靠性。
掌握這些核心要點將幫助開發者更有效地面對 Apple 的審核流程,確保其應用程式獲得批准。


macOS DevHelper v.2 應用程式打包與DMG檔案製作:從Electron開發到App Store外分發及安全性強化

在這篇文章中,我將分享我在分發我的 DevHelper v.2 應用程式於 macOS 的經驗。首先要提到的是,該應用程式是使用 Electron 建置的,但我使用了一個現成的範本,即 electron-react-boilerplate。我強烈建議每個新的 Electron 專案都以此作為起點,因為它內建了許多非常實用的功能,例如:整合的渲染程序與 React、完整的 Webpack 設定、主程序與渲染程序之間 IPC 通訊範例、即時支援 TypeScript、方便跨平台二進位制檔案分發的 electron-builder 設定等等。

我們來看看如何建立 DMG 檔案,以便將我們的應用程式分發至 App Store 之外,或許可以直接上傳到你的網站並提供下載連結,這是一種簡易的分發方式。對於開發者而言,DMG 打包不僅是一項基本技能,更是確保最佳使用者體驗的重要步驟。在當今環境中,加強應用程式安全性也是至關重要的一環。因此,在打包過程中考慮 Notarization(備註)過程中的問題也十分必要。例如,我們需要處理可能出現的錯誤碼,如 `errSecInternalComponent` 等,以及針對不同版本 macOS 系統調整 Notarization 策略,以提升成功率和部署速度。

總之,不僅要專注於 DMG 的製作流程,也要著重於如何把自動化流程融入 CI/CD 系統中,比如利用 fastlane 或其他工具來實現 DMG 的完全自動化打包、程式碼簽名和 Notarization。這樣一來,即使是在高度敏感資料處理上,我們也能夠有效地加強應用程式安全性,並解決反向工程帶來的挑戰。同時,要選擇合適的硬體安全模組 (HSM) 解決方案以及混淆技術,以平衡效能與保護需求。

多虧了 electron-react-boilerplate,這個過程變得相當簡單,我們唯一需要做的就是在 Apple Developer 網站上生成所有所需的專案,包括:Bundle Identifier 和 App Specific Password。接下來,我們要將這些值匯出為系統環境變數,因此我會在我的 .zshrc 檔案中進行匯出。在主資料夾中,我將執行以下命令:nano .zshrc,並新增以下內容:

export APPLE_ID="[email protected]" export APPLE_APP_SPECIFIC_PASSWORD="the-app-specific-password-we-generated" export APPLE_TEAM_ID="MY-APPLE-TEAM-ID"

zsh/bash 環境設定與 npm 套件打包最佳化實戰

接著,我們執行 ctrl+X 並按下 Y 來儲存檔案。接下來,我們需要載入這個檔案,因此輸入 source .zshrc,然後就完成了。為了測試一切是否正常運作,我們可以嘗試輸入 echo ¥APPLE_ID,這樣應該會顯示出我們的 Apple ID 電子郵件。如果沒有顯示,請重新啟動終端機,再次嘗試 echo 某些變數。如果仍然不行,就要重新檢查整個設定,可能在某處有打錯字,或是需要使用 bash profile 而不是 .zshrc。

接下來,我們需要檢視 packages.json 檔案,其中所有配置應該都是預設好的,但現在也許是更新以下屬性的好時機:
- name - 這是你的應用程式名稱
- version - 確保它是一個字串,而且有三個以點分隔的版本號(例如 version: ′1.0.0′)
- author - 在 author 物件中,你應該包含 name、email 和 url。

有了這些設定,我們就準備好了,只需執行 npm run package,在幾分鐘內你應該能在 release/build 中找到你的應用程式、dmg 和 zip 檔案。

**一、關於 Shell 設定檔與環境變數永續性的進階探討:** 原步驟中提及 `source .zshrc` 僅對目前終端機視窗生效。對於頂尖專家而言,更需了解如何確保環境變數的永續性,不因終端機關閉而失效。這涉及不同 Shell(如 zsh 和 bash)的設定檔優先順序和作用範圍。例如,在 zsh 中 ` ̄/.zshenv` 會比 `.zshrc` 更早載入,適用於所有 zsh 終端機視窗;而在 bash 中 ` ̄/.bashrc` 則扮演同樣角色。系統級的環境變數設定(例如透過 `sudo` 修改 `/etc/profile` 或其他系統級設定檔)具有最高優先順序,因此對於複雜的環境變數設定,需要根據需求選擇合適的設定檔,以避免衝突。同時,也建議考慮使用像 `direnv` 或 `dotenv` 等工具來管理專案特定的環境變數,以提高可移植性和安全性。

**二、npm 包裝流程的最佳化與 CI/CD 整合:** 原步驟提到只需執行 `npm run package` 即可產生應用程式,但對專家而言,此乃基礎步驟。在進階場景中,需要考慮最佳化打包流程,例如利用 `webpack` 或 `Parcel` 等工具進行程式碼壓縮和最佳化,以減少應用大小並提升效能;使用 `rollup.js` 等模組打包工具針對不同環境(開發、測試、生產)生成相異版本;以及整合 CI/CD 流程,自動化打包、測試和部署,例如利用 GitHub Actions、GitLab CI/CD 或 Jenkins 建立自動化建置流水線,把打包好的 dmg 和 zip 檔案部署至指定位置。這將顯著提升開發效率與產品品質,同時符合現代軟體開發最佳實踐。

現在進入最艱難的部分,那就是將我們的應用程式發布到 App Store。我原以為只需修改一些在 packages.json 中的 electron-builder 配置就能輕鬆完成,實際情況卻並非如此。首先讓我們來看看所需的一切:Mac Transporter 應用程式(免費)、付費的 Apple 開發者帳戶(99 美元)、Apple 開發者憑證、Apple 發行憑證、Mac 安裝程式分發、Apple 應用包識別符號、Mac 開發配置檔、Mac 分發配置檔、entitlements.mas.inherit.plist、entitlements.mas.plist、新應用於 App Store Connect 和應用圖示。

如果你在這一切中感到困難,請記住,我花了一整個星期才讓它運作起來,而且我還沒有將應用程式提交給 App Store 審核,但至少它已成功上傳。之前,每當我透過 Transporter 應用程式上傳時,它總是出現錯誤,所以務必仔細閱讀這些內容,不要漏掉任何細節(在我研究的過程中,我發現有些人因此卡了幾個月,因此喜歡這篇文章並關注我或許也無妨)。

首先我們來談談 Apple 的相關內容,第一步是生成一個應用程式包識別碼(App Bundle Identifier)。為此,只需進入 Apple 開發者網站的 Identifiers 區域(點選這裡)並建立一個。接下來,我們將重點放在證書上。要建立任何型別的 Apple 證書,我們需要使用 Mac 上的鑰匙串訪問工具來生成 .certSigningRequest 檔案。因此,開啟鑰匙串訪問,在頂部的鑰匙串訪問選單中選擇「證書助理」->「向證書授權機構請求證書...」。


在「使用者電子郵件地址」欄位中輸入您用於登入 Apple 開發者帳戶的電子郵件,然後在「通用名稱」欄位中輸入您的姓名。在「請求為」欄位選擇「儲存到磁碟」,然後點選繼續。


Xcode 15 證書管理與 CI/CD 自動化最佳實踐

現在將此檔案儲存到某個地方,我通常會放在倉庫的根目錄中,並在完成後刪除它。接下來,我們需要前往 Apple Developer 網站的證書部分,並逐一建立以下型別的證書:Apple Developer、Apple Distribution 和 Mac Installer Distribution。建立這些證書時,它會要求你上傳我們用 Keychain Access 建立的檔案,因此對於這三個證書,都使用相同的檔案。在每次建立後下載 .cert 檔案,而我目前擁有如下三個檔案:assets/AppleDevelopment.cer、assets/AppleDistribution.cer 和 assets/MacInstallerDistribution.cer。由於我正在使用 electron-react-boilerplate,因此這個 assets 資料夾是純粹 Electron 專案中的構建資料夾,所以我們已經在 packages.json 中更改了 buildResources 的路徑以指向這個資料夾(我沒有變更,是 electron-react-boilerplate 團隊所做的,我也沒有進行修改)。

**深入探討 Xcode 15 及其對證書管理的影響:** 原段落描述的證書建立過程適用於較舊的 Xcode 版本。Xcode 15 引入了一些自動化和簡化的證書管理機制,例如更完善的自動簽名功能,以及對多種簽名配置檔案的更佳支援。頂尖專家需要了解如何在 Xcode 15 中有效利用這些新功能,例如配置自動化簽名以減少手動建立 `.cer` 檔案的需求,並針對不同目標平台(macOS、iOS、watchOS 等)自動選擇正確簽名配置檔案,以減少錯誤和提升效率。

**利用 Apple 的自動化工具提升 CI/CD 流程:** 當前描述著重於手動建立和管理證書。對於專業開發團隊,尤其是在持續整合/持續部署 (CI/CD) 流程中,手動操作效率低且容易出錯。頂尖專家應了解如何利用 Apple 提供的自動化工具,例如 fastlane 或其他 CI/CD 平台相關外掛,將證書建立、程式碼簽名和應用程式部署整合到自動化流程中。同時,需要針對不同環境(開發、測試、生產)配置不同證書與配置檔案,並確保自動化流程能正確處理這些差異。

macOS 應用程式開發:證書與描述檔安裝及 entitlements.plist 設定

一旦安裝完成,我們需要進行證書的安裝。開啟「鑰匙圈存取」,在左側面板的「預設鑰匙圈」區域中選擇「登入」,然後雙擊每個證書以進行安裝。接下來,讓我們從 Apple 開發者網站的配置檔案區域生成配置描述檔。在那裡,你需要建立以下型別的配置描述檔:macOS 應用程式開發和 Mac App Store Connect。生成後,請下載每一個並將它們放入我們庫中的某個資料夾,在我的情況下,它們是這樣安排的:assets/profiles/AppleDevelopment.provisionprofile 和 assets/profiles/MacAppStore.provisionprofile。

我不確定是否有必要安裝這些配置描述檔,但為了確保,可以雙擊兩者。如果出現錯誤,例如安裝失敗,那麼我想這也沒關係,因為我們要構建用於 App Store 提交的應用程式具有特殊型別的簽署,需要在 App Store 重新簽署,以便從那裡下載時能正常運作。現在讓我們建立兩個 entitlements.plist 檔案,在我的情況下,我將它們放在這裡:

資產/許可權/entitlements.mas.inherit.plist 資產/許可權/entitlements.mas.plist。我們來看看 entitlements.mas.inherit.plist,這個您可以直接複製並貼上,無需更改任何內容:

          com.apple.security.app-sandbox          com.apple.security.inherit         

這是 entitlements.mas.plist:

          com.apple.security.app-sandbox          com.apple.security.application-groups            APPLE-TEAM-ID.com.mydomain.myapp                   

macOS App Store 上架:Electron 應用程式打包與設定

在這裡,APPLE-TEAM-ID是您的Apple團隊識別碼,您可以在Apple Developer首頁的會員區中找到。接著,在它後面加上點(.),然後我們有了我們的應用程式包識別碼。現在,我們需要對`packages.json`進行一些更改,而這些更改專門針對將應用程式上傳至App Store所需,這將構建一個.pkg檔案,而預設的electron-react-boilerplate則會生成.dmg作為建置輸出。因此,要同時擁有兩者,您可能需要編寫一些shell指令碼或其他工具來修改`packages.json`,以便為.dmg和App Store建置提供不同的指令碼。

首先要更新以下屬性:
- name:這是應用程式名稱
- version:這是應用程式版本,將顯示在App Store Connect上,其格式必須為字串形式 ′x.y.z′(例如:′1.0.0′)
- author:這是一個包含名稱、電子郵件和網址屬性的物件。

接下來,在指令碼中,我將新增一個新的指令碼 package:appstore。

 "package:appstore": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --mac && codesign --display --verbose=2 \"release/build/mas/DevHelper v2.app\" && npm run build:dll",

這部分的程式碼簽署主要是為了輸出一個程式碼簽署驗證,以確保我們打包的應用程式能夠正確地進行簽署。在你的情況下,你需要在命令結尾更新應用程式名稱,因為生成的 .app 檔案將會因名稱屬性而不同。接下來,讓我們來看看我的構建區域是如何安排的:

"build": {     "productName": "DevHelper v2",     "appId": "com.mydomain.myapp",     "asar": true,     "asarUnpack": "**\\*.{node,dll}",     "files": [       "dist",       "node_modules",       "package.json"     ],     "mac": {       "target": "mas",       "type": "distribution",       "provisioningProfile": "assets/profiles/MacAppStore.provisionprofile",       "hardenedRuntime": false,       "gatekeeperAssess": false,       "category": "public.app-category.education",       "icon": "assets/icon.icns",       "entitlements": "assets/entitlements/entitlements.mas.plist",       "entitlementsInherit": "assets/entitlements/entitlements.mas.inherit.plist"     },     ...   },

如果您正在使用 electron-boilerplate,則需要移除 :signnoratizeidentity。還需移除 dmg 物件,我也已將 hardenRuntime 更改為 false。目標需要設定為 mas(可能是 MacAppStore),型別則設定為 distribution。

我再提一下,當我們使用目標:′mas′ 進行構建時,我們將無法在本地安裝和使用該應用程式,因為它需要在 App Store 上重新簽名。一旦我們從那裡下載它,就可以使用。為了在提交到 App Store 之前測試應用程式,我們可以使用 mas-dev 作為目標,而為此我們還需要將型別更改為開發模式,並新增額外的屬性身份。在我的情況下,我有以下值 ′identity′: ′Apple Development′,而且你只能在你工作的那台機器上執行這個應用程式。請使用我們生成的另一個配置檔案 AppleDevelopment.provisionprofile。

"mac": {       "target": "mas-dev",       "type": "development",       "identity": "Apple Development",       "provisioningProfile": "assets/profiles/AppleDevelopment.provisionprofile",       "hardenedRuntime": false,       "gatekeeperAssess": false,       "category": "public.app-category.education",       "icon": "assets/icon.icns",       "entitlements": "assets/entitlements/entitlements.mas.plist",       "entitlementsInherit": "assets/entitlements/entitlements.mas.inherit.plist"     }

macOS App 圖示製作與 App Store Connect 上架自動化

如您所見,在 packages.json 中,我們有一個 .icns 檔案用於圖示的設定。要生成這個檔案,讓我們建立一個名為 icon.iconset 的資料夾,並在該資料夾中需要兩個圖示:一個解析度為 512x512 畫素,另一個則是 1024x1024 畫素,而它們必須符合 Apple 特殊命名格式。在我的情況下,我擁有以下檔案:assets/icon.iconset/icon_512x512.png 和 assets/icon.iconset/icon_512x512@2x.png。接下來,我們執行以下指令:iconutil --convert icns icon.iconset,這將生成所需的圖示檔案。

在我們最終構建應用程式之前,需要在 App Store Connect 上建立一個應用。我不會詳細描述這部分過程,因為其實相當簡單,只需建立一個新應用,選擇捆綁識別符即可。在構建應用之前還有一步重要的步驟。如果我們直接執行 npm run package:appstore,它可能仍然會失敗,而這種失敗通常與您的系統中是否已匯出某些 APPLE 環境變數有關(在我這裡是.zshrc 檔案)。因此,如果您有任何這樣的環境變數存在……

更進一步地說,在現代開發流程中,自動化和可重複性至關重要。專業開發者可以考慮使用圖示資產生成工具,例如 ImageOptim 或 Sketch 等,這些工具可以批次處理不同尺寸的圖示,自動生成符合 Apple 命名規範的圖示檔案。不僅如此,可以將整個圖示生成過程整合到構建指令碼中,比如使用 npm scripts 或 Makefile,以自動化過程並降低人為錯誤,提高開發效率。

另外,在提到 App Store Connect 的應用建立時,非常值得注意的是手動操作容易使流程冗長且易出錯。因此,頂尖開發者應該專注於 CI/CD 的整合,利用 App Store Connect API 自動化應用上傳、版本管理及其他相關操作。透過 Fastlane 等常見 CI/CD 工具,可以設定指令碼以自動化整體流程,包括圖示生成、打包、測試以及上傳到 App Store Connect,有效提升效率並確保一致性。

一定要重視環境變數管理的重要性。不推薦將 Apple 的私有變數硬編碼在 .zshrc 或其他 shell 設定檔中,更佳做法是採用像 dotenv 或 AWS Secrets Manager 等秘密管理工具,以安全地儲存和管理敏感資訊。用環境變數區分不同的構建環境(例如開發、測試與生產)也可提升系統穩定性和安全性,是任何大型專案都應遵循的最佳實務。

export APPLE_ID="[email protected]" export APPLE_APP_SPECIFIC_PASSWORD="the-app-specific-password-we-generated" export APPLE_TEAM_ID="MY-APPLE-TEAM-ID"

macOS App Store 上架:Electron 應用程式打包與釋出完整指南

您必須移除這些值,並重新獲取檔案,因為 Electron Builder 如果找到任何這些值,將會嘗試對應用程式進行 notarization,而我們不希望這樣。現在執行 npm run package:appstore,幾分鐘後您將獲得 .pkg 檔案,在首次執行時可能會要求您輸入電腦密碼。在我們擁有 .pkg 檔案後,啟動 Transporter 應用程式,用您的 Apple Developer 帳戶登入,只需將 pkg 拖放到其中。在它驗證完畢後,您需要點選 Distribute,它應該會將其上傳到 App Store Connect,如果一切都正確無誤。

如果您需要在另一台機器上構建應用程式,可以前往 Keychain Access -> My Certificates,切換所有三個憑證,然後選擇所有六個專案(每個憑證應有一個子項),並將其匯出為 p12 格式。設定一個密碼,然後將該 p12 檔案傳送到新機器中,再雙擊安裝並輸入密碼。另一種選擇是建立新的憑證;或者簡單地開啟 Xcode -> Settings -> Accounts 選擇您的帳戶,點選 Manage Certificates,再使用 + 按鈕逐一新增所有三種型別的憑證。我已經嘗試精練過程,因此我建立了一個 shell 指令碼,在我們的 npm 指令碼中呼叫,以便我們可以擁有多個指令碼:

package - 這個將構建我們的 dmg,因此我們使用 changeMacTarget.sh 指令碼,目標為 dmg; package:appstore-mas-dev - 這個將構建我們的 pkg 用於測試,因此我們使用 changeMacTarget.sh 指令碼,目標為 mas-dev; package:appstore - 這個將構建我們的 pkg 用於 App Store,因此我們使用 changeMacTarget.sh 指令碼,目標為 mas。 我也建立了一個 .env 檔案,在裡面放置了 APPLE 環境變數,看起來大致如下:

APPLE_ID="[email protected]" APPLE_APP_SPECIFIC_PASSWORD="the-app-specific-password-we-generated" APPLE_TEAM_ID="MY-APPLE-TEAM-ID"

以下是更改 MacTarget.sh 的內容:

#!/bin/bash  # Exit immediately if a command exits with a non-zero status set -e  # Define usage function usage() {   echo "Usage: $0 --target=[dmg|mas|mas-dev]"   exit 1 }  # Parse arguments for arg in "$@"; do   case $arg in     --target=*)       TARGET="${arg#*=}"       shift       ;;     *)       usage       ;;   esac done  # Ensure the target is provided if [ -z "$TARGET" ]; then   echo "Error: --target is required."   usage fi  # Path to package.json PACKAGE_JSON="package.json"  # Define the mac configurations MAC_DMG='{   "sign": ".erb/scripts/notarize.js",   "notarize": true,   "target": {     "target": "default",     "arch": ["arm64", "x64"]   },   "type": "distribution",   "hardenedRuntime": true,   "entitlements": "assets/entitlements/entitlements.mas.plist",   "entitlementsInherit": "assets/entitlements/entitlements.mas.inherit.plist",   "gatekeeperAssess": false }'  MAC_MAS='{   "target": "mas",   "type": "distribution",   "provisioningProfile": "assets/profiles/MacAppStore.provisionprofile",   "hardenedRuntime": false,   "gatekeeperAssess": false,   "category": "public.app-category.education",   "icon": "assets/icon.icns",   "entitlements": "assets/entitlements/entitlements.mas.plist",   "entitlementsInherit": "assets/entitlements/entitlements.mas.inherit.plist" }'  MAC_MAS_DEV='{   "target": "mas-dev",   "type": "development",   "identity": "Apple Development",   "provisioningProfile": "assets/profiles/AppleDevelopment.provisionprofile",   "hardenedRuntime": false,   "gatekeeperAssess": false,   "category": "public.app-category.education",   "icon": "assets/icon.icns",   "entitlements": "assets/entitlements/entitlements.mas.plist",   "entitlementsInherit": "assets/entitlements/entitlements.mas.inherit.plist" }'  # Determine the target configuration case $TARGET in   dmg)     MAC_CONFIG="$MAC_DMG"      # Load and export environment variables from .env file     if [ -f .env ]; then       echo "Loading environment variables from .env for $TARGET"       export $(grep -v '^#' .env | xargs)     else       echo "Error: .env file not found"       exit 1     fi     ;;   mas)     MAC_CONFIG="$MAC_MAS"     echo "Skipping environment variable loading for $TARGET"     ;;   mas-dev)     MAC_CONFIG="$MAC_MAS_DEV"     echo "Skipping environment variable loading for $TARGET"     ;;   *)     echo "Error: Invalid target '$TARGET'."     usage     ;; esac  # Modify package.json echo "Updating package.json for target: $TARGET" node -e " const fs = require('fs'); const path = '$PACKAGE_JSON'; const data = JSON.parse(fs.readFileSync(path, 'utf8'));  data.build.mac = $MAC_CONFIG;  fs.writeFileSync(path, JSON.stringify(data, null, 2)); console.log('package.json updated successfully for target:', '$TARGET'); "  # Verify environment variables if target is dmg if [ "$TARGET" == "dmg" ]; then   echo "Environment variables for notarizing:"   echo "APPLE_ID: ${APPLE_ID:-unset}"   echo "APPLE_APP_SPECIFIC_PASSWORD: ${APPLE_APP_SPECIFIC_PASSWORD:-unset}"   echo "APPLE_TEAM_ID: ${APPLE_TEAM_ID:-unset}" fi  # Add your build command here if needed echo "Ready to build for target: $TARGET"

更新後的指令碼位於 packages.json 內。

... "package": "sh ./changeMacTarget.sh --target=dmg && ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never && npm run build:dll", "package:appstore": "sh ./changeMacTarget.sh --target=mas && ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --mac && npm run build:dll", "package:appstore-mas-dev": "sh ./changeMacTarget.sh --target=mas-dev && ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --mac && npm run build:dll", ...


EA

專家

相關討論

❖ 相關專欄