【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

knysa:异步等待风格 PhantomJS 脚本编程

  • 2016-10-10
  • 本文字数:6275 字

    阅读完需:约 21 分钟

要点

  • knysa 允许异步等待风格的 PhantomJS 异步编程;
  • knysa 减少对柯里化(curry)的需求;
  • knysa 支持 try/catch/finally 流程块;
  • knysa 对浏览器的 AJAX 调用有更好的支持;
  • knysa 试程序流程更加自然;

PhantomJS是提供 JavaScript API 的可编程无头浏览器(无图形界面)。它非常适合页面自动化和测试。其 JavaScript API 非常优秀,提供了许多高级功能,但同时也陷入了 JavaScript 常常遇到的“回调地狱(callback hell)”,既深度嵌套的回调。

目前,已经有很多库和框架致力于解决这个问题。对于 PhantomJS 来说,CasperJS 是其中一个流行的解决方案,但是它仅仅减轻了问题,并没有解决问题。knysa 从另一方面优雅的解决了这个问题。与类似 CasperJS,knysa 允许开发者有顺序的编写步骤。不同于 CasperJS,knysa 不会添加大量的样板代码(如 casper.then() 等)。

更重要的是,knysa 允许开发者使用诸如 if/else/while/break/try/catch/finally 等代码结构,更加自然的控制程序流程。

让我们使用一个示例来演示嵌套问题和 knysa 的理念。以下示例是一段 CasperJS 脚本,其流程是在 Google 上搜索关键字“CasperJS”,然后检查搜索结果页面上的每个链接到的页面是否包含关键字“CasperJS”:

  • (第 9 行)打开 Google 网页,等待页面加载完毕;
  • (第 11 行)网页加载完毕后,填充搜索框并提交,然后等待响应;
  • (第 13 行)处理响应:
    • (第 16、17 行)访问响应中的每个链接,并且等待页面加载;
    • (第 18-23 行)当链接的页面加载完毕后,检查关键字“CasperJS”是否存在;

上面的描述非常简单直接,但是 CasperJS 的嵌套语法使得代码看上去比较复杂。

复制代码
1 var links = [];
2 var casper = require('casper').create();
3 function getLinks() {
4 var links = document.querySelectorAll('h3.r a');
5 return Array.prototype.map.call(links, function(e) {
6 return e.getAttribute('href');
7 });
8 }
9 casper.start('http://google.com/', function() {
10 // 通过 google 表单搜索“CasperJS”关键字
11 this.fill('form[action="/search"]', { q: 'CasperJS' }, true);
12 });
13 casper.then(function() {
14 // 聚合“CasperJS”关键字搜索结果
15 links = this.evaluate(getLinks);
16 for (var i = 0; i < links.length; i++) {
17 casper.thenOpen(links[i]);
18 casper.then(function() {
19 var isFound = this.evaluate(function() {
20 return document.querySelector('html').textContent.indexOf('CasperJS') >= 0;
21 });
22 console.log('CasperJS is found on ' + links[i] + ':' + isFound);
23 });
24 }
25 });
26 casper.run();

我们可以看到,第 18 行的 casper.then() 嵌套在 13 行的另外一个 casper.then() 函数中。这样的嵌套模糊了程序逻辑,使得程序流程混乱。脚本执行过程中,执行流程不是仅仅向前的,程序流程有 3 个混杂的阶段:

  1. 阶段 1(第 9、13、26 行):通过使用 casper.start()(第 9 行)和 casper.then()(第 13 行)创建执行步骤(匿名函数)。这些步骤最后通过执行 capser.run()(第 26 行)开始执行。
  2. 阶段 2(第 11、15、16、17、18 行):随着步骤的执行,步骤中的代码(匿名行数)被执行。
  3. 阶段 3(第 19、20、21、22 行):在原步骤列表中增加更多步骤,并且执行。

于是每个嵌套级别增加了一个执行阶段。

由于这些混杂的阶段,脚本中的每行代码和脚本执行顺序不再匹配。例如,13 行在第 11 行前执行。这对于程序来说难以阅读和定位问题。另一个问题是难以增加“if/else”的判断逻辑或者处理任何异常。第三个问题是:第 22 行的links[i]总是会打印“undefined”!

