学习 Gutenberg:构建自定义卡片区块

Avatar of Andy Bell
Andy Bell on

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

我们已经掌握了一些基础知识,玩了一些 React,现在我们也设置了项目工具。让我们深入构建自定义区块。

文章系列

  1. 系列简介
  2. 什么是 Gutenberg?
  3. create-guten-block 入门
  4. 现代 JavaScript 语法
  5. React 101
  6. 设置自定义 webpack
  7. 自定义“卡片”区块 (本文)

我们将构建什么

我们将构建一个自定义卡片区块,其中包含图像、标题和摘要。这是网络中非常常见的模式,它也让我们可以查看一些核心 Gutenberg 组件,以及核心 WordPress 元素,例如媒体库。我们还将使用 JSX 针对前端标记玩一些显示逻辑。

我们辉煌的自定义卡片区块!

我们辉煌的自定义卡片区块!

在本教程中,我们将专注于此区块的 CMS 方面。不过,它在前端呈现了一些漂亮、简洁的标记。如果你愿意,可以扩展此区块来包含前端样式。

入门

我们要做的第一件事是打开我们在 上一节 中创建的 block.js 文件。在您的活动插件文件夹中,它位于 blocks/src/block/block.js

如果您使用 create-guten-block 创建的文件结构,您可能希望首先删除 block.js 中的所有内容,并与教程一起从头开始编写您的代码。

在该文件的顶部,添加以下内容

const { RichText, MediaUpload, PlainText } = wp.editor;
const { registerBlockType } = wp.blocks;
const { Button } = wp.components;

我们在 第三部分 中介绍了解构赋值。这是一个很好的例子,您将在 Gutenberg 代码中经常看到。在这里,wp.components 包含 Button 以外的其他内容,但我们只想要 Button,因此我们只会获取它。这很方便,因为它可以防止我们不得不编写类似 wp.components.Button 的内容,这对于保持代码简洁轻巧非常有用。

我们已经解决了这个问题,现在让我们导入 Sass 文件。这样 webpack 就能检测到它们。

import './style.scss';
import './editor.scss';

现在让我们开始编写为区块提供动力的组件。在这些行下方,添加以下内容

registerBlockType('card-block/main', {   
  title: 'Card',
  icon: 'heart',
  category: 'common'
});

这段代码告诉 Gutenberg,“嘿,我有一个区块要添加到您的集合中。它被称为“卡片”,它有一个“心形”图标,它应该位于“通用”类别中。” 这是我们组件的基本定义,让我们添加更多代码。

这应该看起来很熟悉——还记得我们在 第二部分,create-guten-block 中的挑战吗?如果您需要提醒,请查看此处。前六个相对简单,涉及替换字符串或 HTML 片段。第七个项目,“使段落文本可编辑”,实现起来要复杂得多,旨在让您思考一下。现在是时候了,我们将真正使 Gutenberg 中的一些文本可编辑!

您可能还记得我们在 上一篇文章 中使用的 PHP register_block_type 函数中的 registerBlockType 函数。虽然该函数从 WordPress 的服务器端注册了区块,但此函数将我们的区块注册到客户端的 React 生态系统中。这两个函数都需要才能创建一个使用 React 的区块,并且它们的注册名称(card-block/main)必须匹配。

添加以下代码,但请确保在 'common' 后添加逗号,使其看起来像这样:'common',

以下是代码

attributes: {
  title: {
    source: 'text',
    selector: '.card__title'
  },
  body: {
    type: 'array',
    source: 'children',
    selector: '.card__body'
  },
  imageAlt: {
    attribute: 'alt',
    selector: '.card__image'
  },
  imageUrl: {
    attribute: 'src',
    selector: '.card__image'
  }
}

在这里,我们定义了区块的可编辑属性和分配给它们的 DOM 选择器。此 attribute 对象的工作方式与 React state 对象非常相似。它甚至还有一个非常相似的更新方法,名为 setAttributes。不过,我们稍后会讲到这一点。

此时,值得简要概述一下状态和属性,因为它们代表着 WordPress 开发人员思维方式的全新变化。我将短暂接管,介绍一下它们。

关于属性和状态

它可能看起来像一个简单的 JavaScript 对象,但 attributes 那部分引入了一系列 WordPress 主题开发人员脑海中全新的概念,其中最重要的是状态。状态的概念在计算机科学,以及现实生活中,都有着悠久的历史。几乎所有事物都有状态。您现在的咖啡杯处于什么状态?空着,还是几乎空着?您的衣服呢?您的鞋子是脏的还是新的?您的身体呢?您是疲惫不堪,还是精力充沛?

从高层次上讲,状态简单地指的是某件事的当前情况。在计算机科学中,那件是一个计算机程序,而该程序可能比我们在网络上创建的程序要简单得多。例如,一台自动售货机。自动售货机有一个状态,每次您投入硬币时都会更新。当机器的状态达到预定义的值时,例如 1.25 美元,机器就会知道允许您选择零食。

