构建插件
本指南是关于开发 Appium 插件的高级指南,大多数 Appium 用户不需要了解或关心它。如果您还不熟悉 Appium 插件的用户视角,请查看 插件列表,尝试使用一些插件,并了解插件可以做些什么。插件是一个强大的系统,可以增强 Appium 的功能或改变 Appium 的工作方式。它们可以分发给其他 Appium 用户,并以各种有趣的方式扩展 Appium 的生态系统!(这里与开发 Appium 驱动程序有很大的重叠,因此您可能还想查看 构建驱动程序 指南以获得更多灵感。)
在创建插件之前¶
在创建插件之前,最好对您希望插件完成什么以及在 Appium 平台的限制下是否可以实现它有一个大致的了解。阅读本指南将帮助您了解哪些是可能的。总的来说,Appium 的插件系统非常强大,没有尝试人为地限制插件的可能性(这是所有插件都由负责启动 Appium 服务器的系统管理员选择加入的主要原因——插件很强大,只有在明确信任的情况下才应使用!)。
其他可供参考的插件¶
有各种各样的开源 Appium 插件可供参考。在开始编写自己的插件之前,强烈建议您探索其他一些插件的代码。Appium 团队在 Appium GitHub 仓库 中维护着一组官方插件。其他开源插件的链接可以在 插件列表 中找到。
插件的基本要求¶
如果您希望您的插件成为有效的 Appium 插件,那么它必须满足以下要求。
具有 Appium 扩展元数据的 Node.js 包¶
所有 Appium 插件本质上都是 Node.js 包,因此必须具有有效的 package.json
。您的驱动程序并不局限于 Node.js,但它必须提供一个用 Node.js 编写的适配器,以便 Appium 可以加载它。
您的 package.json
必须包含 appium
作为 peerDependency
。对依赖项版本的依赖关系应尽可能宽松(除非您恰好知道您的插件只适用于特定版本的 Appium)。例如,对于 Appium 2,这将类似于 ^2.0.0
,声明您的插件适用于任何以 2.x 开头的 Appium 版本。
您的 package.json
必须包含一个 appium
字段,如下所示(我们称之为“Appium 扩展元数据”)
```json
{
...,
"appium": {
"pluginName": "fake",
"mainClass": "FakePlugin"
},
...
}
```
必需的子字段是
pluginName
:这应该是您的插件的简短名称。mainClass
:这是您main
字段的命名导出(以 CommonJS 样式)。它必须是一个扩展 Appium 的BasePlugin
的类(见下文)。
扩展 Appium 的 BasePlugin
类¶
最终,您的插件更容易编写,因为大部分定义覆盖命令模式的繁重工作都为您完成了。所有这些都编码为一个类,Appium 将其导出供您使用,称为 BasePlugin
。它从 appium/plugin
导出,因此您可以使用以下样式之一导入它并创建您自己的扩展它的类
import {BasePlugin} from 'appium/plugin';
// or: const {BasePlugin} = require('appium/plugin');
export class MyPlugin extends BasePlugin {
// class methods here
}
注意
在下面所有代码示例中,每当我们引用一个示例方法时,都假设它是在类内部定义的,尽管为了清晰和节省空间,没有明确写出来。
使您的插件可用¶
基本上就是这样!使用导出插件类的 Node.js 包以及正确的 Appium 扩展元数据,您就拥有了一个 Appium 插件!现在它还没有做任何事情,但是您可以在 Appium 中加载它,激活它等等……
要使其对用户可用,您可以通过 NPM 发布它。当您这样做时,您的插件可以通过 Appium CLI 安装
当然,最好先测试您的插件。在 Appium 中查看它如何工作的一种方法是先在本地安装它
当然,插件必须在 Appium 服务器启动期间“激活”,因此请确保您指导用户这样做
开发您的插件¶
如何开发您的插件由您决定。但是,从 Appium 内部运行它很方便,而不必进行大量的发布和安装。最直接的方法是将最新版本的 Appium 作为 devDependency
包含在内(尽管在较新版本的 NPM 中,它已经被包含为 peerDependency
就足够了),然后还包含您自己的插件,如下所示
现在,您可以在本地运行 Appium(npm exec appium
或 npx appium
),并且由于您的插件与它一起列为依赖项,因此它将被自动“安装”并可用。您可以这样设计您的 e2e 测试,或者如果您用 Node.js 编写它们,您可以简单地导入 Appium 的启动服务器方法来处理在 Node 中启动和停止 Appium 服务器。
当然,您也可以像上面描述的那样在本地安装它。
每当您对插件代码进行更改时,都需要重新启动 Appium 服务器以确保它获取最新的代码。与驱动程序一样,您可以设置 APPIUM_RELOAD_EXTENSIONS
环境变量,如果您希望 Appium 在新会话启动时尝试重新加载您的插件模块。
标准插件实现思路¶
这些是您在创建插件时可能想要做的事情。
在构造函数中设置状态¶
如果您定义了自己的构造函数,则需要调用 super
以确保所有标准状态都正确设置
这里的 args
参数是包含用于启动 Appium 服务器的所有 CLI 参数的对象。
拦截和处理特定的 Appium 命令¶
这是 Appium 插件最常见的行为——修改或替换通常由活动驱动程序处理的一个或多个命令的执行。要覆盖默认的命令处理,您需要在您的类中实现与要处理的 Appium 命令同名的 async
方法(就像 驱动程序本身的实现方式 一样)。好奇有哪些命令名称?它们在 Appium 基本驱动程序的 routes.js 文件中定义,当然您也可以在下一节中定义更多命令。
每个命令方法都发送以下参数
next
:这是一个对async
函数的引用,该函数封装了如果此插件没有处理命令将发生的的一系列行为。您可以在逻辑中的任何点选择调用链中的下一个行为(确保在某处包含await next()
),也可以不调用。如果您不调用,则意味着默认行为(或在此插件之后注册的任何插件)将不会运行。driver
: 此对象代表处理当前会话的驱动程序。您可以访问它以执行任何所需的操作,例如调用其他驱动程序方法、检查功能或设置等。...args
: 一个扩展数组,包含用户应用于命令的任何参数。
例如,如果我们想覆盖setUrl
命令,只需在顶部添加一些额外的日志记录,我们可以按如下方式实现
async setUrl(next, driver, url) {
this.log(`Let's get the page source for some reason before navigating to '${url}'!`);
await driver.getPageSource();
const result = await next();
this.log(`We can also log after the original behaviour`);
return result;
}
拦截并处理所有 Appium 命令¶
您可能会发现自己需要处理所有命令,以便检查有效负载以确定是否采取某种行动。如果是这样,您可以实现async handle
,任何未由您的命名方法处理的命令都将由此方法处理。它接受以下参数(语义与上面相同)
next
driver
cmdName
- 表示正在运行的命令的字符串...args
例如,假设我们想将所有 Appium 命令的计时记录为插件的一部分。我们可以通过在插件类中实现handle
来做到这一点,如下所示
async handle(next, driver, cmdName, ...args) {
const start = Date.now();
try {
const result = await next();
} finally {
const elapsedMs = Date.now() - start;
this.log(`Command '${cmdName}' took ${elapsedMs}`);
}
return result;
}
绕过驱动程序代理¶
处理 Appium 命令时存在一个问题。Appium 驱动程序能够开启一种特殊的“代理”模式,在这种模式下,Appium 服务器进程会查看传入的 URL,并决定是否将它们转发到上游 WebDriver 服务器。可能会发生插件想要处理的命令被指定为转发到上游服务器的命令。在这种情况下,我们遇到了问题,因为插件永远没有机会处理该命令!为此,插件可以实现一个名为shouldAvoidProxy
的特殊成员函数,它接受以下参数
method
- 表示 HTTP 方法的字符串(GET
、POST
等)route
- 表示请求资源的字符串,例如/session/8b3d9aa8-a0ca-47b9-9ab7-446e818ec4fc/source
body
- 表示 WebDriver 请求主体的任何类型的可选值
这些参数定义了一个传入请求。如果您想在插件中处理通常会直接通过驱动程序代理的命令,您可以禁用或“避免”代理请求,而是让请求进入典型的 Appium 命令执行流程(从而进入您自己的命令函数)。要避免代理请求,只需从shouldAvoidProxy
返回true
。有关此方法如何使用的一些示例,请参见通用 XML 插件(我们希望避免代理getPageSource
命令),或图像插件(我们希望有条件地避免代理任何命令,如果它看起来包含图像元素)。
抛出 WebDriver 特定错误¶
WebDriver 规范定义了一组错误代码,用于在命令响应中附带错误代码。Appium 为每个代码创建了错误类,因此您可以从命令内部抛出适当的错误,它将在协议响应方面对用户执行正确操作。要访问这些错误类,请从appium/driver
导入它们
将消息记录到 Appium 日志¶
当然,您始终可以使用console.log
,但 Appium 为您提供了一个不错的记录器,即this.logger
(它具有.info
、.debug
、.log
、.warn
、.error
方法,用于不同的日志级别)。如果您想在插件上下文之外(例如在脚本或辅助文件中)创建 Appium 记录器,您始终可以构建自己的记录器
Appium 插件的更多可能性¶
这些是您的插件可以做的事情,以利用额外的插件功能或更方便地完成其工作。
为自定义命令行参数添加模式¶
如果您希望插件在启动 Appium 服务器时从命令行接收数据(例如,服务器管理员应设置的端口,不应作为功能传递),则可以添加自定义 CLI 参数。
这在很大程度上与插件对驱动程序的作用相同,因此有关更多详细信息,请查看构建驱动程序文档中的等效部分。
唯一的区别是,要构造 CLI 参数名称,您需要在前面加上--plugin-<name>
。例如,如果您有一个名为pluggo
的插件,并且一个 CLI 参数定义为electro-port
,则可以在启动 Appium 时通过--plugin-pluggo-electro-port
设置它。
还支持通过配置文件设置参数,就像对驱动程序一样,但位于plugin
字段下。例如
添加插件脚本¶
有时您可能希望插件用户能够在会话上下文之外运行脚本(例如,运行预构建插件方面的脚本)。为了支持这一点,您可以将脚本名称和 JS 文件的映射添加到 Appium 扩展元数据中的scripts
字段中。因此,假设您在项目中创建了一个脚本,该脚本位于项目中的scripts
目录中,名为plugin-prebuild.js
。然后,您可以添加一个scripts
字段,如下所示
现在,假设您的插件名为myplugin
,插件用户可以运行appium plugin run myplugin prebuild
,您的脚本将执行。
添加新的 Appium 命令¶
如果您想提供不映射到驱动程序支持的任何现有命令的功能,您可以通过两种方式之一创建新命令,就像对驱动程序一样
- 扩展 WebDriver 协议并创建客户端插件以访问扩展
- 通过定义执行方法来重载执行脚本命令
如果您想遵循第一条路径,您可以指示 Appium 识别新方法并将它们添加到其允许的 HTTP 路由和命令名称集中。您可以通过将驱动程序类中的newMethodMap
静态变量分配给与 Appium 的routes.js
对象形式相同的对象来做到这一点。例如,以下是FakePlugin
示例驱动程序的newMethodMap
的一部分
static newMethodMap = {
'/session/:sessionId/fake_data': {
GET: {command: 'getFakeSessionData', neverProxy: true},
POST: {
command: 'setFakeSessionData',
payloadParams: {required: ['data']},
neverProxy: true,
},
},
'/session/:sessionId/fakepluginargs': {
GET: {command: 'getFakePluginArgs', neverProxy: true},
},
};
注意
如果您使用的是 TypeScript,则应将此类静态成员对象定义为as const
。
在此示例中,我们添加了一些新的路由和总共 3 个新命令。有关如何以这种方式定义命令的更多示例,最好查看routes.js
。现在您需要做的就是以与实现任何其他 Appium 命令相同的方式实现命令处理程序。
还要注意命令的特殊neverProxy
键;这通常是一个好主意,将其设置为true
,因为您的插件可能对处于代理模式但没有费心拒绝代理这些(新的,因此未知)命令的驱动程序处于活动状态。将neverProxy
设置为true
将导致 Appium 永远不会代理这些路由,从而确保您的插件处理它们,即使驱动程序处于代理模式。
通过newMethodMap
添加新命令的缺点是,使用标准 Appium 客户端的人员将没有设计用于定位这些端点的良好客户端函数。因此,您需要为要支持的每种语言创建和发布客户端插件(方向或示例可以在相关客户端文档中找到)。
另一种方法是重载所有 WebDriver 客户端都可以访问的命令:执行脚本。请务必阅读有关在构建驱动程序指南中添加新命令的部分,以了解此方法的一般工作原理。它在插件中的工作原理略有不同。让我们看一个来自 Appium 的fake-plugin
的示例
static executeMethodMap = {
'fake: plugMeIn': {
command: 'plugMeIn',
params: {required: ['socket']},
},
};
async plugMeIn(next, driver, socket) {
return `Plugged in to ${socket}`;
}
async execute(next, driver, script, args) {
return await this.executeMethod(next, driver, script, args);
}
我们在这里展示了三个重要的组件,使该系统能够正常工作,所有这些组件都在插件类中定义
executeMethodMap
,与驱动程序的定义方式相同- 在
executeMethodMap
中定义的命令方法的实现(在本例中为plugMeIn
) execute
命令的覆盖/处理。就像任何插件命令处理程序一样,前两个参数是next
和driver
,后面是脚本名称和参数。BasePlugin
实现了一个辅助方法,我们可以简单地使用所有这些参数调用它。
从驱动程序中重载执行方法按预期工作:如果您的插件定义了与驱动程序同名的执行方法,则您的命令(在本例中为plugMeIn
)将首先被调用。如果您愿意,可以选择通过next
运行驱动程序的原始行为。
构建 Appium Doctor 检查¶
您的用户可以运行appium plugin doctor <pluginName>
来运行安装和运行状况检查。访问构建 Doctor 检查指南以获取有关此功能的更多信息。
更新 Appium 服务器对象¶
您通常不需要更新 Appium 服务器对象(这是一个Express 服务器,已经以各种方式配置)。但是,例如,您可以向服务器添加新的 Express 中间件以支持插件的要求。要更新服务器,您必须在类中实现static async updateServer
方法。此方法接受三个参数
expressApp
: Express 应用对象httpServer
: Node HTTP 服务器对象cliArgs
: 用于启动 Appium 服务器的 CLI 参数的映射
您可以在updateServer
方法中对它们执行任何操作。您可能希望参考这些对象在 BaseDriver 代码中的创建和使用方式,以便您知道您没有撤消或覆盖任何标准和重要的内容。但是,如果您坚持,您可以这样做,结果需要您测试!警告:这应该被视为一项高级功能,需要了解 Express,以及注意不要做任何可能影响 Appium 服务器其他部分操作的事情!
处理意外的会话关闭¶
在开发插件时,您可能希望添加一些清理逻辑,以便在会话结束时执行。您自然会通过添加deleteSession
的处理程序来做到这一点。这在大多数情况下都有效,除了会话没有干净地完成的情况。Appium 有时会确定会话意外结束,在这种情况下,Appium 会在您的插件类中查找名为onUnexpectedShutdown
的方法,该方法将被调用(将当前会话驱动程序作为第一个参数传递,将表示关闭原因的错误对象作为第二个参数传递),让您有机会采取可能必要的任何步骤来清理会话。例如,请记住该函数没有await
,您可以实现类似以下内容