knysa:異步等待風格PhantomJS腳本編程

qhaw2921 8年前發布 | 13K 次閱讀 PhantomJS CasperJS JavaScript開發

要點

  • 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”是否存在;

    </li> </ul>

    上面的描述非常簡單直接,但是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”是否存在;

      </li> </ul>

      嵌套代碼和柯里化都消失了!現在,代碼的執行順序和腳本中的代碼行想對應了。這個順序也和上面描述的流程相同。整個代碼流程中只有一個階段,代碼變得可讀,問題定位也更方便。

      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("
      

      knysa.knysa_exec(new knycon_search_CasperJS_10001);</code></pre>

      注意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 提取;

       

       

      來自:http://www.infoq.com/cn/articles/knysa-phantomjs-async-await

       

 本文由用戶 qhaw2921 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!