关于 vtex-io-session-apps
面向构建 VTEX IO 会话转换的开发者,指导如何声明依赖、计算并传播会话状态。重点涵盖命名空间所有权、转换顺序、缓存和前端读取规范。
主要功能
- 01声明会话转换的输入与输出
- 02规划命名空间所有权和依赖
- 03通过 public.* 传播输入
- 04使用 LRU 与 VBase 缓存
- 05排查过期状态和竞争条件
适用场景
- 01为会话转换定义配置字段
- 02从上游状态计算业务字段
- 03让原生转换重新计算派生数据
- 04诊断 B2B 会话和命名空间冲突
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vtex/ skills --skill vtex-io-session-apps安装后在 Claude Code 中输入/vtex-io-session-apps确认已加载。
详细介绍
VTEX IO 会话转换应用
适用场景
当你的 VTEX IO 应用与 VTEX 会话系统(vtex.session)集成,以派生、计算或传播下游转换、店面或结账所依赖的状态时,请使用此技能。
- 构建会话转换,根据上游会话状态计算自定义字段(例如,根据外部后端提供的定价上下文、根据组织数据进行区域化)
- 在
vtex.session/configuration.json中声明输入/输出字段 - 确定应用应拥有哪个命名空间,以及应从哪个命名空间读取
- 将值传播到
public.*输入,以便原生转换(个人资料、搜索、结账)重新运行 - 调试过期的会话字段、竞态条件或应用之间的命名空间冲突
- 设计 B2B 会话流程,其中
storefront-permissions、自定义转换和结账会相互作用
以下情况不适用此技能:
- 通用 IO 后端模式(使用
vtex-io-service-apps) - 会话转换之外的性能模式(使用
vtex-io-application-performance) - GraphQL 架构或解析器设计(使用
vtex-io-graphql-api)
决策规则
命名空间所有权
- 每个会话应用恰好拥有一个输出命名空间(或其中的一小组字段)。命名空间名称通常与应用概念一致(例如
rona、myapp、storefront-permissions)。 - 绝不写入其他应用的输出命名空间。 如果
storefront-permissions拥有storefront-permissions.organization,你的转换就不得覆盖它,而应将其作为输入读取。 - 当 VTEX 拥有的字段(组织、成本中心、邮政编码、国家/地区)已存在于
storefront-permissions、profile、checkout或store中时,绝不要将其复制到你的命名空间。你的命名空间应仅包含来自你自己的后端或计算的数据。
public 是输入,私有命名空间是读取模型
public.*字段是一个输入界面:购物者或流程设置这些值,以便运行会话转换(例如地理位置、标记、UTM 参数、用户意图)。不要在店面代码中将public.*视为规范读取模型。- 私有命名空间(
profile、checkout、store、search、storefront-permissions、你的自定义命名空间)是读取模型:根据输入派生出的计算结果。前端组件应读取私有命名空间字段,以处理业务规则和展示内容。 - 如果你的转换必须影响原生应用(例如,根据成本中心地址设置邮政编码),请更新原生应用声明为输入的
public.*输入字段,以便平台重新运行这些上游转换,并使私有输出保持一致。这属于输入传播,而非重复存储事实数据。
转换顺序(DAG)
- VTEX 会根据各应用
vtex.session/configuration.json中声明的输入/输出依赖关系,以有向无环图(DAG)的形式运行会话转换。 - 任一已声明输入字段发生变化时,就会运行对应转换。如果你依赖
storefront-permissions.costcenter,你的转换会在storefront-permissions输出该字段后运行。 - 仔细安排依赖关系:如果转换同时需要
storefront-permissions和profile的输出,请将两者都声明为输入,以便平台在两者之后调度你的转换。
转换中的缓存
- 会话转换会在触及已声明输入的每次会话变更时执行,因此必须快速。
- 对热点查询(组织数据、配置、令牌)使用 LRU 缓存(进程内、按工作进程隔离),并设置较短的 TTL。
- 对可容忍短暂过期的数据(外部后端响应、计算出的映射)使用 VBase stale-while-revalidate。立即返回过期数据,并在后台重新验证。
- 遵循与任何 IO 服务相同的租户键控规则:内存缓存键必须包含
account和workspace(参见vtex-io-application-performance)。
前端会话数据消费
- Storefront 组件应通过
items=查询参数请求特定的会话项(例如,items=rona.storeNumber,storefront-permissions.costcenter)。 - 对于权威状态,应从相关的私有命名空间(
rona.*、storefront-permissions.*、profile.*等)中读取。 - 仅在设置用户意图时(例如,选择位置、切换标记)才写入
public.*。绝不要将public.*用作“缓存”已由私有命名空间提供的值。
硬性约束
约束:不要将其他应用输出命名空间中的字段复制到自己的命名空间
会话转换必须仅输出来自自身计算或后端的字段。复制 storefront-permissions、profile 或 checkout 已拥有的身份、地址或组织字段,会产生两个在部分失败时可能不一致的事实来源。
为何重要——当两个命名空间包含相同事实时(例如,你的命名空间和 storefront-permissions 中都有 costCenterId),在会话发生部分更新后,使用方读取的值会不一致。这会大幅增加调试时间,并导致竞态条件。
如何检测——转换输出包含 organization、costcenter、postalCode、country 等字段,而这些字段与 storefront-permissions.* 或 profile.* 的输出重复。或者,前端从两个不同的命名空间读取同一个逻辑字段。
正确做法——将 storefront-permissions.costcenter 作为输入;用它计算特定于后端的字段(例如,myapp.priceTable、myapp.storeNumber);仅输出这些派生字段。
{
"my-session-app": {
"input": {
"storefront-permissions": ["costcenter", "organization"]
},
"output": {
"myapp": ["priceTable", "storeNumber"]
}
}
}
错误做法——输出 VTEX 所有字段的副本。
{
"my-session-app": {
"output": {
"myapp": [
"costcenter",
"organization",
"postalCode",
"priceTable",
"storeNumber"
]
}
}
}
约束:使用输入传播来影响原生转换,而非直接覆盖
当转换派生出原生应用会使用的值(例如,从成本中心地址派生邮政编码)时,应将其设为这些应用声明的输入字段(通常是 public.postalCode、public.country),不要直接写入 checkout.postalCode 或 search.postalCode。
为何重要——原生转换需要其输入字段发生变化,才能重新计算其输出字段。直接写入它们的输出命名空间会绕过重新计算,并留下过期的派生状态(例如,regionId 未更新、结账地址不一致)。
如何检测——转换在其他应用拥有的命名空间中声明输出字段(例如,output: { checkout: [...] } 或 output: { search: [...] })。或者,对不属于自己的命名空间中的值执行 PATCH 会话。
正确做法——对于原生应用作为输入使用的字段,在 public 中声明输出,并通过各原生应用的 vtex.session/configuration.json 进行确认。
{
"my-session-app": {
"output": {
"myapp": ["storeNumber", "priceTable"],
"public": ["postalCode", "country", "state"]
}
}
}
错误做法——直接写入 search 或 checkout 输出命名空间。
{
"my-session-app": {
"output": {
"myapp": ["storeNumber", "priceTable"],
"checkout": ["postalCode", "country"],
"search": ["facets"]
}
}
}
约束:前端必须从私有命名空间读取权威业务状态,而非从 public 读取
Storefront 组件和中间件必须从权威私有命名空间(例如,storefront-permissions.organization、profile.email、myapp.priceTable)读取会话数据,而不是读取 public.* 字段。
为何重要——public.* 字段是输入,可能过期、由用户设置或不完整。所有转换运行后,私有命名空间字段才是计算得出的真实状态。读取 public.postalCode,而非从 profile 或 checkout 派生的值,会导致显示过期或不一致的数据。
如何检测——React 组件或中间件在显示信息或处理业务逻辑时读取 public.storeNumber、public.organization 或 public.costCenter,而不是读取相应的私有字段。
正确
// Read from the authoritative namespace
const { data } = useSessionItems([
"myapp.storeNumber",
"myapp.priceTable",
"storefront-permissions.costcenter",
"storefront-permissions.organization",
]);
错误
// Reading from public as if it were the source of truth
const { data } = useSessionItems([
"public.storeNumber",
"public.organization",
"public.costCenter",
]);
推荐模式
vtex.session/configuration.json
声明转换的输入依赖项和输出字段:
{
"my-session-app": {
"input": {
"storefront-permissions": [
"costcenter",
"organization",
"costCenterAddressId"
]
},
"output": {
"myapp": ["storeNumber", "priceTable"]
}
}
}
转换处理程序
// node/handlers/transform.ts
export async function transform(ctx: Context) {
const { costcenter, organization } = parseSfpInputs(ctx.request.body);
if (!costcenter) {
ctx.body = { myapp: {} };
return;
}
const costCenterData = await getCostCenterCached(ctx, costcenter);
const pricing = await resolvePricing(ctx, costCenterData);
ctx.body = {
myapp: {
storeNumber: pricing.storeNumber,
priceTable: pricing.priceTable,
},
};
}
转换内部的缓存
// Two-layer cache: LRU (sub-ms) -> VBase (persistent, SWR) -> API
const costCenterLRU = new LRU<string, CostCenterData>({
max: 1000,
ttl: 600_000,
});
async function getCostCenterCached(ctx: Context, costCenterId: string) {
const { account, workspace } = ctx.vtex;
const key = `${account}:${workspace}:${costCenterId}`;
const lruHit = costCenterLRU.get(key);
if (lruHit) return lruHit;
const result = await staleFromVBaseWhileRevalidate(
ctx.clients.vbase,
"cost-centers",
costCenterId,
() => fetchCostCenterFromAPI(ctx, costCenterId),
{ ttlMs: 1_800_000 },
);
costCenterLRU.set(key, result);
return result;
}
service.json 路由
{
"routes": {
"transform": {
"path": "/_v/my-session-app/session/transform",
"public": true
}
}
}
会话生态感知
构建 transform 时,请梳理出商店的 transform DAG:
authentication-session → impersonate-session → profile-session
profile-session → store-session → checkout-session
profile-session → search-session
authentication-session + checkout-session + impersonate-session → storefront-permissions
storefront-permissions → YOUR-TRANSFORM (reads SFP outputs)
你的 transform 位于其所需依赖链的末端。正确声明输入可确保平台在所有上游 transform 之后调度你的 transform。
常见故障模式
- 前端通过
updateSession写入 B2B 状态——前端没有让storefront-permissions和你的 transform 来计算 B2B 会话字段,而是直接 PATCH 这些字段。这会导致竞态条件、状态不完整,以及真相来源重复。 - 重复 VTEX 管理的字段——当
costcenter、organization或postalCode已存在于storefront-permissions或profile中时,仍将它们复制到自己的命名空间。 - 未启用缓存的慢速 transform——每次调用 transform 都请求外部 API,却未使用 LRU 和 VBase SWR。每当会话变更涉及已声明的输入时,transform 都会运行,因此必须保持快速。
- 将
public.*作为真实数据源读取——前端组件读取public.organization或public.storeNumber,而不是私有命名空间字段,导致显示内容过时或不一致。 - 写入其他应用的输出命名空间——在自己无权管理的
checkout、search或storefront-permissions命名空间中声明输出字段,绕过原生 transform 重新计算。 - LRU 中缺少租户键——组织或定价数据的内存缓存仅以实体 ID 作为键,没有包含
account:workspace;在多租户共享 Pod 上不安全。
审查清单
- transform 输出的字段是否仅来自其自身计算或后端,而非其他命名空间中的重复字段?
- 是否在
vtex.session/configuration.json中正确声明了输入依赖? - 输出字段是否仅限于自己的命名空间(以及需要传播时的
public.*输入)? -
public.*是否仅用于输入传播,而非作为第二种读取模型? - 前端组件是否从私有命名空间读取业务状态,而非从
public.*读取? - transform 中的上游 API 调用是否已缓存(LRU 和 VBase SWR),以降低 transform 延迟?
- 内存缓存键是否包含
account:workspace,以确保多租户安全? - transform 顺序(DAG)是否正确——它是否在所有依赖 transform 之后运行?
- 前端代码中是否已移除对 transform 所计算字段的
updateSession调用?
相关技能
- vtex-io-application-performance — 适用于 transform 内部的缓存层和并行 I/O
- vtex-io-service-paths-and-cdn — transform 端点的路由前缀
- vtex-io-service-apps — 服务类、客户端和中间件基础知识
- vtex-io-app-contract — Manifest、构建器和策略
参考
- VTEX Session System — 会话管理器概览和 API
- App Development — VTEX IO 应用开发中心
- Clients — VBase、MasterData 和自定义客户端
- Engineering Guidelines — 可扩展性和 IO 开发实践