这是为什么呢?

因为在阶段 3 的第 22 行之前时,变量“i”已经在阶段 2 中被修改成了links.length。为了修复这个问题,我们必须采取柯里化方式(10a/18b 和 22a 行)。这里我们使用变量“link”来保存 links[i] 的值(第 18a 行),然后执行一个匿名函数来返回另一个匿名函数(第 18b 行):

复制代码
18 casper.then(function() {
18a var link = links[i];
18b return function() {
19 var isFound = this.evaluate(function() {
20 return document.querySelector('html').textContent.indexOf('CasperJS') >= 0;
21 });
22 console.log('CasperJS is found on ' + link + ':' + isFound);
22a }
23 }());

我们可以看见,通过柯里化,“link”现在有了正确的值,但是柯里化增加了更多的嵌套代码。这太糟糕了,我们能够做的更好吗?

答案是肯定的。

事实上,通过 knysa,我们可以做的更好:我们可以完全去除代码中的嵌套和柯里化,脚本将会更加干净和可读,同时程序执行流程也会更加自然。

以下是实现相同功能的 knysa 脚本(注意我们引入了隐式变量“kflow”和“kflow”上的函数,同时还有一些“knysa_”开头的函数,我们将在后面进行介绍):

  • (第 9 行)打开 Google 网页并等待网页加载;
  • (第 10 行)在网页加载后,填充和提交搜索表单,并等待响应返回;
  • (第 13 行)处理响应:
    • (第 14 行)访问响应中的每个链接,并等待网页加载完毕;
    • (第 15-18 行)当链接对应的页面加载完毕后,检查关键字“CasperJS”是否存在;

嵌套代码和柯里化都消失了!现在,代码的执行顺序和脚本中的代码行想对应了。这个顺序也和上面描述的流程相同。整个代码流程中只有一个阶段,代码变得可读,问题定位也更方便。

复制代码
1 var links = [];
2 var i, num, isFound;
3 function getLinks() {
4 var links = document.querySelectorAll('h3.r a');
5 return Array.prototype.map.call(links, function(e) {
6 return e.getAttribute('href');
7 });
8 }
9 kflow.knysa_open('http://google.com/');
10 kflow.knysa_fill('form[action="/search"]', { q: 'CasperJS' });
11 links = kflow.evaluate(getLinks);
12 i = -1;
13 while (++i < links.length) {
14 kflow.knysa_open(links[i]);
15 isFound = kflow.evaluate(function() {
16 return document.querySelector('html').textContent.indexOf('CasperJS') >= 0;
17 });
18 console.log('CasperJS is found on ' + links[i] + ':' + isFound);
19 }
20 phantom.exit();

这是什么魔法?魔法位于每个以“knysa_”为前缀的函数(位于第 9、10 和 14 行),这些函数都是异步(async)执行,knysa 等待(await)当前异步调用结束,再继续执行下一行。

knysa 将每个脚本作为流程,并且在执行时赋予其一个 ID。流程对象可以通过隐式变量“kflow”暴露出来。流程 ID 可以通过 kflow.getId() 获取。

kflow 提供了一些异步等待风格的浏览器导航行数,如 knysa_open、knysa_fill、knysa_click 和 knysa_evaluate。对于新的网页,knysa_open、knysa_fill 和 knysa_click 行数会等待他们加载结束:

  1. knysa_open(url):打开一个网页;
  2. knysa_click(selector):触发点击操作;
  3. knysa_fill(formSelector, values):填充和提交表单

knysa_evaluate(func, kflowId[, arg0, arg1, …]):和 PhantomJS 的 page.evaluate() 函数相同,可以在浏览器端(沙盒中)执行包括 AJAX 调用在内的任意 JavaScript。相比于 PhantomJS 的 page.evaluate() 函数,knysa_evaluate 提升了对 AJAX 的支持。它挂起脚本执行。为了恢复执行,“回调函数”内部的代码(通常是 AJAX 调用的成功 / 失败回调)必须调用“window.callPhantom(data)”,其中“data.kflowId”需设置成“kflowId”。这里有一个来自 opl.kns 的示例:AJAX 请求用于续借图书,脚本执行会在续借响应请求收到后恢复:

