Nigtmare.js разъясните поведение метода evaluate?

Всем доброго дня. Ищу поддержки исключительно тех, кто работал с nightmare и понимает его в действии.
Суть вопроса следующая, базируясь на примере из документации
var Nightmare = require('nightmare');
var nightmare = Nightmare({ show: true })
nightmare
  .goto('http://yahoo.com')
  .type('form[action*="/search"] [name=p]', 'github nightmare')
  .click('form[action*="/search"] [type=submit]')
  .wait('#main')
  .evaluate(function () {
    return document.querySelector('#main .searchCenterMiddle li a').href
  })
  .end()
  .then(function (result) {
    console.log(result)
  })
  .catch(function (error) {
    console.error('Search failed:', error);
  });

меня интересует методика сбора информации с многостраничной таблицы, так как evaluate передает собранные данные через return не совсем понятно, как в одном вызове можно использовать evaluate несколько раз и тем более в цикле, и можно ли вообще так действовать.
Условно меня интересует следующее
1. зашли на страницу
2. через evaluate получили данные со страницы (куда-то вернули)
3. через событие клик ушли на следующую табу и повторили пункт 2
в простом случае число табов заранее известно и циклы не используются.
3.а запустили клик и evaluate в цикле и потом агрегирвоанные данные возвратили пользователю через then

Отдельно не совсем понимаю до вызова nightmare можно объявить любую переменную, к примеру var arr = [] и потом пытаться пушить в нее через evaluateб однако начинается ругань, что данная переменная не объявлена (предполагаю, что виной этому асинхронность).
В целом готов дополнять вопрос комментариями, детализировать по мере необходимости.
Спасибо заранее.
  • Вопрос задан
  • 1822 просмотра
Решения вопроса 1
@likemusic
Пример рабочего кода:
const Nightmare = require('nightmare');
const nightmare = Nightmare({ show: true });
let results = [];
let res = nightmare
    .goto('http://yahoo.com')
    .type('form[action*="/search"] [name=p]', 'github nightmare')
    .click('form[action*="/search"] [type=submit]')
    .wait('#main')
    .evaluate(function () {
        return document.querySelector('#main .searchCenterMiddle li a').href;
    })
    .then(function(yahooResult) {
        console.log('Yahoo result:', yahooResult);
        results.push(yahooResult);
    })
    .then(function () {
        return nightmare.goto('http://google.com')
            .type('form[action*="/search"] [name=q]', 'github nightmare')
            .click('form[action*="/search"] [type=submit]')
            .wait('#main')
            .evaluate(function () {
                return document.querySelector('#search .g h3 a').href;
            })
        ;
    })
    .then(function (googleResult) {
        console.log('Google result:', googleResult);
        results.push(googleResult);
    })
    .then(function () {
        return nightmare.end();
    })
    .then(function() {
        console.log('Full results:', results);
    })
    .catch(function (error) {
        console.error('Search failed:', error);
    })
;

Комментарии к коду:

не совсем понятно, как в одном вызове можно использовать evaluate несколько раз и тем более в цикле, и можно ли вообще так действовать.


Почти все методы nightmare возвращают указатель на самого себя, чтобы вызовы методов можно было объединять в цепочки (как в jQuery).
Метод `evaluate()` также возвращает this (а не promise, как многие думают)!

Методами-исключениями, которые не возвращают `this` являются методы `.then()` и `.catch()` - они оба возвращают Promise.
Сами Promise также имеют методы `.then()` и `catch()`, поэтому при написании скриптом надо четко понимать и следить на объекте Promise или NightmareJS вызываются последующие методы.

Также nightmarejs при вызове цепочечных методов по факту просто добавляет нужные действия в буфер комманд. Команды из буфер начинают выполняться только при вызове методов `.then()`, и `.catch()`.

Поэтому код вроде приведенного ниже вообще даже не запустит браузер и не выполнит ничего, т.к. в нем нет вызова методов возвращающих промисы.

Код который даже не запустит браузер
nightmare
    .goto(url1).click(selector1).evaluate(func1)
    .goto(url2).click(selector1).evaluate(func1)
    .end()



Но следующий код будет выполнен браузером:
Код который будет выполнен в браузере
nightmare
    .goto(url1).click(selector1).evaluate(func1)
    .goto(url2).click(selector1).evaluate(func1)
    .end()
    .catch(errorHandler)


Также надо иметь в виду, что если функция вызываемае в `.thеn()` методе возвращает promise, то этот promise также будет разрешаться/выполняться и результат его работы будет возвращемым значением передаваемым во второй `.then()` верхнего уровня.

