puppeteer前端利器

Puppeteer 是 Chrome 開(kāi)發(fā)團(tuán)隊(duì)在 2017 年發(fā)布的一個(gè) Node.js 包,同時(shí)還有 Headless Chrome哆档。用來(lái)模擬 Chrome 瀏覽器的運(yùn)行诗良。它提供了高級(jí)API來(lái)通過(guò) DevTools 協(xié)議控制無(wú)頭 Chrome 或 Chromium 萝衩,它也可以配置為使用完整(非無(wú)頭)Chrome 或 Chromium侦锯。

1604821424315_puppeteer-img.png

學(xué)習(xí) Puppeteer 之前我們先來(lái)了解一下 Chrome DevTool ProtocolHeadless Chrome液兽。

Chrome DevTool Protocol 是什么

  • CDP 基于 WebSocket脸秽,利用 WebSocket 實(shí)現(xiàn)與瀏覽器內(nèi)核的快速數(shù)據(jù)通道儒老。
  • CDP 分為多個(gè)域(DOM,Debugger豹储,Network贷盲,Profiler,Console...),每個(gè)域中都定義了相關(guān)的命令和事件(Commands and Events)巩剖。
  • 我們可以基于 CDP 封裝一些工具對(duì) Chrome 瀏覽器進(jìn)行調(diào)試及分析铝穷,比如我們常用的 “Chrome 開(kāi)發(fā)者工具” 就是基于 CDP 實(shí)現(xiàn)的。
  • 很多有用的工具都是基于 CDP 實(shí)現(xiàn)的佳魔,比如 Chrome 開(kāi)發(fā)者工具曙聂,chrome-remote-interfacePuppeteer 等鞠鲜。

Headless Chrome 是什么

  • 可以在無(wú)界面的環(huán)境中運(yùn)行 Chrome宁脊。
  • 通過(guò)命令行或者程序語(yǔ)言操作 Chrome。
  • 無(wú)需人的干預(yù)贤姆,運(yùn)行更穩(wěn)定榆苞。
  • 在啟動(dòng) Chrome 時(shí)添加參數(shù) --headless,便可以 headless 模式啟動(dòng) Chrome霞捡。
  • chrome 啟動(dòng)時(shí)可以加一些什么參數(shù)坐漏,大家可以點(diǎn)擊這里查看。

總而言之 Headless Chrome 就是 Chrome 瀏覽器的無(wú)界面形態(tài)碧信,可以在不打開(kāi)瀏覽器的前提下赊琳,使用所有 Chrome 支持的特性運(yùn)行你的程序。

Puppeteer 是什么

  • Puppeteer 是 Node.js 工具引擎砰碴。
  • Puppeteer 提供了一系列 API躏筏,通過(guò) Chrome DevTools Protocol 協(xié)議控制 Chromium/Chrome 瀏覽器的行為。
  • Puppeteer 默認(rèn)情況下是以 headless 啟動(dòng) Chrome 的呈枉,也可以通過(guò)參數(shù)控制啟動(dòng)有界面的 Chrome趁尼。
  • Puppeteer 默認(rèn)綁定最新的 Chromium 版本,也可以自己設(shè)置不同版本的綁定猖辫。
  • Puppeteer 讓我們不需要了解太多的底層 CDP 協(xié)議實(shí)現(xiàn)與瀏覽器的通信弱卡。

Puppeteer 能做什么

官方介紹:您可以在瀏覽器中手動(dòng)執(zhí)行的大多數(shù)操作都可以使用 Puppeteer 完成!示例:

  • 生成頁(yè)面的屏幕截圖和PDF住册。
  • 爬取 SPA 或 SSR 網(wǎng)站。
  • 自動(dòng)化表單提交瓮具,UI測(cè)試荧飞,鍵盤(pán)輸入等。
  • 創(chuàng)建最新的自動(dòng)化測(cè)試環(huán)境名党。使用最新的JavaScript和瀏覽器功能叹阔,直接在最新版本的Chrome中運(yùn)行測(cè)試。
  • 捕獲站點(diǎn)的時(shí)間線跟蹤传睹,以幫助診斷性能問(wèn)題耳幢。
  • 測(cè)試Chrome擴(kuò)展程序。
  • ...

Puppeteer API 分層結(jié)構(gòu)

Puppeteer 中的 API 分層結(jié)構(gòu)基本和瀏覽器保持一致,下面對(duì)常使用到的幾個(gè)類(lèi)介紹一下:

