编写 Actor

简单的 Hello World

这是一个简单的 Hello World Actor。它是一个全局 Actor(不与给定的浏览器标签关联)。它有两个部分:规范和实现。规范将放在类似 devtools/shared/specs/hello-world.js 的位置,并且看起来像这样

const {Arg, RetVal, generateActorSpec} = require("devtools/shared/protocol");

const helloWorldSpec = generateActorSpec({
  typeName: "helloWorld", // I'll explain types later, I promise.

  methods: {
    sayHello: {
      // The request packet template.  There are no arguments, so
      // it is empty.  The framework will add the "type" and "to"
      // request properties.
      request: {},

      // The response packet template.  The return value of the function
      // will be plugged in where the RetVal() appears in the template.
      response: {
        greeting: RetVal("string") // "string" is the return value type.
      }
    },
  },
});

// Expose the spec so it can be imported by the implementation.
exports.helloWorldSpec = helloWorldSpec;

Actor 实现将放在类似 devtools/server/actors/hello-world.js 的位置,并且看起来像这样

const { Actor } = require("devtools/shared/protocol");
const {helloWorldSpec} = require("devtools/shared/specs/hello-world");

class HelloActor extends Actor {
  constructor(conn) {
    super(conn, helloWorldSpec);
  }

  sayHello() {
   return "hello";
  }
}

// You also need to export the actor class in your module for discovery.
exports.HelloActor = HelloActor;

要激活您的 Actor,请在 server/actors/utils/actor-registry.js 中的 addBrowserActors 方法中注册它。注册代码看起来像这样

this.registerModule("devtools/server/actors/hello-world", {
  prefix: "hello",
  constructor: "HelloActor",
  type: { global: true }
});

您的规范允许 Actor 支持 sayHello 请求。请求/回复将如下所示

-> { to: <actorID>, type: "sayHello" }
<- { from: <actorID>, greeting: "hello" }

现在我们可以创建一个客户端对象。我们称这些对象为前端对象,它们通常位于 devtools/client/fronts/ 中。

这是 HelloActor 的前端

const HelloFront = protocol.FrontClassWithSpec(helloWorldSpec, {
  initialize: function (client, form) {
    protocol.Front.prototype.initialize.call(this, client, form);
    // This call may not be required but it's a good idea. It will
    // guarantee that your instance is managed in the pool.
    this.manage(this);
  }
});

请注意,没有 sayHello 方法。FrontClass 将在 Front 对象上生成一个与 Actor 类中的方法声明匹配的方法。

生成的方法将返回一个 Promise。该 Promise 将解析为 Actor 方法的 RetVal。

因此,如果我们有一个对 HelloFront 对象的引用,我们可以发出 sayHello 请求

hello.sayHello().then(greeting => {
  console.log(greeting);
});

如何获得对前端的初始引用?这有点棘手,但基本上有两种方法

  • 手动

  • 自动

手动 - 如果您正在使用 DevToolsClient 实例,您可以手动发现 actorID 并为其创建 Front

let hello = new HelloFront(this.client, { actor: <hello actorID> });

自动 - 一旦您拥有对 protocol.js 对象的初始引用,它就可以返回其他 protocol.js 对象,并且将自动创建前端。

参数

sayHello 没有参数,所以让我们添加一个确实需要参数的方法。这是对规范的调整

methods: {
  echo: {
    request: { echo: Arg(0, "string") },
    response: { echoed: RetVal("string") }
  }
}

这是对实现的调整

echo: function (str) {
  return str + "... " + str + "...";
}

这告诉库将第 0 个参数(应该是字符串)放在请求数据包的 echo 属性中。

这将生成一个请求处理程序,其请求和响应数据包如下所示

{ to: <actorID>, type: "echo", echo: <str> }
{ from: <actorID>, echoed: <str> }

客户端用法应该可以预测

hello.echo("hello").then(str => { assert(str === "hello... hello...") })

库尽力使使用前端感觉像自然的 javascript(或者像你认为 Promise 是那样自然,我想)。在构建响应时,它会将函数的返回值放在响应模板中指定 RetVal() 的位置,并在客户端使用该位置的值来解析 Promise。

