在 WordPress 块的后端渲染外部 API 数据

Avatar of Manoj Kumar
Manoj Kumar

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费额度!

这是我上一篇关于 “在 WordPress 块的前端渲染外部 API 数据” 的文章的延续。 在上一篇文章中,我们学习了如何获取外部 API 并将其与一个块集成,该块在 WordPress 网站的前端渲染获取的数据。

问题在于,我们以一种阻止我们在 WordPress 块编辑器中看到数据的方式完成了此操作。 换句话说,我们可以在页面上插入块,但我们看不到它的预览。 我们只有在发布后才能看到该块。

让我们重新审视一下我们在上一篇文章中创建的示例块插件。 只是这一次,我们将利用 WordPress 的 JavaScript 和 React 生态系统在后端块编辑器中也获取和渲染该数据。

在 WordPress 块中使用外部 API

我们上次停在哪里

在我们开始之前,这是一个演示,我们在上一篇文章中停留的地方,您可以参考它。 您可能已经注意到,我在上一篇文章中使用了 render_callback 方法,以便我可以利用 PHP 文件中的属性并渲染内容。

好吧,这在您可能必须使用某些原生 WordPress 或 PHP 函数来创建动态块的情况下可能很有用。 但是,如果您只想利用 WordPress 的 JavaScript 和 React(特别是 JSX)生态系统来渲染静态 HTML 以及存储在数据库中的属性,您只需要关注块插件的 EditSave 函数。

  • Edit 函数根据您希望在块编辑器中看到的内容渲染内容。 您可以在此处使用交互式 React 组件。
  • Save 函数根据您希望在前端看到的内容渲染内容。 您不能在此处使用常规的 React 组件或钩子。 它用于返回保存到数据库中的静态 HTML 以及属性。

Save 函数是我们今天要关注的。 我们可以在前端创建交互式组件,但为此我们需要像我们在上一篇文章中所做的那样,在 Save 函数之外的文件中手动包含和访问它们。

因此,我将介绍我们在上一篇文章中介绍的内容,但这次您可以在发布到前端之前在块编辑器中看到预览。

块属性

我故意在上一篇文章中省略了任何关于 edit 函数的 props 的解释,因为这会分散主要重点,即渲染。

如果您来自 React 背景,您可能会理解我所说的内容,但如果您是新手,我建议您查看 React 文档中的组件和 props

如果我们将 props 对象记录到控制台,它将返回与我们的块相关的 WordPress 函数和变量列表。

Console log of the block properties.

我们只需要 attributes 对象和 setAttributes 函数,我将在我的代码中从 props 对象中解构它们。 在上一篇文章中,我修改了 RapidAPI 的代码,以便我可以通过 setAttributes() 存储 API 数据。 Props 只能读取,因此我们无法直接修改它们。

块 props 类似于 React 中的状态变量和 setState,但 React 在客户端工作,而 setAttributes() 用于在保存帖子后将属性永久存储在 WordPress 数据库中。 因此,我们需要做的是将它们保存到 attributes.data 中,然后将其作为 useState() 变量的初始值调用。

edit 函数

我将复制粘贴我们在上一篇文章的 football-rankings.php 中使用的 HTML 代码,并对其进行一些编辑以转移到 JavaScript 背景。 还记得我们在上一篇文章中为前端样式和脚本创建了两个附加文件吗? 按照我们今天的方法,无需创建这些文件。 相反,我们可以将所有内容移动到 Edit 函数中。

完整代码
import { useState } from "@wordpress/element";
export default function Edit(props) {
  const { attributes, setAttributes } = props;
  const [apiData, setApiData] = useState(null);
    function fetchData() {
      const options = {
        method: "GET",
        headers: {
          "X-RapidAPI-Key": "Your Rapid API key",
          "X-RapidAPI-Host": "api-football-v1.p.rapidapi.com",
        },
      };
      fetch(
        "https://api-football-v1.p.rapidapi.com/v3/standings?season=2021&league=39",
          options
      )
      .then((response) => response.json())
      .then((response) => {
        let newData = { ...response }; // Deep clone the response data
        setAttributes({ data: newData }); // Store the data in WordPress attributes
        setApiData(newData); // Modify the state with the new data
      })
      .catch((err) => console.error(err));
    }
    return (
      <div {...useBlockProps()}>
        <button onClick={() => getData()}>Fetch data</button>
        {apiData && (
          <>
          <div id="league-standings">
            <div
              className="header"
              style={{
                backgroundImage: `url(${apiData.response[0].league.logo})`,
              }}
            >
              <div className="position">Rank</div>
              <div className="team-logo">Logo</div>
              <div className="team-name">Team name</div>
              <div className="stats">
                <div className="games-played">GP</div>
                <div className="games-won">GW</div>
                <div className="games-drawn">GD</div>
                <div className="games-lost">GL</div>
                <div className="goals-for">GF</div>
                <div className="goals-against">GA</div>
                <div className="points">Pts</div>
              </div>
              <div className="form-history">Form history</div>
            </div>
            <div className="league-table">
              {/* Usage of [0] might be weird but that is how the API structure is. */}
              {apiData.response[0].league.standings[0].map((el) => {
                
                {/* Destructure the required data from all */}
                const { played, win, draw, lose, goals } = el.all;
                  return (
                    <>
                    <div className="team">
                      <div class="position">{el.rank}</div>
                      <div className="team-logo">
                        <img src={el.team.logo} />
                      </div>
                      <div className="team-name">{el.team.name}</div>
                      <div className="stats">
                        <div className="games-played">{played}</div>
                        <div className="games-won">{win}</div>
                        <div className="games-drawn">{draw}</div>
                        <div className="games-lost">{lose}</div>
                        <div className="goals-for">{goals.for}</div>
                        <div className="goals-against">{goals.against}</div>
                        <div className="points">{el.points}</div>
                      </div>
                      <div className="form-history">
                        {el.form.split("").map((result) => {
                          return (
                            <div className={`result-${result}`}>{result}</div>
                          );
                        })}
                      </div>
                    </div>
                    </>
                  );
                }
              )}
            </div>
          </div>
        </>
      )}
    </div>
  );
}

