mcpField
C
canvas-data-fetching

canvas-data-fetching

github.com/drupal-canvas开发3225 次安装6 周前更新
代码仓库

关于 canvas-data-fetching

面向 Drupal Canvas、Workbench 和 React 头less项目的开发技能,指导组件读取上下文并通过 JSON:API 获取内容。重点包括查询验证、反序列化数据处理与请求安全。

主要功能

  1. 01使用 React 上下文 Hook
  2. 02通过 SWR 获取异步数据
  3. 03用 JSON:API 查询 Drupal 内容
  4. 04验证查询结果与数据形状
  5. 05处理关联实体与字段筛选

适用场景

  1. 01为 React 组件加载 Drupal 内容
  2. 02构建内容列表或详情组件
  3. 03排查 JSON:API 查询字段与筛选
  4. 04迁移旧的数据读取调用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/drupal-canvas/skills --skill canvas-data-fetching

安装后在 Claude Code 中输入/canvas-data-fetching确认已加载。

详细介绍

数据获取

项目类型

应用此技能前,请检查 package.json,确认是否存在名为 @drupal-canvas/headless 或以 @drupal-canvas/headless- 开头的依赖。如果存在, 则这是一个 Canvas Headless 代码库——请先阅读 canvas-headless。在无头项目中,使用 SDK 的 fetchPage() 获取页面树,并使用其请求感知型 getClient() 获取服务器端内容, 遵循框架惯用的数据加载方式。可移植的 React 组件使用 drupal-canvas/react 中的 useJsonApiClient() 和应用程序的同源 SDK 代理。下方的 React 模式适用于 Drupal、Workbench 和基于 React 的无头前端。有关无头服务器端预取和回退规则, 也请遵循 canvas-headless;原生非 React 组件应使用其框架的数据加载模式,而非 React hooks。如果不存在此类依赖,则这是一个由 Canvas 渲染的 React 代码库:组件 使用 React(index.jsx/.tsx),本技能中的所有内容均适用。这是仅有的两种项目类型。

可移植 React 运行时 API

使用 drupal-canvas/react 中的 useJsonApiClient()、usePageContext() 和 useSiteContext()。Drupal islands、Workbench 和已配置的无头 React 渲染器会提供 这些 provider。Hooks 会读取 context;它们不会获取数据,也不会在每次渲染时创建 客户端。在 Canvas 树之外,集成必须按需提供 CanvasContextProvider 和/或 JsonApiClientProvider。

在组件或自定义 hooks 的顶层无条件调用 hooks,并且要在任何可能的 return 之前调用。 如果 context 不可用,每个 hook 都可能返回 null。保持组件同步,并且不要在组件中 处理后端 URL、Drupal 全局变量、凭据、代理配置和环境分支。将数据或客户端传给辅助 函数;不要在普通辅助函数中调用 hooks。

getPageData()、getSiteData() 和 new JsonApiClient() 已弃用。在 Drupal 和 Workbench 中它们仍可用,但在其他环境中会抛出异常,即使构造函数接收了显式的后端 URL 也一样。不要在新代码中使用它们。getNodePath 和 sortMenu 等现有工具导入保持不变。

迁移已拉取的组件

canvas pull 可以自动将安全的 getPageData() 和 getSiteData() 调用改写为 context hooks。它要求已连接站点的 capabilities.contextHooks 元数据,以及已安装的 drupal-canvas/react 入口将这两个 hooks 导出为运行时值;不要仅凭版本号推断是否支持。 如果无法确认支持情况,请查看报告的限制,而不要强行导入运行时不支持的内容。

每个组件文件的迁移要么全部完成,要么完全不变。不安全的 hook 位置、标识符冲突或 直接解构 getter 返回值,都可能导致文件保持不变。客户端构造和辅助模块会收到诊断信息, 但不会自动转换。--skip-overwrite 文件保持原样。

拉取后,请检查已更改的源文件和诊断信息。Codemod 不会添加 null 检查,也不会证明后续 访问安全;请检查可空结果、hook 顺序、类型和渲染行为。手动迁移剩余的客户端调用。 在组件/自定义 hooks 之外,将数据或客户端传给辅助函数,或在无头服务器端代码中使用 page.context 和请求感知型 getClient()。保留访问控制,绝不要暴露凭据。