复制代码
oneRenewResult = kflow.knysa_evaluate(renew, kflow.getId(), ...);

其中沙盒中的函数“renew”有以下几行:

复制代码
1 $.ajax({
2 dataType: 'json',
3 inline_messaging: 1,
4 url: form.attr("action"),
5 data: form.serialize(),
6 success: function(e) {
7 console.log("success: " + JSON.stringify(e));
8 window.callPhantom({kflowId : kflowId, status: 'success', data: e});
9 },
10 failure: function(e) {
11 console.log("failure: " + JSON.stringify(e));
12 window.callPhantom({kflowId : kflowId, status: 'failure', data: e});
13 }
14 });

脚本会再 AJAX 调用结束之后恢复。根据 AJAX 调用的结果,oneRenewResult 将被设置为不同的值:

  • 当 AJAX 调用成功,第 8 行恢复执行并将 oneRenewResult 设置为:{kflowId : kflowId, status: ‘success’, data: e}
  • 当 AJAX 调用失败,第 12 行恢复执行,并将 oneRenewResult 设置为:{kflowId : kflowId, status: ‘failure’, data: e}

注意:传入 window.callPhantom() 函数的所有数据都将作为 knysa_evaluate() 的返回值。

kflow.sleep(milliseconds) 是另一个异步等待函数,但是它被 knysa 特殊处理。

kflow 同时也提供一些常规(非异步等待)函数。这些函数直接来自 CasperJS API:

  • open(url)
  • click(selector)
  • fill(selector)
  • getHTML(selector, outer)
  • exists(selector)
  • download(url, path, method, data)
  • getElementAttr(selector, attrName)
  • render(path)
  • evaluate(func[, arg0, arg1…])

实现自己的异步等待风格函数

为了实现这个目的,只需要将函数名字加上“knysa_”前缀。这将告知 knysa 这是一个异步等待风格函数。当这样的函数调用时,脚本执行将会挂起。但是自己实现的异步等待风格函数需要通过调用 kflow.resume(data) 函数自行恢复脚本执行。当执行恢复时,传给 kflow.resume 函数的“data”参数将会变成异步等待函数的返回值。这里是一个来自 resume.kns 的示例:它首先休眠 1 秒,然后将输入值“num”乘以 100 并返回:

复制代码
1 function knysa_f1(kflow, num) {
2 setTimeout(function() {
3 kflow.resume(num * 100);
4 }, 1000);
5 // return num + 10;
6 }

该函数的返回值是传递给 kflow.resume() 函数的参数,例如 num * 100。

重要提示 1:在类似异步等待函数中,常规返回值将被忽略。例如,即使第 5 行没有注释,“return num + 10”语句的结果也会被简单的丢弃。

重要提示 2:异步等待风格函数的调用必须是一个单独的语句。可以是:

复制代码
knysa_my_func(...);
或者
ret = knysa_my_func(...);

也可以作为对象函数使用:

复制代码
myObj.knysa_my_func(...);
或者
ret = myObj.knysa_my_func(...);

下面的调用方式无法支持:

复制代码
if (knysa_my_func(...)) ...
可以改成这样:
val = knysa_my_func(...);
if (val) ...
var1 = abc * knysa_my_func(...)
可以改成这样:
val = knysa_my_func(...);
var1 = abc * val;

这里是调用前面定义的 knysa_f1 函数的示例,其返回值会被赋值到一个变量:

复制代码
ret = knysa_f1(5);

当这行代码执行时,ret 将在 1 秒延迟后被设置为 500。

异常处理

knysa 的异常处理机制出奇的简单:老式的 try/catch/finally 结构。这样的基础设施在 CasperJS 中是缺失的。示例: try.kns

catch”示例:以下代码在发生任何异常时渲染一张调试图片。