在 Gutenberg 中,属性跟踪区块中数据的当前情况。属性是我们能够与 Gutenberg 中的自定义字段建立的最接近的类比,但它们仅存在于 Gutenberg 和 JavaScript 的上下文中。例如,让我们看看上面 title 的属性

title: {
  source: 'text',
  selector: 'card__title'
},

当 Gutenberg 启动时,它会说,“我需要在一个名为 .card__title选择器中找到一些文本,并将 title 的值填充为我找到的内容。”

Gutenberg 中的属性不像自定义字段那样直接连接到数据库,自定义字段连接到 post_meta。条目 sourceselector 是 Gutenberg 用于填充每个区块状态的指令。当我们加载编辑器时,它会遵循这些指令,并根据数据库中保存的标记(在指示此类型区块的 HTML 注释之间)为 title 分配一个值。我们没有看到 attributes 中注册的 title 的值,但是如果我要访问 props.attributes.title,我会得到 .card__title 中存在的任何 text

我们已经设置了一些基础,现在让我们深入研究编辑函数。当从 Gutenberg 编辑器中的视觉模式访问区块时,会调用此函数。用户将看到丰富的界面,而不是它生成的 HTML 代码。这就是我接下来要讲的内容。

添加我们的编辑函数

让我们添加一些代码。在 attributes 对象的结束括号 } 后添加以下内容。像以前一样,确保您添加一个尾随逗号,使其看起来像这样 },

在之后添加以下代码

edit({ attributes, className, setAttributes }) {
  return (
  );
}

因此,我们使用另一个解构赋值来选择性地选择传递给编辑函数的参数。最重要的两个参数是 attributessetAttributesattributes 参数与 attributes 区块相同,但它是当前的、响应式状态。这意味着,如果 setAttributes 函数更新了 attributes 中的某个值,它将自动更新任何引用它的位置,这与我们在 第三部分 中的 React 组件类似。

此函数中有一个很大的 return。你能猜出它里面有什么吗?没错!我们将把一些 JSX 放进去。在 return 圆括号中添加以下内容

<div className="container">
  <MediaUpload
    onSelect={ media => { setAttributes({ imageAlt: media.alt, imageUrl: media.url }); } }
    type="image"
    value={ attributes.imageID }
    render={ ({ open }) => getImageButton(open) }
  />
  <PlainText
    onChange={ content => setAttributes({ title: content }) }
    value={ attributes.title }
    placeholder="Your card title"
    className="heading"
  />
  <RichText
    onChange={ content => setAttributes({ body: content }) }
    value={ attributes.body }
    multiline="p"
    placeholder="Your card text"
  />
</div>

好的,这里有很多东西,但它们都是我们在本系列之前部分中介绍过的内容。我们在这里看到的是一个包含三个现有的 Gutenberg 组件 的容器。对于每个组件,我们都将其值设置为相关的 attribute,设置相关的占位符,以及 onChange/onSelect 处理程序。我们还将一个自定义渲染器传递给 <MediaUpload />,我们稍后会介绍它。

每个onChange处理程序都是一个方便的小表达式,它将触发onChange的新内容传递给setAttributes函数,我们在此设置要更新哪个attributes对象。 此更新随后级联到该属性的任何引用,其中内容将像魔法一样更新。 <MediaUpload />元素具有一个onSelect事件,该事件在用户选择或上传项目到媒体库时触发。

说到<MediaUpload />元素,您会注意到有一个自定义的render属性,它引用了getImageButton函数。 让我们接下来编写它。 在edit函数中的return上方添加以下内容

const getImageButton = (openEvent) => {
  if(attributes.imageUrl) {
    return (
      <img 
        src={ attributes.imageUrl }
        onClick={ openEvent }
        className="image"
      />
    );
  }
  else {
    return (
      <div className="button-container">
        <Button 
          onClick={ openEvent }
          className="button button-large"
        >
          Pick an image
        </Button>
      </div>
    );
  }
};

此函数的作用是检测attributes对象中是否存在imageUrl。 如果存在,它将呈现该<img />标签,并允许用户单击它以选择另一个。 如果没有图像,它将呈现一个WordPress<Button />,提示用户选择一个图像。 这会调用传递到函数中的相同openEvent

为了在本教程中保持简单,我们将单击绑定到<img />元素。 您应该考虑构建一些利用<button />的精美内容,以使您的生产就绪块具有更好的可访问性支持。

好的,我们的edit函数完成了。 考虑到它的实际作用,那里没有太多代码,这很棒!

添加我们的保存函数

