在 WordPress 中使用 Web Components 比你想象的更容易

Avatar of John Rhea
John Rhea

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

现在我们已经看到 web components交互式 web components 都比你想象的更容易,让我们看看如何将它们添加到内容管理系统中,即 WordPress。

我们有三种主要方法可以添加它们。 首先,通过手动输入到站点将它们直接放入小部件或文本块中,基本上在我们可以放置其他 HTML 的任何位置。 其次,我们可以将它们作为主题文件的主题输出添加。 最后,我们可以将它们作为自定义块的输出添加。

文章系列

加载 web component 文件

现在无论我们最终以哪种方式添加 web components,我们都必须确保

  1. 我们的自定义元素模板在我们需要时可用,
  2. 我们需要的任何 JavaScript 都已正确排队,并且
  3. 我们需要的任何非封装样式都已排队。

我们将添加来自 我之前关于交互式 web components 的文章<zombie-profile> web component。 查看 CodePen 上的代码

让我们先看一下第一个点。 一旦我们有了模板,就可以很容易地将其添加到 WordPress 主题的 footer.php 文件中,但与其直接将其添加到主题中,不如挂钩到 wp_footer,这样组件就可以独立于 footer.php 文件和整个主题加载——假设主题使用 wp_footer,大多数主题都使用。 如果在尝试时主题中没有显示模板,请仔细检查主题的 footer.php 模板文件是否调用了 wp_footer

<?php function diy_ezwebcomp_footer() { ?>
  <!-- print/echo Zombie profile template code. -->
  <!-- It's available at https://codepen.io/undeadinstitute/pen/KKNLGRg -->
<?php } 
add_action( 'wp_footer', 'diy_ezwebcomp_footer');

接下来是排队我们的组件的 JavaScript。 我们也可以通过 wp_footer 添加 JavaScript,但是 排队是将 JavaScript 链接到 WordPress 的推荐方法。 所以让我们将我们的 JavaScript 放入一个名为 ezwebcomp.js 的文件中(该名称完全是任意的),将该文件放入主题的 JavaScript 目录(如果有),然后排队(在 functions.php 文件中)。

wp_enqueue_script( 'ezwebcomp_js', get_template_directory_uri() . '/js/ezwebcomp.js', '', '1.0', true );

我们要确保最后一个参数设置为 true,即它在结束 body 标记之前加载 JavaScript。 如果我们在头部而不是在尾部加载它,它将找不到我们的 HTML 模板,并且会变得非常生气(抛出大量错误)。

如果您能够完全封装您的 web component,那么您可以跳过此步骤。 但是,如果您(像我一样)无法做到这一点,则需要排队那些未封装的样式,以便它们在使用 web component 的任何位置都可用。(与 JavaScript 类似,我们可以直接将此添加到页脚,但排队样式是推荐的方法)。 所以我们将 排队我们的 CSS 文件

wp_enqueue_style( 'ezwebcomp_style', get_template_directory_uri() . '/ezwebcomp.css', '', '1.0', 'screen' );

这并不难,对吧? 而且,如果您不打算让除管理员以外的任何用户使用它,那么您应该可以随时随地添加它们。 但是情况并非总是如此,所以我们将继续前进!

不要过滤掉您的 web component

WordPress 有几种不同的方法来帮助用户创建有效的 HTML 并防止您的 Eddie 叔叔将从 Shady Al 那里获得的“有趣”图片直接粘贴到编辑器中(包括用于攻击您所有访客的脚本)。

因此,在将 web-components 直接添加到块或小部件中时,我们需要小心 WordPress 的内置代码过滤。 完全禁用它将让 Eddie 叔叔(以及 Shady Al)为所欲为,但我们可以对其进行修改以让我们的出色 web component 通过(幸运的是)阻止 Eddie 叔叔的关卡。

首先,我们可以使用 wp_kses_allowed 过滤器将我们的 web component 添加到要过滤掉的元素列表中。 这有点像我们正在将组件列入白名单,我们通过将其添加到传递给过滤器函数的允许标签数组中来实现这一点。