1604821438803_puppeteer-framework.png
  • Browser: 對(duì)應(yīng)一個(gè)瀏覽器實(shí)例睛藻,一個(gè) Browser 可以包含多個(gè) BrowserContext
  • BrowserContext: 對(duì)應(yīng)瀏覽器一個(gè)上下文會(huì)話启上,就像我們打開(kāi)一個(gè)普通的 Chrome 之后又打開(kāi)一個(gè)隱身模式的瀏覽器一樣,BrowserContext 具有獨(dú)立的 Session(cookie 和 cache 獨(dú)立不共享)店印,一個(gè) BrowserContext 可以包含多個(gè) Page
  • Page:表示一個(gè) Tab 頁(yè)面冈在,通過(guò) browserContext.newPage()/browser.newPage() 創(chuàng)建,browser.newPage() 創(chuàng)建頁(yè)面時(shí)會(huì)使用默認(rèn)的 BrowserContext按摘,一個(gè) Page 可以包含多個(gè) Frame
  • Frame: 一個(gè)框架包券,每個(gè)頁(yè)面有一個(gè)主框架(page.MainFrame()),也可以多個(gè)子框架,主要由 iframe 標(biāo)簽創(chuàng)建產(chǎn)生的
  • ExecutionContext: 是 javascript 的執(zhí)行環(huán)境炫贤,每一個(gè) Frame 都一個(gè)默認(rèn)的 javascript 執(zhí)行環(huán)境
  • ElementHandle: 對(duì)應(yīng) DOM 的一個(gè)元素節(jié)點(diǎn)溅固,通過(guò)該該實(shí)例可以實(shí)現(xiàn)對(duì)元素的點(diǎn)擊,填寫(xiě)表單等行為兰珍,我們可以通過(guò)選擇器侍郭,xPath 等來(lái)獲取對(duì)應(yīng)的元素
  • JsHandle:對(duì)應(yīng) DOM 中的 javascript 對(duì)象,ElementHandle 繼承于 JsHandle俩垃,由于我們無(wú)法直接操作 DOM 中對(duì)象励幼,所以封裝成 JsHandle 來(lái)實(shí)現(xiàn)相關(guān)功能
  • CDPSession:可以直接與原生的 CDP 進(jìn)行通信,通過(guò) session.send 函數(shù)直接發(fā)消息口柳,通過(guò) session.on 接收消息苹粟,可以實(shí)現(xiàn) Puppeteer API 中沒(méi)有涉及的功能
  • Coverage:獲取 JavaScript 和 CSS 代碼覆蓋率
  • Tracing:抓取性能數(shù)據(jù)進(jìn)行分析
  • Response: 頁(yè)面收到的響應(yīng)
  • Request: 頁(yè)面發(fā)出的請(qǐng)求

Puppeteer 安裝與環(huán)境

注意:在v1.18.1之前,Puppeteer至少需要Node v6.4.0跃闹。從v1.18.1到v2.1.0的版本依賴(lài)于Node 8.9.0+嵌削。從v3.0.0開(kāi)始,Puppeteer開(kāi)始依賴(lài)于Node 10.18.1+望艺。若要使用 async / await苛秕,只有Node v7.6.0或更高版本才支持。

Puppeteer是一個(gè)node.js包找默,所以安裝很簡(jiǎn)單:

npm install puppeteer
// 或者
yarn add puppeteer

npm 在安裝 puppeteer 的時(shí)候可能會(huì)報(bào)錯(cuò)艇劫!這是由于外網(wǎng)導(dǎo)致,使用科學(xué)上網(wǎng)或者使用淘寶鏡像 cnpm 安裝可解決惩激。

安裝Puppeteer時(shí)店煞,它將下載 Chromium 的最新版本。從1.7.0版開(kāi)始风钻,官方發(fā)布了該 puppeteer-core 軟件包顷蟀,默認(rèn)情況下不會(huì)下載任何瀏覽器,用于啟動(dòng)現(xiàn)有的瀏覽器或連接到遠(yuǎn)程瀏覽器骡技。需要注意安裝的 puppeteer-core 版本與打算連接的瀏覽器兼容鸣个。

Puppeteer 使用

Case1: 截圖

