mcpField
V
vtex-io-session-apps

vtex-io-session-apps

github.com/vtex开发471.1k 次安装2 周前更新
代码仓库

关于 vtex-io-session-apps

面向构建 VTEX IO 会话转换的开发者,指导如何声明依赖、计算并传播会话状态。重点涵盖命名空间所有权、转换顺序、缓存和前端读取规范。

主要功能

  1. 01声明会话转换的输入与输出
  2. 02规划命名空间所有权和依赖
  3. 03通过 public.* 传播输入
  4. 04使用 LRU 与 VBase 缓存
  5. 05排查过期状态和竞争条件

适用场景

  1. 01为会话转换定义配置字段
  2. 02从上游状态计算业务字段
  3. 03让原生转换重新计算派生数据
  4. 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 调用?

相关技能

参考

评论

支持 Markdown