function add_diy_ezwebcomp_to_kses_allowed( $the_allowed_tags ) {
  $the_allowed_tags['zombie-profile'] = array();
}
add_filter( 'wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed');

我们向 <zombie-profile> 组件添加了一个空数组,因为 WordPress 除了过滤元素之外还会过滤属性——这又给我们带来了另一个问题:slot 属性默认情况下是不允许的。 因此,我们必须在您预计在其中使用它的每个元素上明确地允许它,并且,通过扩展,在用户可能决定将其添加到其中的任何元素上。(等等,这些元素列表相同,即使您已经与每个用户一起仔细检查了六次……谁知道呢?)因此,我在下面将 slot 设置为 <span><img><ul> 上的 true,这是我在 <zombie-profile> 组件中放入插槽的三个元素。

function add_diy_ezwebcomp_to_kses_allowed( $the_allowed_tags ) {
  $the_allowed_tags['zombie-profile'] = array();
  $the_allowed_tags['span']['slot'] = true;
  $the_allowed_tags['ul']['slot'] = true;
  $the_allowed_tags['img']['slot'] = true;
  return $the_allowed_tags;
}
add_filter( 'wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed');

我们还可以使用类似这样的代码在所有允许的元素中启用 slot 属性

function add_diy_ezwebcomp_to_kses_allowed($the_allowed_tags) {
  $the_allowed_tags['zombie-profile'] = array();
  foreach ($the_allowed_tags as &$tag) {
    $tag['slot'] = true;
  }
  return $the_allowed_tags;
}
add_filter('wp_kses_allowed_html', 'add_diy_ezwebcomp_to_kses_allowed');

遗憾的是,这方面可能还有另一种情况。 如果您放入插槽的所有元素都是内联/短语元素,那么您可能不会遇到这种情况,但如果您有一个要放入 web component 的块级元素,您可能会与代码编辑器中的块解析器发生冲突。 您可能比我更擅长拳击,但我总是输。

代码编辑器是一个选项,允许您检查和编辑块的标记。

出于我无法完全解释的原因,客户端解析器假定 web component 应该只在其内包含内联元素,如果将 <ul><div><h1> 或其他一些块级元素放在其中,它会将结束 web component 标记移到最后一个内联/短语元素之后。 更糟糕的是,根据 WordPress 开发人员手册中的注释,目前“无法替换客户端解析器”。

虽然这很令人沮丧,并且您需要对 web 编辑器进行培训,但确实有解决方法。 如果我们将 web component 直接放在块编辑器中的自定义 HTML 块中,客户端解析器就不会让我们在人行道上哭泣、来回摇晃并质疑自己的编码能力……当然这从未发生在任何人身上……尤其是那些写文章的人……

组件向上到主题

只要不在 HTML 块之外更新,在我们的主题文件中输出我们花哨的 web component 就很简单。 我们按照在任何其他上下文中添加它的方式添加它,并且,假设我们已经有了模板、脚本和样式,事情就会正常工作。

但是,假设我们想在 web component 中输出 WordPress 帖子或自定义帖子类型的内容。 你知道,写一篇帖子,然后这篇帖子是组件的内容。 这使我们可以使用 WordPress 编辑器来输出一个 <zombie-profile> 元素的存档。 这是很棒的,因为 WordPress 编辑器已经拥有我们输入 <zombie-profile> 组件之一内容的大部分 UI

  • **帖子标题** 可以是僵尸的名字。
  • 帖子内容中的常规**段落块**可以用于僵尸的陈述。
  • **特色图片**可以用于僵尸的个人资料图片。

这就是大部分! 但我们仍然需要僵尸年龄、感染日期和兴趣的字段。 我们将使用 WordPress 内置的 自定义字段 功能创建这些字段。

我们将使用处理打印每个帖子的模板部分,例如 content.php,来输出 web component。 首先,我们将打印打开的 <zombie-profile> 标记,然后是帖子缩略图(如果存在)。

<zombie-profile>
  <?php 
    // If the post featured image exists...
    if (has_post_thumbnail()) {
      $src = wp_get_attachment_image_url(get_post_thumbnail_id()); ?>
      <img src="<?php echo $src; ?>" slot="profile-image">
    <?php
    }
  ?>