页面和站点 context
import { usePageContext, useSiteContext } from 'drupal-canvas/react';

export default function PageHeader() {
  const page = usePageContext();
  const site = useSiteContext();
  if (!page || !site) return null;

  return (
    <header>
      <a href={site.branding.homeUrl}>{site.branding.siteName}</a>
      <h1>{page.pageTitle}</h1>
    </header>
  );
}

页面 context 包含 pageTitle、breadcrumbs 和 mainEntity。即使页面 context 存在, mainEntity 也可能为 null;使用实体元数据、翻译或其 UUID 发起查询前,请先检查它。 站点 context 包含 branding、baseUrl 和 themeAssets;请处理资源 URL 为空的情况。 除非应用程序明确进行适配,否则请保留 Drupal 生成的导航 URL。

使用 SWR 获取数据

在可移植的 React 组件中进行异步数据获取时,请使用 SWR。它提供缓存、重新验证和简洁的基于 Hook 的 API。读取页面或站点上下文时,无需使用 SWR。

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

export default function Profile() {
  const { data, error } = useSWR('https://my-site.com/api/user', fetcher);

  if (error) return <div>Failed to load</div>;
  if (!data) return <div>Loading...</div>;
  return <div>Hello, {data.name}!</div>;
}

使用 JSON:API 获取 Drupal 内容

要从 Drupal 获取内容(例如文章、活动或其他内容类型),请使用 drupal-canvas/react 中的 useJsonApiClient() 读取已配置的客户端,并使用 DrupalJsonApiParams 构建查询。客户端不可用时,请传入 null 作为 SWR 键。请使用包含资源及所有查询输入的稳定键,这样不同的筛选条件、语言和分页就不会共享结果。

请渲染可用数据,包括预取的备用数据,不要仅仅因为 SWR 报告 isLoading 就将其隐藏。常规的 useSWR 渲染不会在服务器上运行或等待其 fetcher。为了生成包含数据的初始 HTML,无头应用会使用服务器端 getClient() 进行预取,并提供匹配的 SWR 备用键;请参阅 canvas-headless。

重要: 请在最终组件代码中保持默认序列化器启用。Canvas 组件的运行时约定是 JsonApiClient 返回的反序列化数据结构,而不是原始 JSON:API 文档结构。

重要: 不要在 Workbench 模拟中伪造 JSON:API 资源负载。除非用户明确要求静态的非获取预览结构,否则获取数据的组件应在 Workbench 中渲染真实的加载、空数据或错误状态。

原始 JSON:API 与反序列化后的 Canvas 数据

不要根据原始 JSON:API 的假设编写组件逻辑,例如 data[0].attributes.title 或 data[0].relationships.field_image。使用 JsonApiClient 的组件接收的是反序列化后的对象。

  • 仅在检查连通性和广义的端点是否存在时使用普通 HTTP 请求。
  • 使用 JsonApiClient 检查组件实际会使用的数据结构。
  • 如果为了调试而检查原始 JSON:API 文档,请将其视为辅助诊断视图,而非组件代码的真实数据来源。
  • 不要在最终组件代码中禁用序列化器。
验证每个 JSON:API 请求都返回预期结果

你生成的任何 JSON:API 请求——用于新组件、重构、筛选条件更改、添加 include、更改排序,或为现有组件添加新查询——都必须在针对它编写或更改任何渲染逻辑之前执行并验证。不要因为查询“看起来没问题”就假定它正确。构建查询、运行查询,并确认响应符合预期。

