编写 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));
库提供基本的 boolean
、number
、string
和 json
类型。
继续前进,假设您想传递/返回 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
属性,该属性返回一个拥有前端,以确保客户端和服务器的生命周期保持同步。