摘要
本文探討了如何在 App Store 中成功釋出 Electron 應用程式與 DMG 安裝包,並提供實用策略以提升通過率。 歸納要點:
- 深入分析 Apple 的 Electron 應用程式審核標準,了解近期的趨勢變化及如何撰寫更有效的審核說明文件。
- 探討硬體加速與效能最佳化技巧,包括使用 Metal 提升圖形渲染速度,以及 V8 引擎的調校和資源預載技術。
- 強調 DMG 安裝包的安全性,涵蓋程式碼簽章、Gatekeeper 機制,以及如何結合 Notarization 確保應用程式可靠性。
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", ...
相關討論