WordPress如何创建自定义区块

在 wordpress 中有多种方式创建自定义区块,如:使用 Wordpress 插件创建自定义区块,使用 Node.js、NPM 包的形式创建区块。

在本教程中,我们将介绍使用创建区块工具的基本步骤,让你无需使用插件就能创建完全自定义的区块。

Info

警告:您需要了解 PHP、JavaScript、HTML 和 CSS,本教程使用的前端框架是 React。

设置 Node.js、NPM、和本地 WordPress 安装

在创建新的 block 之前,我们需要正确的设置和访问权限。

在此之前,需要:

  • 本地 WordPress 安装
  • 本地安装 Node.js

您可能还想使用 nvm(Node 版本管理器)来安装和管理您的 NodeJs 版本。

有了这些设置,我们将可以使用 npm 命令,使用npx可以直接从云中的托管目录执行 create-block 软件包。

创建区块软件包

在终端进入 WordPress 插件目录/wp-content/plugins,然后使用 npx 运行 create-block 软件包。

npx @wordpress/create-block plugin-name

运行该命令后,会自动下载并完成block scaffolding的设置过程,它将注册所有相关元素,生成正确的文件和目录结构,并为整个程序生成可编辑和可用的默认代码。

激活插件

新创建的区块将以插件的形式出现

创建区块完成后,需要你跳转到 WP 的管理页面,进入插件页面,激活新创建的插件,插件名称与在你之前使用 npx 创建区块时的名称相同。

自定义区块代码

你将在插件目录中的**/src**文件夹中编写你的业务代码。

你将在index.js中注册程序中编写主要输出代码。

从 WordPress 的区块 API 中导入 registerBlockType 函数,这个函数用于注册一个新的区块类型,使其能够出现在区块编辑器中。

index.js
import { registerBlockType } from "@wordpress/blocks";
import Editor from "./Editor";
import Save from "./Save";
import metadata from "./block.json";
 
registerBlockType(metadata.name, {
  edit: Editor,
  save: Save,
});

Editor 是用户在后台编辑文章时,区块的显示和交互部分。它负责渲染区块的编辑器界面,比如表单、文本框、图片上传等。

