CasperJS 的 then() 语句中必须包含什么?如何确定sync/async函数的执行顺序?

What must be wrapped in then() statements in CasperJS? How to determine execution order of sync/async functions?

在运行使用 CasperJS 时,我很难确定什么是异步的,什么不是异步的,什么必须包含在 then() 语句中,什么时候要评估什么。

我将 运行 解决与 fall-through break 语句、变量范围或 evaluate() 语句有关的问题,然后我将开始包装我的所有代码() 语句...事实证明这不是问题。

我注意到我的代码 运行 在我单步执行时分为两个级别,一个是解析代码的评估级别,然后是 then() 语句。另外,我的打印语句有时以一种莫名其妙的顺序出现。

我的问题:这些 then() 语句实际上是如何排队的?我已经阅读了文档,并且我有点理解。我想了解规则并有一些简单的方法来确定什么是同步什么是异步。

我什至读过一本关于异步编码的书的部分内容,但似乎没有任何内容专门针对 CasperJS 结构。有资源吗?

此外,关于放置 then() 语句的最佳做法是什么?他们应该在整个过程中自由穿插,还是应该在调用其他函数的控制主 casper.begin() 函数中?

谢谢大家,我已经习惯了PHP。

经验法则:所有包含单词 thenwait 的 CasperJS 函数都是异步的。这个说法有很多例外。

then()在做什么?

CasperJS 被组织为一系列处理脚本控制流的步骤。 then() 处理定义步骤结束的许多 PhantomJS/SlimerJS 事件类型。当 then() 被调用时,传递的函数被放入一个简单的 JavaScript 数组的步骤队列中。如果上一步完成,要么是因为它是一个简单的同步函数,要么是因为 CasperJS 检测到触发了特定事件,下一步将开始执行并重复此过程,直到执行完所有步骤。

所有这些步骤函数都绑定到 casper 对象,因此您可以使用 this.

引用该对象

下面的简单脚本显示了两个步骤:

casper.start("http://example.com", function(){
    this.echo(this.getTitle());
}).run();

第一步是在 start() 后面进行隐式异步 ("stepped") open() 调用。 start() 函数还接受一个可选的回调,它本身是此脚本中的第二步。

在第一步的执行过程中,页面被打开。当页面完全加载时,PhantomJS 触发 onLoadFinished event, CasperJS triggers its own events 并继续下一步。第二步是一个简单的完全同步函数,所以这里没有什么特别的事情发生。完成后,CasperJS 退出,因为没有更多的步骤要执行。

这条规则有一个例外:当一个函数传入run()函数时,它会作为最后一步执行,而不是默认退出。如果你不在那里调用 exit()die(),你将需要终止进程。

then()如何检测下一步必须等待?

以下面的例子为例:

casper.then(function(){
    this.echo(this.getTitle());
    this.fill(...)
    this.click("#search");
}).then(function(){
    this.echo(this.getTitle());
});

如果在步骤执行期间触发了表示加载新页面的事件,则 CasperJS 将等待页面加载,直到执行下一步。在这种情况下,触发了一次点击,它本身从底层浏览器触发了 onNavigationRequested event。 CasperJS 看到这一点并使用回调暂停执行,直到加载下一页。其他类型的此类触发器可能是表单提交,甚至当客户端 JavaScript 使用 window.open()/window.location.

执行类似其自己的重定向时

当然,当我们谈论单页应用程序时(使用静态 URL),这会被打破。 PhantomJS 无法检测到例如在单击后呈现不同的模板,因此不能等到它完成加载(从服务器加载数据时这可能需要一些时间)。如果以下步骤依赖于新页面,您将需要使用例如waitUntilVisible() 寻找对要加载的页面唯一的选择器。

你怎么称呼这种 API 风格?

有些人称它为 Promises,因为它可以链接步骤。除了名称 (then()) 和动作链之外,相似之处仅此而已。在 CasperJS 中,没有通过步骤链从一个回调传递到另一个回调的结果。要么将结果存储在全局变量中,要么将其添加到 casper 对象中。然后只有有限的错误处理。当遇到错误时,CasperJS 将在默认配置中死亡。

我更喜欢称它为构建器模式,因为一旦你调用 run() 就会开始执行,之前的每个调用都只是将步骤放入队列(参见第一个问题)。这就是为什么在步骤函数之外编写同步函数没有意义。简而言之,它们是在没有任何上下文的情况下执行的。该页面甚至没有开始加载。

当然,将其称为构建器模式并不是全部事实。步骤可以嵌套,这实际上意味着,如果您在另一个步骤中安排一个步骤,它将被放入队列中,排在当前步骤之后,并且排在已经从当前步骤开始安排的所有其他步骤之后。 (这是很多步骤!)