我們使用 Puppeteer 既可以對(duì)某個(gè)頁(yè)面進(jìn)行截圖,也可以對(duì)頁(yè)面中的某個(gè)元素進(jìn)行截圖:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  //設(shè)置可視區(qū)域大小,默認(rèn)的頁(yè)面大小為800x600分辨率
  await page.setViewport({width: 1920, height: 800});
  await page.goto('https://www.baidu.com/');
  //對(duì)整個(gè)頁(yè)面截圖
  await page.screenshot({
      path: './files/baidu_home.png',  //圖片保存路徑
      type: 'png',
      fullPage: true //邊滾動(dòng)邊截圖
      // clip: {x: 0, y: 0, width: 1920, height: 800}
  });
  //對(duì)頁(yè)面某個(gè)元素截圖
  let element = await page.$('#s_lg_img');
  await element.screenshot({
      path: './files/baidu_logo.png'
  });
  await page.close();
  await browser.close();
})();

我們?cè)趺慈カ@取頁(yè)面中的某個(gè)元素呢?

  • page.$('#uniqueId'):獲取某個(gè)選擇器對(duì)應(yīng)的第一個(gè)元素
  • page.$$('div'):獲取某個(gè)選擇器對(duì)應(yīng)的所有元素
  • page.$x('//img'):獲取某個(gè) xPath 對(duì)應(yīng)的所有元素
  • page.waitForXPath('//img'):等待某個(gè) xPath 對(duì)應(yīng)的元素出現(xiàn)
  • page.waitForSelector('#uniqueId'):等待某個(gè)選擇器對(duì)應(yīng)的元素出現(xiàn)

Case2: 模擬用戶操作

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({
        slowMo: 100,    //放慢速度
        headless: false, //開(kāi)啟可視化
        defaultViewport: {width: 1440, height: 780},
        ignoreHTTPSErrors: false, //忽略 https 報(bào)錯(cuò)
        args: ['--start-fullscreen'] //全屏打開(kāi)頁(yè)面
    });
    const page = await browser.newPage();
    await page.goto('https://www.baidu.com/');
    //輸入文本
    const inputElement = await page.$('#kw');
    await inputElement.type('hello word', {delay: 20});
    //點(diǎn)擊搜索按鈕
    let okButtonElement = await page.$('#su');
    //等待頁(yè)面跳轉(zhuǎn)完成囤萤,一般點(diǎn)擊某個(gè)按鈕需要跳轉(zhuǎn)時(shí)昼窗,都需要等待 page.waitForNavigation() 執(zhí)行完畢才表示跳轉(zhuǎn)成功
    await Promise.all([
        okButtonElement.click(),
        page.waitForNavigation()  
    ]);
    await page.close();
    await browser.close();
})();

那么 ElementHandle 都提供了哪些操作元素的函數(shù)呢?

  • elementHandle.click():點(diǎn)擊某個(gè)元素
  • elementHandle.tap():模擬手指觸摸點(diǎn)擊
  • elementHandle.focus():聚焦到某個(gè)元素
  • elementHandle.hover():鼠標(biāo) hover 到某個(gè)元素上
  • elementHandle.type('hello'):在輸入框輸入文本

Case3: 植入 javascript 代碼

Puppeteer 最強(qiáng)大的功能是阁将,你可以在瀏覽器里執(zhí)行任何你想要運(yùn)行的 javascript 代碼膏秫。下面代碼是對(duì)百度首頁(yè)新聞推薦爬取數(shù)據(jù)的例子。

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('https://www.baidu.com/');
    //通過(guò) page.evaluate 在瀏覽器里執(zhí)行代碼
    const resultData = await page.evaluate(async () =>  {
      let data = {};
      const ListEle = [...document.querySelectorAll('#hotsearch-content-wrapper .hotsearch-item')];
      data = ListEle.map((ele) => {
        const urlEle = ele.querySelector('a.c-link');
        const titleEle = ele.querySelector('.title-content-title');
        return {
          href: urlEle.href,
          title: titleEle.innerText,
        };
      });
      return data;
    });
    console.log(resultData)
    await page.close();
    await browser.close();
})();

