Appium Drivers 深入解析:从 BaseDriver 继承到多层代理架构
【免费下载链接】appiumCross-platform automation framework for all kinds of apps, built on top of the W3C WebDriver protocol项目地址: https://gitcode.com/GitHub_Trending/ap/appium
在 Appium 中,"driver"(驱动)是连接 WebDriver 协议与具体平台(iOS、Android、桌面、Web 等)之间的桥梁,它决定了 Appium 能否自动化某个平台。本文以 drivers.md 为核心,系统讲解 driver 的本质——一个继承自BaseDriver的 Node.js 类,并深入剖析其命令实现机制、平台自动化映射、多层架构与代理(Proxy)模式。读完你将理解 driver 的内部工作方式,掌握在测试遇到问题时快速定位故障层的思路,甚至具备编写自己的 Appium driver 的基础知识。
一、什么是 Appium Driver
正如 Appium in a Nutshell 所述,Appium 被拆分为四个部分:Appium Core(定义核心 API)、Drivers(实现到具体平台的连接)、Clients(在特定语言中实现 Appium API)与Plugins(改变或扩展 Appium 核心功能)。Driver 在其中扮演的角色是回答一个根本问题:如何支持对多个互不相关的平台进行自动化?
对大多数使用者而言,了解 driver 内部机制的收益主要体现在调试上:当测试遇到问题时,熟悉 driver 的典型复杂度与架构能帮助你快速定位问题出在哪一层。而对想要编写自己的 driver 或为现有 driver 贡献代码的开发者,这部分内容则是必修课。
二、最小可用的 Driver:继承 BaseDriver
从技术角度看,一个 Appium driver 本质上就是继承自 Appium 内置的BaseDriver类的 Node.js 类。甚至可以写出一个"几乎可用"的最小 driver:
import BaseDriver from '@appium/base-driver' class MyNewDriver extends BaseDriver { }这个空 driver 本身不做任何事情,但你可以把它打包成一个 Node.js 模块,在package.json中加上 Appium 相关的字段,然后通过appium driver install安装使用。
在仓库中,BaseDriver的实现位于 packages/base-driver/lib/basedriver/driver.ts,它继承自DriverCore并实现了@appium/types中的Driver接口。BaseDriver的核心价值在于:它封装了整个 WebDriver 协议。因此,一个 driver 要做有用的事情,只需要实现名字与 WebDriver 协议命令一一对应的 Node.js 方法即可。
从源码看,这种"方法名即协议命令"的机制体现在executeCommand方法中(driver.ts):它以命令字符串为键,从 driver 实例上直接查找同名方法(const command = invoker[cmd]),如果找不到就抛出NotYetImplementedError。这就是为什么 driver 作者只需按约定命名方法,而无需关心 HTTP 路由细节。
如何实现一个协议命令:以 Navigate To 为例
假设我们想用上面这个空 driver 做点什么,第一步是决定要实现哪个 WebDriver 命令。以 W3C 规范的 Navigate To(导航到)命令为例:只需在 driver 类中定义一个setUrl方法:
async setUrl(url) { // 在这里做任何你想做的事 }你可能会疑惑:setUrl和 "Navigate To" 看起来毫无关联,怎么知道要用这个方法名?答案在于 Appium 的协议到方法名的映射表。在@appium/base-driver包中,这个映射定义在 packages/base-driver/lib/protocol/routes/w3c.ts:
'/session/:sessionId/url': { POST: {command: 'setUrl', payloadParams: {required: ['url']}}, },即 HTTP 路由POST /session/:sessionId/url被映射到方法名setUrl,并声明必需参数url。这个仓库中的路由定义即是原文档提到的routes.js在当前版本中的形态(位于packages/base-driver/lib/protocol/routes/目录下,按w3c.ts、mjsonwp.ts、jsonwp.ts、appium.ts等分文件组织)。所以编写 driver 时,去这个目录查方法名与参数即可;参考任何主流 Appium driver 的源码也是好办法。
命令如何实现完全由 driver 作者决定,取决于要支持的平台。同一个 Navigate To 命令在不同平台的典型实现差异很大:
- 浏览器:执行 JavaScript 设置
window.location.href - iOS 应用:通过 deep link 启动应用
- Android 应用:通过 deep link 启动应用
- React 应用:加载特定路由
- Unity:跳转到指定场景
可以看到,同一个 WebDriver 命令在不同平台上的实现千差万别;但不变的是它们表达"能处理某个协议命令"的方式——即实现同名方法。
仓库中的 FakeDriver 就是一个很好的参考实现:它在 packages/fake-driver/lib/driver.ts 中继承BaseDriver,通过desiredCapConstraints声明自己的能力约束,并以模块化方式(commands/目录下的alert.ts、contexts.ts、element.ts、find.ts、general.ts)将一组方法挂载到类上,例如getUrl、bidiNavigate、click、findElement等。
需要强调的一点是:driver 本身并不天生属于任何特定平台,它只是一段能够处理 WebDriver 协议命令的 JS 代码。它最终变成什么,取决于 driver 作者的后续设计。
三、自动化映射:从 WebDriver 协议到平台 API
通常情况下,driver 作者的目标是为某个(或多个)平台提供与浏览器 WebDriver 规范语义高度一致的自动化行为:查找元素时返回 UI 元素的引用,点击或轻触元素时的行为应与人手操作一致,如此等等。
因此,driver 作者面临的真正挑战不是如何使用 WebDriver 协议(BaseDriver已经封装好了这一切),而是如何在目标平台上真正执行自动化。每个 driver 都依赖自己的一套底层技术:
- iOS driver 使用苹果的XCUITest技术;
- UiAutomator2 driver 不仅依赖 Google 的UiAutomator2技术,还依赖只有通过ADB才可用的功能,以及一个辅助 App 中只有通过 Android SDK 才可用的功能。
这些底层自动化技术通常拥有专有或特立独行的 API。编写 driver 的工作就变成:把 WebDriver 协议映射到这个底层 API(有时是一组不同的底层 API),再把它们整合成统一、可用的 WebDriver 接口——这正是 driver 开发中最有价值也最具挑战性的艺术。
值得注意的是,如今BaseDriver还通过 packages/base-driver/lib/basedriver/extension-core.ts 提供了对 WebDriver BiDi 命令的支持:driver 可以声明自己的 BiDi 模块与命令(bidiCommands),并通过executeBidiCommand按moduleName.methodName的形式执行,最终同样落到以协议约定命名的方法处理器上。这让 driver 在传统 WebDriver 命令之外,还能扩展支持新一代的 BiDi 自动化能力(FakeDriver 中即有NEW_BIDI_COMMANDS的示例)。
四、多层架构:以 XCUITest driver 为例
在实践中,driver 架构往往相当复杂。以 iOS 为例:XCUITest 框架要求调用它的代码用 Objective-C 或 Swift 编写,而且 XCUITest 代码只能在 Xcode(直接或间接地,包括 Xcode 命令行工具)触发的特殊模式下运行。换句话说,不存在一条从 Node.js 函数实现(如上面的setUrl)直达 XCUITest API 调用的简单路径。
XCUITest driver 作者采取的方案是把 driver 拆成两部分:
- Node.js 部分:被集成进 Appium,最初处理 WebDriver 命令;
- Objective-C 部分:真正运行在 iOS 设备上、发起 XCUITest API 调用。
这种拆分让对接 XCUITest 成为可能,但也引入了新问题:两部分之间如何协调通信?driver 作者最终选择的通信方式出人意料又顺理成章——还是 WebDriver 协议!Objective-C 那一半本身就是 WebDriver 实现,它叫做WebDriverAgent。
理论上,你甚至可以把 WebDriver 客户端直接指向 WebDriverAgent 而完全绕过 Appium。但这通常并不方便,原因有二:
- Appium XCUITest driver 会为你构建和管理 WebDriverAgent,这本身涉及 Xcode,比较麻烦;
- XCUITest driver 能做的远不止 WebDriverAgent 的能力范围,例如管理模拟器或真机、安装 App 等。
这个例子的启示是:由于问题的本质,driver 架构可能变得相当复杂、多层化。这也意味着,当某个测试出问题时,有时很难判断故障出在整个技术链的哪一环。以 XCUITest 世界为例,同一时刻可能牵涉的技术栈包括:
- 你的测试代码(某种编程语言)——归你所有
- Appium 客户端库——归 Appium
- Selenium 客户端库——归 Selenium
- 网络(本地或互联网)
- Appium 服务器——归 Appium
- Appium XCUITest driver——归 Appium
- WebDriverAgent——归 Appium
- Xcode——归 Apple
- XCUITest——归 Apple
- iOS 本身——归 Apple
- macOS(运行 Xcode 与 iOS 模拟器)——归 Apple
这是一条相当深的调用栈!排查问题时,沿着这条链路逐层检查(测试代码 → 客户端库 → 网络 → Appium 服务器 → driver → 下游 WebDriver 服务 → 系统 API),往往能快速定位问题所在。
五、代理模式:把命令直接转发给下游 WebDriver
除了多层架构,driver 还有一个重要的架构特性,可以用 XCUITest driver 再次说明。既然 driver 的"两半"都讲 WebDriver 协议——Node.js 一半直接嵌入 Appium 的 WebDriver 服务器,Objective-C 一半(WebDriverAgent)则是独立的 WebDriver 实现——这就为 Appium 在某些情况下"抄近路"创造了可能。
设想 XCUITest driver 需要实现 Click Element 命令。其内部实现无非是:取适当参数,向 WebDriverAgent 服务器构造一个 HTTP 请求。这本质上就是把客户端最初发给 Appium 服务器的调用重新构造了一遍(不完全相同——Appium 服务器与 WebDriverAgent 服务器会生成不同的 session ID,但这些差异会被透明处理)。既然如此,根本没有必要为 Click Element 编写实现函数;XCUITest driver 只需让 Appium 知道:这个命令应该被直接代理到某个下游 WebDriver 服务器。
这里的"代理(proxying)"含义是:XCUITest driver 完全不会参与该命令的处理,命令在协议层面被重新打包并转发给 WebDriverAgent;WebDriverAgent 的响应也直接回传给客户端,中间没有任何 XCUITest driver 的代码看到或修改它。
这个架构模式给"处处使用 WebDriver 协议、而非自造专有协议"的 driver 作者带来了很好的红利:Appium 可以非常容易地为任何其他现存的 WebDriver 实现创建包装型(wrapper)driver。例如 Appium Safari driver 几乎没有实现任何标准命令,因为所有这些命令都被直接代理到下游的 SafariDriver 进程。
理解这个代理行为很重要:当你深入某个开源 driver 代码想弄清某个命令是在哪里实现的时候,可能会惊讶地发现 Node.js driver 代码里根本没有实现!这种情况下,你需要弄清楚命令被代理到了哪里,然后去那个位置寻找真正的实现。
在仓库源码中,代理能力由 packages/base-driver/lib/jsonwp-proxy/proxy.ts 中的JWProxy类提供:它封装了向"下游服务器"(downstream server)转发请求所需的一切——协议转换(ProtocolConverter)、请求超时(默认DEFAULT_REQUEST_TIMEOUT = 240000毫秒)、会话 ID 处理、HTTP/HTTPS 连接池(keepAlive默认开启)等。同时,DriverCore 定义了proxyActive()接口,driver 可以借此告知 Appium 当前会话是否处于代理模式。FakeDriver 中也有完整的演示:它在 driver.ts 中实现了proxyActive、canProxy、proxyReqRes与proxyCommand四个方法,用于模拟"命令被转发给下游"的行为,相关的验证测试可以在 packages/fake-driver/test/e2e/driver.e2e.spec.ts 中找到。
六、小结:从理解到实战
回顾全文,driver 的核心知识可以归纳为四点:
- 本质:driver 就是继承自
BaseDriver的 Node.js 类,BaseDriver封装了整个 WebDriver 协议; - 命令实现:实现与协议命令同名的方法(如
setUrl),路由到方法名的映射见 packages/base-driver/lib/protocol/routes/ 目录;方法未实现时会被BaseDriver以NotYetImplementedError拒绝; - 平台自动化:真正的工作量在于把 WebDriver 语义映射到平台专有 API(XCUITest、UiAutomator2 + ADB 等);
- 架构模式:driver 常被拆成多层(Node.js + 平台侧),层间甚至用 WebDriver 协议通信,并且大量命令可走代理模式直接转发给下游 WebDriver 服务器。
对普通用户而言,理解这些细节的意义在于调试:当测试失败时,你可以顺着"测试代码 → 客户端库 → 网络 → Appium → driver → 平台自动化服务"这条链逐层排查,并留意那些"在 driver 源码里根本找不到实现"的命令——它们很可能被代理到了下游服务。而对想参与 driver 生态的开发者,可以参考 packages/fake-driver 这一完整的参考实现,从继承BaseDriver、声明desiredCapConstraints、挂载命令方法开始,写自己的第一个 driver,并通过appium driver install安装使用。
【免费下载链接】appiumCross-platform automation framework for all kinds of apps, built on top of the W3C WebDriver protocol项目地址: https://gitcode.com/GitHub_Trending/ap/appium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考