Puppeteer国产镜像地址不能用了?

先说背景。

组里同学反馈说GitLab流水线里有时候会失败,我看了下错误,安装Puppeteer失败了。我们知道,Puppeteer依赖于Chrome内核,所以会下载一个无头浏览器,这个是比较耗时的,而且它的资源地址多半在国外,所以下载慢是正常的,有时候失败也是正常的。

针对我们的特殊国情,国内有个npm的淘宝镜像,这两年还更换了域名为cdn.npmmirror.com。它里面就有Puppeteer需要的Chrome资源包
image.pngimage.png
所以,我们只需要在项目根目录的.npmrc目录下配置:

puppeteer-download-host="https://cdn.npmmirror.com/binaries"

我自信满满地给组内同学这么说了,得到的反馈是还是不行。
image.png
打脸来的这么突然,难道老经验不好使了?

我确实已经有几年没有安装过Node.js版本的Puppeteer了(这两年在用Deno版本的),不知道是哪个环节出了问题。所以还去GitHub看源码吧。

版本号探究

Puppeteer依赖Chromium的版本号在这里:github.com/puppeteer/p…

export const PUPPETEER_REVISIONS = Object.freeze({

  chrome: '114.0.5735.90',
  firefox: 'latest',
});

我去对照看了下Deno版本的,是这样的github.com/lucacasonat…

export const PUPPETEER_REVISIONS = Object.freeze({

  chromium: "1022525",
  firefox: "latest",
});

这两个版本号有什么关联吗?一头雾水,就去问ChatGPT,答曰:

114.0.5735.90,Chromium版本号中的第一部分是主版本号,第二部分是次版本号,第三部分是修订版本号。

而494755是Chromium代码库中的提交版本号(revision number),它是用于跟踪代码库中的更改的唯一标识符。在不同的Chromium版本中,提交版本号会不断递增,而主、次、修订版本号则会根据发布策略和更新内容进行变更。

在发版页面github.com/puppeteer/p…搜索:
image.png

可以看到修改了这几处:
image.png
这里的提交记录就是二者的对应关系。

在我们上面的淘宝镜像网站registry.npmmirror.com/binary.html…就能找到文件了:
image.png

这时首先冒出的想法是,大不了把这个文件下载下来,按照报错信息里『cdn.npmmirror.com/binaries/11…』按照同样的路径放到我们CDN上,这样也能解决问题。

下载路径

本着来都来了的心思,看下代码吧。找到下载路径的代码:

// Prepare variables used in browser downloading
if (!configuration.skipDownload) {
  configuration.browserRevision =
    process.env['PUPPETEER_BROWSER_REVISION'] ??
    process.env['npm_config_puppeteer_browser_revision'] ??
    process.env['npm_package_config_puppeteer_browser_revision'] ??
    configuration.browserRevision;

  const downloadHost =
    process.env['PUPPETEER_DOWNLOAD_HOST'] ??
    process.env['npm_config_puppeteer_download_host'] ??
    process.env['npm_package_config_puppeteer_download_host'];

  if (downloadHost && configuration.logLevel === 'warn') {
    console.warn(
      `PUPPETEER_DOWNLOAD_HOST is deprecated. Use PUPPETEER_DOWNLOAD_BASE_URL instead.`
    );
  }

  configuration.downloadBaseUrl =
    process.env['PUPPETEER_DOWNLOAD_BASE_URL'] ??
    process.env['npm_config_puppeteer_download_base_url'] ??
    process.env['npm_package_config_puppeteer_download_base_url'] ??
    configuration.downloadBaseUrl ??
    downloadHost;

  configuration.downloadPath =
    process.env['PUPPETEER_DOWNLOAD_PATH'] ??
    process.env['npm_config_puppeteer_download_path'] ??
    process.env['npm_package_config_puppeteer_download_path'] ??
    configuration.downloadPath;
}

.npmrc中配置的是其实就是上面的npm_config_puppeteer_download_host,_换成了-:

puppeteer-download-host=https://cdn.npmmirror.com/binaries

注意看那行警告,现在推荐的是使用PUPPETEER_DOWNLOAD_PATH,对应到.npmrc中也就是puppeteer-download-base-url

下载url的拼接逻辑在这里:

// packages/browsers/src/install.ts
function getDownloadUrl(
  browser: Browser,

  platform: BrowserPlatform,

  buildId: string,
  baseUrl?: string
): URL {
  return new URL(downloadUrls[browser](platform, buildId, baseUrl));
}

// packages/browsers/src/browser-data/browser-data.ts
export const downloadUrls = {
  [Browser.CHROMEDRIVER]: chromedriver.resolveDownloadUrl,
  [Browser.CHROME]: chrome.resolveDownloadUrl,
  [Browser.CHROMIUM]: chromium.resolveDownloadUrl,
  [Browser.FIREFOX]: firefox.resolveDownloadUrl,
};

// packages/browsers/src/browser-data/chromedriver.ts
export function resolveDownloadUrl(
  platform: BrowserPlatform,
  buildId: string,
  baseUrl = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing'
): string {
  return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
}

也就是说,如果前面如果没有得到配置downloadBaseUrl,就会找edgedl.me.gvt1.com/edgedl/chro…。这个与packages/puppeteer-core/src/common/Configuration.ts代码注释中是一致的:

/**
 * Specifies the URL prefix that is used to download the browser.
 *
 * Can be overridden by `PUPPETEER_DOWNLOAD_BASE_URL`.
 *
 * @remarks
 * This must include the protocol and may even need a path prefix.
 *
 * @defaultValue Either https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing or
 * https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central,
 * depending on the product.
 */
downloadBaseUrl?: string;

从这个url的命名上看,应该是测试用的。

这时灵光一闪,淘宝镜像里也有一个同名的地址,被我习惯地忽略了:registry.npmmirror.com/binary.html…
image.png

上面的113.0.5672.63是同事报错的版本,下面的114.0.5735.90是我们看的源码中的版本。
只是怎么只有2个文件夹啊?

先不管三七二十一,修改.npmrc,直接换成这个地址试试:

puppeteer-download-base-url="https://cdn.npmmirror.com/binaries/chrome-for-testing"

可以了!

按理说事情就该结束了,但这肯定不是一个程序员解决问题的正确打开方式。我还有个疑问是,下载路径中的chrome版本哪里来的呢?

// packages/browsers/src/browser-data/browser-data.ts
export async function resolveBuildId(
  browser: Browser,

  platform: BrowserPlatform,

  tag: string
): Promise<string> {
  switch (browser) {
    case Browser.FIREFOX:
      switch (tag as BrowserTag) {
        case BrowserTag.LATEST:
          return await firefox.resolveBuildId('FIREFOX_NIGHTLY');
        case BrowserTag.BETA:
        case BrowserTag.CANARY:
        case BrowserTag.DEV:
        case BrowserTag.STABLE:
          throw new Error(`${tag} is not supported for ${browser}`);
      }
    case Browser.CHROME:
      switch (tag as BrowserTag) {
        case BrowserTag.LATEST:
          return await chrome.resolveBuildId(
            platform,
            ChromeReleaseChannel.CANARY
          );
        case BrowserTag.BETA:
          return await chrome.resolveBuildId(
            platform,
            ChromeReleaseChannel.BETA
          );
        case BrowserTag.CANARY:
          return await chrome.resolveBuildId(
            platform,
            ChromeReleaseChannel.CANARY
          );
        case BrowserTag.DEV:
          return await chrome.resolveBuildId(
            platform,
            ChromeReleaseChannel.DEV
          );
        case BrowserTag.STABLE:
          return await chrome.resolveBuildId(
            platform,
            ChromeReleaseChannel.STABLE
          );
      }
    case Browser.CHROMEDRIVER:
      switch (tag as BrowserTag) {
        case BrowserTag.LATEST:
        case BrowserTag.CANARY:
          return await chromedriver.resolveBuildId(ChromeReleaseChannel.CANARY);
        case BrowserTag.BETA:
          return await chromedriver.resolveBuildId(ChromeReleaseChannel.BETA);
        case BrowserTag.DEV:
          return await chromedriver.resolveBuildId(ChromeReleaseChannel.DEV);
        case BrowserTag.STABLE:
          return await chromedriver.resolveBuildId(ChromeReleaseChannel.STABLE);
      }
    case Browser.CHROMIUM:
      switch (tag as BrowserTag) {
        case BrowserTag.LATEST:
          return await chromium.resolveBuildId(platform, 'latest');
        case BrowserTag.BETA:
          return await chromium.resolveBuildId(
            platform,
            ChromeReleaseChannel.BETA
          );
        case BrowserTag.CANARY:
          return await chromium.resolveBuildId(
            platform,
            ChromeReleaseChannel.CANARY
          );
        case BrowserTag.DEV:
          return await chromium.resolveBuildId(
            platform,
            ChromeReleaseChannel.DEV
          );
        case BrowserTag.STABLE:
          return await chromium.resolveBuildId(
            platform,
            ChromeReleaseChannel.STABLE
          );
      }
  }
  // We assume the tag is the buildId if it didn't match any keywords.
  return tag;
}

// packages/browsers/src/browser-data/chrome.ts
export async function getLastKnownGoodReleaseForChannel(
  channel: ChromeReleaseChannel
): Promise<{version: string; revision: string}> {
  const data = (await getJSON(
    new URL(
      'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json'
    )
  )) as {
    channels: {
      [channel: string]: {version: string};
    };
  };

  for (const channel of Object.keys(data.channels)) {
    data.channels[channel.toLowerCase()] = data.channels[channel]!;
    delete data.channels[channel];
  }

  return (
    data as {
      channels: {
        [channel in ChromeReleaseChannel]: {version: string; revision: string};
      };
    }
  ).channels[channel];
}

export async function resolveBuildId(
  _platform: BrowserPlatform,
  channel: ChromeReleaseChannel
): Promise<string> {
  return (await getLastKnownGoodReleaseForChannel(channel)).version;
}

一步一步找到了getLastKnownGoodReleaseForChannel这个函数。我们打开googlechromelabs.github.io/chrome-for-…

image.png

为什么只有114.0.5735.90,而没有113.0.5672.63?
我猜测过了一天,刚好114成为正式版本了?

那么以前版本难道也用的最新版本的chrome?不太可能吧。我试着修改.npmrc:

puppeteer-download-host="https://cdn.npmmirror.com/xxx" # 故意改错

这时安装puppeteer的16.0.0,报错了:

npm WARN deprecated puppeteer@16.0.0: < 19.4.0 is no longer supported
npm ERR! code 1
npm ERR! path /wk/puppeteer-test/node_modules/puppeteer
npm ERR! command failed
npm ERR! command sh -c -- node install.js
npm ERR! ERROR: Failed to set up Chromium r1022525! Set "PUPPETEER_SKIP_DOWNLOAD" env variable to skip download.
npm ERR! Error: Download failed: server returned code 404. URL: https://cdn.npmmirror.com/xxx/chromium-browser-snapshots/Linux_x64/1022525/chrome-linux.zip

1022525这个路径看着可不像现在的东西,到GitHub上看源码,将版本切换到16.0.0,找到revisions:
image.png
难道是19.0.0有了修改吗?切换到19的最新版本,仍然是带具体版本号的:
image.png
在GitHub代码上19版本的tag只能找到19.2.2,现在还没有20的标签。但在外面的tag里是有的:
image.png
再看发版本说明里大大的警告,是破坏性更新:
image.png合并声明里面是这么说的:
image.png
大意是:Puppeteer现在默认使用Chrome进行测试二进制文件,而不是Chromium。这些二进制文件将来会更类似于Chrome二进制文件。由于二进制文件不同,因此我们将更改标记为破坏性,并包括与浏览器安装方式相关的其他破坏性更改。其中包括删除BrowserFetcher、启用mac arm64版本的下载以及删除一些标志和脚本。
总之,这个破坏性的更新很危险。

PS:chromium与puppeteer的对应关系在这里:pptr.dev/chromium-su…

Chromium 114.0.5735.90 - Puppeteer v20.6.0
Chrome for Testing 113.0.5672.63 - Puppeteer v20.1.0
Chrome for Testing 112.0.5615.121 - Puppeteer v20.0.0

注意上面我们看到的registry.npmmirror.com/binary.html…只有两个文件夹,113和114两个版本,所以20.0.0是不能用这个镜像源的。

TIPS

Deno版本

回头再看Deno版本,可以看到它停留在16.2.0这个版本上已经快10个月了。
image.png
注意它不是Puppeteer官方维护的,所以很可能看到Puppeteer这么大刀阔斧地更新,没有迭代的动力了吧?

随着Deno对Node.js API兼容的完善,直接支持运行Puppeteer可能也不远了。

缓存目录变更

自v19.0.0起,chrome内核文件被下载到$HOME/.cache/puppeteer目录下(以前是node_modules/puppeteer/.local-chromium)。这样在流水线中使用的话,要么将这个目录缓存,要么修改路径。

如果是修改路径的话,可以在项目根目录下新建一个文件.puppeteerrc.cjs 或者puppeteer.config.cjs

const { join } = require('path');

/**
 * @type {import("puppeteer").Configuration}
 */
module.exports = {
  // Changes the cache location for Puppeteer.
  cacheDirectory: join(__dirname, 'node_modules', '.cache', 'puppeteer'),
};

跳过下载

在某些时候,你可能并不需要下载这一步,比如将下载资源前置或后置。通过设置环境变量PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true,可以跳过这一步。

Linux中安装依赖和字体

比如在centos7中安装依赖库和字体:

#依赖库
yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y

#字体
yum install ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y

总结

我们通过查看代码,在淘宝镜像中找到了新的Puppeteer对应的Chrome资源包地址。对于Puppeteer20.1以上的版本,可以通过以下方式使用国产镜像源:

puppeteer-download-base-url="https://cdn.npmmirror.com/binaries/chrome-for-testing"

19以下版本还是原来的方式:

puppeteer-download-host="https://cdn.npmmirror.com/binaries"

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYRWgTWA' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片