我包含了来自 @wordpress/element 的 React 钩子 useState(),而不是使用来自 React 库的钩子。 这是因为如果我以常规方式加载,它会为我使用的每个块下载 React。 但是,如果我使用 @wordpress/element,它会从单个源加载,即 React 之上的 WordPress 层。

这次,我没有将代码包装在 useEffect() 中,而是包装在一个仅在点击按钮时调用的函数中,以便我们可以获得获取数据的实时预览。 我使用了名为 apiData 的状态变量来有条件地渲染联赛表。 因此,一旦点击按钮并获取数据,我就会将 apiData 设置为 fetchData() 中的新数据,并且会重新渲染包含足球排名表 HTML 的内容。

您会注意到,一旦保存帖子并刷新页面,联赛表就会消失。 这是因为我们使用 apiData 的初始值的空状态(null)。 当帖子保存时,属性将保存到 attributes.data 对象中,我们将其作为 useState() 变量的初始值调用,如下所示

const [apiData, setApiData] = useState(attributes.data);

save 函数

我们将对 save 函数执行几乎完全相同的事情,但对其进行一些修改。 例如,前端不需要“获取数据”按钮,apiData 状态变量也不需要,因为我们已经在 edit 函数中检查它了。 但我们确实需要一个随机的 apiData 变量来检查 attributes.data 以有条件地渲染 JSX,否则它会抛出未定义的错误,并且块编辑器 UI 会变为空白。

完整代码
export default function save(props) {
  const { attributes, setAttributes } = props;
  let apiData = attributes.data;
  return (
    <>
      {/* Only render if apiData is available */}
      {apiData && (
        <div {...useBlockProps.save()}>
        <div id="league-standings">
          <div
            className="header"
            style={{
              backgroundImage: `url(${apiData.response[0].league.logo})`,
            }}
          >
            <div className="position">Rank</div>
            <div className="team-logo">Logo</div>
            <div className="team-name">Team name</div>
            <div className="stats">
              <div className="games-played">GP</div>
              <div className="games-won">GW</div>
              <div className="games-drawn">GD</div>
              <div className="games-lost">GL</div>
              <div className="goals-for">GF</div>
              <div className="goals-against">GA</div>
              <div className="points">Pts</div>
            </div>
            <div className="form-history">Form history</div>
          </div>
          <div className="league-table">
            {/* Usage of [0] might be weird but that is how the API structure is. */}
            {apiData.response[0].league.standings[0].map((el) => {
              const { played, win, draw, lose, goals } = el.all;
                return (
                  <>
                  <div className="team">
                    <div className="position">{el.rank}</div>
                      <div className="team-logo">
                        <img src={el.team.logo} />
                      </div>
                      <div className="team-name">{el.team.name}</div>
                      <div className="stats">
                        <div className="games-played">{played}</div>
                        <div className="games-won">{win}</div>
                        <div className="games-drawn">{draw}</div>
                        <div className="games-lost">{lose}</div>
                        <div className="goals-for">{goals.for}</div>
                        <div className="goals-against">{goals.against}</div>
                        <div className="points">{el.points}</div>
                      </div>
                      <div className="form-history">
                        {el.form.split("").map((result) => {
                          return (
                            <div className={`result-${result}`}>{result}</div>
                          );
                        })}
                      </div>
                    </div>
                  </>
                );
              })}
            </div>
          </div>
        </div>
      )}
    </>
  );
}

如果您在块已存在于块编辑器中后修改 save 函数,它会显示如下错误

The football rankings block in the WordPress block Editor with an error message that the block contains an unexpected error.

这是因为保存的内容中的标记与我们新的 save 函数中的标记不同。 由于我们处于开发模式,因此更容易从当前页面删除块并将其重新插入为新块——这样,将使用更新的代码,并且一切恢复同步。

如果我们使用了 render_callback 方法,则可以避免删除并重新添加的情况,因为输出是动态的,由 PHP 控制而不是 save 函数控制。 因此,每种方法都有其自身的优缺点。

