跳至内容

构建插件

本指南是关于开发 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 plugin install --source=npm <plugin-package-on-npm>

当然,最好先测试您的插件。在 Appium 中查看它如何工作的一种方法是先在本地安装它

appium plugin install --source=local /path/to/your/plugin

当然,插件必须在 Appium 服务器启动期间“激活”,因此请确保您指导用户这样做

appium --use-plugins=plugin-name

开发您的插件

如何开发您的插件由您决定。但是,从 Appium 内部运行它很方便,而不必进行大量的发布和安装。最直接的方法是将最新版本的 Appium 作为 devDependency 包含在内(尽管在较新版本的 NPM 中,它已经被包含为 peerDependency 就足够了),然后还包含您自己的插件,如下所示

{
    "devDependencies": {
        ...,
        "appium": "^2.0.0",
        "your-plugin": "file:.",
        ...
    }
}

现在,您可以在本地运行 Appium(npm exec appiumnpx appium),并且由于您的插件与它一起列为依赖项,因此它将被自动“安装”并可用。您可以这样设计您的 e2e 测试,或者如果您用 Node.js 编写它们,您可以简单地导入 Appium 的启动服务器方法来处理在 Node 中启动和停止 Appium 服务器。

当然,您也可以像上面描述的那样在本地安装它。

每当您对插件代码进行更改时,都需要重新启动 Appium 服务器以确保它获取最新的代码。与驱动程序一样,您可以设置 APPIUM_RELOAD_EXTENSIONS 环境变量,如果您希望 Appium 在新会话启动时尝试重新加载您的插件模块。

标准插件实现思路

这些是您在创建插件时可能想要做的事情。

在构造函数中设置状态

如果您定义了自己的构造函数,则需要调用 super 以确保所有标准状态都正确设置

constructor(...args) {
    super(...args);
    // now do your own thing
}

这里的 args 参数是包含用于启动 Appium 服务器的所有 CLI 参数的对象。

拦截和处理特定的 Appium 命令

这是 Appium 插件最常见的行为——修改或替换通常由活动驱动程序处理的一个或多个命令的执行。要覆盖默认的命令处理,您需要在您的类中实现与要处理的 Appium 命令同名的 async 方法(就像 驱动程序本身的实现方式 一样)。好奇有哪些命令名称?它们在 Appium 基本驱动程序的 routes.js 文件中定义,当然您也可以在下一节中定义更多命令。

每个命令方法都发送以下参数

  1. next:这是一个对 async 函数的引用,该函数封装了如果此插件没有处理命令将发生的的一系列行为。您可以在逻辑中的任何点选择调用链中的下一个行为(确保在某处包含 await next()),也可以不调用。如果您不调用,则意味着默认行为(或在此插件之后注册的任何插件)将不会运行。
  2. driver: 此对象代表处理当前会话的驱动程序。您可以访问它以执行任何所需的操作,例如调用其他驱动程序方法、检查功能或设置等。
  3. ...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,任何未由您的命名方法处理的命令都将由此方法处理。它接受以下参数(语义与上面相同)

  1. next
  2. driver
  3. cmdName - 表示正在运行的命令的字符串
  4. ...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的特殊成员函数,它接受以下参数

  1. method - 表示 HTTP 方法的字符串(GETPOST 等)
  2. route - 表示请求资源的字符串,例如/session/8b3d9aa8-a0ca-47b9-9ab7-446e818ec4fc/source
  3. body - 表示 WebDriver 请求主体的任何类型的可选值

这些参数定义了一个传入请求。如果您想在插件中处理通常会直接通过驱动程序代理的命令,您可以禁用或“避免”代理请求,而是让请求进入典型的 Appium 命令执行流程(从而进入您自己的命令函数)。要避免代理请求,只需从shouldAvoidProxy返回true。有关此方法如何使用的一些示例,请参见通用 XML 插件(我们希望避免代理getPageSource命令),或图像插件(我们希望有条件地避免代理任何命令,如果它看起来包含图像元素)。

抛出 WebDriver 特定错误

WebDriver 规范定义了一组错误代码,用于在命令响应中附带错误代码。Appium 为每个代码创建了错误类,因此您可以从命令内部抛出适当的错误,它将在协议响应方面对用户执行正确操作。要访问这些错误类,请从appium/driver导入它们

import {errors} from 'appium/driver';

throw new errors.NoSuchElementError();

将消息记录到 Appium 日志

当然,您始终可以使用console.log,但 Appium 为您提供了一个不错的记录器,即this.logger(它具有.info.debug.log.warn.error 方法,用于不同的日志级别)。如果您想在插件上下文之外(例如在脚本或辅助文件中)创建 Appium 记录器,您始终可以构建自己的记录器

import {logging} from 'appium/support';
const log = logging.getLogger('MyPlugin');

Appium 插件的更多可能性

这些是您的插件可以做的事情,以利用额外的插件功能或更方便地完成其工作。

为自定义命令行参数添加模式

如果您希望插件在启动 Appium 服务器时从命令行接收数据(例如,服务器管理员应设置的端口,不应作为功能传递),则可以添加自定义 CLI 参数。

这在很大程度上与插件对驱动程序的作用相同,因此有关更多详细信息,请查看构建驱动程序文档中的等效部分

唯一的区别是,要构造 CLI 参数名称,您需要在前面加上--plugin-<name>。例如,如果您有一个名为pluggo的插件,并且一个 CLI 参数定义为electro-port,则可以在启动 Appium 时通过--plugin-pluggo-electro-port设置它。

还支持通过配置文件设置参数,就像对驱动程序一样,但位于plugin字段下。例如

{
  "server": {
    "plugin": {
      "pluggo": {
        "electro-port": 1234
      }
    }
  }
}

添加插件脚本

有时您可能希望插件用户能够在会话上下文之外运行脚本(例如,运行预构建插件方面的脚本)。为了支持这一点,您可以将脚本名称和 JS 文件的映射添加到 Appium 扩展元数据中的scripts字段中。因此,假设您在项目中创建了一个脚本,该脚本位于项目中的scripts目录中,名为plugin-prebuild.js。然后,您可以添加一个scripts字段,如下所示

{
    "scripts": {
        "prebuild": "./scripts/plugin-prebuild.js"
    }
}

现在,假设您的插件名为myplugin,插件用户可以运行appium plugin run myplugin prebuild,您的脚本将执行。

添加新的 Appium 命令

如果您想提供不映射到驱动程序支持的任何现有命令的功能,您可以通过两种方式之一创建新命令,就像对驱动程序一样

  1. 扩展 WebDriver 协议并创建客户端插件以访问扩展
  2. 通过定义执行方法来重载执行脚本命令

如果您想遵循第一条路径,您可以指示 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);
}

我们在这里展示了三个重要的组件,使该系统能够正常工作,所有这些组件都在插件类中定义

  1. executeMethodMap,与驱动程序的定义方式相同
  2. executeMethodMap中定义的命令方法的实现(在本例中为plugMeIn
  3. execute命令的覆盖/处理。就像任何插件命令处理程序一样,前两个参数是nextdriver,后面是脚本名称和参数。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,您可以实现类似以下内容

async onUnexpectedShutdown(driver, cause) {
  try {
    // do some cleanup
  } catch (e) {
    // log any errors; don't allow anything to be thrown as they will be unhandled rejections
  }
}