为 WebThings 网关创建 UI 扩展

Mozilla 的 WebThings 网关0.10 版本 引入了对扩展类型插件的支持。此强大的新功能于上周发布,允许开发人员使用 JavaScript 和 CSS 根据自己的喜好修改用户界面 (UI)。

尽管初始的扩展 API 集相当小,但我们相信它们已经能够实现大量功能。除了 UI 扩展之外,开发人员还可以使用自己的处理程序扩展网关的 REST API,例如允许后端分析。

在这篇文章中,我们将逐步介绍一个简单的示例,帮助您开始构建自己的扩展。

基础知识

如果您完全不熟悉为 WebThings 网关构建插件,则需要了解一些事项。

插件是一组与网关一起运行的代码。对于扩展,代码作为浏览器中的 UI 的一部分运行。插件可以提供各种功能,包括对新设备的支持、通过某个出口通知用户的能力,以及现在扩展用户界面。

插件以特定的方式 打包,然后可以发布到 插件列表,以便其他用户可以安装它们。为了获得最佳效果,开发人员应遵守这些 基本指南

此外,理论上可以使用任何语言编写插件,只要它们知道如何通过 IPC(进程间通信)与网关通信即可。我们为 Node.jsPython 提供了库。

新 API

您应该了解两组新的 API。

首先是前端 API。您的扩展应扩展 Extension 类,该类在浏览器窗口中是全局的。这使您可以访问所有新的 API。在此 0.10 版本中,扩展可以向顶级菜单添加新条目,并显示和隐藏顶级按钮。每个扩展都获得一个空块元素,它们可以根据需要绘制到该元素中,可以通过菜单条目或其他方式访问该元素。

其次是后端 API。插件可以注册一个新的 APIHandler。当对 /extensions/<extension-id>/api/* 发出经过身份验证的请求时,将使用请求信息调用您的 API 处理程序。它应发送回相应的响应。

基本示例

现在我们已经介绍了基础知识,让我们逐步介绍一个简单的示例。您可以在 GitHub 上找到此示例的代码。是否希望查看 Python 而不是 JavaScript 中的示例?它 在这里提供。

下一个示例非常基本:创建表单、提交表单并将结果作为 JSON 回显。

让我们继续创建我们的 API 处理程序。对于此示例,我们将只回显我们收到的内容。

const {APIHandler, APIResponse} = require('gateway-addon');
const manifest = require('./manifest.json');

/**
* Example API handler.
*/
class ExampleAPIHandler extends APIHandler {
  constructor(addonManager) {
    super(addonManager, manifest.id);
    addonManager.addAPIHandler(this);
  }

  async handleRequest(request) {
    if (request.method !== 'POST' || request.path !== '/example-api') {
      return new APIResponse({status: 404});
    }

    // echo back the body
    return new APIResponse({
      status: 200,
      contentType: 'application/json',
      content: JSON.stringify(request.body),
    });
  }
}
module.exports = ExampleAPIHandler;

gateway-addon 库为 API 请求和响应提供了不错的包装器。您需要填写基本信息:状态代码、内容类型和内容。如果没有内容,您可以省略这些字段。

现在,让我们创建一个可以实际使用我们刚刚创建的新 API 的 UI。

(function() {
  class ExampleExtension extends window.Extension {
    constructor() {
      super('example-extension');
      this.addMenuEntry('Example Extension');

      this.content = '';
      fetch(`/extensions/${this.id}/views/content.html`)
        .then((res) => res.text())
        .then((text) => {
          this.content = text;
        })
        .catch((e) => console.error('Failed to fetch content:', e));
    }

    show() {
      this.view.innerHTML = this.content;

      const key =
        document.getElementById('extension-example-extension-form-key');
      const value =
        document.getElementById('extension-example-extension-form-value');
      const submit =
        document.getElementById('extension-example-extension-form-submit');
      const pre =
        document.getElementById('extension-example-extension-response-data');

      submit.addEventListener('click', () => {
        window.API.postJson(
          `/extensions/${this.id}/api/example-api`,
          {[key.value]: value.value}
        ).then((body) => {
          pre.innerText = JSON.stringify(body, null, 2);
        }).catch((e) => {
          pre.innerText = e.toString();
        });
      });
    }
  }

  new ExampleExtension();
})();

以上代码执行以下操作

  1. 为我们的扩展添加顶级菜单条目。
  2. 从服务器异步加载一些 HTML。
  3. 设置表单的事件侦听器以提交它并显示结果。

从服务器加载的 HTML 不是完整文档,而是一个片段,因为我们使用它来填充 <section> 标记。您可以在 JavaScript 中同步执行所有这些操作,但将视图内容分开可能很好。此插件的清单指示网关加载哪些资源以及哪些资源可以通过网络访问

{
  "author": "Mozilla IoT",
  "content_scripts": [
    {
      "css": [
        "css/extension.css"
      ],
      "js": [
        "js/extension.js"
      ]
    }
  ],
  "description": "Example extension add-on for Mozilla WebThings Gateway",
  "gateway_specific_settings": {
    "webthings": {
      "exec": "{nodeLoader} {path}",
      "primary_type": "extension",
      "strict_max_version": "*",
      "strict_min_version": "0.10.0"
    }
  },
  "homepage_url": "https://github.com/mozilla-iot/example-extension",
  "id": "example-extension",
  "license": "MPL-2.0",
  "manifest_version": 1,
  "name": "Example Extension",
  "short_name": "Example",
  "version": "0.0.3",
  "web_accessible_resources": [
    "css/*.css",
    "images/*.svg",
    "js/*.js",
    "views/*.html"
  ]
}

清单的 content_scripts 属性告诉网关将哪些 CSS 和 JavaScript 文件加载到 UI 中。同时,web_accessible_resources 告诉它哪些文件可以通过 HTTP 由扩展访问。此格式基于 WebExtension manifest.json 格式,因此,如果您曾经构建过浏览器扩展,它可能看起来很熟悉。

作为对开发人员的快速说明,现在所有插件都需要此新的 manifest.json 格式,因为它取代了旧的 package.json 格式。

测试插件

要进行测试,您可以在 Raspberry Pi 或开发机器上执行以下操作。

  1. 克隆存储库
    cd ~/.mozilla-iot/addons
    git clone https://github.com/mozilla-iot/example-extension
  2. 重新启动网关
    sudo systemctl restart mozilla-iot-gateway
  3. 通过在 UI 中导航到设置 -> 插件来启用插件。单击“示例扩展”的“启用”按钮。然后,您需要刷新页面才能使扩展在 UI 中显示,因为扩展是在页面首次加载时加载的。

总结

希望这对你有所帮助。示例本身不是很有用,但它应该为您提供一个很好的起点。

我们发现的另一个可能的用例是为复杂设备创建自定义 UI,其中自动生成的 UI 不尽如人意。例如,适配器插件可以添加一个 备用 UI 链接,该链接仅链接到扩展,例如 /extensions/<extension-id>。访问后,UI 将显示扩展的界面。

如果您还有其他问题,您始终可以在 DiscourseGitHubIRC(#iot) 上联系我们。我们迫不及待地想看看您将构建什么!

关于 Michael Stegeman

Michael 是 Mozilla 的软件工程师,负责 WebThings。

Michael Stegeman 的更多文章……