只有通过以下所有检查后,请求才算经过验证:

  • 请求可以运行。 没有 HTTP 错误、JSON:API 错误文档或客户端异常。
  • 结果数量符合预期。 如果存在该类型的内容,列表查询应返回非空集合。筛选查询返回的项目应少于未筛选查询(只有在确实预期为零时,结果才应为零)。单个资源查询应返回一个资源,而不是 null。
  • 反序列化对象中包含并填充了预期字段——包括通过 addFields 请求的字段。缺失或始终为 null 的字段需要调查:区分合法的可选值和访问限制,以及错误的查询或字段名称。
  • Include 已解析为真实的关联实体,而非简单引用。 如果使用了 addInclude,请确认反序列化对象中组件将读取的关联已填充。
  • 筛选和排序按预期生效。 抽查筛选后的项目,确认它们确实符合筛选条件;并检查排序后的项目是否按请求顺序排列。

如果任何检查失败,请在更改渲染逻辑之前调查查询、字段名称、内容模型以及访问权限/会话状态。不要用可选链、回退字符串或“在 UI 中看起来没问题”来掩盖空响应或错误响应。每次修复后都重新运行探测,只有响应符合预期后才能继续。

下一节中的探测模式是执行这些检查的默认方法。探测结果若显示 count: 0、keys: [],或缺少组件所需字段,就不能视为通过,除非该结果确实符合预期且已有说明。

草稿集合读取会为返回的项目填充工作副本,但集合筛选/排序不会使用工作副本的值,也不会发现所有未发布实体。原始响应不会进行工作副本填充。匿名探测只能验证公开结果;请通过预期的、能识别会话的集成来验证草稿行为。绝不要通过以公开用户身份重试,或将静态回退内容视为成功读取草稿,来掩盖 DraftSessionError。

编码前探测反序列化后的数据结构

编写渲染逻辑之前,运行一次性的 JavaScript 探测,使用组件将采用的相同 JsonApiClient 调用和 DrupalJsonApiParams 查询模式。检查第一个返回项,并根据该反序列化后的数据结构编写组件。

此公开数据探测在 Canvas 运行时之外执行。请使用共享的、与框架无关的 createJsonApiClient() 工厂,并传入解析后的后端配置。不要伪造 window、Drupal 设置或运行时标记来绕过旧版守卫。对于无头草稿探测,请在应用的服务端集成中使用能够感知请求的 getClient()。最终的 React 组件代码应使用 useJsonApiClient(),而不是显式构造客户端。

请使用以下模式的命令:

node --input-type=module -e "
import { createJsonApiClient } from 'drupal-canvas/jsonapi-client';
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';

const describeShape = (value) => {
  if (Array.isArray(value)) {
    return value.length > 0 ? [describeShape(value[0])] : ['empty-array'];
  }
  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value).map(([key, nestedValue]) => [key, describeShape(nestedValue)]),
    );
  }
  if (value === null) {
    return 'null';
  }
  return typeof value;
};

const client = createJsonApiClient({
  baseUrl: 'https://example.ddev.site',
  apiPrefix: 'jsonapi',
});
const queryString = new DrupalJsonApiParams()
  .addSort('created', 'DESC')
  .addFields('node--article', ['title', 'created', 'body', 'path'])
  .getQueryString();

const items = await client.getCollection('node--article', { queryString });
const first = items?.[0];

console.log('count:', items?.length ?? 0);
console.log('keys:', first ? Object.keys(first) : []);
console.log('shape:', JSON.stringify(first ? describeShape(first) : null, null, 2));
console.log(JSON.stringify(first, null, 2));
"

将站点根目录作为 baseUrl 传入,而不是 /jsonapi 端点。请根据要构建的组件调整资源类型、筛选条件、include、排序和字段。不要只检查一种查询结构,却在组件中实现另一种。

如果此探测在本地 HTTPS 开发环境中失败,请先检查 Node 是否信任本地证书链,再判断是 JSON:API 客户端或查询出错。

import { getNodePath } from 'drupal-canvas';
import { useJsonApiClient } from 'drupal-canvas/react';
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
import useSWR from 'swr';

const Articles = () => {
  const client = useJsonApiClient();
  const { data, error } = useSWR(
    client
      ? [
          'node--article',
          {
            queryString: new DrupalJsonApiParams()
              .addSort('created', 'DESC')
              .getQueryString(),
          },
        ]
      : null,
    ([type, options]) => client.getCollection(type, options),
  );

  if (error) return 'An error has occurred.';
  if (!data) return 'Loading...';
  return (
    <ul>
      {data.map((article) => (
        <li key={article.id}>
          <a href={getNodePath(article)}>{article.title}</a>
        </li>
      ))}
    </ul>
  );
};