Edit.jsx
import { __ } from "@wordpress/i18n";
import clsx from "clsx";
import { InspectorControls, useBlockProps } from "@wordpress/block-editor";
import {
  PanelBody,
  ComboboxControl,
  TextControl,
  Spinner,
  Notice,
} from "@wordpress/components";
import { useSelect } from "@wordpress/data";
import View from "./View";
import useGetPostOptions from "../hooks/useGetPostOptions";
import { debounce } from "../utils";
const modeOptions = [
  { label: "Compact", value: "concise" },
  { label: "Compact + Description", value: "summary" },
  { label: "Insight Series", value: "general" },
];
const sourceOptions = [
  { label: "Insights", value: "insight" },
  { label: "Services", value: "service" },
  { label: "Solutions", value: "solution" },
  { label: "Work", value: "work" },
  { label: "Pages", value: "page" },
  { label: "Downloads", value: "download" },
  { label: "Series", value: "series" },
];
export default function Editor({ attributes, setAttributes }) {
  const { postId, ctaText, labelText, mode, source } = attributes;
  const blockProps = useBlockProps({ className: "tmo-featured-post-card" });
  const parentPostId = useSelect((select) =>
    select("core/editor").getCurrentPostId(),
  );
  const selectedPost = useSelect(
    (select) => {
      if (!postId) return null;
      return select("core").getEntityRecord(
        "postType",
        (["insight", "series"].includes(source) ? "post" : source) || "post",
        postId,
      );
    },
    [postId, source],
  );
  const { category, options, isLoading, searchPosts, getSourcePosts } =
    useGetPostOptions({
      source,
      parentPostId,
      title: selectedPost?.title?.rendered,
    });
  const handleChangeQuery = (val) => {
    if (["insight", "download"].includes(source) && val?.length >= 2) {
      searchPosts(val, source);
    }
  };
  const featuredMedia = useSelect(
    (select) => {
      if (!selectedPost?.featured_media) return null;
      return select("core").getMedia(selectedPost.featured_media);
    },
    [selectedPost?.featured_media],
  );
  const handleModeChange = async (val) => {
    setAttributes({ mode: val, ctaText: "Read more" });
  };
  const handleSourceChange = (val) => {
    getSourcePosts(val);
    setAttributes({ source: val, postId: null });
  };
  const handlePostChange = (val) => {
    setAttributes({
      postId: Number(val) || 0,
      ctaText: source === "series" ? category?.name : "Read more",
    });
  };
  return (
    <>
      <InspectorControls>
        <PanelBody title={__("Content", "tmo")} initialOpen>
          <ComboboxControl
            label={__("Blog Card Type", "tmo")}
            options={modeOptions}
            value={mode}
            onChange={handleModeChange}
          />
          <ComboboxControl
            label={__("Pick a Source", "tmo")}
            help={__("选择博客数据来源", "tmo")}
            value={source}
            options={sourceOptions}
            onChange={handleSourceChange}
          />
          <ComboboxControl
            label={__("Pick a post", "tmo")}
            help={__("输入2个字以上搜索文章", "tmo")}
            value={postId || ""}
            onChange={handlePostChange}
            onFilterValueChange={debounce(handleChangeQuery)}
            options={options}
          />
          {isLoading && <Spinner />}
          {mode === "general" && (
            <TextControl
              label={__("Label text", "tmo")}
              value={labelText}
              onChange={(v) => setAttributes({ labelText: v })}
            />
          )}
          <TextControl
            label={__("CTA text", "tmo")}
            value={ctaText}
            onChange={(v) => setAttributes({ ctaText: v })}
          />
        </PanelBody>
      </InspectorControls>
      <div
        {...blockProps}
        className={clsx(blockProps.className, {
          related: mode === "related",
        })}
      >
        {!postId && (
          <Notice status="info" isDismissible={false}>
            {__("请选择一篇文章(右侧面板搜索/选择)", "tmo")}
          </Notice>
        )}
        {selectedPost && (
          <View
            sourceUrl={featuredMedia?.source_url}
            post={selectedPost}
            ctaText={ctaText}
            mode={mode}
            labelText={labelText}
          />
        )}
      </div>
    </>
  );
}

Save 组件负责输入区块在前端的 HTML,通常它会根据用户在编辑器时的选择,生成并保存对应的内容。

Save.jsx
import { useBlockProps } from "@wordpress/block-editor";
export default function Save({ attributes }) {
  return <p {...useBlockProps.save()}>{attributes.content}</p>;
}
Info

这里没有采用这种方式将所选择的内容展示在前端,这里采用的是利用 render 来对所选择的内容进行展示,后面会做详细的说明。

View.jsx
import ArrowRight from "./ArrowRight";
import "../style.scss";
export default function View({
  post = {},
  ctaText,
  mode,
  labelText,
  sourceUrl,
}) {
  return (
    <>
      <div className="tmo-card-post">
        {sourceUrl && <img className="tmo-post-thumb" src={sourceUrl} alt="" />}
        <div className="tmo-post-content">
          <div
            className="tmo-post-title"
            dangerouslySetInnerHTML={{
              __html: post.title?.rendered || "",
            }}
          />
          {post.excerpt?.rendered && mode !== "concise" && (
            <div
              className="tmo-post-excerpt"
              dangerouslySetInnerHTML={{
                __html: post.excerpt?.rendered || "",
              }}
            />
          )}
          {mode !== "general" && ctaText && (
            <div class="tmo-post-series">
              <a class="tmo-series-link">
                {ctaText}
                <span className="tmo-series-link-icon">
                  <ArrowRight />
                </span>
              </a>
            </div>
          )}
        </div>
      </div>
      {mode === "general" && (
        <div class="tmo-post-series">
          {labelText && <span class="tmo-series-kicker">{labelText}</span>}
          {ctaText && (
            <a class="tmo-series-link">
              {ctaText}
              <span className="tmo-series-link-icon">
                <ArrowRight />
              </span>
            </a>
          )}
        </div>
      )}
    </>
  );
}
发布时间:2025-11-05