在puppeteer中实现以步为单位的暂停/继续

在使用puppeteer的过程中,我们经常会用它来帮助我们完成自动填写表单,做一些自动化测试等等之类的工作,但程序一旦开始运行就不可以再暂停/继续,我们要么等待所有程序步骤执行完成,要么终止程序从头再来,这种方式不仅浪费时间而且还可能浪费数据,本文我们将介绍如何在puppeteer中实现以步为单位的暂停/继续。

一、暂停/继续实现分析

首先我们抛开puppeteer不谈,谈一谈如何实现单步暂停/继续,以下面的程序为例:

(async () => {
  // 步骤一
  console.log(1);

  // 步骤二
  console.log(2);

  // 步骤三
  await sleep(10000);
  console.log(3);

  // 步骤四
  console.log(4);

  // 步骤五
  console.log(5);
}());

function sleep(time) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, time);
  });
}

我们定义了五个步骤,将其保存为run.js并使用node运行,程序一旦运行,我们只能选择终止或者是等待程序运行完成,如果要让每一步可暂停/继续我们需要一个信号变量,这里将其命名为signal,如果signal0,则正常运行; 如果signal1,则暂停执行后面的步骤,并等待signal状态以确定程序后续该怎么执行; 如果signal2,则终止程序,不再执行后续的步骤。我们定义一个函数用于检测信号signal

async function waitRunnable() {
  while (true) {
    if (signal === 1) {
      await sleep(1000);
    } else if (signal === 2) {
      process.exit();
    } else {
      break;
    }
  }
}

这里使用while循环来模拟等待,如果signal0,跳出while执行后续步骤,如果signal1,等待1秒,然后继续循环,如果signal2,则终止进程。改造一下run.js,改造后的代码如下:

// 信号 默认为0 表示可执行 1 表示暂停 2表示直接退出程序
let signal = 0;

process.on('message', message => {
  signal = message.signal;
});

(async () => {
  // 步骤一
  await waitRunnable();
  console.log(1);

  // 步骤二
  await waitRunnable();
  console.log(2);

  // 步骤三
  await waitRunnable();
  await sleep(10000);
  console.log(3);

  // 步骤四
  await waitRunnable();
  console.log(4);

  // 步骤五
  await waitRunnable();
  console.log(5);

  process.exit();
})();

function sleep(time) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, time);
  });
}

async function waitRunnable() {
  while (true) {
    if (signal === 1) {
      console.log('已暂停');
      await sleep(1000);
    } else if (signal === 2) {
      process.exit();
    } else {
      break;
    }
  }
}

然后编写代码调用run.js并测试单步暂停/继续功能,代码如下:

const { fork } = require('child_process');

const procc = fork('./run.js');

setTimeout(() => {
  procc.send({ signal: 1 });

  setTimeout(() => {
    procc.send({ signal: 0 });
  }, 10000);
}, 4000);

这里使用fork方法来运行run.js, 选用fork是因为它自带IPC通信管道,然后4秒后向run.js发送暂停执行后续步骤的信号,注意此时run.js已经在执行步骤三,由于已经在执行中步骤三不会终止,等待步骤三执行完毕,会暂停执行后续步骤,10秒后向run.js发送继续执行的命令,run.js收到信号,开始执行后续步骤,将上面的代码保存为test.js并使用node执行,执行截图如下:

二、优化并提取代码

在上面的步骤中,我们已经简单实现了单步骤的暂停/继续,但目前我们需要在每一个步骤前加上检测是否运行执行后续步骤的函数,这种方式显然不太合理,我们需要优化代码,首先我们定义一个MemoryTasks类用于处理暂停/继续逻辑,类的雏形如下:

class MemoryTasks {
  constructor() {
    this.tasks = [];
    this.signal = 0; // 信号 默认为0 表示可执行 1 表示暂停 2表示直接退出程序
    this.taskIndex = 0;
  }
}

tasks用于存储所有的步骤,signal记录执行信号,taskIndex用于记录当前执行步骤的编号,既然有步骤存储,则必然需要一个方法添加步骤,该方法方法定义如下:

...
add(task) {
  if (typeof task === 'function') {
    this.tasks.push(task);
  }
}
...

然后我们需要一个方法用于检测步骤是否可执行并执行步骤:

