从CDP与源码的角度简单分析下pptr中的常用API

Should know

puppeteer

Chrome DevTools Protocol

开放了用程序控制页面行为的接口。

允许工具在chromium、chrome和其他基于Blink的浏览器上插桩、监测、调试。其中插桩(instrument)操作根据特点被分成了多种域(DOM, 调试器,网络等等)。每个域中都定义了它所支持的命令及生成的事件,命令与事件都被序列化成了固定结构的JSON对象。

ws远程连接步骤

  1. 使用--remote-debugging-port=0命令启动chrome.exe
  2. 请求/json/version获取数据,得到ws连接地址: webSocketDebuggerUrl
  3. 连接ws,就可以访问到浏览器实例

Example

const puppeteer = require('puppeteer');

(async() => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.goto('https://developers.google.com/web/');

  // Type into search box.
  await page.type('#searchbox input', 'Headless Chrome');

  // Wait for suggest overlay to appear and click "show all results".
  const allResultsSelector = '.devsite-suggest-all-results';
  await page.waitForSelector(allResultsSelector);
  await page.click(allResultsSelector);

  // Wait for the results page to load and display the results.
  const resultsSelector = '.gsc-results .gsc-thumbnail-inside a.gs-title';
  await page.waitForSelector(resultsSelector);

  // Extract the results from the page.
  const links = await page.evaluate(resultsSelector => {
    const anchors = Array.from(document.querySelectorAll(resultsSelector));
    return anchors.map(anchor => {
      const title = anchor.textContent.split('|')[0].trim();
      return `${title} - ${anchor.href}`;
    });
  }, resultsSelector);
  console.log(links.join('\n'));

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

Most used

puppteer.launch()

发生了什么:

  1. 根据参数拼接命令 包含三种类型设置: LaunchOptions, ChromeArgOptions, BrowserOptions。 根据配置选项决定chrome执行命令,通过可执行路径寻找chrome应用
  2. 启动子进程,绑定流 使用node的childProcess.spawn()启动一个子进程,并绑定默认IO流: stdin, stdout, stderr
  3. 获取地址,建立连接
    • 默认情况。获取node与chrome的ws连接地址后建立连接,当检测到子进程的stderr流中有满足/^DevTools listening on (ws:\/\/.*)$/规则的输出时表示与chrome连接成功.
    • 自定义pipe。使用PipeTransport对象连通自定义的可读与可写流,之后会在它们之间传递Buffer数据。
  4. 监听链路上的消息与事件 使用由EventEmitter扩展的Connection类扩展连接的通信链路,在外层进一步封装链路中的消息格式等。返回Connection和CDPSession对象(作为client收发遵循CDP的消息)。
  5. 创建Browser实例,并新建初始空白页ensureInitialPage(),最终返回Promise<Browser>

browser.newPage()

newPage为browser中浏览器上下文的方法,在进行页面的操作时需要传递浏览器contextId

使用CDP中Target域的Target.createTarget创建页面:

async _createPageInContext(contextId) {
  const {targetId} = await this._connection.send('Target.createTarget', {url: 'about:blank', browserContextId: contextId || undefined});
  const target = await this._targets.get(targetId);
  assert(await target._initializedPromise, 'Failed to create target for page');
  const page = await target.page();
  return page;
}

page.goto()

实际上是执行FrameManager对象的navigate()方法:

// Page -> FrameManager(_frameManager)
async function navigate(client, url, referrer, frameId) {
  try {
    const response = await client.send('Page.navigate', {url, referrer, frameId});
    ensureNewDocumentNavigation = !!response.loaderId;
    return response.errorText ? new Error(`${response.errorText} at ${url}`) : null;
  } catch (error) {
    return error;
  }
}

Page对象中的manager

page对象主要使用三种manager来管理常见操作:

  • _frameManager - 管理页面相关行为。页面跳转(goto),等待加载(waitFor), 元素选择与处理(evaluate)…
  • _networkManager - 管理网络相关行为。请求拦截(setRequestInterception),离线模式(setOfflineMode)…
  • _emulationManager - 管理模拟行为。设置移动设备与视口尺寸(setViewport)

page.click()

同样属于_frameManager控制的操作行为,并且是属于在domWorld中操作的行为。

FrameManager中的DOMWorld实例

FrameManager中使用两个DomWorld对象实例管理对于元素的不同操作:

  • _mainWorld - 负责使用选择器的元素选择与注入函数等操作
  • _secondaryWorld - 负责操作行为,
// Page -> FrameManager(_frameManager) -> DOMWorld(_secondaryWorld)
async click(selector, options) {
  const handle = await this.$(selector);
  assert(handle, 'No node found for selector: ' + selector);
  await handle.click(options);
  await handle.dispose();
}

page.\$(), page.evaluate(), page.\$eval()

属于_frameManager_mainWorld的操作,executionContext中执行元素选择与evaluate等操作。

// Page -> FrameManager(_frameManager) -> DOMWorld(_mainWorld)
async $(selector) {
  const document = await this._document();
  const value = await document.$(selector);
  return value;
}
...
async evaluate(pageFunction, ...args) {
  const context = await this.executionContext();
  return context.evaluate(pageFunction, ...args);
}
...
async $eval(selector, pageFunction, ...args) {
  const document = await this._document();
  return document.$eval(selector, pageFunction, ...args);
}
...

除此之外,还有用来选择多个元素和Handle的page.$$(), page.$$eval()方法。

Page.waitForSelector()

// Page -> FrameManager(_frameManager)
async waitForSelector(selector, options) {
  const handle = await this._secondaryWorld.waitForSelector(selector, options);
  if (!handle)
    return null;
  // executionContext -> [CDP]Runtime Domain -> ExecutionContextDescription
  const mainExecutionContext = await this._mainWorld.executionContext();
  // _adoptElementHandle -> [CDP]DOM Domain -> describeNode + resolveNode
  const result = await mainExecutionContext._adoptElementHandle(handle);
  await handle.dispose();
  return result;
}

// Page -> FrameManager(_frameManager) -> DOMWorld(_secondaryWorld)
async _waitForSelectorOrXPath(selectorOrXPath, isXPath, options = {}) {
  ...
  const polling = waitForVisible || waitForHidden ? 'raf' : 'mutation';
  const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${waitForHidden ? ' to be hidden' : ''}`;
  const waitTask = new WaitTask(this, predicate, title, polling, timeout, selectorOrXPath, isXPath, waitForVisible, waitForHidden);
  const handle = await waitTask.promise;
  if (!handle.asElement()) {
    await handle.dispose();
    return null;
  }
  return handle.asElement();
  ...
}

Page.screenshot() & Page.pdf()

  1. 处理传入配置:格式,路径,质量,剪裁等参数
  2. 加入到截图任务队列中,screenshotTaskQueue

Other

puppeteer的其他相关项目

juggler

  • 实现了符合puppeteer API规范的FireFox自动化协议。
  • 该项目在gecko的基础上,将juggler远程调试协议作为测试工具添加了进来。
  • 测试时需要在本地手动构建添加了juggler的firefox。

puppeteer-firefox

  • 实现了使用puppeteer的语法来操纵firefox
  • 使用时需要有绑定了juggler的firefox
  • 开发者目前跑测试中

例子:

const pptrFirefox = require('puppeteer-firefox');

(async () => {
  const browser = await pptrFirefox.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});
  await browser.close();
})();

语法与puppeteer保持一致。