有哪些函數(shù)可以在瀏覽器環(huán)境中執(zhí)行代碼呢做盅?

  • page.evaluate(pageFunction[, ...args]):在瀏覽器環(huán)境中執(zhí)行函數(shù)
  • page.evaluateHandle(pageFunction[, ...args]):在瀏覽器環(huán)境中執(zhí)行函數(shù)缤削,返回 JsHandle 對(duì)象
  • page.$$eval(selector, pageFunction[, ...args]):把 selector 對(duì)應(yīng)的所有元素傳入到函數(shù)并在瀏覽器環(huán)境執(zhí)行
  • page.$eval(selector, pageFunction[, ...args]):把 selector 對(duì)應(yīng)的第一個(gè)元素傳入到函數(shù)在瀏覽器環(huán)境執(zhí)行
  • page.evaluateOnNewDocument(pageFunction[, ...args]):創(chuàng)建一個(gè)新的 Document 時(shí)在瀏覽器環(huán)境中執(zhí)行,會(huì)在頁(yè)面所有腳本執(zhí)行之前執(zhí)行
  • page.exposeFunction(name, puppeteerFunction):在 window 對(duì)象上注冊(cè)一個(gè)函數(shù)吹榴,這個(gè)函數(shù)在 Node 環(huán)境中執(zhí)行亭敢,有機(jī)會(huì)在瀏覽器環(huán)境中調(diào)用 Node.js 相關(guān)函數(shù)庫(kù)

Case4: 請(qǐng)求攔截

請(qǐng)求在有些場(chǎng)景下很有必要,攔截一下沒(méi)必要的請(qǐng)求提高性能图筹,我們可以在監(jiān)聽(tīng) Page 的 request 事件帅刀,并進(jìn)行請(qǐng)求攔截,前提是要開(kāi)啟請(qǐng)求攔截 page.setRequestInterception(true)远剩。

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    const blockTypes = new Set(['image', 'media', 'font']);
    await page.setRequestInterception(true); //開(kāi)啟請(qǐng)求攔截
    page.on('request', request => {
        const type = request.resourceType();
        const shouldBlock = blockTypes.has(type);
        if(shouldBlock){
            //直接阻止請(qǐng)求
            return request.abort();
        }else{
            //對(duì)請(qǐng)求重寫(xiě)
            return request.continue({
                //可以對(duì) url扣溺,method,postData瓜晤,headers 進(jìn)行覆蓋
                headers: Object.assign({}, request.headers(), {
                    'puppeteer-test': 'true'
                })
            });
        }
    });
    await page.goto('https://www.baidu.com/');
    await page.close();
    await browser.close();
})();

那 page 頁(yè)面上都提供了哪些事件呢锥余?

  • page.on('close') 頁(yè)面關(guān)閉
  • page.on('console') console API 被調(diào)用
  • page.on('error') 頁(yè)面出錯(cuò)
  • page.on('load') 頁(yè)面加載完
  • page.on('request') 收到請(qǐng)求
  • page.on('requestfailed') 請(qǐng)求失敗
  • page.on('requestfinished') 請(qǐng)求成功
  • page.on('response') 收到響應(yīng)
  • page.on('workercreated') 創(chuàng)建 webWorker
  • page.on('workerdestroyed') 銷(xiāo)毀 webWorker

Case5: 獲取 WebSocket 響應(yīng)

Puppeteer 目前沒(méi)有提供原生的用于處理 WebSocket 的 API 接口里初,但是我們可以通過(guò)更底層的 Chrome DevTool Protocol (CDP) 協(xié)議獲得

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    //創(chuàng)建 CDP 會(huì)話
    let cdpSession = await page.target().createCDPSession();
    //開(kāi)啟網(wǎng)絡(luò)調(diào)試,監(jiān)聽(tīng) Chrome DevTools Protocol 中 Network 相關(guān)事件
    await cdpSession.send('Network.enable');
    //監(jiān)聽(tīng) webSocketFrameReceived 事件匹涮,獲取對(duì)應(yīng)的數(shù)據(jù)
    cdpSession.on('Network.webSocketFrameReceived', frame => {
        let payloadData = frame.response.payloadData;
        if(payloadData.includes('push:query')){
            //解析payloadData仙逻,拿到服務(wù)端推送的數(shù)據(jù)
            let res = JSON.parse(payloadData.match(/\{.*\}/)[0]);
            if(res.code !== 200){
                console.log(`調(diào)用websocket接口出錯(cuò):code=${res.code},message=${res.message}`);
            }else{
                console.log('獲取到websocket接口數(shù)據(jù):', res.result);
            }
        }
    });
    await page.goto('https://netease.youdata.163.com/dash/142161/reportExport?pid=700209493');
    await page.waitForFunction('window.renderdone', {polling: 20});
    await page.close();
    await browser.close();
})();

Case6: 如何抓取 iframe 中的元素