...
async start() {
  while (true) {
    if (this.signal === 1) {
      console.log('已暂停');
      await this.sleep(1000);
    } else if (this.signal === 2) {
      process.exit();
    } else {
      const func = this.tasks[this.taskIndex++];
      if (func) {
        await func();
      } else {
        break;
      }
    }
  }
}
...

然后我们补充一下其他函数,完整的类代码如下:

class MemoryTasks {
  constructor() {
    this.tasks = [];
    this.signal = 0; // 信号 默认为0 表示可执行 1 表示暂停 2表示直接退出程序
    this.taskIndex = 0;
  }

  add(task) {
    if (typeof task === 'function') {
      this.tasks.push(task);
    }
  }

  sleep(time) {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, time);
    });
  }

  sendSignal(signal) {
    this.signal = signal;
  }

  pasuse() {
    this.signal = 1;
  }

  resume() {
    this.signal = 0;
  }

  end() {
    this.signal = 2;
  }

  async start() {
    while (true) {
      if (this.signal === 1) {
        console.log('已暂停');
        await this.sleep(1000);
      } else if (this.signal === 2) {
        process.exit();
      } else {
        const func = this.tasks[this.taskIndex++];
        if (func) {
          await func();
        } else {
          break;
        }
      }
    }
  }
}

module.exports = MemoryTasks;

将上面的代码保存为memoryTasks.js, 并改造run.js如下:

const MemoryTasks = require('./memoryTasks');

const memoryTasks = new MemoryTasks();

process.on('message', message => {
  memoryTasks.sendSignal(message.signal);
});

(async () => {
  // 步骤一
  memoryTasks.add(() => {
    console.log(1);
  });

  // 步骤二
  memoryTasks.add(() => {
    console.log(2);
  });

  // 步骤三
  
  memoryTasks.add(async () => {
    await memoryTasks.sleep(10000);
    console.log(3);
  });

  // 步骤四
  memoryTasks.add(() => {
    console.log(4);
  });

  // 步骤五
  memoryTasks.add(() => {
    console.log(5);
  });

  await memoryTasks.start();

  process.exit();
})();

使用node执行调用函数test.js,会发现执行和步骤是一模一样的。

三、在puppeteer中使用MemoryTasks类

上面我们实现了可单步暂停/继续的MemoryTasks类,要让其在puppeteer中使用,实现方式和改造后的run.js的方式类似,只需要将我们要执行的步骤通过add方法添加到任务池,在所有任务添加完成后,通过start方法开始执行步骤,并使用IPC管道接受父进程传递的信号signal并传递给MemoryTasks类即可,示例代码如下:

const puppeteer = require('puppeteer');
const MemoryTasks = require('./memoryTasks');
const memoryTasks = new MemoryTasks();

process.on('message', message => {
  memoryTasks.sendSignal(message.signal);
});
 
(async () => {
 const browser = await puppeteer.launch({
    headless: false
 });
 const page = await browser.newPage();
 await page.goto('https://demo.deanhan.cn/php-login-demo/login.html');
 
 memoryTasks.add(async () => {
   await page.waitForSelector('[name="username"]');
   await memoryTasks.sleep(4000);
   await page.type('[name="username"]', 'Admin');
 });

 memoryTasks.add(async () => {
   await memoryTasks.sleep(4000);
   await page.type('[name="password"]', '111111');
 });
 
 memoryTasks.add(async () => {
   await memoryTasks.sleep(4000);
   await page.tap('button');
 });

 await memoryTasks.start();
 
 await browser.close();
})();

将上面的代码保存为js文件,然后可以使用electron等框架编写界面程序并通过fork调用上面的程序,然后可以通过界面程序上面的开始/暂停/继续/停止按钮向子进程传递信号,控制步骤的执行。需要注意的是memoryTasks.start方法需要放在添加所有步骤的后面,且前面的await必须得有。

四、写在最后

上面我们实现了MemoryTasks类并将其在puppeteer中做了简单的应用,实现了单步暂停/继续,其实我们能做的还有很多,比如我们可以通过界面程序传递步骤编号给MemoryTasks类,让其从指定步骤开始执行,要实现这点我们只需要添加一个方法即可:

...
goto(taskIndex = 0) {
  this.taskIndex = taskIndex;
}
...

当然本文主要讲的是一个实现思路,大家可以按照自己的理解去实现去扩展,如果有什么问题可以进入本站全栈技术交流群提问交流。

  • 支付宝二维码 支付宝
  • 微信二维码 微信
相关文章