接下来,我们将打印用于名称的标题

<?php
  // If the post title field exits...
  if (get_the_title()) { ?>
  <span slot="zombie-name"><?php echo get_the_title(); ?></span>
  <?php
  }
?>

在我的代码中,我已经测试了这些字段在打印之前是否存在,原因有两个

  1. 在大多数情况下,隐藏空字段周围的标签和元素是良好的编程实践。
  2. 如果我们最终输出一个空的 `<span>` 作为名称(例如 `<span slot="zombie-name"></span>`),那么该字段在最终的个人资料中将显示为空,而不是使用我们 Web 组件内置的默认文本、图像等。(例如,如果你希望文本字段在没有内容时为空,则可以在自定义字段中添加一个空格,或跳过代码中的 `if` 语句)。

接下来,我们将获取自定义字段并将其放置到它们所属的插槽中。同样,这将进入输出帖子内容的主题模板。

<?php
  // Zombie age
  $temp = get_post_meta(the_ID(), 'Age', true);
  if ($temp) { ?>
    <span slot="z-age"><?php echo $temp; ?></span>
    <?php
  }
  // Zombie infection date
  $temp = get_post_meta(the_ID(), 'Infection Date', true);
  if ($temp) { ?>
    <span slot="idate"><?php echo $temp; ?></span>
    <?php
  }
  // Zombie interests
  $temp = get_post_meta(the_ID(), 'Interests', true);
  if ($temp) { ?>
    <ul slot="z-interests"><?php echo $temp; ?></ul>
    <?php
  }
?>

使用 WordPress 自定义字段的缺点之一是,你无法进行任何特殊格式化。非技术性的网页编辑人员在填写时需要为列表中的每个兴趣写出列表项的 HTML 代码(`<li>`)。(你可能可以使用更强大的自定义字段插件来解决此界面限制,例如 Advanced Custom FieldsPods,或类似的插件。)

最后,我们添加僵尸的陈述和结束标签 `<zombie-profile>`。

<?php
  $temp = get_the_content();
  if ($temp) { ?>
    <span slot="statement"><?php echo $temp; ?></span>
  <?php
  }
?>
</zombie-profile>

由于我们使用帖子的主体作为我们的陈述,因此我们会获得一些额外的代码,例如内容周围的段落标签。将个人资料陈述放在自定义字段中将减轻这种情况,但根据你的目的,它也可能是预期/期望的行为。

然后,你可以通过将每个帖子发布为一个帖子来添加任意数量的帖子/僵尸个人资料!

派对:自定义块中的 Web Components

创建自定义块是添加 Web 组件的好方法。你的用户将能够填写所需字段并获得 Web 组件的魔力,而无需任何代码或技术知识。此外,块完全独立于主题,因此,实际上,我们可以在一个站点上使用此块,然后将其安装到其他 WordPress 站点上——有点类似于我们期望 Web 组件的工作方式!

自定义块有两个主要部分:PHP 和 JavaScript。我们还将添加一些 CSS 来改善编辑体验。

首先,是 PHP

function ez_webcomp_register_block() {
  // Enqueues the JavaScript needed to build the custom block
  wp_register_script(
    'ez-webcomp',
    plugins_url('block.js', __FILE__),
    array('wp-blocks', 'wp-element', 'wp-editor'),
    filemtime(plugin_dir_path(__FILE__) . 'block.js')
  );

  // Enqueues the component's CSS file
  wp_register_style(
    'ez-webcomp',
    plugins_url('ezwebcomp-style.css', __FILE__),
    array(),
    filemtime(plugin_dir_path(__FILE__) . 'ezwebcomp-style.css')
  );

  // Registers the custom block within the ez-webcomp namespace
  register_block_type('ez-webcomp/zombie-profile', array(
    // We already have the external styles; these are only for when we are in the WordPress editor
    'editor_style' => 'ez-webcomp',
    'editor_script' => 'ez-webcomp',
  ));
}
add_action('init', 'ez_webcomp_register_block');

