1. 为什么 EasyClick 大型项目需要工程化
1.1 从五百行脚本到十二个业务流程
先说一段真实经历。最早接触 EasyClick 时,一个项目基本就是单个几百行的脚本文件。控件、业务逻辑、异常处理全部放在一起,也没觉得有什么问题。真正难堪的是后来有一个项目,业务流程从两个扩展到十二个,还要兼容三种不同分辨率。我当时打开项目文件,光是想找到“首页那个按钮的控件定义在第几行”,就花了大半天。
那时候的体验用一个词来形容就是“连坐”。改一个控件名,可能牵动首页判断、登录跳转、订单确认三条业务逻辑;改一个等待时间,可能让十几个步骤全部重新排队。更头疼的是,因为界面和逻辑混在一起,完全没法并行开发。两个人同时改同一个文件,Git 合并时满屏冲突,与其解决冲突不如重写一遍。
这种痛点的本质,不是脚本语言不好,也不是工具不够强,而是我们一直用“写小脚本”的思维去做大项目。传统软件开发里早就验证过的分层思想、模块化、单一职责,在自动化脚本领域,常常被人忽略。我后来花了很大力气做工程化改造,才慢慢把这套大脚本从“能跑就行”变成“能维护、能扩展”。
1.2 工程化前后做同一件事的差距
我做的第一件改革,是先把项目按三个层次拆开:
- UI 层,只管描述界面,定义所有控件和可操作元素;
- 业务层,只管流程编排,负责判断、调度、重试;
- 基础设施层,封装日志、等待、重试、设备配置等低频变化的部分。
改造后我做了个对比:屏幕适配新增一个方案,开发成本从修改十几个文件,变成只新增一个 UI 描述文件;业务流程新增一个入口,只需要新建一个业务模块,引用已有控件就好。类似的需求变化,改动量大概降了一个数量级。
工程化在短期看起来是“绕远路”,因为要多维护目录结构、要写规范、要考虑接口约定。但只要你遇到过一次“因为一个控件变更,整个项目都要重新测一遍”的情况,就会知道这条路省下来的时间是实实在在的。尤其是当 AI 参与开发后,一个清晰的工程边界,本身就等于给 AI 画好了干活的范围,它能少出很多错。
1.3 工程化的本质是降低“调用成本”
随手贴一张改造前后的项目文件对比,可能更直观。改造前是:
main.js ├── 打开 App ├── 判断登录 ├── 找到首页搜索框 ├── 输入关键词 ├── 点击搜索按钮 ├── 等待结果列表 ├── ...改造后,项目变成:
app/ ├── ui/ │ ├── home.js │ ├── login.js │ └── order.js ├── flows/ │ ├── task_login.js │ ├── task_search.js │ └── task_order.js └── libs/ ├── logger.js └── auto.js两者对比,改造后的每一层文件都短了很多,每个文件的职责非常清楚。识别一个页面,就去ui/对应的文件里找;跟界面无关的流程调度,就去flows/里找。这种“一眼就能定位”的体验,其实比代码本身的优化更能提升效率。
2. 核心架构:UI 与业务分离的分层设计
2.1 一份可以直接参照的目录结构
上面已经出现一份简化的目录,我再把完整版展开,方便直接复制参考:
project_root/ ├── app/ │ ├── ui/ │ │ ├── home.js │ │ ├── login.js │ │ ├── order.js │ │ └── common.js │ ├── flows/ │ │ ├── task_main.js │ │ ├── task_order.js │ │ └── task_exception.js │ ├── libs/ │ │ ├── auto.js │ │ ├── logger.js │ │ └── retry.js │ └── config/ │ ├── device.js │ ├── settings.js │ └── env.js ├── assets/ │ └── images/ ├── rules/ │ ├── spec.md │ └── .coderules.json └── dist/ └── release.js关键点在于ui/、flows/、libs/三个目录的边界。ui/里的文件只描述界面,不写流程;flows/里的文件只写流程,不直接操作原生控件;libs/里放所有跨项目可复用的东西。这个边界一开始就得靠规则守住,否则时间一长又会变得一坨。
2.2 UI 层写什么东西,不写什么东西
UI 层一个文件的实例大概是这样的(以首页为例):
// app/ui/home.js const HomeUI = { entry: id("main_entry"), searchInput: id("search_input"), searchBtn: text("搜索"), profileTab: desc("我的,标签页"), floatingAction: id("float_action"), }; function openSearchPage(timeout = 10) { return waitClick(HomeUI.searchBtn, timeout); }它允许做的事:
- 定义选择器和坐标;
- 提供
hasXxx、tapXxx、inputXxx等基础操作; - 处理坐标换算和多分辨率适配。
它不允许做的事:
- 不允许判断“如果订单已完成就不点击搜索”;
- 不允许操作返回值继续驱动流程;
- 不允许读写业务数据。
一句话,UI 层是“界面字典加手势服务”,它不关心用户最终想完成什么业务,只关心当前屏幕上有哪些东西、怎么点。这个认知非常重要,因为我见过不少项目嘴上说分层,实际 UI 文件里全是业务判断,最后等于没分。
2.3 业务层如何与 UI 层协作
业务层的代码风格也相对固定:
// app/flows/task_search.js function runSearchTask(keyword) { const inHome = ui.home.hasHomeEntry(); if (!inHome.found) return { success: false, reason: "not_in_home" }; ui.home.openSearchPage(); ui.home.inputSearch(keyword); ui.home.tapSearchBtn(); const list = ui.search.waitResultList(15); if (!list.found) return { success: false, reason: "search_timeout" }; return { success: true, count: list.count }; }它通过ui.*暴露出来的方法一步步推进,自身的判断集中在“应该走哪一步”“上一步有没有成功”“失败要怎么处理”上。至于控件是id还是text、在屏幕哪个位置,业务层完全不知道。这样一旦界面改版,只需要回到 UI 层更新定义,业务层的判断和流程不用动。
2.4 两层之间的统一返回结构
为了让两层协作顺畅,我定了一条硬规则:UI 层方法统一返回带found和reason的对象,而不是直接返回null或难懂的异常。例如:
// 找不到时的返回 { found: false, reason: "timeout" } // 找到时的返回 { found: true, reason: "ok", element: selector }这么做有三个好处:
- 业务层拿到结果后,直接用
if (!res.found)判断,不需要捕获各种异常; - AI 生成代码时,不会因为空对象而臆造奇怪的兼容逻辑;
- 日志里能清晰看到每一步失败的原因,方便定位问题。
这条规范同样写进rules/spec.md,AI 和人工开发都必须遵守。不要小看这个返回结构约定,它其实是整个分层架构能稳定运行的“通信协议”。
2.5 多分辨率适配怎么处理
手机自动化最烦的就是分辨率适配。以前我在业务代码里到处写“1080 分辨率时 x=500,2340 分辨率时 x=300”,最后自己都会搞混。分层之后,我把所有坐标换算都收敛到 UI 层。
具体做法是在config/device.js里记录当前设备的基准分辨率,UI 层定义坐标时只写逻辑坐标,再提供一个convertPoint()函数做换算:
// app/libs/auto.js function convertPoint(x, y, fromWidth = 1080, fromHeight = 2340) { const sw = device.width / fromWidth; const sh = device.height / fromHeight; return { x: Math.round(x * sw), y: Math.round(y * sh) }; }业务层请求某个坐标时,UI 层负责把缩放换算好。这样换设备时,只需要改设备配置,不需要满项目搜索坐标。这个做法在多 AI 并行时尤其重要,因为 AI 不会记得你哪个坐标写在了哪一行,但统一的换算入口能把错误范围限制住。
3. 多 AI IDE 并行开发:工具选型与角色分工
3.1 一个编码助手解决不了所有环节
我前面说“工程化是基础”,但真正让这个模式跑得飞快的,其实是多 AI IDE 并行。为什么要用多个?我举几个真实感受:
- 有的助手擅长长上下文总结,能站在全局分析项目结构;
- 有的助手优点在于代码风格统一,生成大量重复 UI 定义时很稳定;
- 有的助手逻辑推理更强,适合写复杂流程,但它的长上下文能力弱,不适合让它读整个项目。
如果整个项目只用其中一个,要么 UI 层写得还行但流程一复杂就开始乱,要么业务逻辑挺强但控件命名五花八门。所以我把任务按角色拆开,把不同 IDE 当成不同风格的程序员,各管一摊。不要指望同一个工具所有场景都最强,分工之后反而能发挥每一个的长处。
3.2 典型角色分工表
为了避免涉及具体产品,我用助手 A、B、C 来代表不同能力的 AI IDE:
| 角色 | 适合承担的模块 | 能力要求 | 主要产出 |
|---|---|---|---|
| 助手 A | UI 层、控件定义 | 风格稳定、结构化输出 | ui/*.js、坐标换算、截图素材说明 |
| 助手 B | 业务层、流程实现 | 逻辑推理、异常处理 | flows/*.js、状态判断、重试方案 |
| 助手 C | 全局审查、代码巡检 | 长上下文、规则理解 | 风险清单、模块接口一致性检查 |
这里想强调一点:角色分工会随模型能力变化而调整。没有永恒固定的“哪个助手负责哪一层”,只要你觉得某类任务换另一个工具产出更稳定,就随时换。唯一不变的是“角色边界”本身,界面归界面,流程归流程。
3.3 用一份规则文档约束所有 AI
多工具协作最怕各写各的。我解决的办法是在项目根目录放一份rules/spec.md,并且把它作为每个 AI IDE 的“项目规则”注入。关键规则如下:
# 项目协作规则(rules/spec.md) - UI 层禁止写业务判断,业务层禁止直接写原生选择器。 - 控件命名一律使用“页面前缀 + 功能名”,如 homeSearch、loginBtn。 - UI 方法统一返回 {found, reason} 结构,禁止返回 null。 - 等待操作必须调用 libs/auto.js 的 waitClick,禁止自行写 while + sleep。 - AI 输出代码时,必须说明函数来源:本文件定义 / ui 目录 / libs 目录。 - 如果 AI 认为需要新增函数,必须先输出新增说明,再在代码中实现。 - 所有 AI 生成的代码,默认假设会接受另一个助手审查。这份文档,相当于团队的“代码规范 + 接口协议”。每次新开对话,我会先让 AI 读一遍这份文件,再让它干活。用同一个规范约束多个模型,出来的代码才会有统一的风格。规则不在多,在于能被强制检查。
4. 拿来即用的三种 AI 提示模板
4.1 UI 层助手提示模板
如果你要请一个 AI 专门生成 UI 层,可以直接复制下面这段提示词,把其中的页面信息替换一下:
你是 EasyClick 项目的 UI 层代码生成助手。 项目规则见 rules/spec.md,请先阅读并严格遵守。 现有页面:首页 页面结构描述: - 顶部搜索框,占位文案“搜索你想找的商品” - 中间 Banner 区域,resource-id 为 home_banner - 底部 Tab 栏依次为:首页、分类、购物车、我的 - 右下角有悬浮入口,resource-id 为 float_action 请输出 app/ui/home.js: - 只定义选择器和 UI 操作封装; - 不允许写任何业务判断; - 每个方法返回 {found, reason}; - 控件命名以 home 前缀开头。这个模板的关键是给了 AI 三个东西:角色定位、页面事实、输出约束。角色定位让它知道自己是 UI 层,不会跑偏去写流程;页面事实是真实信息,避免 AI 编造控件;输出约束则对齐了整体架构。
4.2 业务层助手提示模板
业务层更适合把需求写成一个“用例式流程”,让 AI 基于流程来编排:
你是 EasyClick 项目的业务层代码生成助手。 项目规则见 rules/spec.md,请先阅读项目规则。 流程:每日签到任务 前置条件:App 已登录,首页已完成加载 步骤: 1. 如果首页存在“签到中心”入口,点击进入。 2. 进入后查找“立即签到”按钮并点击。 3. 点击后 3 秒内,页面应出现“签到成功”提示。 4. 如果提示未出现,重新点击一次;再次失败则截图并记录日志。 5. 完成后返回首页,并保存本次签到状态到日志文件。 请输出 flows/task_sign.js: - 只调用 ui.* 模块,禁止直接写原生选择器; - 所有点击使用 waitClick 等待; - 返回结果统一为 {success, reason}。“用例式”写法比“帮我做个签到”清晰得多。AI 能把每个判断点翻译成代码,不会漏掉关键步骤,也不会脑补需求。如果你需求里有多条分支,一定要在流程里写清楚 when / then。
4.3 全局审查助手提示模板
第三类模板用于代码审查,它是整个多 AI 并行体系里的“保险丝”:
你是一名自动化项目代码审查员。 项目规则见 rules/spec.md,请先阅读。 请检查 app 目录: 1. 是否存在 UI 层写业务判断、业务层写原生选择器的情况; 2. 是否存在重复定义的控件名; 3. 所有点击操作是否都有超时保护; 4. 是否有 AI 直接调用不存在的函数; 5. 业务层返回结果是否符合 {success, reason} 约定; 最后按严重程度输出风险清单。这类审查类提示词,建议固定成项目级模板,不要每次重写。审查助手不需要很强的代码生成能力,但需要能跨文件找矛盾。实际用下来,它的精确度相当高。
4.4 模板设计里的几个细节
有几个细节值得留意:
- 每个模板都先要求 AI “阅读规则文件”,这能拉齐所有工具对项目的认知;
- 模板里明确列出“允许做什么、禁止做什么”,比只让 AI 自由发挥稳定得多;
- 模板末尾要求“统一返回结构”,是为了让审查环节有固定的检查依据;
- 模板里不要用模糊词,比如“尽量、大概、可能”,这些词会让 AI 产生不稳定的判断。
这些模板不是一成不变的。随着项目变大,可以继续往里面追加“设备配置说明”“日志约定”等章节,但核心的三大块——角色、事实、约束——始终不要丢。
5. 实操篇:多 IDE 并行的完整工作流
5.1 UI 层:截图、控件树、描述三件套
拿 UI 层举例,标准操作步骤是:
- 打开界面分析工具,导出目标页面的控件树 XML,或直接截一张清晰的页面图。
- 把截图、XML、以及一两句“人话”需求同时发给助手 A。比如“首页顶部有搜索框,占位文案是搜索你想找的商品;底部标签栏是首页、分类、购物车、我的”。
- 让助手 A 输出符合
rules/spec.md的 UI 文件。 - 人工逐项核对资源 id、text、desc 与界面实际是否一致。
- 核对完的文件提交到
app/ui/。
这五步里,最关键的是第 4 步。AI 偶尔会脑补一个不存在的控件,或者把两个控件混在一起。因为 UI 文件是业务层的地基,这里出错后期会很头痛。
5.2 业务层:用“用例式描述”驱动流程实现
业务层给 AI 的输入,不要直接说“给我写个函数执行订阅任务”,而是给一份接近测试用例的流程描述。比如:
流程:搜索商品 前置条件:已登录,首页加载完成 步骤: 1. 点击首页顶部搜索框。 2. 输入关键词“蓝牙耳机”。 3. 点击键盘上的搜索按钮。 4. 等待结果列表出现,最多等 15 秒。 5. 如果列表为空,重新触发一次搜索;再次为空则截图并记录日志。然后把这段描述交给助手 B,加上一句“按 rules/spec.md 实现到 flows/task_search.js”。
这种“用例式”写法的好处,是流程里的每个判断点都会被 AI 明确翻译成代码逻辑。它比“帮我做个搜索”这种口头需求清晰得多,AI 不至于漏掉关键步骤,也不会自己脑补一些奇怪的需求。
5.3 审查环节:长上下文助手当“巡查员”
当 A 和 B 分别产出一批代码后,我会在助手 C 的工作区打开整个项目目录,给它一个固定角色:“你是一名自动化项目代码审查员”。让它检查 UI 层是否写了业务判断、业务层是否直接写了原生选择器、有没有重复控件名,以及所有点击是否带超时。
这相当于给项目安排了一个客观的“代码评审”。助手 C 不太擅长从零写功能,但让它跨文件找矛盾,往往很精准。有几次两个层级的代码各自看着都对,一对接就报错,都是它提前抓出来的。审查出的问题不要直接让 C 改,最好是收回对应模块让 A 或 B 修,职责边界不能乱。
5.4 分支隔离:四个工具并行时的版本管理
除了逻辑上分层,物理上也要隔离。我强烈建议用 Git 分支管理不同 AI 的工作区:
main分支:始终是可运行的稳定版本。feature/ui-xxx分支:助手 A 的改动。feature/flow-xxx分支:助手 B 的改动。feature/review分支:助手 C 审查意见汇总和修复分支。
每个分支只改自己的目录,合并时重点检查接口部分,冲突大大减少。之前没做分支隔离时,两个 AI 同时在根目录写文件,覆盖核心公共函数的情况出现过不止一次,教训挺深刻。
6. 多 AI 协作的翻车与排查实录
6.1 控件名漂移:A 定义的,B 找不到
最常遇到的问题就是命名漂移。助手 A 在ui/home.js里写了homeTopSearch,助手 B 在业务代码里预期的是searchEntry,两者一看,引用不存在的控件。后来我在app/ui/index.js里做了一次“控件登记”,把所有 UI 方法统一导出。然后要求业务层助手必须先从这个入口 import,不允许自创控件名。这就等于给 IDE 加了一个中央注册表,问题根除。
6.2 等待逻辑乱写:死等和抢跑
AI 生成代码时,特别喜欢自己写sleep(1000)和while(true),这在自动化里非常危险。页面还没出来就点击,叫抢跑;无脑死等,叫死等。我的应对是封装libs/auto.js的waitClick和waitSelector,并规定所有 AI 代码只能调这两个函数,不许自己写等待循环。
// libs/auto.js function waitClick(selector, timeout = 10) { let waited = 0; while (waited < timeout) { if (exists(selector)) { click(selector); return { found: true, reason: "ok" }; } sleep(1000); waited++; } return { found: false, reason: "timeout" }; }有同学会问,用系统自带的 click 不就行了吗?问题是系统自带的 click 在你调用时元素可能还没渲染出来,而 AI 又特别喜欢在前后两个操作之间补一个随机 sleep。与其让 AI 乱来,不如集中管理等待策略。
6.3 幻觉函数:调用了一个不存在的 API
业务层 AI 有时候会输出clickButtonByText("立即签到"),看着像模像样,但项目里根本没这个函数。规范里的一条就是“函数必须有来源”。AI 只能调用三类函数:当前文件内定义的、从 ui 模块导入的、从 libs 导入的。如果它觉得缺函数,必须先说明并新增,再调用。这招基本治住了 AI 自嗨。
如果你遇到某个 AI 助手总会写幻觉函数,建议在提示词里加一句“如果调用不存在的函数,将导致运行失败,你必须严格使用已有函数”。多数情况下,加上这句话之后情况会明显改善。
6.4 并行合并冲突:三个物理预防手段
- 物理目录隔离:UI 和业务、公共库放不同的目录;
- Git 分支隔离:不同助手在不同分支作业;
- 统一发布构建:用构建脚本把多个模块打包成单个
dist/release.js,不手动在发布脚本里改代码。
有了这三层,合并冲突从“每次都有”变成“很少出现”。剩下的零散冲突,多半是修改了同一个公共方法,走一次代码审查就能解决。
7. 工程化之后的长期红利
7.1 把自己从“写脚本”升级成“搭框架”
工程化做到位之后,项目的每个模块其实都可以沉淀下来。我现在做新项目,第一件事不是从零写代码,而是先复制一套自己的libs/和rules/,然后针对具体业务填 UI 层和流程层。一个基础框架复用三四个项目之后,开发速度会变得肉眼可见地快。公共层代码慢慢变成自己的“资产”,不再是一个项目一份代码。
7.2 preflight 自检:浪费十几分钟之前先排雷
页面改版是自动化项目最大的敌人。AI 时代也一样,谁能更快发现“元素不存在”,谁就省时间。我在业务层加了一个启动自检函数:
function preflight() { const required = [ ui.home.hasHomeEntry(), ui.login.hasLoginInput(), ui.order.hasOrderList(), ]; const failed = required.filter((r) => !r.found); if (failed.length > 0) { logger.error("preflight failed", failed); return false; } return true; }每次正式执行前先跑一遍preflight。如果没有自检,一个小页面改版就能让脚本白跑十几分钟;有了它,几秒钟就能判断该不该继续。这个习惯很浅显,但不少项目都忽略了。
7.3 规则文档同时约束 AI 也约束新人
很多人以为rules/spec.md只是给 AI 看的,其实不是。它的价值在于让所有参与项目的人都在同一套约定下工作。新同事或新伙伴接手项目,先读规则文档再读代码,上手速度快不少。可以说,这份文档是整个工程从“个人脚本”走向“可以被协作维护的项目”的标志。
提示:规则文档建议放在项目根目录,命名固定为
rules/spec.md。这样不管谁来接手,打开项目第一眼就能看到“这个项目是怎么被约束的”。
8. 最后分享几个自己的习惯
把多 AI IDE 并行这套玩法跑顺之后,我印象最深的不是生成速度,而是“收尾成本”降下来了。以前改一处功能要全局搜代码,现在只要顺着 UI 和 flows 的边界找,定位通常在一分钟内。
我个人比较受用的几个习惯:
- 新项目启动时,先花半天写清楚
rules/spec.md,后面省下的时间绝对大于这半天; - 每个 AI 助手开新对话前,都重新粘贴一次项目结构和规则文件,别指望它记住上次的会话;
- 不同层级的代码交给不同工具不是固定的,哪一环节产出不稳定,就换工具试试;
- 自动化脚本的核心依然是可维护性,不要为了追求“一次生成完”而跳过审查。
如果你手头正好有一个规模上来的 EasyClick 项目,建议先别急着让 AI 帮你加功能。先把目录拆开,定好 UI 与业务的边界,再引入多工具并行。这套组合跑顺之后,你大概率也会觉得,传统那种把所有代码堆一个文件里的做法,真的回不去了。