时长:40-50分钟
2.1 Electron 基础架构
2.1.1 主进程与渲染进程通信
架构设计
我们的应用采用 Context Isolation(上下文隔离) 架构,确保安全性:
// electron/main.js
function createMainWindow() {
mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.cjs'),
contextIsolation: true, // 启用上下文隔离
nodeIntegration: false // 禁用 nodeIntegration,使用 contextBridge
}
})
}
关键点:
- ✅
contextIsolation: true- 隔离渲染进程和主进程的上下文 - ✅
nodeIntegration: false- 禁止渲染进程直接访问 Node.js API - ✅ 通过
preload.js作为桥梁,安全地暴露 API
IPC Handler 设计模式
主进程注册 Handler:
// electron/main.js
import { ipcMain } from 'electron'
// 基础命令执行
ipcMain.handle('execute-command', async (event, { command, timeout = 15000, maxBuffer = 1024 * 1024 }) => {
return new Promise(async (resolve) => {
const { exec } = await import('child_process')
const bashCommand = `/bin/sh -c "${command.replace(/"/g, '\"')}"`
exec(bashCommand, { timeout, env, maxBuffer }, (error, stdout, stderr) => {
resolve({
success: !error,
stdout,
stderr,
error: error?.message
})
})
})
})
// 文件操作
ipcMain.handle('read-file-as-base64', async (event, { filePath }) => {
try {
const fs = await import('fs')
const buffer = fs.readFileSync(filePath)
return { success: true, data: buffer.toString('base64') }
} catch (error) {
return { success: false, error: error.message }
}
})
Preload 暴露 API:
// electron/preload.cjs
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
// 基础命令执行
executeCommand: (params) => ipcRenderer.invoke('execute-command', params),
// 文件操作
readFileAsBase64: (params) => ipcRenderer.invoke('read-file-as-base64', params),
// Photo Proxy API
photoProxy: {
setDevice: (device) => ipcRenderer.invoke('photo-proxy:set-device', device),
connect: () => ipcRenderer.invoke('photo-proxy:connect'),
listAssets: (limit, offset) => ipcRenderer.invoke('photo-proxy:list-assets', limit, offset)
},
// URL Service API
urlService: {
setDevice: (device) => ipcRenderer.invoke('url-service:set-device', device),
httpRequest: (options) => ipcRenderer.invoke('url-service:http-request', options)
}
})
渲染进程调用:
// src/utils/adb.js (渲染进程)
const result = await window.electronAPI?.executeCommand({
command: `test -f "${filePath}" && echo exists || echo missing`,
timeout: 5000
})
安全通信机制
为什么使用 Context Isolation?
- 安全性:防止渲染进程直接访问 Node.js,避免 XSS 攻击
- 可控性:通过
preload.js精确控制暴露的 API - 类型安全:TypeScript 可以定义接口类型
最佳实践:
// ✅ 好的做法:通过 contextBridge 暴露
contextBridge.exposeInMainWorld('electronAPI', {
executeCommand: (params) => ipcRenderer.invoke('execute-command', params)
})
// ❌ 不好的做法:直接暴露 Node.js
window.require = require // 危险!
window.process = process // 危险!
2.2 多平台设备检测
2.2.1 iOS 设备检测(usbmuxd)
实现原理:
// src/utils/ios.js
class IOSManager {
async getDevices() {
// 使用 go-ios 工具检测设备
const result = await this.execute('list')
if (result.success) {
// 解析 JSON 输出
const deviceList = JSON.parse(result.stdout)
return deviceList.deviceList.map(udid => ({
id: udid,
status: 'device',
platform: 'iOS'
}))
}
return []
}
}
关键点:
- 使用
go-ios工具(封装了 usbmuxd 协议) - 通过
ios list命令获取设备列表 - 支持多设备并行检测
2.2.2 Android 设备检测(ADB)
实现原理:
// src/utils/adb.js
class ADBManager {
async getDevices() {
// 使用 adb devices 命令
const result = await this.execute('devices -l')
if (result.success) {
const lines = result.stdout.split('n')
const devices = []
for (const line of lines) {
if (line.includes('tdevice')) {
const parts = line.split('t')
const id = parts[0]
devices.push({
id,
status: 'device',
platform: 'Android'
})
}
}
return devices
}
return []
}
}
关键点:
- 使用
adb devices -l获取设备列表 - 解析输出格式:
设备IDt状态 - 支持通过
-s参数指定设备
2.2.3 Harmony 设备检测(HDC)
实现原理:
// src/utils/hdc.js
class HDCManager {
async getDevices() {
// 使用 hdc list targets 命令
const result = await this.execute('list targets')
if (result.success) {
const lines = result.stdout.split('n')
const devices = []
for (const line of lines) {
if (line.includes('device')) {
const parts = line.split(/s+/)
devices.push({
id: parts[0],
status: 'device',
platform: 'Harmony'
})
}
}
return devices
}
return []
}
}
2.2.4 设备状态实时监控
统一设备服务:
// src/utils/deviceService.js
class DeviceService {
async getDeviceStatus() {
const timeout = 8000
// 并行检测所有平台
const [
androidAvailable,
harmonyAvailable,
iosAvailable,
androidDevices,
harmonyDevices,
iosDevices
] = await Promise.all([
withTimeout(this.adb.isAvailable(), timeout),
withTimeout(this.hdc.isAvailable(), timeout),
withTimeout(this.ios.isAvailable(), timeout),
withTimeout(this.adb.getDevices(), timeout),
withTimeout(this.hdc.getDevices(), timeout),
withTimeout(this.ios.getDevices(), timeout)
])
return {
android: {
available: !!androidAvailable,
devices: androidDevices || [],
hasDevices: (androidDevices || []).length > 0
},
harmony: { / ... / },
ios: { / ... / }
}
}
}
优化策略:
- ✅ 并行检测,提高响应速度
- ✅ 超时保护,避免长时间阻塞
- ✅ 容错处理,单个平台失败不影响其他平台
2.3 服务启动与管理
2.3.1 Photo Proxy 服务启动
服务初始化:
// WebDriverAgent/photo-proxy/ipc-handler.ts
export function initPhotoProxyIPC(): void {
// 创建 PhotosService 实例
photosService = new PhotosService()
// 注册 IPC Handlers
ipcMain.handle('photo-proxy:set-device', async (event, device: IOSDevice) => {
photosService!.setDevice(device)
return { success: true }
})
ipcMain.handle('photo-proxy:connect', async () => {
const connected = await photosService!.connect()
return { success: connected }
})
ipcMain.handle('photo-proxy:list-assets', async (event, limit, offset) => {
return await photosService!.listAssets(limit, offset)
})
}
服务连接流程:
// WebDriverAgent/photo-proxy/usb/service.ts
class PhotosService {
async connect(): Promise {
// 1. 检查 Developer Disk Image 是否已挂载
await this.ensureImageMounted()
// 2. 建立端口转发(12345)
await this.bridgeService.setupPortForward()
// 3. 连接到 Companion Service
await this.bridgeService.connect()
return this.bridgeService.connected
}
}
2.3.2 URL Service 服务管理
HTTP 请求代理:
// WebDriverAgent/photo-proxy/usb/url-service.ts
class URLService {
async httpRequest(options: {
url: string
method?: string
headers?: Record
body?: string
timeoutMs?: number
skipWDA?: boolean
}): Promise {
// 1. 快速检查 WDA 是否可用(1秒超时)
if (!options.skipWDA) {
try {
const statusResponse = await fetch('http://127.0.0.1:8100/status', {
signal: AbortSignal.timeout(1000)
})
if (statusResponse.ok) {
// 使用 WDA HTTP API
return await this.requestViaWDA(options)
}
} catch (error) {
// WDA 不可用,回退到 Companion Service
}
}
// 2. 回退到 Companion Service
if (!this.connected) {
await this.connect()
}
return this.bridgeService!.httpRequest(options)
}
}
关键优化:
- ✅ 快速失败机制(1秒超时)
- ✅ 自动回退到 Companion Service
- ✅ 支持跳过 WDA 直接使用 Companion
2.3.3 端口转发与资源管理
端口转发实现:
// WebDriverAgent/photo-proxy/protocol/bridge-port-forward.ts
class BridgeServicePortForward {
private async setupPortForward(udid: string): Promise {
// 1. 清理旧进程
await this.cleanupExistingPortForward()
// 2. 启动端口转发
const forwardProcess = spawn(iosPath, [
'forward',
'12345',
'12345',
`--udid=${udid}`
])
// 3. 等待转发建立
await new Promise(resolve => setTimeout(resolve, 1500))
// 4. 连接到本地端口
await this.connectToLocalPort()
}
}
资源清理:
private async cleanupExistingPortForward(): Promise {
// 查找现有进程
const psCmd = `ps aux | grep -E '[i]os.forward.12345' | grep -v grep`
const result = await execAsync(psCmd)
if (result.stdout) {
const processes = result.stdout.split('n').filter(line => line.trim())
for (const line of processes) {
const pid = line.split(/s+/)[1]
if (pid) {
process.kill(parseInt(pid), 'SIGTERM')
}
}
}
}
2.3.4 错误处理与重试机制
超时与重试:
// src/utils/ios.js
async ensureWdaInstalled(deviceId = '') {
// 防抖:按设备并发保护
const key = deviceId || '_default'
if (this._ensureWdaInstallingMap.has(key)) {
// 等待其他安装完成(最多 10 秒)
for (let i = 0; i setTimeout(r, 500))
const check = await this.isWdaInstalled(deviceId)
if (check) return { success: true, already: true }
}
}
// 检查是否已安装
const check = await this.isWdaInstalled(deviceId)
if (check.installed) {
return { success: true, already: true }
}
// 执行安装
this._ensureWdaInstallingMap.add(key)
try {
const installRes = await this.execute(`install --path="${ipaPath}"`, { timeout: 120000 })
// ...
} finally {
this._ensureWdaInstallingMap.delete(key)
}
}
错误分类处理:
// WebDriverAgent/photo-proxy/usb/url-service.ts
catch (wdaError: any) {
const errorMsg = wdaError.message || String(wdaError)
// 连接错误:回退到 Companion Service
if (errorMsg.includes('fetch failed') ||
errorMsg.includes('ECONNREFUSED') ||
errorMsg.includes('not available')) {
console.warn('WDA 不可用,回退到 Companion Service')
return this.bridgeService!.httpRequest(options)
}
// 其他错误:直接抛出
throw wdaError
}
2.4 打包与部署
2.4.1 Electron Builder 配置
基础配置:
// package.json
{
"build": {
"appId": "com.xiaoying.cardloan.tool",
"productName": "小赢MobileTool",
"directories": {
"output": "release",
"buildResources": "build"
},
"files": [
"dist/**",
"electron/**",
"package.json",
"assets/icons/**",
"WebDriverAgent/photo-proxy/*/.js"
],
"extraResources": [
{
"from": "commands",
"to": "commands",
"filter": ["*/"]
}
]
}
}
关键配置说明:
files: 包含在应用包中的文件extraResources: 作为资源文件(不打包进 asar),可通过process.resourcesPath访问
2.4.2 多平台打包策略
Mac 平台配置:
{
"mac": {
"category": "public.app-category.developer-tools",
"icon": "assets/icons/icon-512.png",
"target": [
{
"target": "dmg",
"arch": ["x64", "arm64"]
}
],
"identity": null // 使用临时签名
}
}
Windows 平台配置:
{
"win": {
"icon": "assets/icons/icon.ico",
"target": [
{
"target": "nsis",
"arch": ["x64", "arm64"]
}
]
}
}
2.4.3 资源文件管理
命令工具路径检测:
// src/utils/commandDetector.js
class CommandDetector {
static async getAppPath() {
if (window.electronAPI) {
const appPath = await window.electronAPI.getAppPath()
// 打包后:appPath 指向 .app/Contents/Resources/app.asar
// 资源文件在:appPath/../commands/
if (appPath.includes('.asar')) {
return appPath.replace('app.asar', 'app.asar.unpacked')
}
// 开发环境:直接返回
return appPath
}
return process.cwd()
}
static async getCommandPath(commandName) {
const appPath = await this.getAppPath()
const commandPath = `${appPath}/commands/${commandName}`
// 检查文件是否存在
const check = await window.electronAPI?.executeCommand({
command: `test -f "${commandPath}" && echo exists || echo missing`
})
if (check?.stdout?.includes('exists')) {
return commandPath
}
// 回退到系统 PATH
return commandName
}
}
关键点:
- ✅ 开发环境:使用项目根目录的
commands/ - ✅ 打包后:使用
app.asar.unpacked/commands/ - ✅ 自动回退到系统 PATH
2.4.4 签名与分发
签名配置:
// build/afterSign.cjs
exports.default = async function(context) {
// 自动签名后的处理
console.log('签名完成:', context.packager.platform, context.packager.arch)
}
打包脚本:
{
"scripts": {
"dist": "yarn build:photo-proxy && yarn build && electron-builder --mac --x64 --arm64",
"dist:mac": "electron-builder --mac dmg --x64 --arm64"
}
}
打包流程:
- 编译 TypeScript:
yarn build:photo-proxy - 构建前端:
yarn build - 打包 Electron:
electron-builder --mac --x64 --arm64
2.5 实际案例:iOS 设备环境信息获取
2.5.1 完整流程
// 1. 渲染进程发起请求
const result = await DeviceService.ios.fetchUrlOnDevice(url, deviceId)
// 2. 通过 IPC 调用主进程
// src/utils/ios.js
async fetchUrlOnDevice(url, deviceId) {
const resp = await window.electronAPI.urlService.httpRequest({
url,
method: 'GET',
headers: { 'Cache-Control': 'no-cache' },
timeoutMs: 10000
})
return {
success: resp.success,
data: resp.data
}
}
// 3. 主进程处理请求
// electron/main.js
ipcMain.handle('url-service:http-request', async (event, options) => {
// 初始化 URLService(懒加载)
if (!urlServiceInstance) {
await initURLServiceIPC()
}
// 调用 URLService
return await urlServiceInstance.httpRequest(options)
})
// 4. URLService 选择执行路径
// WebDriverAgent/photo-proxy/usb/url-service.ts
async httpRequest(options) {
// 快速检查 WDA(1秒超时)
try {
await fetch('http://127.0.0.1:8100/status', { signal: AbortSignal.timeout(1000) })
// 使用 WDA
} catch {
// 回退到 Companion Service
await this.connect()
return this.bridgeService!.httpRequest(options)
}
}
// 5. Companion Service 在设备上执行请求
// WebDriverAgent/photo-proxy/protocol/bridge-port-forward.ts
async httpRequest(options) {
// 通过端口转发发送请求到设备
const response = await this.sendRequest(FrameType.HTTP_REQUEST, payload)
// 解析响应(支持 HTML 格式)
if (response.body?.includes('<pre>')) {
const jsonMatch = response.body.match(/<pre>]>([sS]?)/i)
if (jsonMatch) {
return JSON.parse(jsonMatch[1])
}
}
return response
}
2.5.2 关键优化点
- 快速失败:WDA 检查 1 秒超时,立即回退
- 自动回退:WDA 失败自动使用 Companion Service
- 格式兼容:支持 JSON 和 HTML
包裹的 JSON
- 错误处理:完善的错误分类和重试机制
2.6 总结
核心要点
- 安全性优先:使用 Context Isolation,通过 preload.js 安全暴露 API
- 并行检测:多平台设备检测并行执行,提高响应速度
- 服务管理:Photo Proxy 和 URL Service 的懒加载和生命周期管理
- 资源管理:端口转发进程的清理和复用
- 错误处理:完善的超时、重试和回退机制
最佳实践
- ✅ 使用
contextBridge而不是直接暴露 Node.js API - ✅ IPC Handler 统一返回
{ success, data, error }格式 - ✅ 资源文件放在
extraResources,不打包进 asar - ✅ 命令工具路径检测支持开发和生产环境
- ✅ 服务启动采用懒加载,按需初始化
附录:代码结构
mobile-tools-new/
├── electron/
│ ├── main.js # 主进程入口
│ ├── preload.cjs # Preload 脚本
│ └── ipc-handler.ts # IPC Handler 定义
├── src/
│ └── utils/
│ ├── adb.js # Android 设备管理
│ ├── hdc.js # Harmony 设备管理
│ ├── ios.js # iOS 设备管理
│ └── deviceService.js # 统一设备服务
├── WebDriverAgent/
│ └── photo-proxy/
│ ├── usb/
│ │ ├── service.ts # PhotosService
│ │ └── url-service.ts # URLService
│ └── protocol/
│ └── bridge-port-forward.ts # 端口转发服务
└── commands/ # 命令工具(extraResources)
├── adb
├── ios
├── hdc
└── wda.ipa / lower-wda.ipa







Comments | NOTHING