export default Articles;
使用 addInclude 包含关联内容

需要关联实体(例如图片、分类术语)时,请使用 addInclude 在单个请求中获取它们。

避免 JSON:API 响应中出现循环引用。 SWR 使用深度相等性检查 比较缓存数据;当响应包含循环引用时,此检查会失败并报出“递归过深”错误。

不要包含自引用字段。 引用所查询的同一实体类型的字段(例如文章查询中的 field_related_articles)会产生循环引用:文章 A 引用文章 B,而文章 B 又引用 文章 A。如果需要同一类型的关联内容,请单独发起查询。

使用 addFields 限制响应内容。 始终只指定所需字段。这能提升性能,并有助于避免循环引用问题:

const params = new DrupalJsonApiParams();
params.addSort('created', 'DESC');
params.addInclude(['field_category', 'field_image']);

// Limit fields for each entity type
params.addFields('node--article', [
  'title',
  'created',
  'field_category',
  'field_image',
]);
params.addFields('taxonomy_term--categories', ['name']);
params.addFields('file--file', ['uri', 'url']);

创建内容列表组件

构建用于显示内容条目列表的组件时(例如新闻列表、活动日历或资源库),请遵循以下工作流:

设置检查关卡

在进行任何 JSON:API 探索或内容类型检查之前,请先验证本地设置:

  1. 在编写代码或探查 Drupal 之前,解析 Canvas 配置值。按以下顺序检查:
    • shell 环境变量
    • 项目根目录中的 .env
    • ~/.canvasrc
  2. 确定生效的 CANVAS_SITE_URL。
  3. 使用项目集成解析 JSON:API 端点。读取公开的 /canvas/api/v0/site-data 元数据,获取已发现的前缀;不要假定端点为 /jsonapi。在无头项目中,CANVAS_JSONAPI_URL 会覆盖发现结果; 如果发现失败,SDK 会使用已配置的前缀或 CANVAS_JSONAPI_PREFIX,然后使用 jsonapi。保留站点安装路径。 对于单独的 JSON:API 站点,请遵循 SDK 关于 CANVAS_JSONAPI_SITE_URL 的指南,处理带语言前缀的请求。不要假定 Workbench 支持仅适用于无头项目的配置选项。
  4. 记录解析出的站点根路径和 JSON:API 端点(绝不记录凭据)。 在独立探查中使用这些值进行匹配;适当时使用工厂的 apiUrl 覆盖项, 不要用 API 端点替代 baseUrl。
  5. 验证 CANVAS_SITE_URL 是站点根路径,而不是 JSON:API 端点。 例如,应使用 https://example.ddev.site,而不是 https://example.ddev.site/jsonapi。
  6. 向解析出的 JSON:API 端点发送 HTTP 请求。成功状态为 HTTP 200。
  7. 如果请求成功,继续获取 Drupal 数据。
  8. 如果请求失败(或缺少必需值),询问用户是否希望:
    • 立即配置 Drupal 连接,或
    • 使用静态内容,而不从 Drupal 获取内容。
  9. 如果用户选择配置连接,请提供设置说明:
    • CANVAS_SITE_URL=<their Drupal site root>
    • CANVAS_JSONAPI_PREFIX=jsonapi(可选;默认值为 jsonapi)然后等待 用户确认已更新 shell 环境变量、.env 或 ~/.canvasrc,并在重试请求前重新解析这些值。
  10. 如果用户选择不配置连接,则使用静态内容。
  11. 在确定生效的 CANVAS_SITE_URL 和 JSON:API 端点之前,不要开始内容类型探索、字段检查或组件编码。
  12. 不要通过更新 Vite 配置(vite.config.*)来排查连接问题。 必须通过正确的配置值和 Drupal 站点可用性解决连接问题,而不是更改构建工具。
第 1 步:分析列表结构