一個(gè) Frame 包含了一個(gè)執(zhí)行上下文(Execution Context)揭措,我們不能跨 Frame 執(zhí)行函數(shù),一個(gè)頁(yè)面中可以有多個(gè) Frame脖捻,主要是通過(guò) iframe 標(biāo)簽嵌入的生成的愈诚。其中在頁(yè)面上的大部分函數(shù)其實(shí)是 page.mainFrame().xx 的一個(gè)簡(jiǎn)寫(xiě)荣堰,F(xiàn)rame 是樹(shù)狀結(jié)構(gòu)淹辞,我們可以通過(guò) frame.childFrames() 遍歷到所有的 Frame医舆,如果想在其它 Frame 中執(zhí)行函數(shù)必須獲取到對(duì)應(yīng)的 Frame 才能進(jìn)行相應(yīng)的處理

以下是在登錄 188 郵箱時(shí),其登錄窗口其實(shí)是嵌入的一個(gè) iframe象缀,以下代碼時(shí)我們?cè)讷@取 iframe 并進(jìn)行登錄

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({headless: false, slowMo: 50});
    const page = await browser.newPage();
    await page.goto('https://www.188.com');
    
    for (const frame of page.mainFrame().childFrames()){
        //根據(jù) url 找到登錄頁(yè)面對(duì)應(yīng)的 iframe
        if (frame.url().includes('passport.188.com')){
            await frame.type('.dlemail', 'admin@admin.com');
            await frame.type('.dlpwd', '123456');
            await Promise.all([
                frame.click('#dologin'),
                page.waitForNavigation()
            ]);
            break;
        }
    }
    await page.close();
    await browser.close();
})();

Case7: 頁(yè)面性能分析

Puppeteer 提供了對(duì)頁(yè)面性能分析的工具彬向,目前功能還是比較弱的,只能獲取到一個(gè)頁(yè)面性能執(zhí)行的數(shù)據(jù)攻冷,如何分析需要我們自己根據(jù)數(shù)據(jù)進(jìn)行分析,據(jù)說(shuō)在 2.0 版本會(huì)做大的改版: - 一個(gè)瀏覽器同一時(shí)間只能 trace 一次 - 在 devTools 的 Performance 可以上傳對(duì)應(yīng)的 json 文件并查看分析結(jié)果 - 我們可以寫(xiě)腳本來(lái)解析 trace.json 中的數(shù)據(jù)做自動(dòng)化分析 - 通過(guò) tracing 我們獲取頁(yè)面加載速度以及腳本的執(zhí)行性能

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.tracing.start({path: './files/trace.json'});
    await page.goto('https://www.google.com');
    await page.tracing.stop();
    /*
        continue analysis from 'trace.json'
    */
    browser.close();
})();

Case8: 文件的上傳和下載

在自動(dòng)化測(cè)試中遍希,經(jīng)常會(huì)遇到對(duì)于文件的上傳和下載的需求等曼,那么在 Puppeteer 中如何實(shí)現(xiàn)呢?

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    //通過(guò) CDP 會(huì)話設(shè)置下載路徑
    const cdp = await page.target().createCDPSession();
    await cdp.send('Page.setDownloadBehavior', {
        behavior: 'allow', //允許所有下載請(qǐng)求
        downloadPath: 'path/to/download'  //設(shè)置下載路徑
    });
    //點(diǎn)擊按鈕觸發(fā)下載
    await (await page.waitForSelector('#someButton')).click();
    //等待文件出現(xiàn),輪訓(xùn)判斷文件是否出現(xiàn)
    await waitForFile('path/to/download/filename');

    //上傳時(shí)對(duì)應(yīng)的 inputElement 必須是<input>元素
    let inputElement = await page.waitForXPath('//input[@type="file"]');
    await inputElement.uploadFile('/path/to/file');
    browser.close();
})();

Case9: 跳轉(zhuǎn)新 tab 頁(yè)處理

在點(diǎn)擊一個(gè)按鈕跳轉(zhuǎn)到新的 Tab 頁(yè)時(shí)會(huì)新開(kāi)一個(gè)頁(yè)面禁谦,這個(gè)時(shí)候我們?nèi)绾潍@取改頁(yè)面對(duì)應(yīng)的 Page 實(shí)例呢胁黑?可以通過(guò)監(jiān)聽(tīng) Browser 上的 targetcreated 事件來(lái)實(shí)現(xiàn),表示有新的頁(yè)面創(chuàng)建:

let page = await browser.newPage();
await page.goto(url);
let btn = await page.waitForSelector('#btn');
//在點(diǎn)擊按鈕之前州泊,事先定義一個(gè) Promise丧蘸,用于返回新 tab 的 Page 對(duì)象
const newPagePromise = new Promise(res => 
  browser.once('targetcreated', 
    target => res(target.page())
  )
);
await btn.click();
//點(diǎn)擊按鈕后,等待新tab對(duì)象
let newPage = await newPagePromise;

