经验法则:包含全部的字词CasperJS功能then
和wait
是异步的。这个说法有很多例外。
在then()
做什么?
CasperJS 被组织成一系列处理脚本控制流的步骤。then()
处理定义步骤结束的许多 PhantomJS/SlimerJS 事件类型。当then()
被调用时,传递的函数被放入一个简单的 JavaScript 数组的步骤队列中。如果上一步完成了,要么是因为它是一个简单的同步函数,要么是因为 CasperJS 检测到触发了特定事件,下一步将开始执行并重复此操作,直到所有步骤都执行完毕。
所有这些步骤函数都绑定到该casper
对象,因此您可以使用this
.
以下简单脚本显示了两个步骤:
casper.start("http://example.com", function(){
this.echo(this.getTitle());
}).run();
第一步是隐式异步(“步进”)open()
调用start()
。该start()
函数还接受一个可选的回调,它本身就是这个脚本的第二步。
在执行第一步期间,页面被打开。当页面完全加载时,PhantomJS 触发onLoadFinished
事件,CasperJS 触发自己的事件并继续下一步。第二步是一个简单的完全同步的函数,所以这里没有发生任何花哨的事情。完成后,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
事件。CasperJS 看到这一点并使用回调暂停执行,直到加载下一页。其他类型的此类触发器可能是表单提交,甚至当客户端 JavaScript 使用window.open()
/ 进行自己的重定向时window.location
。
当然,当我们谈论单页应用程序(带有静态 URL)时,这会崩溃。PhantomJS 无法检测到例如在单击后正在呈现不同的模板,因此无法等到它完成加载(这可能需要一些时间从服务器加载数据)。如果以下步骤依赖于新页面,您将需要使用 egwaitUntilVisible()
来查找要加载的页面所独有的选择器。
你怎么称呼这种 API 风格?
有些人称它为 Promises,因为步骤可以链接起来。除了名称 ( then()
) 和操作链之外,这就是相似之处的结尾。在 CasperJS 中没有通过步骤链从回调传递到回调的结果。要么将结果存储在全局变量中,要么将其添加到casper
对象中。然后只有有限的错误处理。当遇到错误时,CasperJS 将在默认配置中死亡。
我更喜欢将其称为 Builder 模式,因为一旦您调用它就会立即执行,run()
并且之前的每个调用都只是为了将步骤放入队列中(请参阅第一个问题)。这就是为什么在 step 函数之外编写同步函数是没有意义的。简单地说,它们是在没有任何上下文的情况下执行的。该页面甚至没有开始加载。
当然,将其称为构建器模式并不是全部事实。步骤可以嵌套,这实际上意味着如果您在另一个步骤中安排一个步骤,它将在当前步骤之后以及从当前步骤已经安排的所有其他步骤之后放入队列。(这是很多步骤!)
以下脚本很好地说明了我的意思:
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();
第一个数字显示脚本中同步代码片段的位置,第二个数字显示实际执行/打印的位置,因为echo()
是同步的。
要点:
为避免混淆和难以发现问题,请始终在同步函数之后一步调用异步函数。如果它看起来不可能,分成多个步骤或考虑递归。
如何waitFor()
工作?
waitFor()
是wait*
家族中最灵活的函数,因为其他所有函数都使用这个函数。
waitFor()
以最基本的形式(只通过一个检查函数,不通过其他)一步。check
传递给它的函数会被重复调用,直到满足条件或达到(全局)超时。当额外传递一个then
和/或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 module:back()
, forward()
, reload()
, repeat()
, start()
, withFrame()
,withPopup()
测试module:begin()
如果您不确定查看源代码是否特定函数使用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()
,因此无需使用它们。
我可以回到 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 module)?
从技术上讲,tester module中没有任何内容是异步的。调用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()
调用不会在多个流之间混合。这使您可以为每个文件使用多个完整的测试用例。
笔记:
- 当我谈论同步函数/执行时,我的意思是一个阻塞调用,它实际上可以返回它计算的东西。