复制代码
var err; // 变量必须在开头定义
...
try {
...
} catch (err) {
kflow.render(image_path);
console.log(err.stack);
}

“finally”示例:以下代码确保在发生异常时登出:

复制代码
// 填充并提交表单,登录网站
kflow.knysa_fill(...);
try {
...
} finally {
// 打开登出链接以登出
kflow.knysa_open(logout_link);
}

注意事项:

  1. “else if”语法不支持,请使用嵌套的“if/else”语句替代;
  2. “for”循环体不能有异步等待函数调用或者“break”语句,请使用“while”循环替代;
  3. 所有变量必须在开头定义,包括 catch(err) 语句中的“err”变量;
  4. 隐式变量“kflow”不能用于变量定义;

内部工作原理:

knysa 脚本在执行前首先会被转换成 JavaScript。转换后的脚本是很多步骤的流程,每个步骤一个函数。每个函数的名字被编码上流程控制信息:

  • 每个函数都被编号(为了决定执行顺序)。
  • _async”后缀表示脚本执行将会被挂起。脚本执行将会在恰当的条件满足后恢复:例如页面响应接收到,或者 AJAX 响应接受到等。每个异步等待语句被转换成类似的函数。
  • _while”后缀但中间不包含“endwhile”的函数名表示 while 循环的开始。
  • _while”后缀但中间包含“endwhile”的函数名表示 while 循环的结束。
  • 虽然没有说明,“if/else/try/catch/finally/break”语句的转化方式和“while”语句类似。

下面是之前示例中去 Google 搜索的 knysa 脚本转换后的 JavaScript 脚本:

复制代码
var knysa = require("./knysa.js");
function knycon_search_casperjs_10001() {
var links = [];
var i, num, isFound;
function getLinks() {
var links = document.querySelectorAll("h3.r a");
return Array.prototype.map.call(links, function(e) {
return e.getAttribute("href");
});
}
this.n50002_async = function(kflow) {
kflow.knysa_open("http://google.com/");
}
this.n50003_async = function(kflow) {
kflow.knysa_fill('form[action="/search"]', {
q: "CasperJS"
});
}
this.n50004 = function(kflow) {
links = kflow.evaluate(getLinks);
i = -1;
}
this.n50005_while = function(kflow) {
return ++i < links.length;
};
this.n50006_async = function(kflow) {
kflow.knysa_open(links[i]);
}
this.n50007 = function(kflow) {
isFound = kflow.evaluate(function() {
return document.querySelector("html").textContent.indexOf("CasperJS") >= 0;
});
console.log("CasperJS is found on " + links[i] + ":" + isFound);
}
this.n50008_endwhile_n50005_while = function() {};
this.n50009 = function(kflow) {
phantom.exit();
}
}
knysa.knysa_exec(new knycon_search_CasperJS_10001);

注意 1:以上转换后的 JavaScript 只是为了展示当前的实现细节。knysa 的实现可能改变。例如,将来的版本可能会使用 Promises。当然,当 PhantomJS 完全支持 ES6 的 generators 或者 ES7 中的 async/await,knysa 可能就不再需要。

注意 2:虽然 knysa 减少了通过使用回调来控制脚本执行顺序,knysa 本身使用了 PhantomJS 的回调机制,例如 page.onCallback() 和 page.onLoadFinished()。

实践时间

现在我们已经看见通过 kynsa 来操作 PhantomJS 是多么容易和自然,为什么不自己尝试呢? knysa 托管在 github。我们可以从示例开始。我(作者)也期待听到大家的反馈。由于 knysa 是新项目,还有很多提升空间,欢迎大家能够对项目做出贡献。贡献的方式有多种:

  1. 处理 ticket
  2. 提供更多的示例脚本,不论大小;
  3. 或者更好的是,共享可以帮助处理日常零活的 knysa 脚本,这样可以帮助其他人节省时间,提高工作效率;

致谢

  1. uglifyjs1 用于解析 knysa 脚本并生成响应 javascript;
  2. 许多“kflow”函数直接从 CasperJS 提取;

关于作者

