Puppeteer是一款网页自动化测试工具。支持编写一些JS脚本来控制浏览器执行一些行为。它可用于运行测试用例或用作爬虫。它的脚本看起来像这样:constpuppeteer=require('puppeteer');constfs=require('fs/promises');(async()=>{constbrowser=awaitpuppeteer.launch({headless:false});constpage=awaitbrowser.newPage();awaitpage.goto('https://baidu.com');const$input=awaitpage.$('#kw');await$input.type('guangguangguang');const$button=awaitpage.$('#su');await$button.click();awaitpage.waitForSelector('#container');constscreenshot=awaitpage.screenshot();awaitfs.writeFile('./screenshot.png',screenshot);awaitbrowser.close();})();我们启动浏览器,打开一个标签页,访问百度,在输入框中输入一些内容,点击搜索按钮,等待结果出现在页面上,截图保存到本地文件,然后关闭浏览器。运行起来是这样的:其实运行这种脚本是不需要看到界面的,所以puppeteer默认是headless的,也就是没有界面。(以上是我关掉了headless)这种脚本写起来还是很简单的,按照你操作的步骤一步步写出对应的脚本就行了,甚至还有一个记录你行为的工具生成puppeteer脚本。今天我们不谈它的应用,而是探讨一下它的实现原理。我们可以自己实施吗?Puppeteer是基于ChromeDevTools协议实现的。它会在调试模式下运行一个Chromium实例,然后通过WebSocket连接到它,然后通过CDP协议对其进行远程控制。我们写的脚本最终会被转换成CDP协议发送给Chrome浏览器,它就是这样工作的。接下来,我们尝试实现一个简单版本的puppeteer来深入理解它的原理。想要控制Chromium,就得先下载他,所以本期我们来实现Chromium的自动下载。Google有一个网站存储所有版本和平台的Chromium。它的url是这样的:macurl:https://storage.googleapis.com/chromium-browser-snapshots/Mac/versionnumber/chrome-mac.ziplinux的URL:https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/versionnumber/chrome-linux.zipwin32的URL:https://storage.googleapis.com/chromium-browser-snapshots/Win/versionnumber/chrome-win32.zipWin64的url:https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/versionnumber/chrome-win32.zip你可以试试把url改成具体的版本号,比如468266和546920的所有版本号都可以在a上看到中国镜像网站:https://registry.npmmirror.com/binary.html?path=chromium-browser-snapshots/解压下载的zip包,这不就是我们要的chromium浏览器吗?过程就是这样一个过程,但是我们一定不能手工去做,一定要自动化。因为下载完puppeteer之后还需要下载这个chromium,所以不能让开发者手动下载。所以接下来我们将这个过程自动化。一步步来,先下载chromium到本地目录:constos=require('os');constpath=require('路径');constextract=require('extract-zip');constutil=require('util');constCHROMIUM_PATH=path.join(__dirname,'..','.local-chromium');constdownloadURLs={linux:'https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/%d/chrome-linux.zip',:'https://storage.googleapis.com/chromium-browser-snapshots/Mac/%d/chrome-mac.zip',win32:'https://storage.googleapis.com/chromium-browser-snapshots/Win/%d/chrome-win32.zip',win64:'https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/%d/chrome-win32.zip',};异步函数downloadChromium(revision,progressCallback){leturl=null;constplatform=os.platform();if(platform==='darwin')url=downloadURLs.darwin;elseif(platform==='linux')url=downloadURLs.linux;elseif(platform==='win32')url=os.arch()==='x64'?下载URls.win64:下载网址.win32;console.assert(url,`不支持的平台:${platform}`);url=util.format(url,revision);首先,下载的本地目录为.local-chromium,我们根据输入的版本号和从os.platform()获取的系统信息来确定下载的url。这里有两个nodeAPI需要说明:console.assert是第一个参数的值为false的时候。输出第二个参数的信息:util.format用于格式化字符串,有一些占位符,%d是一个数字,%s是一个字符串,%j是一个JSON等:这里可以下载chromium网址。那么下一步就是用https访问这个url,下载到本地目录。接下来我们实现下载到本地的功能:consthttps=require('https');functiondownloadFile(url,destinationPath,progressCallback){让resolve,reject;constpromise=newPromise((x,y)=>{resolve=x;reject=y;});constrequest=https.get(url,response=>{if(response.statusCode!==200){consterror=newError(`下载失败:服务器返回代码${response.statusCode}.URL:${url}`);response.resume();reject(error);return;}constfile=fs.createWriteStream(destinationPath);file.on('finish',()=>resolve());file.on('错误',error=>reject(error));response.pipe(file);});request.on('error',error=>reject(error));returnpromise;}因为这个下载过程是异步的,所以我们想要返回一个promise。很多人把返回promsie的方法写成这样:functionfunc(){returnnewPromise((resolve,reject)=>{//resolve();//reject()});}其实也可以写成这样:functionfunc(){letresolve,reject;constpromise=newPromise((x,y)=>{resolve=x;reject=y;});//解决();//拒绝();回报承诺;}中间部分是使用https.get下载url的数据,但是这个回调函数的response参数是一个stream。为什么?因为如果数据很多,需要很长时间传输,难道要等到所有数据都传输完了再处理吗?不,您可以在每次通过一个零件时处理一个零件。这就是流量的思想。几乎所有处理网络和文件IO的语言API都是基于流的。我们创建一个写流,写入到本地文件中,然后将响应流管道到文件流中,也就是直接写入到文件中:constfile=fs.createWriteStream(destinationPath);file.on('finish',()=>resolve());file.on('error',error=>reject(error));response.pipe(file);当它失败时,流中的数据不需要,所以调用response.resume()来消费。这样就实现了下载功能。接下来实现第二步,解压:自己处理比较麻烦,直接用第三方包就好了,比如extract-zip:constextract=require('extract-zip');functionextractZip(zipPath,folderPath){returnnewPromise(resolveextract(zipPath,{dir:folderPath},resolve));}处理完下载的url后,调用接下来的两步:constzipPath=path.join(CHROMIUM_PATH,`download-${revision}.zip`);constfolderPath=path.join(CHROMIUM_PATH,revision);if(fs.existsSync(folderPath)){return;}try{if(!fs.existsSync(CHROMIUM_PATH)){fs.mkdirSync(CHROMIUM_PATH);}awaitdownloadFile(url,zipPath,progressCallback);awaitextractZip(zipPath,folderPath);}catch(e){}首先判断zip包的路径和要解压的目录路径,如果目录已经存在则不下载。否则调用刚刚实现的两个方法来下载zip并解压。chromium的下载还是比较慢的。我们给它加一个进度条:即给响应流的data事件加一个回调,传入content-length获取的数据总大小,以及当前chunk的数据大小:consttotalBytes=parseInt(response.headers['content-length'],10);if(progressCallback)response.on('data',onData.bind(null,totalBytes));functiononData(totalBytes,chunk){progressCallback(totalBytes,chunk.length);}使用时,可以在这个回调中显示一个进度条:constDownloader=require('./lib/Downloader');constrevision=require('./package').puppeteer.chromium_revision;constProgressBar=require('progress');Downloader.downloadChromium(revision,onProgress).catch(error{console.error('下载失败:'+error.message);});letprogressBar=null;functiononProgress(bytesTotal,delta){if(!progressBar){progressBar=newProgressBar(`正在下载Chromium-${toMegabytes(bytesTotal)}[:bar]:percent:etas`,{完整:'=',不完整:'',宽度:20,总数:bytesTotal,});}progressBar.tick(delta);}downloader就是我们刚才实现的下载解压的逻辑,revision就是版本号,package.json中这个配置progress就是一个第三方控制台进度条,输入宽度,为总大小和显示的字符,只需在每次调用tick时更新长度。整体试一下:下载、解压、进度条都可以,下载的chromium也可以正常运行:至此,我们实现了chromium的自动下载,只要在package.xml中配置一个版本号即可。json,它可以自动下载。当然现在不是完全自动的,需要手动执行nodeinstall.js在postinstall的npmscripts中进行配置,依赖安装完成后触发下载:第一集完整代码上传至github:https://github.com/QuarkGluonPlasma/mini-puppeteer总结puppeteer是一款基于CDP的网页自动化测试工具,可用于运行测试用例和爬虫。为了深入理解它的实现原理,我们将从0开始实现一个迷你木偶师,这是第一集。我们实现了chromium的自动下载:chromium所有平台和版本的zip包都存放在google的一个网站上,通过os模块获取系统信息,可以根据传入的版本号确定url.url确定后,可以通过https模块下载,通过流式写入本地文件,每次有数据时更新进度条。最后通过第三方的extract-zip包实现解压。并将这个脚本放到postinstall的npm脚本中,只要安装了依赖,就会自动下载。下载Chromium只是第一步。在下一集中,我们将运行Chromium进行远程控制。