CSS 不是必需的,但它有助于防止僵尸的个人资料图像与 WordPress 编辑器中的内容重叠。

/* Sets the width and height of the image.
 * Your mileage will likely vary, so adjust as needed.
 * "pic" is a class we'll add to the editor in block.js
*/
#editor .pic img {
  width: 300px;
  height: 300px;
}
/* This CSS ensures that the correct space is allocated for the image,
 * while also preventing the button from resizing before an image is selected.
*/
#editor .pic button.components-button { 
  overflow: visible;
  height: auto;
}

我们需要的 JavaScript 稍微复杂一点。我尽力简化它,并尽可能使其对所有人易于访问,因此我使用 ES5 编写了它,从而无需编译任何内容。

显示代码
(function (blocks, editor, element, components) {
  // The function that creates elements
  var el = element.createElement;
  // Handles text input for block fields 
  var RichText = editor.RichText;
  // Handles uploading images/media
  var MediaUpload = editor.MediaUpload;
    
  // Harkens back to register_block_type in the PHP
  blocks.registerBlockType('ez-webcomp/zombie-profile', {
    title: 'Zombie Profile', //User friendly name shown in the block selector
    icon: 'id-alt', //the icon to usein the block selector
    category: 'layout',
    // The attributes are all the different fields we'll use.
    // We're defining what they are and how the block editor grabs data from them.
    attributes: {
      name: {
        // The content type
        type: 'string',
        // Where the info is available to grab
        source: 'text',
        // Selectors are how the block editor selects and grabs the content.
        // These should be unique within an instance of a block.
        // If you only have one img or one <ul> etc, you can use element selectors.
        selector: '.zname',
      },
      mediaID: {
        type: 'number',
      },
      mediaURL: {
        type: 'string',
        source: 'attribute',
        selector: 'img',
        attribute: 'src',
      },
      age: {
        type: 'string',
        source: 'text',
        selector: '.age',
      },
      infectdate: {
        type: 'date',
        source: 'text',
        selector: '.infection-date'
      },
      interests: {
        type: 'array',
        source: 'children',
        selector: 'ul',
      },
      statement: {
        type: 'array',
        source: 'children',
        selector: '.statement',
      },
  },
  // The edit function handles how things are displayed in the block editor.
  edit: function (props) {
    var attributes = props.attributes;
    var onSelectImage = function (media) {
      return props.setAttributes({
        mediaURL: media.url,
        mediaID: media.id,
      });
    };
    // The return statement is what will be shown in the editor.
    // el() creates an element and sets the different attributes of it.
    return el(
      // Using a div here instead of the zombie-profile web component for simplicity.
      'div', {
        className: props.className
      },
      // The zombie's name
      el(RichText, {
        tagName: 'h2',
        inline: true,
        className: 'zname',
        placeholder: 'Zombie Name…',
        value: attributes.name,
        onChange: function (value) {
          props.setAttributes({
            name: value
          });
        },
      }),
      el(
        // Zombie profile picture
        'div', {
          className: 'pic'
        },
        el(MediaUpload, {
          onSelect: onSelectImage,
          allowedTypes: 'image',
          value: attributes.mediaID,
          render: function (obj) {
            return el(
              components.Button, {
                className: attributes.mediaID ?
                  'image-button' : 'button button-large',
                onClick: obj.open,
              },
              !attributes.mediaID ?
              'Upload Image' :
              el('img', {
                src: attributes.mediaURL
              })
            );
          },
        })
      ),
      // We'll include a heading for the zombie's age in the block editor
      el('h3', {}, 'Age'),
      // The age field
      el(RichText, {
        tagName: 'div',
        className: 'age',
        placeholder: 'Zombie\'s Age…',
        value: attributes.age,
        onChange: function (value) {
          props.setAttributes({
            age: value
          });
        },
      }),
      // Infection date heading
      el('h3', {}, 'Infection Date'),
      // Infection date field
      el(RichText, {
        tagName: 'div',
        className: 'infection-date',
        placeholder: 'Zombie\'s Infection Date…',
        value: attributes.infectdate,
        onChange: function (value) {
          props.setAttributes({
            infectdate: value
          });
        },
      }),
      // Interests heading
      el('h3', {}, 'Interests'),
      // Interests field
      el(RichText, {
        tagName: 'ul',
        // Creates a new <li> every time `Enter` is pressed
        multiline: 'li',
        placeholder: 'Write a list of interests…',
        value: attributes.interests,
        onChange: function (value) {
          props.setAttributes({
            interests: value
          });
        },
        className: 'interests',
      }),
      // Zombie statement heading
      el('h3', {}, 'Statement'),
      // Zombie statement field
      el(RichText, {
        tagName: 'div',
        className: "statement",
        placeholder: 'Write statement…',
        value: attributes.statement,
        onChange: function (value) {
          props.setAttributes({
            statement: value
          });
        },
      })
    );
  },

  // Stores content in the database and what is shown on the front end.
  // This is where we have to make sure the web component is used.
  save: function (props) {
    var attributes = props.attributes;
    return el(
      // The <zombie-profile web component
      'zombie-profile',
      // This is empty because the web component does not need any HTML attributes
      {},
      // Ensure a URL exists before it prints
      attributes.mediaURL &&
      // Print the image
      el('img', {
        src: attributes.mediaURL,
        slot: 'profile-image'
      }),
      attributes.name &&
      // Print the name
      el(RichText.Content, {
        tagName: 'span',
        slot: 'zombie-name',
        className: 'zname',
        value: attributes.name,
      }),
      attributes.age &&
      // Print the zombie's age
      el(RichText.Content, {
        tagName: 'span',
        slot: 'z-age',
        className: 'age',
        value: attributes.age,
    }),
      attributes.infectdate &&
      // Print the infection date
      el(RichText.Content, {
        tagName: 'span',
        slot: 'idate',
        className: 'infection-date',
        value: attributes.infectdate,
    }),
      // Need to verify something is in the first element since the interests's type is array
      attributes.interests[0] &&
      // Pint the interests
      el(RichText.Content, {
        tagName: 'ul',
        slot: 'z-interests',
        value: attributes.interests,
      }),
      attributes.statement[0] &&
      // Print the statement
      el(RichText.Content, {
        tagName: 'span',
        slot: 'statement',
        className: 'statement',
        value: attributes.statement,
    })
    );
    },
  });
})(
  //import the dependencies
  window.wp.blocks,
  window.wp.blockEditor,
  window.wp.element,
  window.wp.components
);