现在,我们已经编写了块的Gutenberg编辑器端,这是最难的部分。 现在我们要做的就是告诉Gutenberg我们希望块对内容执行的操作。 使用来自attributes的相同反应数据,我们也可以实时渲染出我们的前端标记。 这意味着当有人切换到块上的HTML编辑模式时,它将是最新的。 如果你在HTML编辑模式下编辑它,视觉模式也将保持最新。 非常有用。

让我们深入了解一下。 在我们的edit函数之后,添加一个逗号,使其看起来像},,然后在新行上添加以下内容

save({ attributes }) {

  const cardImage = (src, alt) => {
    if(!src) return null;

    if(alt) {
      return (
        <img 
          className="card__image" 
          src={ src }
          alt={ alt }
        /> 
      );
    }
    
    // No alt set, so let's hide it from screen readers
    return (
      <img 
        className="card__image" 
        src={ src }
        alt=""
        aria-hidden="true"
      /> 
    );
  };
  
  return (
    <div className="card">
      { cardImage(attributes.imageUrl, attributes.imageAlt) }
      <div className="card__content">
        <h3 className="card__title">{ attributes.title }</h3>
        <div className="card__body">
          { attributes.body }
        </div>
      </div>
    </div>
  );
}

看起来很像edit函数,对吧? 让我们逐步执行它。

我们首先使用解构赋值从传递的参数中提取attributes,就像前面的edit函数一样。

然后我们有另一个图像辅助函数,它首先检测是否存在图像,如果不存在则返回null。 请记住:如果我们希望它不渲染任何内容,则在JSX中返回null。 此辅助程序接下来要做的是,如果存在替代文本,则呈现一个略有不同的<img />标签。 对于后者,它通过添加aria-hidden="true"和设置一个空白的alt属性将其从屏幕阅读器中隐藏。

最后,我们的return吐出一个不错的.card块,它具有干净的,由BEM驱动的标记,该标记将在我们主题的前端加载。

这就是我们的save函数。 我们离完成块只有一步之遥!

添加一些样式

好的,我们必须做这些,我们就完成了。 你们中那些善于观察的人可能已经注意到,有些引用指向className。 这些引用的是我们的editor.scss规则,所以让我们添加它们。

打开editor.scss,它位于与block.js相同的目录中。 添加以下内容

@import '../common';

.gutenberg { // This may need to be .wp-block in WordPress 5+
    
  .container {
    border: 1px solid $gray;
    padding: 1rem;
  }

  .button-container {
    text-align: center;
    padding: 22% 0;
    background: $off-white;
    border: 1px solid $gray;
    border-radius: 2px;
    margin: 0 0 1.2rem 0;
  }

  .heading {
    font-size: 1.5rem;
    font-weight: 600;
  }

  .image {
    height: 15.7rem;
    width: 100%;
    object-fit: cover;
  }
}

这是一些松散的CSS,可以为我们的块提供一些卡片样式。 请注意,它全部嵌套在.gutenberg类中? 这样做是为了对抗某些核心样式的特异性。 在编辑器中,<div class="gutenberg"包装在帖子编辑器屏幕的块区域周围,因此我们可以确保仅通过此嵌套来影响这些元素。 您可能还会注意到我们正在导入另一个Sass文件,所以让我们填充它。

打开common.scss,它位于src目录中,它是我们当前所在的block目录的父目录。

/*
 * Common SCSS can contain your common variables, helpers and mixins
 * that are shared between all of your blocks. 
 */

// Colors
$gray: #cccccc;
$off-white: #f1f1f1;

无论如何,猜猜看? 我们已经创建了一个自定义的卡片块!! 让我们试用一下。

首先,检查您的块是否都很好。 完整的block.js文件应如下所示

const { RichText, MediaUpload, PlainText } = wp.editor;
const { registerBlockType } = wp.blocks;
const { Button } = wp.components;

// Import our CSS files
import './style.scss';
import './editor.scss';

