标签: puppeteer

  • 使用 webpack-rpc-inline-plugin 打包内联函数体

    使用 webpack-rpc-inline-plugin 打包内联函数体

    使用 Puppeteer 的时候,我们常常要使用 page.evaluate() 或者 page.evaluateHandle() 函数到目标页面的环境里执行一段代码。

    如果是简单的代码,比如返回页面标题、改变某个节点的属性,那么直接写在 .evaluate() 里面就行了;但实际生产中,尤其是前厂的 Showman 产品里,要执行的函数往往非常复杂,经常需要组合多个函数:

    page.evaluate(() => {
      function func1() {}
      function func2() {}
      // ...
      function funcN() {}
    
      func1();
      func2();
      // ...
      funcN();
    });

    这种场景下,我们必须用上面这种写法,而不是下面这种我们更熟悉的写法:

    import func1 from './func1';
    import func2 from './func2';
    // ...
    import funcN from './funcN';
    
    page.evaluate(() => {
      func1();
      func2();
      // ...
      funcN();
    });

    因为被执行的函数会被转换成字符串,传输到目标环境,然后重新实例化成函数,再执行。所以上面这种写法,引擎会在全局环境下查找需要的函数,而那些函数都没传递过去,就会找不到。

    如果开发时按照方案一组织代码,会遇到几个问题:

    1. 子函数放在主函数体内部,不方便独立开发、调试、测试
    2. 每个主函数内部都需要写死子函数,不方便共享复用

    所以我就想从工具链入手,写一个专用工具,可以继续用方案二的形式组织代码,但是编译打包之后,就恢复到方案一的状态。

    我选择了 webpack 插件,原因有二:

    1. 我比较熟悉 webpack
    2. 这种情况不能用 loader

    最后我选择在 compilation.afterProcessAssets 钩子处理 JS。此时 JS 已经打包了所有资源,并且经过 terser 压缩。所以我会先将 bundle 解开(直接用 string.substring),然后 return webpack 对象,从中找到目标函数替换。

    具体的代码在 GitHub 仓库里,我就不详细解释了(困了),感兴趣的同学可以看看。

    欢迎需要在 rpc 环境下执行 JS 的同学使用,欢迎反馈需求和问题。

  • 在 Puppeteer 里使用代理服务器科学上网

    在 Puppeteer 里使用代理服务器科学上网

    使用 Puppeteer 录制视频的时候,如果服务器在国内,可能会有一些网站打不开。这个时候,我们可以要求 Puppeteer 使用代理服务器。

    0. 配置科学上网

    参考两篇旧文,其实原理一样,只是用的软件不一样:

    配置完成之后,通过浏览器应该可以正常访问。

    1. 使用代理服务器

    启动 Puppeteer 的时候,可以传入参数 args,进行各种调整,完整的列表请参考:List of Chromium Command Line Switches

    关于代理服务器,有若干个选项,我们要用的是 --proxy-server,方法是:

    puppeteer.launch({
      args: [
        '--proxy-server=socks5://127.0.0.1:1080',
      ],
    });

    2. 使用 PAC 文件

    但是这样所有流量都会走代理服务器,也不符合我们的期待,所以最好使用 PAC 文件。参数名称是:--proxy-pac-url,但请注意,因为 Chromium 的 bug,这个参数只在 headless: false,即有界面的时候才会生效。好在我们是为了录视频,所以本来就要打开界面。

    所以最终的启动代码就是:

    puppeteer.launch({
      headless: false,
      args: [
        '--proxy-server=socks5://127.0.0.1:1080',
        '--proxy-pac-url=http://localhost/autoproxy.pac',
      ],
    });

  • 移除 Puppeteer 里的保存密码提示窗

    移除 Puppeteer 里的保存密码提示窗

    大家知道,当我们使用 Chrome 完成登录的时候,Chrome 会询问我们是否要保存密码,如下图所示:

    图一 询问是否保存密码的弹窗

    但是在录制视频的时候,这个弹窗就不太必要了,甚至有些干扰,所以我们就想把它移除。如果搜索“puppteer prevent save password modal”,多半会被引导到这个页面,并得到结果:

    --enable-automation 添加到启动标记里。

    对应 Puppeteer 就是 puppeteer.launch(options) 里的 args,默认已经启用。但是使用这个选项会提示“Chrome is being controlled by automated test software.”,对录视频来说也不够理想。所以只能另辟蹊径,再找别的方法。

    图二 标记浏览器被自动测试软件控制

    后来我发现在 设置 > 密码(chrome://settings/passwords)里,可以关闭“提示保存密码”的选项,就不会再弹窗了。那么,我们只要修改浏览器的默认设置,即可。

    我的第一反应是用 puppeteer 打开这个页面,然后修改配置。尝试之后发现不行,Puppeteer 不能打开非 http/https/file 协议的页面。接下来尝试修改 userDataDir 目录里的内容,我发现,如果使用 headless 即无窗口模式启动 puppeteer,生成的 user data 目录里的内容就很少,尤其没有 Default/Preferences 这个 json 文件,而后者才是保存浏览器设置的位置。

    所以正确的解法就是:

    1. 先用 headless: false 模式启动一次 puppeteer,获得全是默认值的 userDataDir
    2. 修改 userDataDir/Default/Preferences 文件,在根对象上添加 "credentials_enable_service": false,不保存密码
    3. 启动新的 puppeteer 录制视频前,先复制 userDataDir 到目标位置
    4. puppeteer.launch({ignoreDefaultArgs: ['--enable-automation'], userDataDir: '/path/to/userDataDir'}) 启动窗口时禁用 --enable-automation 选项,避免出现图二的提示条,并且使用刚才复制的 userDataDir 保存缓存等信息

    目前看来,这套解决方案最合适,希望对大家有帮助。

  • 在 Raspberry Pi 4B 上跑 Puppeteer

    在 Raspberry Pi 4B 上跑 Puppeteer

    首先,我使用的是 Raspberry Pi 4B,安装的是官方 Debian 10(buster)系统,并且保持升级到最新版。

    因为集成的 chromium 核心组件的关系,Puppeteer 一直无法跑在 Raspberry Pi 上,需要自己安装 Chromium Browser,然后修改 Puppeteer 启动的浏览器,以实现功能。这个一搜就能找到,比如 https://stackoverflow.com/questions/60129309/puppeteer-on-raspberry-pi-zero-w

    但是我之前一直没能跑起来的问题在于,sudo apt install chromium-browser 会失败,报错找不到目标模块,只能装 chromium-codecs-ffmpeg,然后没用。

    然后我受前几天完成 WSL 配置的启发,使用 apt search chromium 搜索名字接近的包,发现了真正原因:很简单,chromium-browser 当然是存在的,只是因为我当前系统配置的关系,它希望安装 stable 版本的软件,不愿意安装 testing 版本,所以不给装。

    接下来我面临两个选择:

    1. 修改配置
    2. 试试 Chromium

    方案二更容易尝试。于是

    # 安装
    sudo apt install chromium
    
    # 查看路径
    whereis chromium

    修改 JS 代码:

    const browser = await puppeteer.launch({
      executablePath: '/usr/bin/chromium',
      // 其它配置项
      // ....
    }); 

    再执行,成功。啊,终于搞定了手边所有平台跑 Puppeteer 了,哦耶。

  • 在 Windows 10 WSL 中使用 Puppeteer

    在 Windows 10 WSL 中使用 Puppeteer

    使用 Puppeteer 很久了,基本上一直都在使用原生版本,macOS 或者 Ubuntu。在 Windows 10 上,我比较喜欢用 WSL,基本上所有其它脚本和程序都跑在 WSL 上,唯独 Puppeteer,我会专门弄一个目录,以便运行 Windows 版本。

    前些日子去深圳办公,需要跑测试。我厂的测试基于 Perl 的 Test::Base,但 Windows 下的 Perl 不太好用(其实是我没仔细研究),远不如 WSL 开箱即用。所以就折腾了一下 WSL,竟然折腾好了,所以记一下

    0. 升级到最新系统

    第一步当然是各种升级,包括系统、WSL 和 node.js。我觉得这次之所以能成功,很大程度上得益于 Windows 10 的发展和 WSL 的升级。对了,Node.js v14 已经 LTS,建议一起升级。

    sudo apt update
    sudo apt install
    
    # node.js 14
    curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - sudo apt-get install -y nodejs
    
    # 在项目目录安装最新版 puppeteer
    npm i puppeteer@5

    1. 运行 puppeteer,查漏补缺

    接下来随便运行一个包含了 puppeteer 的 JS,系统会提示缺少模块,但错误提示大概是找不到文件或者没有权限访问,存在一些迷惑性。仔细看错误提示,能找到缺少的模块名称,对我而言,大概是这些:

    • libnss3
    • libatk1.0-0
    • libatk-bridge2.0-0
    • libcups2
    • libxkbcommon0
    • libgtk-3-0

    这里有个小技巧。错误提示可能是:找不到 libatk1.0.so.0,但是我用 sudo apt install libatk1.0 却无法安装,说找不到这个包(我之前就卡在这里)。此时,可以通过 apt search libatk 搜索,然后就能找到 libatk1.0-0 这个包,最后的 -0 对应的就是 .so.0,安装即可。其它包以此类推。

    重复这个过程,直到所有包都安装成功。

    2. 修改 JS,添加 --no-sandbox

    继续运行,会报告另一个错误,大概意思是目前的版本存在各种问题,建议增加 --no-sandbox 选项。

    修改 JS 文件,在 puppeteer.launch() 增加 --no-sandbox

    const browser = puppeteer.launch({
      args: [
        '--no-sandbox',
      ],
    });

    3. 完成

    现在 Puppeteer 应该可以正常工作了,不过因为缺少 UI 模块,所以暂时无法启动 headless: false,需要的话,还是用原生吧。

  • 使用 site-validator 验证网站链接

    使用 site-validator 验证网站链接

    网站死链的问题比较常见,坏处就不多说了,大家应该都明白。想解决这个问题并不容易,主要难点在发现死链。如果是纯静态网站就比较简单,直接查服务器的 404 日志即可;如果是动态的,比如 SPA,就比较难搞。

    所以我做了这个项目:site-validator。其实非常简单,用 puppeteer 访问网站,找出所有链接,一级一级点下去,直到把一个网站的链接都点一遍为止。记录下所有的死链,输出,然后排查。

    代码简单,但是确实很实用,帮我厂找到不少死链,很好的提升了搜索表现。将来考虑再加一些新功能进去,比如集成 lighthouse 审计之类的。有兴趣的同学可以下载使用,也欢迎提意见和 issue 哦。

  • 给 Hexo 增加替换大图并生成缩略图的功能

    给 Hexo 增加替换大图并生成缩略图的功能

    我厂的官方博客使用 Hexo 搭建,静态页比较符合我厂的技术风格。

    我厂博客可能会用到一些很大的火焰图 SVG,厂长担心打开速度会受拖累,所以让我想办法把大图替换成缩略图,单击再打开原图。

    经过一些研究,我大概得到以下线索:

    1. Hexo 会加载 `/scripts` 下面的脚本
    2. Hexo 是用钩子机制,跟 WordPress 非常像
    3. Hexo 的钩子函数可以使用 Promise

    那么,大概方案就出来了:

    1. 添加脚本,等待 `after_post_render` 钩子。这个钩子触发的时候,博文已经从 markdown 渲染成 html。
    2. 使用 cheerio,找到所有图片,检查图片大小,略过不太大的图片(我的标准是 500K)
    3. 对大图生成缩略图,替换 `src`。

    因为目标是 SVG => PNG/JPG,在 npm 里搜了一圈,发现大家不是 phantom.js 就是 puppeteer,那我就不需要用库了,自己直接写好了。

    经过反复的尝试,最终完成的代码如下:

    hexo.extend.filter.register('after_post_render', async function (data) {
      /** 摘录、详情、内容 */
      const dispose = ['excerpt', 'more', 'content'];
      for (const key of dispose) {
        const $ = cheerio.load(data[key], {
          ignoreWhitespace: false,
          xmlMode: false,
          lowerCaseTags: false,
          decodeEntities: false
        });
    
        // 获取所有需要调整大小的图片路径
        let images = $('img').map(async function () {
          let src = $(this).attr('src');
          if (!src) {
            if (config.log) {
              console.info("no src attr, skipped...");
              console.info($(this));
            }
            return;
          }
    
          // 顺便加上 `loading="lazy"`
          $(this).attr('loading', 'lazy');
    
          // take snapshot fo big SVGs
          const originalSrc = src;
          if (/\.svg$/.test(src)) {
            const img = resolve(__dirname, '../source', originalSrc);
            const {size} = await stat(img);
            if (size <= config.limit) {
              return;
            }
            $(this)
              .attr('data-src', src)
              .attr('src', `${src}.png`);
            return img;
          }
        }).get();
        images = await Promise.all(images);
        images = images.filter(image => !!image);
        // 只启动一次 pupppeteer,希望减少系统消耗
        if (images.length > 0) {
          const browser = await puppeteer.launch({
            defaultViewport: config,
          });
          const page = await browser.newPage();
          for (const image of images) {
            await page.goto(`file://${image}`);
            const buffer = await page.screenshot();
            const name = basename(image);
            console.log('screenshot for svg: ' + name);
            // 这一步很重要,因为 `after_post_render` 的时候图片还没有复制,所以要利用 hexo 自身的复制功能复制图片
            route.set(`images/${name}.png`, buffer);
          }
          await browser.close();
        }
        data[key] = $.html();
      }
      return data;
    });

    这段代码里,搞清楚应该用 route.set() 稍微花了些时间,Hexo 的文档写的真是差强人意。这个方法可以向指定位置写入内容,写入的可以是文本,也可以是二进制 buffer。如果写入的是绝对路径,那么就是简单的写入;如果是相对路径,就是向博文环境内写入,接下来的复制过程也会复制到最终代码里。

  • Puppeteer 笔记

    Puppeteer 笔记

    记录使用 Puppeteer 的一些经验。

    安装使用

    puppeteer 是一个“库”,没有自带的命令行功能。所以要使用的话必须写一个文件,然后实现对应的功能。

    npm i puppeteer
    

    在墙内安装

    puppeteer 里面包含完成的浏览器程序,少说也是 100MB,所以需要下载比较长的时间,在墙内则经常会失败。所以建议国内开发者用淘宝的源:

    npm config set puppeteer_download_host=https://npm.taobao.org/mirrors

    Could not find browser revision xxxxx

    如果安装依赖时,默认的浏览器下载不成功,使用时可能会报这个错误。里面的 xxxxx 是某个版本号。此时可以使用 npm i puppeteer --force 重新安装。如果是墙内用户,那么按照上一小节设置源之后再安装,多半就可以解决问题了。

    在 WSL 下使用

    关于 WSL 使用,请参考这篇博文:在 Windows 10 WSL 中使用 Puppeteer

    我的测试仓库和工具

    参见 GitHub puppeteer-tool