Case10: 模擬不同的設(shè)備

Puppeteer 提供了模擬不同設(shè)備的功能遥皂,其中 puppeteer.devices 對(duì)象上定義很多設(shè)備的配置信息力喷,這些配置信息主要包含 viewport 和 userAgent,然后通過(guò)函數(shù) page.emulate 實(shí)現(xiàn)不同設(shè)備的模擬

const puppeteer = require('puppeteer');
const iPhone = puppeteer.devices['iPhone 6'];
puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.emulate(iPhone);
  await page.goto('https://www.baidu.com');
  await browser.close();
});

性能和優(yōu)化

  • 關(guān)于共享內(nèi)存:
Chrome 默認(rèn)使用 /dev/shm 共享內(nèi)存演训,但是 docker 默認(rèn)/dev/shm 只有64MB弟孟,顯然是不夠使用的,提供兩種方式來(lái)解決:
- 啟動(dòng) docker 時(shí)添加參數(shù) --shm-size=1gb 來(lái)增大 /dev/shm 共享內(nèi)存样悟,但是 swarm 目前不支持 shm-size 參數(shù)
- 啟動(dòng) Chrome 添加參數(shù) - disable-dev-shm-usage拂募,禁止使用 /dev/shm 共享內(nèi)存
  • 盡量使用同一個(gè)瀏覽器實(shí)例,這樣可以實(shí)現(xiàn)緩存共用
  • 通過(guò)請(qǐng)求攔截沒(méi)必要加載的資源
  • 像我們自己打開(kāi) Chrome 一樣窟她,tab 頁(yè)多必然會(huì)卡陈症,所以必須有效控制 tab 頁(yè)個(gè)數(shù)
  • 一個(gè) Chrome 實(shí)例啟動(dòng)時(shí)間長(zhǎng)了難免會(huì)出現(xiàn)內(nèi)存泄漏,頁(yè)面奔潰等現(xiàn)象震糖,所以定時(shí)重啟 Chrome 實(shí)例是有必要的
  • 為了加快性能录肯,關(guān)閉沒(méi)必要的配置,比如:-no-sandbox(沙箱功能)试伙,--disable-extensions(擴(kuò)展程序)等
  • 盡量避免使用 page.waifFor(1000)嘁信,讓程序自己決定效果會(huì)更好
  • 因?yàn)楹?Chrome 實(shí)例連接時(shí)使用的 Websocket,會(huì)存在 Websocket sticky session 問(wèn)題.

參考文獻(xiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末疏叨,一起剝皮案震驚了整個(gè)濱河市潘靖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蚤蔓,老刑警劉巖卦溢,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異秀又,居然都是意外死亡单寂,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)吐辙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)宣决,“玉大人,你說(shuō)我怎么就攤上這事昏苏∽鸱校” “怎么了威沫?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)洼专。 經(jīng)常有香客問(wèn)我棒掠,道長(zhǎng),這世上最難降的妖魔是什么屁商? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任烟很,我火速辦了婚禮,結(jié)果婚禮上蜡镶,老公的妹妹穿的比我還像新娘雾袱。我一直安慰自己,他們只是感情好帽哑,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布谜酒。 她就那樣靜靜地躺著,像睡著了一般妻枕。 火紅的嫁衣襯著肌膚如雪僻族。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,165評(píng)論 1 299
  • 那天屡谐,我揣著相機(jī)與錄音述么,去河邊找鬼。 笑死愕掏,一個(gè)胖子當(dāng)著我的面吹牛度秘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播饵撑,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼剑梳,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了滑潘?” 一聲冷哼從身側(cè)響起垢乙,我...
    開(kāi)封第一講書(shū)人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎语卤,沒(méi)想到半個(gè)月后追逮,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡粹舵,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年钮孵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片眼滤。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡巴席,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诅需,到底是詐尸還是另有隱情情妖,我是刑警寧澤睬关,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站毡证,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蔫仙。R本人自食惡果不足惜料睛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望摇邦。 院中可真熱鬧恤煞,春花似錦、人聲如沸施籍。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)丑慎。三九已至喜喂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間竿裂,已是汗流浹背玉吁。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留腻异,地道東北人进副。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像悔常,于是被迫代替她去往敵國(guó)和親影斑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容