接入 Web Components

现在,如果某个热心、写文章、超级棒的人创建了一个模板,你可以直接将你的 Web 组件插入其中并在你的网站上使用,那不是太好了吗?好吧,那个人没空(他去帮助慈善事业了,或者其他什么事情),所以我做了。它已上传到 github

自己动手:为 WordPress 创建简单的 Web Components

该插件是一个编码模板,它注册你的自定义 Web 组件,加入组件所需的脚本和样式,提供你可能需要的自定义块字段示例,甚至确保在编辑器中样式设置得很好。将其放在 `//wp-content/plugins` 中的一个新文件夹中,就像你手动安装任何其他 WordPress 插件一样,确保使用你的特定 Web 组件更新它,然后在 WordPress 的“已安装插件”屏幕中激活它。

还不错吧?

即使看起来很多代码,我们实际上只是在做一些相当标准的 WordPress 事情来注册和渲染自定义 Web 组件。而且,由于我们将其打包为插件,因此可以将其放到任何 WordPress 站点中,并开始将僵尸个人资料发布到我们的心仪内容中。

我想说,平衡点是尝试让组件在 WordPress 块编辑器中工作得尽可能好,就像它在前端一样。如果没有这种考虑,我们可能可以用更少的代码完成这项工作。

尽管如此,我们还是设法将 我们在之前文章中创建的完全相同的组件 放入 CMS 中,这使我们能够在网站上放置任意数量的僵尸个人资料。我们结合了对 Web 组件和 WordPress 块的了解,为可重用的 Web 组件开发了一个可重用的块。

你会为你的 WordPress 网站构建什么组件?我想这里有很多可能性,我很想知道你最终会做些什么。