Tom Nowell 在 此 Stack Overflow 答案 中对 save 函数中不应做什么进行了详细的解释。

在编辑器和前端设置块的样式

关于样式,它将与我们在上一篇文章中看到的几乎相同,但有一些细微的更改,我在注释中进行了说明。 我只是在这里提供完整的样式,因为这只是一个概念证明,而不是您想要复制粘贴的内容(除非您确实需要一个像这样设置样式的显示足球排名的块)。 请注意,我仍在使用在构建时编译为 CSS 的 SCSS。

编辑器样式
/* Target all the blocks with the data-title="Football Rankings" */
.block-editor-block-list__layout 
.block-editor-block-list__block.wp-block[data-title="Football Rankings"] {
  /* By default, the blocks are constrained within 650px max-width plus other design specific code */
  max-width: unset;
  background: linear-gradient(to right, #8f94fb, #4e54c8);
  display: grid;
  place-items: center;
  padding: 60px 0;

  /* Button CSS - From: https://getcssscan.com/css-buttons-examples - Some properties really not needed :) */
  button.fetch-data {
    align-items: center;
    background-color: #ffffff;
    border: 1px solid rgb(0 0 0 / 0.1);
    border-radius: 0.25rem;
    box-shadow: rgb(0 0 0 / 0.02) 0 1px 3px 0;
    box-sizing: border-box;
    color: rgb(0 0 0 / 0.85);
    cursor: pointer;
    display: inline-flex;
    font-family: system-ui, -apple-system, system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 16px;
    font-weight: 600;
    justify-content: center;
    line-height: 1.25;
    margin: 0;
    min-height: 3rem;
    padding: calc(0.875rem - 1px) calc(1.5rem - 1px);
    position: relative;
    text-decoration: none;
    transition: all 250ms;
    user-select: none;
    -webkit-user-select: none;
    touch-action: manipulation;
    vertical-align: baseline;
    width: auto;
    &:hover,
    &:focus {
      border-color: rgb(0, 0, 0, 0.15);
      box-shadow: rgb(0 0 0 / 0.1) 0 4px 12px;
      color: rgb(0, 0, 0, 0.65);
    }
    &:hover {
      transform: translateY(-1px);
    }
    &:active {
      background-color: #f0f0f1;
      border-color: rgb(0 0 0 / 0.15);
      box-shadow: rgb(0 0 0 / 0.06) 0 2px 4px;
      color: rgb(0 0 0 / 0.65);
      transform: translateY(0);
    }
  }
}
前端样式
/* Front-end block styles */
.wp-block-post-content .wp-block-football-rankings-league-table {
  background: linear-gradient(to right, #8f94fb, #4e54c8);
  max-width: unset;
  display: grid;
  place-items: center;
}

#league-standings {
  width: 900px;
  margin: 60px 0;
  max-width: unset;
  font-size: 16px;
  .header {
    display: grid;
    gap: 1em;
    padding: 10px;
    grid-template-columns: 1fr 1fr 3fr 4fr 3fr;
    align-items: center;
    color: white;
    font-size: 16px;
    font-weight: 600;
    background-color: transparent;
    background-repeat: no-repeat;
    background-size: contain;
    background-position: right;

    .stats {
      display: flex;
      gap: 15px;
      &amp; &gt; div {
        width: 30px;
      }
    }
  }
}
.league-table {
  background: white;
  box-shadow:
    rgba(50, 50, 93, 0.25) 0px 2px 5px -1px,
    rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
  padding: 1em;
  .position {
    width: 20px;
  }
  .team {
    display: grid;
    gap: 1em;
    padding: 10px 0;
    grid-template-columns: 1fr 1fr 3fr 4fr 3fr;
    align-items: center;
  }
  .team:not(:last-child) {
    border-bottom: 1px solid lightgray;
  }
  .team-logo img {
    width: 30px;
    top: 3px;
    position: relative;
  }
  .stats {
    display: flex;
    gap: 15px;
    &amp; &gt; div {
      width: 30px;
      text-align: center;
    }
  }
  .last-5-games {
    display: flex;
    gap: 5px;
    &amp; &gt; div {
      width: 25px;
      height: 25px;
      text-align: center;
      border-radius: 3px;
      font-size: 15px;
    &amp; .result-W {
      background: #347d39;
      color: white;
    }
    &amp; .result-D {
      background: gray;
      color: white;
    }
    &amp; .result-L {
      background: lightcoral;
      color: white;
    }
  }
}

我们将此添加到 src/style.scss 中,它负责处理编辑器和前端的样式。 我将无法分享演示 URL,因为它需要编辑器访问权限,但我已录制了一段视频供您观看演示。


非常棒,对吧?现在我们拥有了一个功能完备的区块,它不仅可以在前端渲染,还可以获取 API 数据并在区块编辑器中直接渲染——甚至还带有一个刷新按钮!

但是,如果我们想要充分利用 WordPress 区块编辑器,就应该考虑将一些区块的 UI 元素映射到 区块控制,用于设置颜色、排版和间距等内容。这是区块开发学习之旅中一个不错的下一步。