返回 JSON

也许您的响应是一个对象。这是一个规范示例

methods: {
  addOneTwice: {
    request: { a: Arg(0, "number"), b: Arg(1, "number") },
    response: { ret: RetVal("json") }
  }
}

这是一个实现示例

addOneTwice: function (a, b) {
  return { a: a + 1, b: b + 1 };
}

这将生成一个如下所示的响应数据包

{ from: <actorID>, ret: { a: <number>, b: <number> } }

这可能是多余的嵌套(如果您确定不会返回一个包含“from”作为键的对象!),因此您可以用以下内容替换 response

response: RetVal("json")

现在您的数据包将如下所示

{ from: <actorID>, a: <number>, b: <number> }

类型和编组

到目前为止,事情一直非常简单——我们传入的所有参数都是 javascript 原语。但对于某些类型(最重要的是 Actor 类型,我最终会讲到),我们不能只是将它们复制到 JSON 数据包中并期望它能工作,我们需要自己编组这些东西。

同样,协议库尽力为 Actor 和客户端提供自然的 API,有时这种自然的 API 可能涉及对象 API。我将使用一个非常牵强的例子,请耐心等待。假设我有一个包含数字并有一些关联方法的小对象

let Incrementor = function (i) {
  this.value = value;
}
Incrementor.prototype = {
    increment: function () { this.value++ },
    decrement: function () { this.value-- }
};

我想从后端函数中返回它

// spec:
methods: {
  getIncrementor: {
    request: { number: Arg(0, "number") },
    response: { value: RetVal("incrementor") } // We'll define "incrementor" below.
  }
}

// implementation:
getIncrementor: function (i) {
  return new Incrementor(i)
}

我希望该响应看起来像 { from: <actorID>, value: <number> },但客户端需要知道返回的是 Incrementor,而不是基本数字。所以让我们告诉协议库关于 Incrementor 的信息

protocol.types.addType("incrementor", {
    // When writing to a protocol packet, just send the value
    write: (v) => v.value,

    // When reading from a protocol packet, wrap with an Incrementor
    // object.
    read: (v) => new Incrementor(v)
});

现在我们的客户端可以按预期使用 API 了

front.getIncrementor(5).then(incrementor => {
    incrementor.increment();
    assert(incrementor.value === 6);
});

您也可以对参数执行相同的操作

// spec:
methods: {
  passIncrementor: {
    request: { Arg(0, "incrementor") },
  }
}

// implementation:
passIncrementor: function (inc) {
  w.increment();
  assert(incrementor.value === 6);
}

front.passIncrementor(new Incrementor(5));

库提供基本的 booleannumberstringjson 类型。

继续前进,假设您想传递/返回 Incrementor 的数组。您可以简单地在类型名称前添加 array:

// spec:
methods: {
  incrementAll: {
    request: { incrementors: Arg(0, "array:incrementor") },
    response: { incrementors: RetVal("array:incrementor") }
  }
}