Bo Zou是一个经验丰富的软件开发者。他对于许多 web 自动化工具都有经验,包括 Perl、HttpUnit、HtmlUnit、Watij 等。最近他一直专注于 PhantomJS 和 Android。

查看英文原文: https://www.infoq.com/articles/knysa-phantomjs-async-await

2016-10-10 18:092379

评论

发布
暂无评论
发现更多内容

通过聚道云软件连接器实现销帮帮软件与i人事软件的智能对接

聚道云软件连接器

案例分享

玩转低代码可视化平台,软件开发如此简单!

互联网工科生

软件开发 低代码 可视化开发 JNPF

喜讯丨上海和今信息科技有限公司入选2023年上海市专精特新中小企业名单

ModelWhale

人工智能 科技 专精特新企业

官宣|硬核阵容曝光!PolarDB开发者大会全议程公布

阿里云瑶池数据库

数据库 阿里云 云原生 开发者大会

Jira停售Server版在即,飞书项目或成为最佳选择

科技热闻

C#最佳工具集合:IDE、分析、自动化工具等

EquatorCoco

C# ide 编程语言

数云引领,神州数码荣获CSA2023安全创新奖

科技热闻

lazada商品详情数据接口(lazada.item_get)丨lazada API接口

tbapi

lazada商品详情数据接口 lazada商品数据接口 lazada商品API接口 lazada API接口

2024 AIGC 应用层十大趋势;iPhone 遭史上最复杂攻击!丨 RTE 开发者日报 Vol.119

声网

软件测试开发/全日制丨自动化测试定位策略 学习笔记

测试人

软件测试 测试开发

软件开发隐藏报价和虚假信息,合约如何来提高安全性

软件开发-梦幻运营部

前端技术-调试工具(上)

不在线第一只蜗牛

前端 框架 前端技术

构建企业级AI中台,实现业务场景价值闭环

ModelWhale

AI 数字化转型 中台架构 AI中台

普及旗舰音质,一加 Buds 3正式发布 售价499元

编程猫

优化独立站网页布局,提升30%用户留存

九凌网络

数字化助力,聚道云软件连接器实现软件公司人事信息自动同步

聚道云软件连接器

案例分享

SEO内容页面性能优化全攻略

九凌网络

数据库内核那些事|细说PolarDB优化器查询变换:IN-List变换

阿里云瑶池数据库

数据库 List 阿里云 云原生 adb

自动化接口测试工具 AREX 0.6.2 版本发布

AREX 中文社区

开源 自动化测试 接口测试

全新升级!腾讯云大数据ES Serverless服务开启日志分析新体验

腾讯云大数据

ES

和鲸解放军总医院连续生理数据分析引擎入选爱分析数据智能最佳实践案例

ModelWhale

人工智能 大数据 数字化转型 企业管理 数智化

DAPP代币燃烧质押项目系统开发丨详情开发

l8l259l3365

大数据开发与低代码:加速数据处理与解决方案开发

EquatorCoco

数据库 低代码 大数据开发

NFT 项目入驻 NFTScan Site 流程说明

NFT Research

NFT NFT\ NFTScan nft工具

和鲸携手上海交大医学院张维拓老师,混合式教学聚焦R语言医学数据分析,从图表开始复现顶刊论文

ModelWhale

人工智能 数据分析 R语言 代码复现 医学

揭秘阿里巴巴:如何通过API实时捕获中国市场商品数据

Noah

爬虫工具(tkinter+scrapy+pyinstaller)

快乐非自愿限量之名

工具 爬虫 爬虫工具

低代码平台受到欢迎的原因有哪些?

这我可不懂

低代码 数字化 应用程序 JNPF

云桌面有什么技术特点?应用场景是什么?

青椒云云电脑

桌面云 云桌面 云桌面解决方案

软件测试开发/全日制丨测试用例-黑盒测试方法论 学习笔记

测试人

软件测试

OmniPlan Pro 4 for Mac(项目流程管理工具) v4.6完美激活版

mac

项目管理软件 苹果mac Windows软件 OmniPlan Pro

knysa:异步等待风格PhantomJS脚本编程_JavaScript_Bo Zou_InfoQ精选文章