检查设计,了解每个列表项需要哪些数据:

  • 显示哪些字段(标题、日期、图片、类别等)?
  • 条目如何排序(最新优先、按字母顺序等)?
  • 是否有筛选器或分页?
步骤 2:确定或请求内容类型

编写代码之前,请先确认 Drupal 中是否存在合适的内容类型:

  1. 检查本地 Drupal 站点的 JSON:API 端点(该端点通过 Setup gate 解析出的 CANVAS_SITE_URL 和 JSON:API 前缀进行配置),查找与所需结构匹配的内容类型。通过 Setup gate 后,仅端点发现可以使用普通 HTTP 请求。

  2. 如果存在匹配的内容类型,请使用该类型,并记录可用字段。

  3. 使用 JsonApiClient 检查样例响应,资源类型和查询模式须与组件将使用的相同。运行一次性探测命令,检查返回的第一项,并在编写渲染逻辑之前确认反序列化后的字段结构。

  4. 如果不存在匹配的内容类型,请停止并提示用户创建一个。请提供:

    • 建议的内容类型名称
    • 根据列表设计确定的必需字段结构
步骤 3:构建组件

使用 JSON:API 获取内容并创建内容列表组件。仅使用内容类型中实际存在的字段,以及 JsonApiClient 返回的反序列化对象中实际存在的字段;不要假设原始 JSON:API 字段嵌套结构与运行时数据结构相同。

处理筛选条件

如果列表包含基于实体引用字段的筛选条件(例如按分类或作者筛选):

  • 不要硬编码筛选选项。 应使用 JSON:API 动态获取筛选选项。
  • 获取每个筛选条件的可用选项(例如某个词汇中的所有分类术语),并使用这些数据填充筛选界面。

这样可以确保筛选条件与 Drupal 中的实际内容保持同步,新增选项也会自动显示,无需更改代码。

导航/菜单组件

页眉、页脚和侧边栏等组件通常需要从 Drupal 获取菜单链接。请使用双重实现:如果 Drupal 中存在菜单,则从中获取链接;如果尚未配置菜单,则回退到静态数组。

这样组件可以立即使用(采用硬编码回退链接),并且在 CMS 编辑器创建对应菜单后,会自动升级为由 Drupal 管理的实时链接。

import { sortMenu } from 'drupal-canvas';
import { useJsonApiClient } from 'drupal-canvas/react';
import useSWR from 'swr';

// Static fallback — always define this; it renders when no Drupal menu exists
const FALLBACK_LINKS = [
  { id: 'home', title: 'Home', url: '/' },
  { id: 'about', title: 'About', url: '/about' },
];

const Navigation = ({ menuName = 'main' }) => {
  const client = useJsonApiClient();
  const { data, error } = useSWR(
    client && menuName ? ['menu_items', menuName] : null,
    ([type, id]) => client.getResource(type, id),
  );

  // Do not disguise a rejected draft session as a successful static preview.
  if (error?.name === 'DraftSessionError') {
    return <div>Preview session expired. Reconnect the preview.</div>;
  }

  // Keep prefetched/live links visible during revalidation.
  const links = data ? Array.from(sortMenu(data)) : FALLBACK_LINKS;

  return (
    <nav>
      {links.map(({ id, title, url }) => (
        <a key={id} href={url}>
          {title}
        </a>
      ))}
    </nav>
  );
};

菜单组件规则:

  • 始终定义一个 FALLBACK_LINKS 常量,其中包含有代表性的链接。这样组件在 Workbench 中以及尚未创建 Drupal 菜单的站点上都能发挥作用。
  • 将 menuName 设为属性,并在 component.yml 中注册,这样 CMS 编辑器无需更改代码即可配置要使用的 Drupal 菜单。
  • menuName = null 会禁用数据获取(SWR 键为 null),并渲染回退链接;这适用于纯静态预览。
  • 构建导航类型组件后,请在手动步骤摘要中提醒用户前往 /admin/structure/menu/add 在 Drupal 中创建对应的菜单。

menuName 的 component.yml 示例:

props:
  properties:
    menuName:
      title: Menu name
      type: string
      examples:
        - main
        - footer

评论

支持 Markdown