Puterputer.apps.list()深度指南:列出账号应用、分页游标与流式遍历全解析
【免费下载链接】puter🌐 The Internet Computer! Free, Open-Source, and Self-Hostable.项目地址: https://gitcode.com/GitHub_Trending/pu/puter
puter.apps.list()是 Puter JavaScript SDK 中用于获取"当前用户拥有或有权限访问的应用"列表的核心方法,广泛适用于网站、Web App、Node.js 与 Workers 等各类客户端场景。本文以官方文档 list.md 为骨架,结合 SDK 源码实现 与 后端 AppDriver,系统讲解其调用语法、六类可选参数、三种返回形态(纯数组 / 分页信封 / 异步迭代器)、底层分页协议及完整可运行示例,帮你彻底掌握应用列表的获取与大规模遍历方案。
方法定位:list 返回什么
根据 list.md 的说明,puter.apps.list()返回一个包含所有归属于当前用户、且当前应用有权访问的App对象数组。若用户当前没有任何应用,则返回空数组。
官方文档 frontmatter 声明该方法支持的平台为platforms: [websites, apps, nodejs, workers],即四类 Puter 客户端均可直接调用,无需额外鉴权配置——SDK 会自动携带当前会话身份。
从 SDK 源码看,真正决定"能列出什么"的是底层驱动请求中的过滤谓词:
const select = utils.makeDriverMethod({ iface: 'puter-apps', driver: 'es:app', method: 'select', argNames: ['uid'], puter, readonly: true, }); const base = { predicate: ['user-can-edit'] };这段代码位于 list.js 的实现:SDK 通过puter-apps接口调用es:app驱动的select方法,并使用user-can-edit谓词过滤——即返回当前用户拥有编辑权(拥有或被授予权限)的应用集合。调用被标记为readonly: true,说明它属于纯查询操作,不会产生副作用或计费写入。
快速上手:创建 3 个应用并列出
基础语法
puter.apps.list(); puter.apps.list(options);两种调用形态中,options均为可选。不带任何参数时,方法返回一个 Promise,resolve 为一个扁平数组,语义最直观:
const apps = await puter.apps.list(); console.log(apps.map(app => app.name));官方完整示例(含创建与清理)
原文档给出了一个"先造数据、再列表、最后清理"的完整自洽示例,这里完整继承(在浏览器 HTML 页面中引入 Puter SDK v2 后执行;SDK 亦可通过 src/puter-js 本地构建使用):
<html> <body> <script src="https://js.puter.com/v2/"></script> <script> (async () => { // (1) Generate 3 random app names let appName_1 = puter.randName(); let appName_2 = puter.randName(); let appName_3 = puter.randName(); // (2) Create 3 apps await puter.apps.create(appName_1, 'https://example.com'); await puter.apps.create(appName_2, 'https://example.com'); await puter.apps.create(appName_3, 'https://example.com'); // (3) Get all apps (list) let apps = await puter.apps.list(); // (4) Display the names of the apps puter.print(JSON.stringify(apps.map(app => app.name))); // (5) Delete the 3 apps we created earlier (cleanup) await puter.apps.delete(appName_1); await puter.apps.delete(appName_2); await puter.apps.delete(appName_3); })(); </script> </body> </html>示例覆盖了 create、list、delete 三个方法,其中puter.randName()用于生成随机应用名,puter.print()用于把结果显示到 UI。注意最后的 delete 步骤是良好的清理习惯,避免测试数据残留到账号中。
options参数全解
当传入对象形态参数时,SDK 会将其中的分页相关字段(limit/offset/cursor/includeTotal/stream)单独解构出来用于驱动请求,其余字段(stats_period、icon_size)作为params原样透传给后端:
const { limit, offset, cursor, includeTotal, stream, ...params } = opts; if ( isObjectForm ) base.params = params; if ( limit !== undefined ) base.limit = limit;以下逐项说明各参数的取值与语义:
stats_period(可选,String)
指定open_count(打开次数)与user_count(访问用户数)的统计周期。可能取值如下,默认值为all(全时段累计):
| 取值 | 含义 |
|---|---|
today | 今天 |
yesterday | 昨天 |
7d | 最近 7 天 |
30d | 最近 30 天 |
this_month | 本月 |
last_month | 上月 |
this_year | 今年 |
last_year | 去年 |
month_to_date | 本月至今 |
year_to_date | 今年至今 |
last_12_months | 最近 12 个月 |
all(默认) | 全时段 |
从后端实现看,统计周期对应 AppDriver.js 中的统计查询逻辑:SDK 将 stats 选项打包在params之下(源码注释明确说明 puter-js 的makeDriverMethod/Apps.get会把 options 打包到params),后端同时兼容顶层扁平形状:
const stats_period = params.stats_period ?? rest.stats_period; const stats_grouping = params.stats_grouping ?? rest.stats_grouping; const needsStats = params.stats !== false && (stats_period || stats_grouping); // Detailed period/grouping is per-app only — skip the batch cache // and go straight to the live query. const hasDetailed = Boolean(stats_period || stats_grouping);可以推断:未指定stats_period时,后端走默认的缓存批量统计路径(性能更优);指定了具体周期后则进入getAppStatsDetailed的实时明细查询路径,换取按周期的精确计数。
icon_size(可选,Integer)
返回应用图标的尺寸(像素),决定App.icon字段输出多大的 Data URL 图片。可选值:null、16、32、64、128、256、512。默认值为null,表示返回图标原始尺寸。当需要渲染列表缩略图时,传64或128可显著降低传输体积。
limit(可选,Number)
单次调用最多返回的应用数量。在流式与游标分页模式下,它控制每一页的大小。
offset(可选,Number)
跳过指定数量的应用后开始返回,用于传统"偏移量"分页。官方文档明确提示:遍历大型列表时优先使用cursor,因为 offset 分页在大数据量下性能较差且容易因数据变动产生重复/遗漏。
cursor(可选,String)
显式启用游标分页。第一次请求传null(代表第一页),之后把每次响应中的cursor字段传给下一次调用,直到响应中不再包含cursor(说明已到最后一页)。注意:只要options对象里出现了cursor键(即使值为null),返回形态就会从数组切换为分页信封对象——这是 SDK 源码中通过hasOwnProperty.call(opts, 'cursor')精确判断的:
const hasCursor = Object.prototype.hasOwnProperty.call(opts, 'cursor');includeTotal(可选,Boolean)
为true时,分页响应会额外携带total字段,表示该用户应用的总数。官方实现与文档一致地指出:只有请求包含cursor(含 null)、offset或includeTotal之一时,返回值才是分页信封;同时从 pagination.js 的实现可以看出,includeTotal只在第一页请求上发送,因为"总数统计在条目越多时成本越高,且总数不会随翻页变化"。
stream(可选,Boolean)
为true时,方法不再返回 Promise,而是返回一个异步迭代器,可直接配合for await ... of逐页消费。可与limit组合控制页大小,也可传cursor从指定页继续;不能与offset组合使用,若同时传入会抛出PuterJSError(错误码invalid_request):
if ( stream === true ) { if ( offset !== undefined ) { throw new PuterJSError( '`offset` cannot be combined with `stream`; pass `cursor` to resume from a position.', 'invalid_request', ); } // ...async generator 逐页 yield }开启includeTotal时,只有第一页会携带total。
返回值:三种形态与分页信封
list()的返回形态由是否携带分页参数决定,这是理解本方法的关键:
形态一:纯数组(未携带任何分页参数)
返回 Promise,resolve 为所有App对象组成的数组。官方文档特别强调:不带分页参数的请求仍返回完整的扁平数组,旧代码完全不受影响——SDK 只是在底层把它拆成了逐页请求再合并:
// Unbound listing: fetch page by page under the hood so no single request // carries the whole result, then return the legacy array. return fetchAllPages(fetchPage).then(items => addUserIterationToApps(puter, items));对应 fetchAllPages 的实现,其内部本质是"把所有分页信封的 items 依次拼接":
async function fetchAllPages (fetchPage) { const items = []; for await ( const page of iteratePages(fetchPage) ) { items.push(...(page.items ?? [])); } return items; }这就保证了:无论请求是否传limit,最终对调用方呈现的都是传统数组形态,既避免了单请求携带全部结果的压力,又不破坏既有集成。
形态二:分页信封对象(请求包含cursor(含 null)、offset或includeTotal之一)
Promise resolve 为一个 page 对象,包含:
| 字段 | 类型 | 说明 |
|---|---|---|
items | Array | 本页的App对象数组 |
cursor | String(可选) | 仅在还有更多页时出现;将其传给下一次调用即可获取下一页 |
total | Number(可选) | 用户应用总数,仅在设置includeTotal时出现 |
在 SDK 源码中,当请求命中分页参数时,select的返回若为信封形态(非数组且含items),则原样返回该信封:
const result = await select(driverArgs); if ( result && !Array.isArray(result) && Array.isArray(result.items) ) { addUserIterationToApps(puter, result.items); return result; }若后端返回的是裸数组(例如旧版后端忽略分页参数),SDK 也会优雅兼容。
形态三:异步迭代器(stream: true)
返回 async iterator,逐页产出 page 对象。官方示例如下,page.items上的每个元素仍是App对象:
for await (const page of puter.apps.list({ stream: true })) { for (const app of page.items) { console.log(app.name); } }其底层由 iteratePages 驱动——一个标准的 async generator,从cursor: null发起请求,yield 当前页后依据page.cursor是否存在决定是否继续拉取:
async function* iteratePages (fetchPage, opts = {}) { let pageParams = { cursor: opts.cursor ?? null, ...(opts.includeTotal === true ? { includeTotal: true } : {}), }; while ( true ) { const result = await fetchPage(pageParams); const page = Array.isArray(result) ? { items: result } : (result ?? { items: [] }); yield page; if ( ! page.cursor ) return; pageParams = { cursor: page.cursor }; } }注意这里对忽略分页参数的后端做了兼容:后端返回裸数组时,该数组被当作唯一的一页;若完全无返回则视为空页。
另外,无论哪种形态,SDK 都会对每个应用调用addUserIterationToApps注入用户遍历辅助方法,使返回的App对象可以继续调用app.users()/app.getUsers()。
返回的App对象字段说明
list()返回的每个元素都是标准 App 对象,核心字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
uid | String | Puter 在应用创建时生成的全局唯一标识 |
name | String | 应用名称(应用 API 调用中的唯一键) |
icon | String | 应用图标的 Data URL(base64 编码图片),尺寸受icon_size影响 |
description | String | 应用描述 |
title | String | 应用显示标题 |
maximize_on_start | Boolean | 启动时是否最大化窗口,默认false |
index_url | String | 应用启动时加载的入口文件 URL |
created_at | String | 创建时间,格式YYYY-MM-DDTHH:MM:SSZ |
background | Boolean | 是否作为后台应用运行,默认false |
filetype_associations | Array | 应用可打开的文件类型,形如[".txt", "image/png"],目录关联用".directory" |
open_count | Number | 应用被打开的次数;设置stats_period后为该周期内次数 |
user_count | Number | 有权访问该应用的用户数;设置stats_period后为该周期内统计 |
metadata | Object | 应用自定义元数据,任意键值对 |
对列表元素做用户遍历
列表接口返回的App对象额外支持两种用户迭代方法(这正是前文提到的addUserIterationToApps所注入的):
// 逐页迭代全部有权用户(默认每页 100,可传 pageSize) for await (const user of app.users()) { console.log(user); // { username, user_uuid, user_email? } } // 按 limit/offset 获取一页用户 const users = await app.getUsers({ limit: 2, offset: 0 });其中user_email仅当用户授予了当前应用user:<uuid>:email:read权限(例如通过puter.perms.request('email'))时才会出现,否则被省略;若用户授权但没有登记邮箱则可能为null。这使"列出应用 → 遍历应用用户"成为一个完整的工作流。
实战:五种典型调用组合
1. 简单列出全部应用名
const apps = await puter.apps.list(); console.log(apps.map(app => ({ name: app.name, uid: app.uid })));2. 游标手动翻页(含总数)
let cursor = null; do { const page = await puter.apps.list({ cursor, // 首轮传 null 表示第一页 limit: 10, includeTotal: true, }); for (const app of page.items) { console.log(page.total, app.name); // 仅第一页可读 total } cursor = page.cursor; // 无 cursor 即最后一页 } while (cursor);3. 流式遍历全部应用
for await (const page of puter.apps.list({ stream: true, limit: 20, })) { for (const app of page.items) { await doSomethingWith(app); } }4. 从某页继续(断点续传)
// 第一次:拿到 cursor const first = await puter.apps.list({ cursor: null, limit: 20 }); // 后续进程恢复:直接带着 cursor 继续 const next = await puter.apps.list({ cursor: first.cursor, limit: 20 });5. 携带统计周期与图标尺寸
const apps = await puter.apps.list({ stats_period: '7d', // open_count / user_count 取最近 7 天 icon_size: 128, // 图标压缩到 128px }); for (const app of apps) { console.log(app.name, app.open_count, app.user_count); }边界行为与注意事项
- 无应用时:返回空数组(
[]),而非报错或null。 - 兼容性设计:不带分页参数的调用行为与旧版本完全一致(返回纯数组),只是底层改为逐页拉取——官方文档原话:"Requests without pagination params keep returning the full list as a plain array, so existing code is unaffected — under the hood the SDK now fetches it page by page."
offset与stream互斥:同时传入会抛出invalid_request错误;流式场景下要用cursor恢复位置。includeTotal成本:总数统计成本随条目数量上升,SDK 只在第一页请求发送includeTotal,后续页不再重复统计。cursor键存在即切换形态:即使显式传cursor: null,返回值也是分页信封{ items, ... }而非扁平数组,写代码时不要假设传null就返回数组。- 大列表首选 cursor:官方明确提示 offset 仅适合小规模分页,遍历大列表应使用
cursor。
相关实现与文档索引
- 官方 API 文档原文:src/docs/src/Apps/list.md
- App 对象属性与方法:src/docs/src/Objects/app.md
- 配套方法:create、get、update、delete、checkName
- SDK 端分页与列表实现:src/puter-js/src/modules/apps/list.js、src/puter-js/src/lib/pagination.js
- 后端统计/查询实现:src/backend/drivers/apps/AppDriver.js
- 通用分页信封协议说明(
{ items, cursor?, total? }):doc/pagination.md
综上,puter.apps.list()在设计上同时照顾了"简单列出"与"大规模遍历"两类诉求:日常场景直接await list()拿数组即可;面对海量应用或需要增量同步时,则可用cursor游标、includeTotal与stream异步迭代三件套实现稳健、可续传、后端友好的分页消费。
【免费下载链接】puter🌐 The Internet Computer! Free, Open-Source, and Self-Hostable.项目地址: https://gitcode.com/GitHub_Trending/pu/puter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考