registerBlockType('card-block/main', {   
  title: 'Card',
  icon: 'heart',
  category: 'common',
  attributes: {
    title: {
      source: 'text',
      selector: '.card__title'
    },
    body: {
      type: 'array',
      source: 'children',
      selector: '.card__body'
    },
    imageAlt: {
      attribute: 'alt',
      selector: '.card__image'
    },
    imageUrl: {
      attribute: 'src',
      selector: '.card__image'
    }
  },
  edit({ attributes, className, setAttributes }) {

    const getImageButton = (openEvent) => {
      if(attributes.imageUrl) {
        return (
          <img 
            src={ attributes.imageUrl }
            onClick={ openEvent }
            className="image"
          />
        );
      }
      else {
        return (
          <div className="button-container">
            <Button 
              onClick={ openEvent }
              className="button button-large"
            >
              Pick an image
            </Button>
          </div>
        );
      }
    };

    return (
      <div className="container">
        <MediaUpload
          onSelect={ media => { setAttributes({ imageAlt: media.alt, imageUrl: media.url }); } }
          type="image"
          value={ attributes.imageID }
          render={ ({ open }) => getImageButton(open) }
        />
        <PlainText
          onChange={ content => setAttributes({ title: content }) }
          value={ attributes.title }
          placeholder="Your card title"
          className="heading"
        />
        <RichText
          onChange={ content => setAttributes({ body: content }) }
          value={ attributes.body }
          multiline="p"
          placeholder="Your card text"
          formattingControls={ ['bold', 'italic', 'underline'] }
          isSelected={ attributes.isSelected }
        />
      </div>
    );

  },

  save({ attributes }) {

    const cardImage = (src, alt) => {
      if(!src) return null;

      if(alt) {
        return (
          <img 
            className="card__image" 
            src={ src }
            alt={ alt }
          /> 
        );
      }
      
      // No alt set, so let's hide it from screen readers
      return (
        <img 
          className="card__image" 
          src={ src }
          alt=""
          aria-hidden="true"
        /> 
      );
    };
    
    return (
      <div className="card">
        { cardImage(attributes.imageUrl, attributes.imageAlt) }
        <div className="card__content">
          <h3 className="card__title">{ attributes.title }</h3>
          <div className="card__body">
            { attributes.body }
          </div>
        </div>
      </div>
    );
  }
});

如果您满意,让我们启动webpack。 在终端中的当前插件目录中,运行以下命令

npx webpack --watch

读者Moritz Karliczek写信说他们在这里遇到了错误:TypeError: Cannot read property 'bindings' of null。 我们现在最好的猜测是,本教程是针对Babel 6编写的,但是Babel 7现在已经发布了。 由您决定是否要确保您的Babel内容锁定在6上,或者是否要自己解决7的问题。 如果您已经克服了这个问题,并且有好的信息供我们更新本系列,请写信

Karen White联系了我们,遇到了同样的问题,发现将babelrc文件更新为使用"presets": ["@babel/preset-env"]可以使其正常工作。

这与本系列的先前部分略有不同,因为我们添加了--watch参数。 这实际上会监视您的js文件,并在它们更改时重新运行webpack

启动编辑器!

让我们通过在WordPress后端加载帖子来加载Gutenberg编辑器。 在Gutenberg编辑器中,单击小加号图标,然后查看“块”选项卡,它就在那里:我们很棒的新卡片块!

继续试用它,并在其中添加一些内容。 感觉很好吧?

以下是一段快速视频,展示了您现在应该看到的内容,以及您新奇的卡片块

就这样,您完成了🎉

您可能正在想:块不是有点像自定义字段的替代品吗? 我现在不能直接在WordPress中创建自己的内容结构,而不是使用像Advanced Custom Fields这样的插件吗? 不完全是...

块与自定义字段

虽然Gutenberg确实为我们提供了从用户体验中自定义数据输入结构的能力,但在后端,它与当前的WYSIWYG编辑器没有什么不同。 从块中保存的数据是wp_posts数据库表中post_content列的一部分-它不是像自定义字段那样在wp_postmeta中单独存储。 这意味着目前,我们无法从另一个帖子中访问卡片块中的数据,就像使用标准的Advanced Custom Fields设置创建titleimagecontent的自定义字段那样。

也就是说,我可以看到一些真正有趣的插件出现,这些插件提供了一种将数据从块移植到网站其他部分的方法。 使用WordPress REST API,可能性几乎是无限的! 在我们的屏幕录制中,Andy和我尝试将API请求合并到我们的卡片块中,尽管结果并不完全如预期,但工具已经到位,您可以体验到将来使用Gutenberg可能实现的功能。 时间会证明一切!

总结和下一步

我们一起经历了一段旅程! 让我们列出您在本系列中学到的知识

那么,从这里您可以去哪里呢? 现在您已经从本系列中获得了扎实的知识基础,可以进行一些进一步的学习。 已经有很多很棒的资源可以帮助您学习

一些有趣的案例研究

关注这些资源,以了解有关该项目的最新信息

Gutenberg的实验性内容

一旦 Gutenberg 成为 WordPress 核心版本 5.0 的一部分(发布日期待定),您也可以在 WordPress 插件目录 中发布一个有用的自定义区块。肯定有空间可以容纳一些方便的组件,例如您刚刚构建的卡片区块。

我们希望您喜欢这个系列,因为我们制作它的时候确实很享受。我们真的希望这能帮助您进入 Gutenberg 并构建一些很酷的东西。您也应该将您构建的东西的链接发送给我们!


文章系列

  1. 系列简介
  2. 什么是 Gutenberg?
  3. create-guten-block 入门
  4. 现代 JavaScript 语法
  5. React 101
  6. 设置自定义 webpack
  7. 自定义“卡片”区块 (本文)