Пример кода со вложенными промисами (https://jsfiddle.net/5kp62v7w/)
promise1 = Promise.resolve('Promise 1')
	.then(function(){ var str = 'Promise 1-1'; console.log(str); return str;})
	.then(function(){ var str = 'Promise 1-2'; console.log(str); return str;})
	.then(function(){ var str = 'Promise 1-3'; console.log(str); return str;})
	.then(function(){ 
		return Promise.resolve('Promise 2')
			.then(function(){ var str = 'Promise 2-1'; console.log(str); return str;})
			.then(function(){ var str = 'Promise 2-2'; console.log(str); return str;})
			.then(function(){ var str = 'Promise 2-3'; console.log(str); return str;})
			.then(function(){
				return Promise.resolve('Promise 3')
					.then(function(){ var str = 'Promise 3-1'; console.log(str); return str;})
					.then(function(){ var str = 'Promise 3-2'; console.log(str); return str;})
					.then(function(){ var str = 'Promise 3-3'; console.log(str); return str;})
				;
			})
		;
	})
;

promise1
	.then(function(res) {console.log('Result value:', res );})
;

Результат выполнения кода
Promise 1-1
Promise 1-2
Promise 1-3
Promise 2-1
Promise 2-2
Promise 2-3
Promise 3-1
Promise 3-2
Promise 3-3

Result value: Promise 3-3


Поэтому при последовательном получении данных код должен быть такой структуры:
Пример кода при многократном вызове метода `.evaluate()`
nightmare.goto(url1).click(selector1).evaluate(func1).then(saveHandler1)
  .then(
     return nightmare.goto(url2).click(selector2).evaluate(func2).then(saveHandler2)
  )
  .then(
     return nightmare.goto(url3).click(selector3).evaluate(func3).then(saveHandler3)
  )
  .then(
     return nightmare.end()
  )
  .catch(errorHandler)
;


Циклы можно реализовать через reduce() метод, но на данный момент мне кажется проще всего писать с использованием await/async функций.

Пример использования `.evaluate()` в цикле с помощью async/await-функций
var urls = ['http://example.com', 'http://example2.com', 'http://example3.com'];

var results = []
async function collectTitles(urls){
  for ( url of urls ) {
    results.push(await nightmare.goto(url).wait('body').title())
  }
  
  await nightmare.end()
  console.dir(results)
}

collectTitles(urls)

до вызова nightmare можно объявить любую переменную, к примеру var arr = [] и потом пытаться пушить в нее через evaluate


Так сделать нельзя. Код передаваемый в метод `.evaluate()` работает в окружении браузера, а не node-сервера, поэтому единственных способ передать данные из кода выполняемого в браузе в код выполняемый на nodejs это вернуть из браузерной ф-ции значение с помощью оператора return и использовать затем это значение внутри `.than()` ф-ции как аргумент функции:

Пример передачи значения из браузер в выполняемый скрипт
var nodeArray = [];

nightmare
   .goto(url)
   .evaluate(function(){
})
   .then(function(){ var ret = []; /* do some work with ret */ return ret;  }) //т.к. этот код выполняется в окружении браузера, то у него нет доступа к переменным объявkенным в окружении nodejs, в том числе и к nodeArray. Значение `ret` будет передано первым аргументом в последющей then()-ф-ции
   .then(function(browserRet){ nodeArray = nodeArray.concat(browserRet);  }) // browserRet равен значению ret вычисленному в браузере, но переменная ret для окружения сервера не определена.


Для того чтобы понимать что происходит в скрипте рекомендую при запуске включить вывод отладочных сообщений как описано в документации.

Также рекомендую ознакомиться с примерами кода и комментариями к ним в репозитории rosshinkley/nightmare-examples
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
AMar4enko
@AMar4enko
var Nightmare = require('nightmare');
var nightmare = Nightmare({ show: true })ж
var results = [];
nightmare
  .goto('http://yahoo.com')
  .type('form[action*="/search"] [name=p]', 'github nightmare')
  .click('form[action*="/search"] [type=submit]')
  .wait('#main')
  .evaluate(function () {
    return document.querySelector('#main .searchCenterMiddle li a').href;
  })
  .then(results.push.bind(results))
  .goto('http://google.com')
  .type('form[action*="/search"] [name=p]', 'github nightmare')
  .click('form[action*="/search"] [type=submit]')
  .wait('#main')
  .evaluate(function () {
    return document.querySelector('#main .searchCenterMiddle li a').href;
  })
  .then(results.push.bind(results))
  .end()
  .then(function() {
    console.log(results);
  })
  .catch(function (error) {
    console.error('Search failed:', error);
  });
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы