news 2026/9/8 18:11:46

从 GetInstalledBrowsersOptions 入手:用 @puppeteer/browsers 枚举缓存中已安装的浏览器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从 GetInstalledBrowsersOptions 入手:用 @puppeteer/browsers 枚举缓存中已安装的浏览器

从 GetInstalledBrowsersOptions 入手:用 @puppeteer/browsers 枚举缓存中已安装的浏览器

【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer

@puppeteer/browsers(位于仓库packages/browsers/)既提供npx @puppeteer/browsers这样的 CLI,也提供一套程序化 API,用来下载、缓存、管理并启动 Chrome/Chromium、Chrome for Testing、ChromeDriver 与 Firefox 等浏览器二进制。其中getInstalledBrowsers(options)与它的唯一入参类型GetInstalledBrowsersOptions,是“盘点”本机缓存目录中到底装了哪些浏览器、版本与平台的关键入口——相当于给浏览器缓存做了一次“库存清点”。

本文以 GetInstalledBrowsersOptions 接口文档为主体,结合其配套的 getInstalledBrowsers() 函数文档以及packages/browsers/下的源码实现与测试用例,带你弄清楚:cacheDir到底指向哪里、缓存目录的内部结构如何解析、返回值InstalledBrowser[]携带哪些元数据,以及如何在真实工程中用它做浏览器安装状态的查询与校验。读完你将能编写出“先查询、后安装/启动”的健壮脚本,并理解browsers list这个 CLI 子命令的底层逻辑。

一、接口定位:一个属性支撑起整个“库存查询”入口

GetInstalledBrowsersOptions@puppeteer/browsers公共 API 中getInstalledBrowsers()的入参对象类型。它的源码声明位于 packages/browsers/src/install.ts,官方接口文档的描述是:

export interface GetInstalledBrowsersOptions { /** * The path to the root of the cache directory. */ cacheDir: string; }

对应类型签名如下:

export interface GetInstalledBrowsersOptions

Properties 一览

PropertyModifiersTypeDescriptionDefault
cacheDirstringThe path to the root of the cache directory(缓存目录的根路径)无(必填)

要点提炼:

  • 该接口只包含一个字段cacheDir,且是必填的string,文档中没有任何默认值。
  • cacheDir的语义是 “path to the root of the cache directory”,即缓存目录的根,而不是某个具体浏览器或某个具体版本子目录的路径。
  • 它描述的是一个“查询动作”的输入:调用方声明“请帮我去这个目录里看看装了什么”。

把接口放进调用场景,其搭档函数 getInstalledBrowsers() 的签名是:

export declare function getInstalledBrowsers( options: GetInstalledBrowsersOptions, ): Promise<InstalledBrowser[]>;

Returns:Promise<InstalledBrowser[]>—— 即返回一个InstalledBrowser对象数组,数组中的每个元素代表一个“在缓存目录中检测到的已安装浏览器”。

二、cacheDir 到底是什么?缓存目录的结构约定

要正确填写cacheDir,就必须理解@puppeteer/browsers的磁盘缓存布局。在 Cache.ts 的类注释中给出了明确的层级约定:

rootDir ├── <browser1> # 例如 chrome / firefox / chromedriver(Browser 枚举名) │ └── <platform>-<buildId> # 例如 linux-116.0.5793.0 │ └── 具体浏览器二进制内容 └── <browser2> └── <platform>-<buildId>

落到代码上(packages/browsers/src/Cache.ts):

browserRoot(browser: Browser): string { return path.join(this.#rootDir, browser); }

也就是说:

  • 第一层:按浏览器种类(Browser枚举)建目录;
  • 第二层:以<platform>-<buildId>命名的安装目录,例如chrome/linux-116.0.5793.0
  • 每个浏览器根目录下还可能有一个.metadata文件,用来记录别名(如stablecanary映射到具体buildId)以及自定义 provider 写入的可执行文件相对路径(详见下文测试部分)。

因此,当 Puppeteer 生态以默认配置运行时,cacheDir的默认值来自 packages/puppeteer-core/src/common/Configuration.ts 的注释说明:

@defaultValue `path.join(os.homedir(), '.cache', 'puppeteer')`

即默认根目录通常为~/.cache/puppeteer(Linux/macOS)。如果你在代码里显式传了自定义的cacheDir(比如/tmp/my-browser-cache),那么后续installuninstalllaunchgetInstalledBrowsers必须使用同一个根目录,查询结果才与实际安装状态一致。

需要特别注意:cacheDir指的是缓存根的父级路径,而不是形如.../chrome/linux-116.0.5793.0的深层目录。传入错误层级会导致检测不到浏览器。

三、底层实现:getInstalledBrowsers 是如何“扫盘”的

getInstalledBrowsers的实现非常轻量——它把所有工作委托给了Cache类(packages/browsers/src/install.ts):

/** * Returns metadata about browsers installed in the cache directory. * * @public */ export async function getInstalledBrowsers( options: GetInstalledBrowsersOptions, ): Promise<InstalledBrowser[]> { return new Cache(options.cacheDir).getInstalledBrowsers(); }

真正执行目录扫描的是 Cache.getInstalledBrowsers():

getInstalledBrowsers(): InstalledBrowser[] { if (!fs.existsSync(this.#rootDir)) { return []; } const types = fs.readdirSync(this.#rootDir); const browsers = types.filter((t): t is Browser => { return (Object.values(Browser) as string[]).includes(t); }); return browsers.flatMap(browser => { const files = fs.readdirSync(this.browserRoot(browser)); return files .map(file => { const result = parseFolderPath( path.join(this.browserRoot(browser), file), ); if (!result) { return null; } return new InstalledBrowser( this, browser, result.buildId, result.platform as BrowserPlatform, ); }) .filter((item: InstalledBrowser | null): item is InstalledBrowser => { return item !== null; }); }); }

从源码结构可以总结出它的判定流程:

  1. 容错优先:若cacheDir指向的根目录根本不存在,直接返回空数组[],不抛异常——这意味着“查询一个尚未初始化的缓存目录”是一种合法且安全的状态。
  2. 只识别已知浏览器:第一层子目录中,只有名字恰好命中Browser枚举值的才会被纳入扫描(filter+Object.values(Browser).includes(t))。例如仓库 docs/browsers-api/browsers.browser.md 中定义的CHROMEFIREFOX等枚举名所对应的目录名。混入的无关目录会被自动忽略。
  3. 目录名解析:对每个浏览器根目录下的子目录,调用parseFolderPath解析(packages/browsers/src/Cache.ts):
function parseFolderPath( folderPath: string, ): {platform: string; buildId: string} | undefined { const name = path.basename(folderPath); const splits = name.split('-'); if (splits.length !== 2) { return; } const [platform, buildId] = splits; if (!buildId || !platform) { return; } return {platform, buildId}; }

可以看到解析依赖“恰好两段”的命名约定:<platform>-<buildId>。之所以成立,是因为 Chrome 的buildId(如116.0.5793.0)用点号分隔而不用连字符;不满足两段格式或段为空的目录会被跳过(返回null后过滤掉)。这是实现层面的推断性约定,命名必须与Cache.installationDir()的写入格式保持一致,否则装得进去却查不出来。

  1. 构造返回值:每个通过解析的目录会被包装成一个 InstalledBrowser 实例,构造函数内部通过cache.computeExecutablePath({...})计算出该浏览器的可执行文件绝对路径。

InstalledBrowser对外暴露的关键元数据(见 docs/browsers-api/browsers.installedbrowser.md)包括:

成员含义
browserBrowser枚举,标识是哪一种浏览器
buildId具体版本号/构建号,如116.0.5793.0
platformBrowserPlatform,如linuxmacwin64(见 docs/browsers-api/browsers.browserplatform.md)
executablePath可执行文件(chrome/firefox/chromedriver 等)的完整绝对路径
path(getter)安装目录根路径,等价于Cache.installationDir()的结果

四、实战示例:查询已安装的浏览器并打印元数据

与 CLI 子命令npx @puppeteer/browsers list的行为一致,程序化调用getInstalledBrowsers的典型用法如下:

import { getInstalledBrowsers, install, Browser, BrowserPlatform, } from '@puppeteer/browsers'; const cacheDir = '/tmp/my-browser-cache'; // 必须与 install 时使用的 cacheDir 一致 // 1) 先确保装了一个浏览器(幂等:已存在则复用缓存) await install({ cacheDir, browser: Browser.CHROME, buildId: 'stable', // 也支持具体 buildId 或 milestone,如 '116.0.5793.0' / '117' platform: BrowserPlatform.LINUX, // 省略时自动探测 }); // 2) 盘点缓存目录里所有已安装浏览器 const installed = await getInstalledBrowsers({cacheDir}); for (const browser of installed) { console.log(`${browser.browser}@${browser.buildId} (${browser.platform})`); console.log(` executable: ${browser.executablePath}`); } if (installed.length === 0) { console.log(`缓存目录 ${cacheDir} 中没有检测到任何已安装的浏览器。`); }

输出效果(形如):

chrome@116.0.5793.0 (linux) executable: /tmp/my-browser-cache/chrome/linux-116.0.5793.0/chrome-linux64/chrome

这段脚本对应了仓库 CLI 中list子命令的等价逻辑——packages/browsers/src/CLI.ts 内部正是先new Cache(cacheDir)再调用cache.getInstalledBrowsers(),然后逐行打印:

for (const browser of browsers) { console.log( `${browser.browser}@${browser.buildId} (${browser.platform}) ${browser.executablePath}`, ); }

CLI 侧对应命令为:

# 列出默认缓存目录下的已安装浏览器 npx @puppeteer/browsers list # 列出指定缓存目录(等价于传入 cacheDir: '/tmp/my-browser-cache') npx @puppeteer/browsers list --path /tmp/my-browser-cache

所以“程序化 API 传cacheDir”与“CLI 传--path”指向的是同一件事:指定缓存根目录。

五、实践中的典型用法:查询 + 按需安装/启动

GetInstalledBrowsersOptions最常见的实战场景是避免盲目重复安装:先查询缓存,命中则直接复用,未命中再安装。可以这样组织(伪代码思路):

import {getInstalledBrowsers, install, launch, Browser} from '@puppeteer/browsers'; const cacheDir = process.env.BROWSER_CACHE_DIR ?? '/tmp/my-browser-cache'; const desiredBuildId = '116.0.5793.0'; async function ensureBrowser() { const installed = await getInstalledBrowsers({cacheDir}); const found = installed.find(b => { return b.browser === Browser.CHROME && b.buildId === desiredBuildId; }); if (!found) { console.log('未检测到目标版本,开始安装…'); await install({ cacheDir, browser: Browser.CHROME, buildId: desiredBuildId, // 如需定位问题可开启调试日志: // 在命令行使用 env NODE_DEBUG="puppeteer:browsers:*" 观察 install/cache 各环节 }); } const current = await getInstalledBrowsers({cacheDir}); const target = current.find(b => b.buildId === desiredBuildId)!; return target.executablePath; // 之后可直接交给 launch() 或 Puppeteer 使用 }

几点工程化建议:

  • 用同一个cacheDir贯穿 install/get/launch。若 scripts 里不一致,会出现“安装了却查不到”“查到了却启动失败”的隐性 bug;当配置错误时,Puppeteer 的错误提示也会引导你检查 cache path(见 packages/puppeteer-core/src/node/BrowserLauncher.ts 相关报错文案)。
  • 观察缓存层行为@puppeteer/browsers支持NODE_DEBUG="puppeteer:browsers:cache"来输出缓存操作日志(相关调试通道定义见 packages/browsers/src/debug.ts),排查查询结果异常时非常有用。
  • 启动前做兜底getInstalledBrowsers返回的executablePath可以直接作为launch()的输入(launch 相关选项见 docs/browsers-api/browsers.launchoptions.md),避免再次解析目录。

六、源码与测试如何验证接口行为

仓库的单元测试对该接口的两种关键场景做了覆盖,是理解cacheDir语义的最好佐证:

  1. “安装后必然可被查询到”。在 packages/browsers/test/src/chrome/install.test.ts 中,测试先通过install({cacheDir: tmpDir, browser: Browser.CHROME, ...})完成安装,随后:
const cache = new Cache(tmpDir); const installed = cache.getInstalledBrowsers(); assert.deepStrictEqual(browser, installed[0]); assert.deepStrictEqual(browser!.executablePath, installed[0]?.executablePath);

这验证了:只要cacheDir传对,安装结果与查询结果是一一对应的,且InstalledBrowserexecutablePath会被原样返回。

  1. “自定义 provider 写入的可执行路径也能被正确读出”。在 packages/browsers/test/src/installWithProviders.test.ts 中,测试确认当使用自定义 provider 安装时,安装程序会把相对可执行路径持久化到.metadataexecutablePaths(键为${platform}-${buildId}),随后:
const installed = await getInstalledBrowsers({cacheDir: tmpDir}); const found = installed.find(b => { return b.buildId === testChromeBuildId; }); assert.strictEqual( found?.executablePath, result.executablePath, 'getInstalledBrowsers should return the correct executable path', );

这说明cacheDir查询不只是“看目录名”,还会回读.metadata中的可执行路径记录(对应 Cache.computeExecutablePath() 中“优先使用存储路径、其次使用默认路径”的逻辑)。同一个文件 packages/browsers/test/src/installWithProviders.test.ts 里还有一处验证:自定义路径写入后,getInstalledBrowsers({cacheDir: tmpDir})能拾取到该记录。

七、小结:使用 GetInstalledBrowsersOptions 的要点清单

关注点结论
cacheDir是否必填必填,无默认值
cacheDir的层级缓存根目录,内部布局为<cacheDir>/<browser>/<platform>-<buildId>
根目录不存在时返回[],不抛错
返回类型Promise<InstalledBrowser[]>,元素含browser/buildId/platform/executablePath/path
目录识别的判据一级目录名须命中Browser枚举;二级目录名须可解析成恰好两段<platform>-<buildId>
与 CLI 的关系getInstalledBrowsers({cacheDir})等价于npx @puppeteer/browsers list --path <cacheDir>的核心逻辑(见 CLI.ts)
联动约束必须与install()/launch()/uninstall()使用同一cacheDir,否则状态不一致

GetInstalledBrowsersOptions虽然只有cacheDir一个字段,却是浏览器管理链路上“安装—查询—启动—卸载”闭环里的关键拼图。理解它背后基于目录约定的检测机制,你就能在 CI 缓存复用、本地多版本浏览器管理、以及 Puppeteer 集成场景中,准确判断“目标浏览器到底在不在、在哪里”。

【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 18:09:14

基于SpringBoot的留学信息管理系统设计与实现毕业设计项目源码

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华
网站建设 2026/9/8 18:08:16

裸机工程快速接入FreeRTOS:任务拆分与移植避坑指南

前阵子有个朋友把跑了一年多的裸机工程发给我&#xff0c;问能不能不推倒重写就把RTOS加进去。他的状态我记得很清楚&#xff1a;main 函数的 while(1) 里塞了五六个模块&#xff0c;按键扫描、传感器读取、OLED刷新、蜂鸣器控制全挤在一起&#xff0c;某个外设偶尔卡一下&…

作者头像 李华
网站建设 2026/9/8 18:08:12

opencode 实战指南:终端 AI 编码代理的安装配置与核心玩法

如果你最近在逛技术社区&#xff0c;大概率刷到过 opencode 这个名字。它是一款开源的终端 AI 编码代理&#xff0c;简单说就是让你在命令行里像和同事聊天一样&#xff0c;把“写代码、改 bug、跑测试”这些活交给 AI 去执行。和 Claude Code 这类绑定单一模型的工具不同&…

作者头像 李华
网站建设 2026/9/8 18:07:46

微调模型上线有多难?从自建GPU到火山方舟托管的成本账

先说一个我最近听得特别多的错觉&#xff1a;微调模型在实验环境里跑通了几条测试用例&#xff0c;团队就觉得距离上线只差一个“部署”。结果真到要服务线上业务的时候才发现&#xff0c;前面省下的功夫都会在部署环节加倍补回来——推理服务起不来、并发一高就超时、模型版本…

作者头像 李华