时长:40-50 分钟
8.1 前端性能优化
8.1.1 组件懒加载
- 目标:减少首屏 bundle 体积,按需加载平台面板等大组件,缩短首屏时间。
- 做法:
- 使用 Vue 3 的 defineAsyncComponent 或 import() 动态加载平台组件,例如:
const AndroidDeviceSection = defineAsyncComponent(() =>
import('./components/AndroidDeviceSection.vue')
)
const IOSDeviceSection = defineAsyncComponent(() =>
import('./components/IOSDeviceSection.vue')
)
const HarmonyDeviceSection = defineAsyncComponent(() =>
import('./components/HarmonyDeviceSection.vue')
)
- 仅在用户切换到对应平台 Tab 时加载该组件,未切换过的平台不加载。
- 当前项目:
目前为同步 import 三端面板;若面板体积较大,可改为上述懒加载,以首屏只加载当前 Tab 对应组件。
- 注意:
懒加载组件首次渲染会有短暂延迟,可配合 Suspense 或局部 loading 提升观感。
8.1.2 虚拟滚动
- 目标:相册/照片列表数量很大时,只渲染可视区域及少量缓冲项,避免 DOM 过多导致卡顿。
- 做法:
- 使用 虚拟列表 组件(如基于
vue-virtual-scroller、el-table的虚拟滚动),根据滚动位置计算当前应渲染的 startIndex/endIndex,只渲染该区间内的项。 - 或采用 按需加载 + 普通滚动:不一次性渲染全部,而是根据滚动位置分批请求并追加列表(当前项目相册/照片列表多采用此种方式)。
- 使用 虚拟列表 组件(如基于
- 当前项目:
- Android/Harmony 相册:通过
@scroll="handlePhotosScroll"做防抖,在接近底部时加载更多;列表为真实 DOM,未做虚拟滚动。 - iOS 照片网格:通过
handlePhotoScroll+loadVisibleThumbnails只加载“可视区域 + 缓冲”内的缩略图,减少同时请求数;列表本身仍为全部 item 的 DOM,若单页数量极大(如上千张),可考虑引入虚拟滚动。
- Android/Harmony 相册:通过
- 最佳实践:
单页列表项超过数百时,建议引入虚拟滚动;否则可继续用“分页/懒加载 + 缩略图懒加载”平衡实现成本与体验。
8.1.3 图片懒加载
- 目标:缩略图/预览图仅在进入视口或即将进入时请求,避免首屏请求过多、流量与内存浪费。
- 做法:
- 缩略图:
监听网格/列表的滚动,计算可视区域(或“可视区域 + 上下缓冲”),仅对该范围内的项调用 loadPhotoThumbnail(item) 或等价接口;已加载过的用 item.thumbUrl 标记,避免重复请求。
- 预览图/原图:
仅在用户点击打开预览时请求原图/视频(如 loadPreviewMedia(item)),并用 AbortController 在切换预览时取消上一次请求。
- 当前项目:
- Android:
loadPhotoThumbnail(photo)在滚动回调中根据photo.fileId与loadingThumbnails去重,仅对未加载且进入视口的项加载;视频用占位 SVG,不预加载视频缩略图。 - iOS:
loadVisibleThumbnails(container)根据container.scrollTop、scrollHeight、clientHeight计算可视区间,对该区间内!item.thumbUrl && !loadingThumbnails.has(...)的项调用缩略图加载;滚动监听使用{ passive: true }减少主线程开销。
- Android:
- 防重复:
用 Set 或 item.thumbUrl != null 标记“已请求/已加载”,避免同一项被重复请求。
8.1.4 状态管理优化
- 目标:避免不必要的响应式更新和重复计算,保持列表/表单流畅。
- 做法:
- 局部状态:
设备列表、当前选中的设备、当前 Tab、loading 等尽量放在使用它们的组件内,用 ref/reactive 管理,避免提升到全局导致无关组件也更新。
- 计算属性:
对依赖设备列表的派生数据(如“当前设备”“是否有设备”)使用 computed,避免在模板中重复计算。
- 避免大对象响应式:
仅对需要驱动视图的字段做响应式;只用于逻辑的大数据结构可保持普通对象,必要时用 shallowRef 或 markRaw。
- 列表更新:
对长列表做增量更新(如只替换变更的项或追加新项),避免整表替换导致大量 DOM 变更。
- 当前项目:
设备状态、平台 Tab、各面板的 loading/列表等均以组件内 ref 为主;DeviceService 为单例但自身非响应式,组件只在使用时调用其方法并更新本地 ref,符合“局部状态优先”的思路。
8.2 后端性能优化
8.2.1 服务启动优化
- 目标:主进程启动时减少阻塞,按需初始化重量级服务(如 photo-proxy、URL Service),缩短应用启动时间。
- 做法:
- 延迟加载:
不在 app ready 时直接启动 photo-proxy/URL Service,而是在首次调用对应 IPC(如 photo-proxy:connect、url-service:set-device)时再初始化并启动服务。
- 异步初始化:
初始化逻辑放在 async 函数中,用 Promise 或队列串行化,避免在 IPC handler 内重复初始化。
- 超时与重试:
启动失败时记录日志并返回明确错误,由前端提示用户;必要时做有限次重试。
- 当前项目:
Electron 主进程中对 photo-proxy、URL Service 等采用首次调用时再 load 模块并创建实例的方式,避免启动时全部拉齐,属于典型的“懒启动”优化。
8.2.2 连接池管理
- 目标:复用与设备的连接,避免频繁建连/断连带来的延迟与资源抖动。
- 做法:
- 单设备单连接:
对同一设备(如同一 UDID)在同一时刻只维护一条 TCP 连接(如 BridgeServicePortForward 到 PhotoCompanion 的 socket);新请求复用该连接,通过 requestId 区分并发请求。
- 连接健康检查:
发送前或收到数据前检查 socket 是否可写、是否已销毁;若不可用则先 disconnect 再重新 connect,再重试请求。
- 超时释放:
长时间无请求时,可选择主动断开以释放设备端资源;下次请求时再建连(按产品需求在“长连”与“省资源”之间权衡)。
- 当前项目:
BridgeServicePortForward 对同一设备复用一条连接,通过 pendingRequests + requestId 匹配响应;ensureConnected() 在连接不健康时自动重连,避免“假连接”导致请求挂死。
8.2.3 资源释放
- 目标:窗口关闭或应用退出时,正确关闭 socket、子进程、监听器,避免句柄泄漏或后台进程残留。
- 做法:
- IPC 与服务:
在 app.on('window-all-closed') 或 before-quit 中,依次调用 photo-proxy、URL Service 的 disconnect(),关闭与设备的连接,并移除或清理 IPC handler(若存在动态注册)。
- 子进程:
对通过 spawn 启动的 go-ios forward、photo-service 等进程,在退出前 kill 或发退出信号,并 on('exit') 做清理。
- 前端:
组件 onUnmounted 时取消未完成的请求(AbortController)、移除滚动/全局监听器,避免回调在组件销毁后仍执行。
- 当前项目:
主进程在窗口关闭或退出流程中调用 photosServiceInstance.disconnect()、urlServiceInstance.disconnect(),并做 IPC 清理;前端在预览切换时使用 AbortController 取消上一次 loadPreviewMedia,在 iOS 照片列表卸载时移除 scroll 监听。
8.2.4 内存管理
- 目标:控制长时间运行时的内存增长,避免大图/大视频、缓存、日志等导致 OOM。
- 做法:
- 流式处理:
大文件(如原图、视频)采用流式收发,避免在内存中一次性拼成完整 Buffer 再处理;若必须整体处理,处理完及时释放引用,便于 GC。
- 缓存上限:
缩略图等缓存使用 LRU 或最大条数/最大容量(如 NSCache 的 countLimit/totalCostLimit),防止无限增长。
- Blob URL 释放:
前端对不再使用的 URL.createObjectURL(blob) 调用 URL.revokeObjectURL(url),释放 Blob 引用。
- 日志:
生产环境可降低日志级别或关闭详细日志,避免大对象、长字符串被 console 持有导致无法回收。
- 当前项目:
iOS 端 PhotoCompanion 使用 NSCache 做缩略图缓存并设 countLimit/totalCostLimit;前端在视频预览等场景会创建 Blob URL,在切换预览或关闭时可考虑对旧 URL 做 revoke;主进程对 get-image/get-video-stream 的流式数据在收集完成后转为 base64 返回,若单次响应过大可考虑分块或限制单次大小。
8.3 用户体验优化
8.3.1 加载状态优化
- 目标:让用户明确知道“正在加载”或“正在执行”,减少“点了没反应”的困惑。
- 做法:
- 全局与局部:
进入页或切换平台时用“全局 loading”(如整块遮罩或顶部进度条);单次操作(截图、录屏、安装、刷新环境)用“局部 loading”(如按钮 loading、列表行 loading)。
- 骨架/占位:
列表、相册在数据未返回前可展示骨架屏或占位项,避免空白或布局跳动。
- 超时提示:
长时间操作(如录屏、拉取相册)若超过一定时间仍未结束,可在日志区或 Toast 提示“仍在执行中,请稍候”,避免用户误以为卡死。
- 当前项目:
各面板使用 loading.value、currentEnvLoading、iosPhotosLoading 等 ref 控制按钮禁用与 loading 展示;相册/照片列表在请求中可配合 v-loading 或占位;部分长耗时操作在控制台打日志,便于排查,前端可在此基础上增加“执行中”提示。
8.3.2 错误提示优化
- 目标:错误信息对测试/用户可读,便于排查和操作。
- 做法:
- 分层:
面向用户的提示:简短、可操作(如“未检测到设备,请检查连接并解锁屏幕”);面向开发的详情:完整 error、stack、命令输出等放在控制台或日志。
- 统一入口:
在 DeviceService 或各 Manager 的 catch 中统一设置 error: error.message 或简短文案;前端用 ElNotification/ElMessage 展示 result.error,避免到处拼接字符串。
- 分类处理:
对“未安装 App”“需要开发者镜像”“Companion 未在前台”等常见错误,可返回固定文案或错误码,前端做针对性提示或引导(如跳转设置、打开 Companion)。
- 当前项目:
各 Manager 返回 { success: false, error: string };组件中根据 result.success 显示成功/失败通知,并将 result.error 直接作为 message,符合“统一格式 + 用户可读”的思路。
8.3.3 交互反馈
- 目标:操作有即时反馈,危险操作有二次确认,长操作可取消或看到进度。
- 做法:
- 即时反馈:
点击按钮后立即进入 loading 或禁用,防止重复提交;操作结束后用 Notification 提示成功/失败。
- 危险操作:
卸载应用、清空缓存等先弹确认框,确认后再执行。
- 可取消:
对录屏、长时间拉取等提供“停止”按钮,并在后端支持取消或超时终止。
- 当前项目:
截图/录屏/安装/卸载等会置 loading 并在完成后 Notification;卸载等有 ElMessageBox 确认;预览切换时通过 AbortController 取消上一次加载,相当于“取消”上一次预览请求。
8.3.4 响应式设计
- 目标:不同窗口尺寸下布局可用、关键信息可见。
- 做法:
- 断点与布局:
使用 Flex/Grid 与 Element Plus 栅格,在窄屏时改为单栏或折叠侧栏,保证主操作区可见。
- 滚动区域:
列表、日志区域使用 overflow: auto 并设置最大高度,避免整页被撑得过长。
- 字体与触控:
关键按钮和文字在触控设备上可点区域足够大,必要时增加 padding 或 min-height。
- 当前项目:
各设备面板有侧栏、主内容区、可滚动列表/日志;部分使用 -webkit-overflow-scrolling: touch 优化移动端滚动;整体以桌面端为主,兼顾小窗口。
8.4 代码质量
8.4.1 代码规范
- 目标:风格统一、命名清晰、结构一致,便于维护和协作。
- 做法:
- Lint:
使用 ESLint(及 Vue/TypeScript 相关规则)在提交前或 CI 中检查,避免未使用变量、隐式 any、不一致的引号等。
- 命名:
组件、文件、方法命名与业务含义一致(如 loadIOSAlbums、takeScreenshot);平台相关变量/参数使用统一标识('android'|'harmony'|'ios')。
- 结构:
单文件过大时按“逻辑块”或“功能”拆成组合式函数或子模块,避免单文件上千行难以定位。
- 当前项目:
设备面板、DeviceService、各 Manager 命名清晰;utils 中单文件行数较多,可结合 8.1/8.4 的“状态管理”“可测试性”逐步抽离函数或模块。
8.4.2 错误处理
- 目标:异常可被捕获、可被记录、可向用户展示,不出现未处理的 Promise rejection 或静默失败。
- 做法:
- 异步:
所有 async 调用在业务层 try/catch,或在顶层用统一的 Promise 错误处理(如返回 { success: false, error }),避免未捕获的 rejection。
- 边界:
对“无设备”“未安装”“网络超时”等做显式判断并返回明确错误,不依赖底层抛出的原始 message 作为唯一信息。
- 降级:
对可选能力(如 photo-proxy、Assist Agent)在不可用时回退到其他方案或明确提示,避免整条链路因单点失败而不可用。
- 当前项目:
各 Manager 在 catch 中返回 { success: false, error: error.message };组件侧对 result.success 做判断并提示;iOS 相册在 photo-proxy 不可用时回退到 fsync tree,符合“统一返回 + 降级”的思路。
8.4.3 日志系统
- 目标:便于排查问题,又不在生产环境产生过多噪音。
- 做法:
- 分级:
使用 console.log(流程/关键节点)、console.warn(可恢复异常、降级)、console.error(错误、异常栈);有需要可封装成 logger,按环境切换级别。
- 前缀:
日志带模块或操作前缀(如 [openXiaoyingTestLogin]、[loadPreviewMedia]),便于按关键字过滤。
- 结构化:
关键步骤可打印对象(如 { success, error, duration }),避免长字符串拼接难以阅读。
- 当前项目:
utils 与组件中有大量带前缀的 console.log/warn/error,便于本地和 Electron 主进程控制台排查;若后续需要生产环境日志上报,可在此基础上增加 logger 封装与级别控制。
8.4.4 测试策略
- 目标:核心逻辑可测、回归可控,减少手动重复验证。
- 做法:
- 单元测试:
对纯函数(如设备列表解析、curEnv 解析、时间戳格式化)用 Jest/Vitest 等写单测,不依赖 DOM 和 Electron。
- Mock:
对 DeviceService、electronAPI 等做 mock,在测试中注入假数据,验证组件或业务逻辑在“成功/失败/无设备”等分支下的行为。
- E2E:
对关键流程(如选择设备 → 截图 → 查看结果)可用 Playwright 等做少量 E2E,覆盖主路径;Electron 应用可配合 electron 的 test 能力或启动打包后的应用做冒烟。
- 手动与清单:
对多平台、多设备场景保留“发布前检查清单”(如三端截图/录屏/登录各测一遍),与自动化互补。
- 当前项目:
以手动测试为主;若引入单测,可优先覆盖 commandDetector、deviceService 的 platform 分支、以及各 Manager 的“解析类”函数(如 getDevices 的 stdout 解析)。
小结
- 前端:通过组件懒加载、滚动与缩略图懒加载、局部状态与列表更新优化,控制首屏与长列表性能。
- 后端:通过服务懒启动、连接复用与健康检查、资源释放与缓存上限,保证主进程稳定与可维护性。
- 体验:通过加载状态、错误提示、交互反馈与响应式布局,让操作可预期、可理解。
- 质量:通过规范、统一错误处理、结构化日志与测试策略,提升可维护性与回归效率。







Comments | NOTHING