// implementation:
incrementAll: function (incrementors) {
  incrementors.forEach(incrementor => {
    incrementor.increment();
  }
  return incrementors;
}

您可以使用迭代器代替数组作为参数或返回值,库将自动处理转换。

或者也许您想返回一个字典,其中一个项目是 incrementor。为此,您需要告诉类型系统字典的哪些成员需要自定义编组器

protocol.types.addDictType("contrivedObject", {
  incrementor: "incrementor",
  incrementorArray: "array:incrementor"
});

// spec:
methods: {
  reallyContrivedExample: {
    response: RetVal("contrivedObject")
  }
}

// implementations:
reallyContrivedExample: function () {
  return {
    /* a and b are primitives and so don't need to be called out specifically in addDictType */
    a: "hello", b: "world",
    incrementor: new Incrementor(1),
    incrementorArray: [new Incrementor(2), new Incrementor(3)]
  }
}

front.reallyContrivedExample().then(obj => {
  assert(obj.a == "hello");
  assert(obj.b == "world");
  assert(incrementor.i == 1);
  assert(incrementorArray[0].i == 2);
  assert(incrementorArray[1].i == 3);
});

可空值

如果参数、返回值或字典属性可以为 null/undefined,则可以在类型名称前添加 nullable:

"nullable:incrementor",  // Can be null/undefined or an incrementor
"array:nullable:incrementor", // An array of incrementors that can have holes.
"nullable:array:incrementor" // Either null/undefined or an array of incrementors without holes.

Actor

可能需要自定义编组的最常见对象是 Actor 本身。它们比 Incrementor 对象更有趣,但默认情况下,它们使用起来相对容易。让我们添加一个 ChildActor 实现,它将由 HelloActor 返回(它正在迅速成为 OverwhelminglyComplexActor)

// spec:
const childActorSpec = generateActorSpec({
  actorType: "childActor",
  methods: {
    getGreeting: {
      response: { greeting: RetVal("string") },
    }
  }
});

// implementation:
class ChildActor extends Actor {
  constructor(conn, id) {
    super(conn, childActorSpec);

    this.greeting = "hello from " + id;
  }

  getGreeting() {
    return this.greeting;
  }
}

exports.ChildActor = ChildActor;

const ChildFront = protocol.FrontClassWithSpec(childActorSpec, {
  initialize: function (client, form) {
    protocol.Front.prototype.initialize.call(this, client, form);
  },
});

库将使用 typeName 作为其标签,为 Actor 类型本身注册一个编组器。

因此,我们现在可以将以下代码添加到 HelloActor 中

// spec:
methods: {
  getChild: {
    request: { id: Arg(0, "string") },
    response: { child: RetVal("childActor") }
  }
}

// implementation:
getChild: function (id) {
  return ChildActor(this.conn, id);
}

front.getChild("child1").then(childFront => {
  return childFront.getGreeting();
}).then(greeting => {
  assert(id === "hello from child1");
});

对话将如下所示

{ to: <actorID>, type: "getChild", id: "child1" }
{ from: <actorID>, child: { actor: <childActorID> }}
{ to: <childActorID>, type: "getGreeting" }
{ from: <childActorID>, greeting: "hello from child1" }

但 ID 是这个虚构示例中唯一有趣的部分。您永远不会想要在不检查其 ID 的情况下引用 ChildActor。为了获取该 ID 而发出额外的请求是浪费的。您真正希望第一个响应看起来像 { from: <actorID>, child: { actor: <childActorID>, greeting: "hello from child1" } }

您可以通过在 ChildActor 类中提供 form 方法来自定义 Actor 的编组

form: function () {
    return {
        actor: this.actorID,
        greeting: this.greeting
    }
},

您可以在 ChildFront 类中通过实现匹配的 form 方法来反编组

form: function (form) {
    this.actorID = form.actor;
    this.greeting = form.greeting;
}

现在您可以立即使用 ID 了

front.getChild("child1").then(child => { assert(child.greeting === "child1) });

您可能会遇到想要根据正在执行的操作自定义 form 方法输出的情况。例如,假设 ChildActor 稍微复杂一些,具有 a、b、c 和 d 成员

ChildActor:
    form: function () {
        return {
            actor: this.actorID,
            greeting: this.greeting,
            a: this.a,
            b: this.b,
            c: this.c,
            d: this.d
        }
    }
ChildFront:
    form: function (form) {
        this.actorID = form.actorID;
        this.id = form.id;
        this.a = form.a;
        this.b = form.b;
        this.c = form.c;
        this.d = form.d;
    }

假设您想更改“c”并返回该对象

// Oops!  If a type is going to return references to itself or any other
// type that isn't fully registered yet, you need to predeclare the type.
types.addActorType("childActor");

...

// spec:
methods: {
  changeC: {
    request: { newC: Arg(0) },
    response: { self: RetVal("childActor") }
  }
}

// implementation:
changeC: function (newC) {
  c = newC;
  return this;
}

...

childFront.changeC('hello').then(ret => { assert(ret === childFront); assert(childFront.c === "hello") });

现在我们的响应将如下所示

{ from: <childActorID>, self: { actor: <childActorID>, greeting: <id>, a: <a>, b: <b>, c: "hello", d: <d> }

生命周期

不,我现在不想谈论生命周期。

事件

您的 Actor 有好消息!

Actor 是 jetpack EventTarget 的子类,因此您可以简单地发出事件。以下是在规范中设置它的方法

events: {
  "good-news": {
    type: "goodNews", // event target naming and packet naming are at odds, and we want both to be natural!
    news: Arg(0)
  }
}

methods: {
  giveGoodNews: {
    request: { news: Arg(0) }
  }
}

以下是实现的外观

const EventEmitter = require("devtools/shared/event-emitter");

// In your Actor class:
giveGoodNews(news) {
  EventEmitter.emit(this, "good-news", news);
}

现在您可以在前端监听事件了

front.on("good-news", news => {
  console.log(`Got some good news: ${news}\n`);
});
front.giveGoodNews().then(() => { console.log("request returned.") });

如果要修改将传递给事件侦听器回调函数的参数,可以在前端定义中使用 before(eventName, fn)。这只能对给定的 eventName 使用一次。fn 函数将在通过 Front 上的 EventEmitter API 发出事件之前被调用,其返回值将传递给事件侦听器回调函数。如果 fn 是异步的,则只有在 fn 调用解析后才会发出事件。

// In front file, most probably in the constructor:
this.before("good-news", function(news) {
  return news.join(" - ");
});

// In any consumer
front.on("good-news", function(news) {
  console.log(news);
});

因此,如果服务器发送了以下数组:[1, 2, 3],则使用者中的 console.log 将打印 1 - 2 - 3

在某种程度上相关的说明是,并非每个方法都需要是请求/响应。就像 Actor 可以发出单向事件一样,方法也可以标记为单向请求。也许我们不关心 giveGoodNews 返回任何内容

// spec:
methods: {
  giveGoodNews: {
    request: { news: Arg(0, "string") },
    oneway: true
  }
}

// implementation:
giveGoodNews: function (news) {
  emit(this, "good-news", news);
}

生命周期

不,让我们改谈谈自定义前端方法。

自定义前端方法

您可能需要在发出请求之前进行一些簿记工作。假设您正在调用 echo,但您想计算发出该请求的次数。只需在前端实现中使用 custom 标签

echo: custom(function (str) {
    this.numEchos++;
    return this._echo(str);
}, {
    impl: "_echo"
})

这将生成的实现放在 _echo 中而不是 echo 中,让您根据需要实现 echo。如果您省略了 impl,它根本不会生成实现。你自己负责。

生命周期

好吧,我想不出任何其他可以推迟的方法了。远程调试协议对于每个 Actor 都有一个“父”的概念。这是为了使分布式内存管理更容易一些。基本上,如果 Actor 被销毁,则其任何后代也将被销毁。

除此之外,基本协议对生命周期没有任何保证。协议中定义的每个接口都需要讨论和记录其生命周期管理方法(尽管有一些常见模式)。

协议库会为您维护子/父关系,但它需要一些帮助来确定子/父关系是什么。

对象的默认父级是创建它后第一个返回它的对象。因此,要重新访问我们之前的 HelloActor getChild 实现

// spec:
methods: {
  getChild: {
    request: { id: Arg(0) },
    response: { child: RetVal("childActor") }
  }
}

// implementation:
getChild: function (id) {
  return new ChildActor(this.conn, id);
}

ChildActor 的父级是 HelloActor,因为它创建了它。

您可以通过两种方式自定义此行为。第一种是在您的 actor 中定义一个 marshallPool 属性。想象一个新的 ChildActor 方法

// spec:
methods: {
  getSibling: {
    request: { id: Arg(0) },
    response: { child: RetVal("childActor") }
  }
}

// implementation:
getSibling: function (id) {
  return new ChildActor(this.conn, id);
}

这会创建一个由当前子 actor 拥有的新子 actor。但在本例中,我们希望子级创建的所有 actor 都由 HelloActor 拥有。因此,我们可以定义一个 defaultParent 属性,它利用 Actor 类提供的 parent 属性

get marshallPool() { return this.parent }

前端需要提供一个匹配的 defaultParent 属性,该属性返回一个拥有前端,以确保客户端和服务器的生命周期保持同步。