下面的脚本很好地说明了我的意思:

casper.on("load.finished", function(){
    this.echo("1 -> 3");
});
casper.on("load.started", function(){
    this.echo("2 -> 2");
});
casper.start('http://example.com/');
casper.echo("3 -> 1");
casper.then(function() {
    this.echo("4 -> 4");
    this.then(function() {
        this.echo("5 -> 6");
        this.then(function() {
            this.echo("6 -> 8");
        });
        this.echo("7 -> 7");
    });
    this.echo("8 -> 5");
});
casper.then(function() {
    this.echo("9 -> 9");
});
casper.run();

第一个数字显示同步代码片段在脚本中的位置,第二个数字显示实际 executed/printed 位置,因为 echo() 是同步的。

要点:

  • 3号在前
  • 数字 8 打印在 4 和 5 之间

为避免混淆和难以发现问题,请始终在单步调用同步函数之后调用异步函数。如果觉得不可能,拆分成多个步骤或者考虑递归。

waitFor() 是如何工作的?

waitFor()wait* 家族中最灵活的函数,因为所有其他函数都使用这个函数。

waitFor() 以最基本的形式(只传递一个检查函数,没有其他)安排一个步骤。传递给它的 check 函数被重复调用,直到满足条件或达到(全局)超时。当then and/or onTimeout step 函数额外传递时,在这些情况下将调用它。

需要注意的是,如果waitFor()超时,脚本会在你没有传入onTimeout回调函数时停止执行,回调函数本质上是一个错误捕获函数:

casper.start().waitFor(function checkCb(){
    return false;
}, function thenCb(){
    this.echo("inner then");
}, null, 1000).then(function() {
    this.echo("outer");
}).run();

还有哪些函数也是异步步进函数?

从 1.1-beta3 开始,有以下不遵循经验法则的额外异步函数:

Casper 模块:back()forward()reload()repeat()start()withFrame()withPopup()
测试模块:begin()

如果您不确定查看 source code 特定函数是使用 then() 还是 wait()

事件侦听器是异步的吗?

事件侦听器可以使用 casper.on(listenerName, callback) 注册,它们将使用 casper.emit(listenerName, values) 触发。就 CasperJS 的内部结构而言,它们不是异步的。异步处理来自那些 emit() 调用所在的函数。 CasperJS 简单地传递大多数 PhantomJS 事件,所以这是异步的。

我可以脱离控制流吗?

控制或执行流程是CasperJS执行脚本的方式。当我们脱离控制流时,我们需要管理第二个流(或更多)。这将使脚本的开发和可维护性变得非常复杂。

例如,您想调用在某处定义的异步函数。让我们假设没有办法以这种方式重写函数,它是同步的。

function longRunningFunction(callback) {
    ...
    callback(data);
    ...
}
var result;
casper.start(url, function(){
    longRunningFunction(function(data){
        result = data;
    });
}).then(function(){
    this.open(urlDependsOnFunResult???);
}).then(function(){
    // do something with the dynamically opened page
}).run();

现在我们有两个相互依赖的流。

直接拆分流的其他方法是使用 JavaScript 函数 setTimeout()setInterval()。由于 CasperJS 提供 waitFor(),因此无需使用这些。

我可以return到CasperJS控制流程吗?

当必须将控制流合并回 CasperJS 流时,有一个明显的解决方案,即设置一个全局变量并同时等待它被设置。

例子与上一题相同:

var result;
casper.start(url, function(){
    longRunningFunction(function(data){
        result = data;
    });
}).waitFor(function check(){
    return result; // `undefined` is evaluated to `false`
}, function then(){
    this.open(result.url);
}, null, 20000).then(function(){
    // do something with the dynamically opened page
}).run();

什么是测试环境(Tester模块)的异步?

从技术上讲,测试器模块中没有任何东西是异步的。调用 test.begin() 只是执行回调。只有当回调本身使用异步代码时(意味着 test.done() 在单个 begin() 回调中异步调用),其他 begin() 个测试用例可以添加到测试用例队列中。

这就是为什么单个测试用例通常包含带有 casper.start()casper.run() 的完整导航,而不是反过来:

casper.test.begin("description", function(test){
    casper.start("http://example.com").run(function(){
        test.assert(this.exists("a"), "At least one link exists");
        test.done();
    });
});

最好坚持在 begin() 内嵌套完整的流,因为 start()run() 调用不会在多个流之间混合。这使您能够为每个文件使用多个完整的测试用例。


备注:

  • 当我谈论 同步 functions/execution 时,我指的是阻塞调用,它实际上可以 return 它计算的东西。