安全漏洞检测工具

为了帮助你开发安全的 WordPress 插件和主题,有许多工具可以帮助你检测代码中的安全漏洞。

在本课中,你将了解一些可用于测试代码安全漏洞的工具,并简要介绍如何使用它们。

你还将了解在哪里可以找到关于 Web 应用程序关键安全风险的更多信息。

插件

WordPress.org 仓库中有两个插件可以帮助你验证代码。

Plugin Check 是一个 WordPress 插件,你可以用它来测试你的插件是否符合 WordPress.org 插件目录的要求标准,其中一项要求就是你的插件代码是安全的。

安装并激活该插件后,你可以从 WordPress 管理仪表盘的“工具”菜单中访问 Plugin Check 管理页面。

选择你要测试的插件,并确保勾选了“安全”复选框。

点击检查!按钮运行测试。

结果会以表格形式显示,列出需要解决的问题,包括文件名和行号。

还有一个 Theme Check 插件,它与 Plugin Check 类似,但适用于主题。

安装此插件后,你可以从 WordPress 管理仪表盘的“外观”菜单中访问 Theme Check 管理页面。

同样,你选择要检查的主题,然后点击检查!按钮运行测试。

结果会显示在页面底部,列出需要解决的问题。

命令行

如果你想要可以从命令行运行的工具,可以使用 PHP_CodeSniffer 配合 WordPress 编码标准规则。

这种组合不仅会根据 WordPress 编码标准检查你的代码,还会检查安全漏洞。

要使用 PHP_CodeSniffer 配合 WordPress 编码标准,你需要安装 Composer,这是一个用于 PHP 项目的依赖管理器。

安装 Composer 不在本课的讨论范围内,但你可以在 Composer 网站上找到适用于 macOS/Linux 和 Windows 操作系统的安装说明。

为了让 Composer 工作,你还需要在计算机上安装 PHP,这样你就可以使用 PHP CLI 二进制文件,它允许你在终端中运行 PHP 脚本,而不仅仅是在浏览器中。

你可以在 PHP 手册的“安装与配置”部分找到在你的系统上安装 PHP 的方法。

安装 PHP 后,请确保将 PHP CLI 二进制文件的路径添加到计算机的操作系统路径中,这样你就可以在计算机的任何位置运行 PHP 命令。

要测试这一点,你可以在终端中运行以下命令:

php -v

它应该会输出 PHP 版本。

安装 Composer 后,你在终端中运行以下命令,在你的插件或主题目录中初始化一个新的 Composer 项目:

composer init

按照终端内的说明创建一个新的 Composer 项目。

这将在当前目录中创建一个 composer.json 文件,其中包含你项目的依赖项列表。

接下来,按照 WordPress 编码标准规则仓库中的说明,在你的插件目录中安装所有必需的依赖项:

composer require --dev wp-coding-standards/wpcs:"^3.0"

安装完成后,你可以使用 WordPress 标准对代码运行 PHP_CodeSniffer,它会输出需要解决的问题列表。

vendor/bin/phpcs --standard=WordPress your-plugin-file.php

代码编辑器

根据你使用的代码编辑器,有多种方法可以在编码时检查漏洞。

例如,Visual Studio Code 有许多扩展可以在编辑器内运行 PHP_CodeSniffer 工具,你可以在 VS Code 扩展市场中搜索“phpcs”找到它们。

此外,还有一些第三方服务可以在你的代码编辑器中扫描代码漏洞,例如 Sonar 的 SonarLint 工具。

SonarLint 对所有开源项目完全免费。只有当你想要分析私有仓库时才需要付费。

SonarLint 可作为 Visual Studio Code、JetBrains 编辑器和 Eclipse 的插件使用。

配置正确后,这些扩展可以在你编写代码时高亮显示问题,这样你就可以在提交代码之前修复它们。

OWASP

虽然不特指某个工具,但熟悉开放 Web 应用程序安全项目(OWASP)也是一个好主意。

OWASP 是一个致力于提高软件安全性的非营利基金会。他们提供了许多资源,包括 OWASP Top 10,这是 Web 应用程序最关键的十大安全风险列表。

安全是一个不断变化的领域,漏洞也会随着时间的推移而演变。通过遵循开放网络应用安全项目(OWASP)十大列表,你可以及时了解最新的安全风险和最佳实践。

修复常见安全漏洞

在本模块的第一课中,您学习了开发自定义插件或主题时应遵循的主要安全原则。

在本课中,您将学习如何在开发 WordPress 插件和主题时应用这些原则,通过修复一个编码不安全的表单提交插件来实践。

编码糟糕的表单提交插件

首先,下载 Learn Plugin Security 插件,并将其安装到您的本地开发环境中。

然后,在代码编辑器中打开主插件文件,查看代码。

  1. 在主插件 PHP 文件的顶部,设置了一些常量,这些常量在插件其他地方使用。前两个用于定义插件将用于重定向的页面别名。为了使插件功能正常工作,这些页面必须存在,且具有正确的别名。
  2. 接下来,在插件激活钩子上注册了一个回调函数。这会在数据库中创建一个自定义的 form_submissions 表。
  3. 之后,插件的管理 JavaScript 和前端样式 CSS 文件被加载。
  4. 接着,注册了一个短代码,用于在前端显示表单。
  5. 然后,一个回调函数被挂载到 wp 动作上,插件使用它来处理表单提交。
  6. 接下来,插件注册了一个管理子菜单,用于显示表单提交列表。
  7. 有一个函数,管理子菜单使用它从数据库中获取表单提交数据。
  8. 最后,有一个回调函数被挂载到 wp_ajax 函数上,这是插件用于从管理子菜单页面删除表单提交的 Ajax 端点。

管理 JavaScript 文件处理从提交页面删除表单提交的 Ajax 请求。

前端 style.css 文件在用户为短代码输入 class 属性时使用。默认颜色为红色,但用户可以将其更改为蓝色。它只是为表单添加了一个边框。

当短代码被添加到文章或页面时,它会渲染表单,用户可以提交他们的详细信息。表单提交后,会根据提交是否成功重定向到成功或错误页面。然后在仪表盘中,管理员可以查看表单提交,并使用 Ajax 删除提交。

SQL 注入

我们要查找的第一个常见漏洞是 SQL 注入。

当输入的值没有被正确清理,允许使用输入数据的任何 SQL 命令可能在数据库上执行时,就会发生 SQL 注入。

我们需要解决潜在 SQL 注入漏洞的第一个地方是在 wp_learn_maybe_process_form 函数中。

function wp_learn_maybe_process_form() {
    if (!isset($_POST['wp_learn_form'])){
        return;
    }
    $name = $_POST['name'];
    $email = $_POST['email'];

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
    $result = $wpdb->query($sql);
    if ( 0 < $result ) {
        wp_redirect( WPLEARN_SUCCESS_PAGE_SLUG );
        die();
    }

    wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
    die();
}
  1. 我们需要确保在查询中使用任何 $_POST 数据之前,先对其进行清理。
  2. 我们需要使用 wpdb 的 prepare 或 insert 函数。
function wp_learn_maybe_process_form() {
    if (!isset($_POST['wp_learn_form'])){
        return;
    }
    $name = sanitize_text_field($_POST['name']);
    $email = sanitize_email($_POST['email']);

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $rows = $wpdb->insert(
        $table_name,
        array(
            'name' => $name,
            'email' => $email,
        )
    );
    if ( 0 < $rows ) {
        wp_redirect( WPLEARN_SUCCESS_PAGE_SLUG );
        die();
    }

    wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
    die();
}

这将确保名称和电子邮件字段值在从表单提交请求中接收时都被清理,并且在用于将记录存储到数据库之前也被清理。虽然这看起来可能有些过度,但如果您只清理输入,而代码后来被更改为以不同方式使用这些值,您仍然可能容易受到 SQL 注入攻击。

另一个需要防止 SQL 注入的地方是在 wp_learn_delete_form_submission 函数中。

  1. 我们需要确保在查询中使用任何 $_POST 数据之前,先对其进行清理。
  2. 我们需要使用 wpdb 的 prepare 或 delete 函数。
function wp_learn_delete_form_submission() {
    $id = (int) $_POST['id'];
    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $rows_deleted = $wpdb->delete( $table_name, array( 'id' => $id ) );
    if ( 0 < $rows_deleted ) {
        $result = 'success';
    } else {
        $result = 'error';
    }
    return wp_send_json( array( 'result' => $result ) );
}

由于这是一个整数,我们可以使用 PHP 类型转换功能来确保它始终被转换为整数。

跨站脚本攻击 (XSS) (3:47)

我们要查找的下一个常见漏洞是跨站脚本攻击 (XSS)。

当恶意方将 JavaScript 注入到网页中时,就会发生跨站脚本攻击 (XSS),这可用于从网站发起多种不同的攻击或恶意活动。

您可以通过转义输出、剥离不需要的数据来避免 XSS 漏洞。您的代码应根据要转义的内容类型,使用适当的函数转义动态内容。

让我们看看一些输出数据的地方,并确保它们被正确转义。

第一个地方是在 wp_learn_form_shortcode 短代码回调的包装 div 中。

<div id="wp_learn_form" class="<?php echo $atts['class'] ?>">

在这里,div 的 class 属性是根据传递给短代码的属性渲染的。这是一个潜在的 XSS 漏洞,因为 class 属性没有被转义。

<div id="wp_learn_form" class="<?php echo esc_attr( $atts['class'] ) ?>">

请注意,您应该专门使用 esc_attr 函数来转义 HTML 属性。

接下来,我们有 wp_learn_render_admin_page 函数,它用于渲染管理页面。

function wp_learn_render_admin_page(){
    $submissions = wp_learn_get_form_submissions();
    ?>
    <div class="wrap" id="wp_learn_admin">
        <h1>Admin</h1>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Email</th>
                </tr>
            </thead>
            <?php foreach ($submissions as $submission){ ?>
                <tr>
                    <td><?php echo $submission->name?></td>
                    <td><?php echo $submission->email?></td>
                    <td><a class="delete-submission" data-id="<?php echo $submission->id?>" style="cursor:pointer;">Delete</a></td>
                </tr>
            <?php } ?>
        </table>
    </div>
    <?php
}

这里,$submission->name、$submission->email 和 $submission->id 应该被转义。

<?php foreach ($submissions as $submission){ ?>
    <tr>
        <td><?php echo esc_html($submission->name)?></td>
        <td><?php echo esc_html($submission->email)?></td>
        <td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>
    </tr>
<?php } ?>

在这个示例中,你可以使用 esc_html,因为当 HTML 元素包含要显示的数据段时,这是正确的函数。务必密切关注每个转义函数的作用,因为有些会移除 HTML,而有些则允许保留。

你必须根据要输出的内容和上下文,选择最合适的函数。

最后,你将 ID 转换为整数,因为它被用于数据属性中。

跨站请求伪造 (CSRF)

下一个需要防范的漏洞是跨站请求伪造。CSRF 是指恶意方诱骗用户在他们已认证的 Web 应用中执行非预期的操作。

在使用 WordPress 开发时,熟悉 WordPress nonce 是帮助防范 CSRF 的必备技能。

Nonce 是“一次性使用的数字”,它提供了一种验证请求来源是否合法的方式。

  1. 当你需要验证请求是否合法时,创建一个 nonce。
  2. 你将 nonce 输出或传递给需要发起请求的地方。
  3. 当请求发起时,你验证该 nonce。

这个插件中存在两个可能的 CSRF 漏洞。

第一个漏洞出现在表单提交并处理数据时。要修复此问题,我们需要在短代码渲染的表单中添加一个 nonce,然后在表单提交时验证它。

在表单中,我们使用 wp_nonce_field 函数来添加一个包含 nonce 的隐藏字段。

<?php
wp_nonce_field( 'wp_learn_form_nonce_action', 'wp_learn_form_nonce_field' );
?>

注意你是如何传入一个 action 和一个 name 的。action 用于标识 nonce,而 name 是添加到表单中的字段名称。

如果你检查表单,可以看到 nonce 字段,它使用了你传递给函数的名称以及 nonce 值。

然后在表单提交函数中,你使用 wp_verify_nonce 函数来验证 nonce,传入 nonce 字段的值和 action。

    /**
     * 04 (b). Verify the nonce
     * https://developer.wordpress.org/apis/security/nonces/
     */
    if ( ! isset( $_POST['wp_learn_form_nonce_field'] ) || ! wp_verify_nonce( $_POST['wp_learn_form_nonce_field'], 'wp_learn_form_nonce_action' ) ) {
        wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
        die();
    }

这里我们检查 nonce 字段是否已在请求中传递,然后验证 nonce 是否有效。如果 nonce 未传递或无效,我们将重定向到错误页面。

另一个需要防范 CSRF 的地方是用于删除表单提交数据的 Ajax 回调。

function wp_learn_delete_form_submission() {
    $id = (int) $_POST['id'];
    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $rows_deleted = $wpdb->delete( $table_name, array( 'id' => $id ) );
    if ( 0 < $rows_deleted ) {
        $result = 'success';
    } else {
        $result = 'error';
    }
    return wp_send_json( array( 'result' => $result ) );
}

要修复此问题,首先我们需要使用 wp_create_nonce 手动创建一个 nonce,然后使用 wp_localize_script 将 nonce 传递给 JavaScript 层。

  /**
   * 04 (a). Add an ajax nonce to the script
   * https://developer.wordpress.org/apis/security/nonces/
   */
  $ajax_nonce = wp_create_nonce( 'wp_learn_ajax_nonce' );
  wp_localize_script(
      'wp-learn-admin',
      'wp_learn_ajax',
      array(
          'ajax_url' => admin_url( 'admin-ajax.php' ),
          'nonce'    => $ajax_nonce,
      )
  );

然后,我们需要在 jQuery POST 请求中包含该 nonce。

jQuery.post(
    wp_learn_ajax.ajax_url,
    {
        '_ajax_nonce': wp_learn_ajax.nonce,
        'action': 'delete_form_submission',
        'id': id,
    },
    function (response) {
        console.log( response );
        alert( 'Form submission deleted' );
        document.location.reload();
    }
);

注意我们如何在 POST 请求中将 nonce 指定为 _ajax_nonce。这是 WordPress 在处理 Ajax 请求时预期的 nonce 名称。

最后,在 Ajax 回调中,我们使用方便的 check_ajax_referrer 函数来验证 nonce。

    check_ajax_referer( 'wp_learn_ajax_nonce' );

你会看到传递给 check_ajax_referer 的字符串与我们创建 nonce 时传递给 wp_create_nonce 的字符串相同。

如果 check_ajax_referrer 失败,它将导致执行停止,因此我们不需要检查该函数的结果。

访问控制失效

这个插件中还有一个漏洞,即访问控制失效。BAC 是指用户能够访问他们本不应访问的资源。例如,用户可能能够访问管理员功能,即使他们不是管理员。

在我们的示例中,Ajax 函数存在一个访问控制失效漏洞。目前,任何人都可以使用正确的数据向 Ajax 请求 URL 发起请求,从而删除一个 form_submission。

要修复此问题,我们可以使用 WordPress 角色和权限 API 来检查用户是否具有删除表单提交数据的正确权限。在这种情况下,只需检查用户是否为管理员用户即可。

    if ( ! current_user_can( 'manage_options' ) ) {
        return wp_send_json( array( 'result' => 'Authentication error' ) );
    }

注意我们在这里进行了两次检查,一次针对 CSRF,一次针对访问控制。在这个示例中,执行顺序不是特别重要,但通常来说,先检查 CSRF,再检查访问控制是一个好习惯。

附加题 – 开放重定向

这个插件中还有一个额外的安全漏洞。你能找到它吗?

这是一个很难发现的漏洞,但所有使用 wp_redirect 的地方都应替换为 wp_safe_redirect。这是因为代码正在重定向到本地 URL,而 wp_safe_redirect 会检查其使用的 $location 在具有绝对路径时是否为允许的主机。这可以防止如果重定向 $location 受到攻击时可能发生的恶意重定向。

进一步阅读

要了解更多关于修复 WordPress 代码中常见漏洞的信息,请务必收藏 WordPress 开发者文档安全部分中关于常见漏洞的页面,以及包含权限检查、数据验证、安全输入、安全输出和 nonce 的示例。

安全开发插件和主题

到目前为止,你已经熟悉了为 WordPress 开发插件和主题的基础知识。现在也是考虑编写代码时安全影响的好时机。

在本课中,你将学习如何以安全为出发点进行开发。

你将了解确保代码安全的好处、保护代码安全的步骤,以及在哪里可以找到更多关于以安全为出发点进行开发的信息。

免责声明

本课旨在作为开发插件和主题时培养安全意识的入门介绍。本课中使用的代码非常简化。你不应在插件或主题中使用本教程中的任何代码。

请务必阅读 WordPress 开发者手册中关于安全的完整文档,网址为 https://developer.wordpress.org/apis/security/,以确保你遵循正确的方法和流程。

什么是以安全为出发点进行开发?

以安全为出发点进行开发,是指确保你的代码不仅能够正常工作,而且不会引入任何安全漏洞的过程。

如果你的插件或主题代码存在安全漏洞,可能会使任何安装了你的产品的 WordPress 网站面临潜在攻击,并可能导致该网站被入侵。

在编写代码时,培养安全思维并思考你的代码可能被恶意利用的方式非常重要。

  • 不要信任任何数据,无论是用户输入、第三方 API 数据,甚至是数据库中的数据。始终检查以确保数据有效且使用安全。
  • WordPress 提供了许多 API,可以帮助你完成常见任务,例如清理用户输入、验证数据和转义输出。依赖这些 API 来帮助验证和清理数据,而不是编写自己的函数。
  • 及时了解常见漏洞,并保持代码更新以防止它们。

安全在开发过程中处于什么位置?

安全应该在开发过程的每个阶段都加以考虑。

通常,发现的大多数安全漏洞都发生在 PHP 层,该层在服务器上执行。

因此,本课将重点介绍 PHP 代码中最常见的预防措施。

清理输入

开发时要采取的首要步骤之一是确保任何用户输入都经过清理。这意味着任何来自用户的数据,例如表单提交或 URL 参数,都要经过检查以确保使用安全。

在此示例代码中,名称和电子邮件字段是从插件生成的表单提交的,然后保存在名为 form_submissions 的自定义数据库表中。

$name = $_POST['name'];
$email = $_POST['email'];
global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';

$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);

如你所见,数据直接保存到数据库,没有任何清理。

这意味着如果用户提交的名称为 John'; DROP TABLE form_submissions;--,SQL INSERT 查询将运行,随后是 DROP 查询,该表将从数据库中删除!

WordPress 提供了一个清理 API,可用于清理传入的数据。你可以使用 sanitize_text_fieldsanitize_email 函数在名称和电子邮件字段用于查询之前对其进行清理。

$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );

global $wpdb;
$table_name = $wpdb->prefix . 'form_submissions';

$sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
$result = $wpdb->query($sql);

请注意,代码遵循了清理数据的一个关键原则:尽可能早地进行清理。

要了解更多关于 WordPress 开发者可用的清理函数,请查看 WordPress 开发者文档中的“清理数据”页面。

验证数据 (4:06)

验证数据是根据预定义模式(或多个模式)对其进行测试的过程,结果明确:有效或无效。

不可信数据可能来自许多来源:用户、第三方 API 数据,甚至你的数据库数据也可能被视为不可信,尤其是当其他内容修改过它时。即使是网站管理员也可能犯错,输入不正确或不安全的数据,因此始终检查数据非常重要。

在此示例中,删除函数要求将数字 ID 发布到 admin-ajax 回调:

add_action( 'wp_ajax_delete_form_submission', 'wp_learn_delete_form_submission' );
function wp_learn_delete_form_submission() {
    if ( ! isset( $_POST['id'] ) ) {
        wp_send_json_error( 'Invalid ID' );
    }
    $id = $_POST['id'];

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql    = "DELETE FROM $table_name WHERE id = $id";
    $result = $wpdb->get_results( $sql );

    return wp_send_json( array( 'result' => $result ) );
}

这里 ID 直接用于 SQL 查询,没有任何验证。同样,这意味着如果用户提交的 ID 为 1; DROP TABLE form_submissions;,相同的 SQL DROP 查询将在删除后运行,表将被删除。

为了防止这种情况,你可以使用 PHP 的类型转换功能,确保 $id 的值始终是整数。可以通过在变量名前添加 (int) 来实现,如下所示:

$id = (int) $_POST['id'];

请注意,这仅当通过 $_POST 数组传递的字符串的第一个字符可以转换为整数时才有效,否则 $id 的值将为 0。在这种情况下,最好更新代码以处理这种情况。

该代码还遵循了验证数据的一个关键原则,即尽早进行验证。

if ($id === 0){
    // return early with an error
    return wp_send_json( array( 'result' => 'Invalid ID passed' ) );

}

要了解更多关于验证数据的不同方法,请查看 WordPress 开发者文档中的“验证数据”部分。

转义输出

安全的另一个方面是确保你输出到浏览器的任何信息都是安全的,包括任何文本、HTML 或 JavaScript 代码,以及来自数据库的数据。

即使你的代码不负责所显示数据的来源,它也有责任安全地显示它。

在这个例子中,代码从数据库中获取表单提交数据,然后循环遍历这些提交数据,并在 WordPress 仪表盘的管理界面中输出提交数据:

    $submissions = wp_learn_get_form_submissions();
    ?>
    <div class="wrap" id="wp_learn_admin">
        <h1>Admin</h1>
        <table>
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Email</th>
                </tr>
            </thead>
            <?php foreach ($submissions as $submission){ ?>
                <tr>
                    <td><?php echo $submission->name; ?></td>
                    <td><?php echo $submission->email; ?></td>
                    <td><a class="delete-submission" data-id="<?php echo $submission->id?>" style="cursor:pointer;">Delete</a></td>
                </tr>
            <?php } ?>
        </table>
    </div>
    <?php

这里我们有三个需要转义的数据项:$submission->name$submission->email 字段,以及 $submission->id

<td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>

对于名称和电子邮件字段,我们可以使用 WordPress 内置的转义函数 esc_html()。ID 可以通过将其转换为整数来转义,就像你在数据验证时可能做的那样。

  <td><?php echo esc_html( $submission->name ); ?></td>
  <td><?php echo esc_html( $submission->email ); ?></td>
  <td><a class="delete-submission" data-id="<?php echo (int) $submission->id?>" style="cursor:pointer;">Delete</a></td>

请注意,这段代码遵循了转义数据的一个关键原则,即尽可能晚地转义数据。

要了解更多关于转义输出的不同方法,请查看 WordPress 开发者文档中的“转义数据”部分。

防止无效请求

每当发出请求时,检查该请求是否有效非常重要。这意味着要检查请求是否来自可信来源。

例如,你可能有一个短代码用于渲染表单,用户可以通过它提交信息。

渲染表单的函数可能看起来像这样。

add_shortcode( 'wp_learn_form_shortcode', 'wp_learn_form_shortcode' );
function wp_learn_form_shortcode() {
    ob_start();
    ?>
    <form method="post">
        <input type="hidden" name="wp_learn_form" value="submit">
        <div>
            <label for="email">Name</label>
            <input type="text" id="name" name="name" placeholder="Name">
        </div>
        <div>
            <label for="email">Email address</label>
            <input type="text" id="email" name="email" placeholder="Email address">
        </div>
        <div>
            <input type="submit" id="submit" name="submit" value="Submit">
        </div>
    </form>
    <?php
    $form = ob_get_clean();
    return $form;
}
?>

当用户提交表单时,数据会被发送到服务器。然后数据被处理、清理并存储在数据库中。

add_action( 'wp', 'wp_learn_maybe_process_form' );
function wp_learn_maybe_process_form() {
    if (!isset($_POST['wp_learn_form'])){
        return;
    }

    $name = sanitize_text_field( $_POST['name'] );
    $email = sanitize_email( $_POST['email'] );

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql = "INSERT INTO $table_name (name, email) VALUES ('$name', '$email')";
    $result = $wpdb->query($sql);
    if ( 0 < $result ) {
        wp_redirect( WPLEARN_SUCCESS_PAGE_SLUG );
        die();
    }

    wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
    die();
}

由于表单可能出现在任何使用该短代码的页面上,恶意用户可能会尝试向表单发送 POST 请求,要么寻找插件中的漏洞,要么发送多个请求。

为了防止这种情况,你可以检查请求是否来自可信来源。为此,你可以实现一种称为 nonce(一次性数字)的机制。

首先,在表单本身中,你可以通过使用 wp_nonce_field 函数添加一个 nonce 字段,并向该函数传递一个 nonce 操作和 nonce 名称:

wp_nonce_field( 'wp_learn_form_nonce_action', 'wp_learn_form_nonce_field' );

当表单在前端渲染时,会向表单中添加一个隐藏字段,使用 nonce 名称作为隐藏字段的 id 和 name 属性,并将生成的 nonce 作为字段值。当表单提交时,这些数据会被发送。

然后,在处理表单数据的函数中,你可以通过使用 wp_verify_nonce 函数来验证 nonce 是否有效,并将 POST 提交的 nonce 字段和 nonce 操作传递给该函数。如果验证结果为 false,你可以提前退出,防止任何进一步的代码执行。

    if ( ! wp_verify_nonce( $_POST['wp_learn_form_nonce_field'], 'wp_learn_form_nonce_action' ) {
        wp_redirect( WPLEARN_ERROR_PAGE_SLUG );
        die();
    }

任何时候你的代码发出网络请求,无论是通过重定向到新 URL、向表单 POST 数据,还是发出 AJAX 请求,你都应该检查请求是否有效。

要了解更多关于如何在插件中使用 nonce,请查看 WordPress 开发者文档中的“Nonce”部分。

防止未认证用户

根据代码的功能,最好将某些功能限制为仅具有特定权限级别的用户使用。例如,你可能有一个从数据库中删除数据的函数。

function wp_learn_delete_form_submission() {
    if ( ! isset( $_POST['id'] ) ) {
        wp_send_json_error( 'Invalid ID' );
    }
    $id = (int) $_POST['id'];

    global $wpdb;
    $table_name = $wpdb->prefix . 'form_submissions';

    $sql    = "DELETE FROM $table_name WHERE id = $id";
    $result = $wpdb->get_results( $sql );

    return wp_send_json( array( 'result' => $result ) );
}

虽然你可能会尽力防止任何没有权限的人运行此函数,但针对这种情况添加检查仍然是一个好主意。

WordPress 包含一个强大的用户角色和权限系统,允许你使用默认的用户角色和权限,或创建自定义角色和权限。

在这种情况下,可以简单地只允许具有管理站点选项权限的用户执行操作,这是管理员角色中包含的标准权限。

function wp_learn_delete_form_submission() {
    if ( ! current_user_can( 'manage_options' ) ) {
        return wp_send_json( array( 'result' => 'Authentication error' ) );
    }
    // rest of function code
}

WordPress 开发者文档中有一个关于用户角色和权限的详细部分,其中包含默认权限列表以及如何创建自定义权限。

进一步阅读

为了准备好以安全为出发点进行开发,请务必阅读 WordPress 开发者文档中关于安全性的条目,其中包含本课程中的所有示例,以及关于安全最佳实践、常见漏洞和更多示例代码的额外信息。

扩展WordPress REST API

WordPress REST API 提供了一种统一的接口,用于从 WordPress 站点获取、添加、更新和删除数据。

虽然 REST API 中可用的数据类型模式相当广泛,但有时你可能需要存储不属于核心模式的其他数据。

在本课中,你将学习两种在 REST API 请求中添加字段的方法:一种是在 REST API 路由中启用自定义字段,另一种是将自定义字段作为顶级字段提供。

你还将了解这两种方法的优缺点。

如果你跳过了本模块中的前几课,请从仓库自述文件中的链接下载 Bookstore 插件 1.0.2 版本,并在本地 WordPress 安装中安装并激活该插件。

此外,如果你还没有这样做,请下载并安装适用于你操作系统的 Postman 应用。

关于修改响应的重要说明

在开始之前,务必注意修改 WP REST API 响应可能会产生意外后果。从核心 REST API 端点响应中更改或删除数据可能会破坏插件或 WordPress 核心行为,应尽可能避免。

如果你需要从 REST API 请求中检索数据子集,推荐的方法是使用 _fields 全局参数来限制响应中返回的字段。

例如,如果你只需要应用程序中的 id 和 title 字段,可以使用 fields 参数将返回的字段限制为仅这两个。

向 REST API 响应添加字段风险较小,因此本课仅涵盖添加字段。

使用自定义字段

如果你观看了“WordPress 插件简介”模块中的课程,你应该已经了解了自定义字段(也称为元数据)。

这些字段通常用于自定义文章类型,以存储特定于该文章类型的额外数据。在底层,这些自定义字段存储在 postmeta 表中,作为一组键/值对,通过文章 ID 附加到文章上。

除了文章,WordPress 还支持其他数据类型(如评论和用户)的元数据。你可以在元数据 API 文档中了解更多信息。

WP REST API 允许你在创建或更新数据时创建或更新自定义字段。

这可以通过将键/值对对象传递给所操作文章类型的 meta 属性来实现。

但是,要使用自定义字段,你必须先注册它。这可以使用 register_meta 函数完成。

注册自定义字段

如果你想在文章上注册一个名为 location 的自定义字段,你可以像这样使用 register_meta 函数:

register_meta( 'post', 'location', array(
    'type'         => 'string',
    'description'  => 'A location for the post',
    'single'       => true,
    'show_in_rest' => true,
) );

虽然可以将其添加到插件的任何位置,但建议将其添加到类似 init 动作钩子的地方。

add_action( 'init', function() {
    register_meta( 'post', 'location', array(
        'type'         => 'string',
        'description'  => 'A location for the post',
        'single'       => true,
        'show_in_rest' => true,
    ) );
} );

务必确保 show_in_rest 属性设置为 true,否则自定义字段将无法在 REST API 中使用。

这将使自定义字段能够添加到文章的 REST API 模式中,并且还允许你使用 REST API 将数据发布到自定义字段。这通过在请求体的 meta 对象 中传递自定义字段作为键值对来处理。

{
    "title": "My Post",
    "content": "This is my post content.",
    "meta": {
        "location": "New York"
    }
}

你可以通过在 Postman 中创建一个新的 POST 请求并将其发送到文章路由来测试这一点。

为特定 WP REST API 路由启用自定义字段

WordPress 4.9.8 之前,使用 register_metashow_in_rest 设置为 true 的自定义字段会注册到给定类型的所有对象上。例如,如果你向文章类型添加了一个自定义字段,然后创建了一个自定义文章类型,该自定义字段将自动在自定义文章类型中可用。

WordPress 4.9.8 开始,可以使用带有 object_subtype 参数的 register_meta,该参数允许将元键的使用范围限制到特定的文章类型。

例如,假设你想仅在 book 自定义文章类型上注册一个 ISBN 自定义字段。你可以将其添加到注册自定义文章类型的代码中,如下所示:

register_post_type( 'book', array(
    'labels'      => array(
        'name'          => __( 'Books', 'bookstore' ),
        'singular_name' => __( 'Book', 'bookstore' ),
    ),
    'public'      => true,
    'has_archive' => true,
    'show_in_rest' => true,
    'supports'    => array( 'title', 'editor', 'custom-fields' ),
) );

register_meta( 'post', 'isbn', array(
    'type'         => 'string',
    'description'  => 'The ISBN of the book',
    'single'       => true,
    'show_in_rest' => true,
    'object_subtype' => 'book',
) );

现在,isbn 自定义字段将仅对 book 自定义文章类型可用。

你可以通过 REST API 添加一本书来测试这一点。

在 Postman 中,创建或更新对书籍路由的 POST 请求,并在 meta 对象中包含 ISBN 字段:

POST /wp-json/wp/v2/books

然后在请求体中发布以下内容:

{
    "title": "My Book",
    "content": "This is my book content.",
    "meta": {
        "isbn": "978-3-16-148410-0"
    }
}

如果你随后在 WordPress 管理后台编辑这本书,并且启用了自定义字段面板,你将看到 isbn 字段的值。它也是 book 文章类型唯一可用的自定义字段。

将自定义字段添加为 API 响应的顶级字段

另一种将自定义字段添加到 WP REST API 的方法是将它们添加为 API 响应的顶级字段。

在之前的示例中,isbn 被注册为元字段,因此它出现在 REST API 响应的元对象中。但如果你希望它作为顶级字段,与标题、内容和摘要并列,该怎么办呢?

这可以通过使用 register_rest_field 函数来实现。让我们看看如何实现这一点。

将自定义字段添加为顶级字段

首先,你需要在 rest_api_init 动作钩子中注册你的 REST 字段。这是为了确保该字段仅在 REST API 上注册。

add_action( 'rest_api_init', 'bookstore_add_rest_fields' );
function bookstore_add_rest_fields() { 
    // register some REST API functionality
}

然后,你使用 register_rest_field 函数来注册该字段。第一个参数是字段应注册的对象类型。对于单个对象,可以是字符串;对于多个对象,可以是数组。在本例中,只需在 book 自定义文章类型上注册该字段。第二个参数是字段的名称。在本例中,只需将其设置为与自定义字段相同的名称,即 isbn

    register_rest_field(
        'book',
        'isbn',
    );

第三个参数是一个参数数组,用于决定字段的功能。你至少需要向该数组传递以下三个参数。

  1. get_callback – 返回字段值的函数
  2. update_callback – 更新字段值的函数
  3. schema – 包含字段模式的数组
    register_rest_field(
        'book',
        'isbn',
        array(
            'get_callback'    => null,
            'update_callback' => null,
            'schema'          => null,
        )
    );

现在,你可以将 REST 字段的模式参数保留为 null,但你需要指定 get_callbackupdate_callback 函数。这些函数将在 API 请求发出时触发,无论是用于获取数据,还是用于创建或更新数据。

    register_rest_field(
        'book',
        'isbn',
        array(
            'get_callback'    => 'bookstore_rest_get_isbn',
            'update_callback' => 'bookstore_rest_update_isbn',
            'schema'          => null,
        )
    );

默认情况下,文章类型的准备数据数组会作为第一个参数传递给 get_callback 函数。这个函数的实现可以简单到直接返回自定义字段的值。

function bookstore_rest_get_isbn( $book ){
    return  get_post_meta( $book['id'], 'isbn', true );
}

从 REST API 创建或更新请求中为字段发送的值会作为第一个参数传递给 update_callback 函数,而模型对象(即文章)则作为第二个参数。这个函数的实现可以简单到直接更新自定义字段的值。

function bookstore_rest_update_isbn( $value, $book ){
    return update_post_meta( $book->ID, 'isbn', $value );
}

如果你通过创建一本新书并为 isbn 字段传递一个值来测试这一点,你会看到数据被保存到数据库的 post_meta 表中,同时也会作为顶级字段显示在 REST API 响应中。

包含模式

模式参数是一个描述字段模式的数组。虽然这不是必须的,但建议包含模式。至少,它有助于未来的开发者理解字段的用途。它还可以用于在创建自动化 API 测试时验证发送的数据。

    register_rest_field(
        'book',
        'isbn',
        array(
            'get_callback'    => 'wp_learn_rest_get_isbn',
            'update_callback' => 'wp_learn_rest_update_isbn',
            'schema'          => array(
                'description' => __( 'The ISBN of the book' ),
                'type'        => 'string',
            ),
        )
    );

你可以在 WP REST API 手册中阅读更多关于如何定义 REST API 资源和字段模式的内容。

是否使用 register_rest_field

在决定是仅使用 register_meta,还是添加 register_rest_field 的使用时,你应该考虑每种方法的优缺点。

仅使用 register_meta 方法的主要优点是,只要你记得在应用程序代码中使用元对象来获取和保存数据,就不需要添加任何额外的代码来启用自定义字段的存储或检索。你只需启用该字段在 REST API 中显示,就可以直接使用它。这也是性能更优的选择,因为它不会增加任何需要执行的额外代码。

另一方面,使用 register_rest_field 方法的优点是,你可以在数据返回或保存之前对其进行额外的处理。例如,你可以在数据保存到数据库之前对其进行一些验证。你还可以向 get_callback 和 update_callback 函数添加钩子,以便对数据进行额外处理,或允许其他开发者扩展你的自定义字段。缺点是它会为 API 请求增加一些开销,因为需要执行更多的代码。

最终,你选择哪种方法应根据具体情况来决定。

进一步阅读

有关修改 REST API 响应的更多信息,请查看 WP REST API 手册中的“修改响应”部分。

与WordPress REST API交互

虽然 WP REST API 通常用于从 WordPress 获取数据,但它也可以用来执行其他操作。

REST API 还允许你创建、更新和删除各种 WordPress 数据类型。

在本课中,你将了解 WP REST API 模式、验证 WP REST API 请求的方法、测试 WP REST API 请求的工具,以及通过 WP REST API 添加、编辑或删除数据的几种方式。

如果你跳过了本模块前面的课程,请从仓库自述文件中的链接下载 Bookstore 插件 1.0.1 版本,并在本地 WordPress 安装中安装并激活该插件。

WP REST API 模式

在使用 REST API 时,最好将 WP REST API 文档中的端点参考部分放在手边。端点参考列出了 WordPress 核心附带的所有端点。

点击某个端点,例如文章,将显示该端点的模式。模式定义了在获取或创建特定类型数据时,该资源存在的所有字段。

如果你创建了自定义文章类型,比如 bookstore 插件中的书籍自定义文章类型,那么自定义文章类型端点的模式将与文章端点类似。

你会注意到,许多端点字段与 WordPress 数据库中与该数据类型相关的表中的字段相匹配。然而,有些字段略有不同。例如,文章端点的标题字段将与文章表中的 post_title 字段对应。记住这些差异很重要,并且在通过 API 交互时使用正确的字段名称。

身份验证

默认情况下,WordPress REST API 使用与登录 WordPress 仪表盘时相同的基于 Cookie 的身份验证方法。

对于任何非公开的 REST API 端点,或者需要验证用户才能查看或修改的端点,必须存在身份验证 Cookie。

例如,块编辑器就是这样工作的。

有多种验证请求的方法,包括 JSON Web 令牌和 OAuth。

WordPress 内置的另一种方法是应用程序密码。

应用程序密码可以按用户设置,用于验证对 WP REST API 的请求。这允许你让用户访问 API,而无需共享他们用于登录 WordPress 仪表盘的密码。

要为你的用户创建应用程序密码,请导航到用户列表中的该用户,点击该用户进行编辑。滚动到屏幕底部,在应用程序密码部分下。

为新的应用程序密码命名,然后点击添加新应用程序密码。

密码将为你生成。请务必复制并安全地存储它,因为你将无法再次看到它。

在此屏幕上,如果密码泄露,你也可以撤销它。

使用应用程序密码为你的用户测试 REST API 请求,是使用 REST API 测试工具的好方法。

如果你打算构建更复杂的东西,比如连接到 WordPress REST API 的移动应用,你应该考虑使用 JSON Web 令牌或 OAuth 1.0a。

Postman

有许多工具可用于测试 REST API 请求。

例如,如果你使用 PhpStorm,它内置了 HTTP 客户端;如果你使用 VS Code,则有像 Postcode 这样的扩展。还有像 Hoppscotch 和 Postman 这样的独立工具。你甚至可以在终端中使用 curl 命令测试 REST API 端点。

为了本课的目的,你将学习如何使用 Postman 测试一些 REST API 请求。

你可以从 Postman 网站下载 Postman。默认情况下,Postman 会创建一个初始工作区,用于存储你的请求集合。

安装后,打开 Postman,点击创建集合按钮。这将创建一个新集合,你可以在其中添加多个请求进行测试。

你可以为集合命名,以区别于其他集合。在集合内部,点击添加请求按钮。

这将打开一个新请求,你可以为请求指定一个唯一名称。然后,输入本地 books 端点的 URL,并点击发送按钮。

http://yourlocal.wordpress.site/wp-json/wp/v2/books

请求将被发送,JSON 响应将被解析并显示在响应区域中。

现在,创建一个新请求,输入相同的 books 端点 URL,但这次将请求方法更改为 POST,然后点击发送。通过将请求方法更改为 POST,你告诉服务器你想要创建或可能更新一本书。

这次你将看到一个错误消息,因为你没有经过身份验证。

要验证请求,请点击“授权”选项卡,然后从下拉菜单中选择“基本认证”。

接着,输入你的用户名以及之前创建的应用程序密码,然后点击保存按钮。

这次你不会遇到同样的错误,因为你已经通过了身份验证。现在你可以创建书籍了。

继续操作,点击请求中的“正文”选项卡,并选择“原始”单选按钮。然后,从下拉菜单中选择 JSON,并输入以下 JSON 内容:

{
    "title": "My Postman Book",
    "content": "This is my Postman book",
    "status": "publish"
}

再次点击发送,书籍将被创建,并返回新书籍的 JSON 响应。

为了确认,请前往 WordPress 后台检查书籍列表,你应该能看到这本书。

要更新一本书,你可以使用与添加书籍相同的请求配置,但需要修改端点 URL,使其包含书籍 ID。

要删除一本书,你可以使用与更新书籍相同的端点 URL,但需要将请求方法改为 DELETE,并且不在请求正文中发送任何数据。

你还会注意到,删除一篇文章实际上会将其移至回收站,而不是永久删除。这与 WordPress 后台的行为一致。

使用 Postman 这样的工具来测试 REST API 端点,是学习如何使用 WP REST API 的好方法。它对于测试 WP REST API 请求也非常有用,可以确保你打算发送的数据格式正确,并且请求被发送到了正确的端点。

创建一本书

让我们使用 WP REST API 和 api-fetch 来创建一本新书。

为此,我们需要将标题和内容字段作为 POST 请求发送到 books 端点。

你已经有一个可以列出书籍的插件,所以可以以此为基础开始。

首先,你需要更新页面,添加一个表单,用于输入要创建的书籍的标题和内容。你可以使用以下 HTML 来创建表单,并将其添加到 bookstore_render_booklist() 管理页面回调函数中,放在现有 HTML 代码下方:

<div style="width:50%;">
    <h2>Add Book</h2>
    <form>
        <div>
            <label for="bookstore-book-title">Book Title</label>
            <input type="text" id="bookstore-book-title" placeholder="Title">
        </div>
        <div>
            <label for="bookstore-book-content">Book Content</label>
            <textarea id="bookstore-book-content" cols="100" rows="10"></textarea>
        </div>
        <div>
            <input type="button" id="bookstore-submit-book" value="Add">
        </div>
    </form>
</div>

这段 HTML 代码在自定义管理页面中添加了一个新表单,允许你输入新书的标题和内容。表单还包含一个提交按钮。

添加表单后,下一步是添加 JavaScript 代码,用于处理按钮点击事件:

const submitBookButton = document.getElementById( 'bookstore-submit-book' );
if ( submitBookButton ) {
    submitBookButton.addEventListener( 'click', function () {
        // create post code
    } );
}

现在你已经添加了按钮点击事件监听器,可以添加处理书籍创建的代码了。

为此,建议创建一个单独的函数来创建书籍,并在点击事件中调用该函数。

首先,你需要创建一个 submitBook 函数:

function submitBook() {
    // create book code
}

然后更新点击事件监听器,使其调用该函数:

submitBookButton.addEventListener( 'click', submitBook );

submitBook 函数内部,你需要从表单字段中获取标题和内容的值:

    const title = document.getElementById( 'bookstore-book-title' ).value;
    const content = document.getElementById( 'bookstore-book-content' ).value;

现在,你可以使用 api-fetch 向 books 端点发送请求,将路径设置为 books 端点,请求方法设置为 POST,并将 titlecontent 作为数据对象传递:

    wp.apiFetch( {
        path: '/wp/v2/books/',
        method: 'POST',
        data: {
            title: title,
            content: content
        },
    } ).then( ( result ) => {
        alert( 'Book saved!' );
    } );

打开自定义管理页面,输入标题和内容,然后点击“添加”按钮。你应该会看到一个提示,显示“书籍已保存!”。

然后,如果你浏览到书籍列表,就会看到新书被列出来了。

更新和删除书籍

你也可以使用 WP REST API 来更新和删除书籍。

你可以使用与添加项目相同的 api-fetch 实现来更新项目。你需要更新路径,使其包含要更新的数据实体(本例中为书籍)的 ID,以便更新该项目,同时更新数据对象,为要更新的字段提供新值。

    wp.apiFetch( {
        path: '/wp/v2/books/' + id,
        method: 'POST',
        data: {
            title: newTitle,
            content: newContent
        },
    } ).then( ( result ) => {
        alert( 'Book Updated!' );
    } );

删除一篇文章只需要将路径设置为该项目的 URL,并将方法设置为 DELETE

    wp.apiFetch( {
        path: '/wp/v2/books/' + id,
        method: 'DELETE',
    } ).then( ( result ) => {
        alert( 'Book deleted!' );
    } );

进一步阅读

有关使用 WP REST API 创建、更新和删除数据的更多信息,请查看 WP REST API 手册中的“使用 REST API”部分,以及区块编辑器手册中的 api-fetch 包参考。

使用WordPress REST API

WordPress REST API 提供了一个统一的接口,用于与 WordPress 站点中的数据进行交互。

在本课中,你将学习如何使用 WP REST API 从你的 WordPress 站点获取数据。

你将发现三种进行 REST API 请求的内部选项,然后使用它们执行 GET 请求来获取一些公开的自定义文章类型数据。

Bookstore 插件

如果你完成了“WordPress 插件入门”模块,你应该已经构建了一个注册名为 book 的自定义文章类型的插件。

如果你跳过了那个模块,你可以通过点击 Bookstore 插件链接,从 GitHub 仓库下载主插件代码。

安装并激活插件后,在你的代码编辑器中打开主插件文件。

你会注意到传递给 register_post_type 函数的参数之一是 show_in_rest

register_post_type( 'book',
    array(
        'labels'      => array(
            'name'          => __( 'Books', 'textdomain' ),
            'singular_name' => __( 'Book', 'textdomain' ),
        ),
        'public'      => true,
        'has_archive' => true,
        'show_in_rest' => true,
    )
);

此参数设置为 true,这意味着该自定义文章类型在 REST API 中可用。

这意味着如果你浏览到 wp-json/wp/v2/book 路由,你将在响应中看到自定义文章类型的数据。

如果你查看 register_post_type 函数参考,你会发现一些额外的参数可用于控制 REST API 响应。

例如,你可以更改 rest_base 参数来改变自定义文章类型数据可用的路由。

考虑到你期望能够从 book 路由获取多本书,将 rest_base 改为 books 是个好主意。

register_post_type( 'book',
    array(
        'labels'      => array(
            'name'          => __( 'Books', 'textdomain' ),
            'singular_name' => __( 'Book', 'textdomain' ),
        ),
        'public'      => true,
        'has_archive' => true,
        'show_in_rest' => true,
        'rest_base'   => 'books',
    )
);

这样做将允许你向 wp-json/wp/v2/books 路由发出请求,通过 REST API 访问书籍。

发起 REST API 请求

假设你想在 WordPress 仪表盘中添加一个页面,该页面获取书籍并以逗号分隔的列表形式显示书名和永久链接。

首先,你可以使用 admin_menu 钩子和 add_submenu_page 函数,在“书籍”菜单下添加一个管理子菜单页面。

add_action( 'admin_menu', 'bookstore_add_admin_menu' );
function bookstore_add_admin_menu() {
    add_submenu_page(
        'edit.php?post_type=book',
        __( 'Book List', 'textdomain' ),
        __( 'Book List', 'textdomain' ),
        'manage_options',
        'book-list',
        'bookstore_render_booklist'
    );
}

然后,你需要创建 bookstore_render_booklist 回调函数,该函数将输出管理页面的 HTML。

function bookstore_render_booklist() {
    ?>
    <div class="wrap">
        <h1>Book List</h1>
        <button id="load-books" class="button button-primary">Load Books</button>
        <textarea id="book-list" rows="10" cols="50"></textarea>
    </div>
    <?php
}

如果你浏览到仪表盘并点击“书籍”菜单,你会看到一个名为“Book List”的新子菜单页面。

点击该链接将带你进入一个包含“Load Books”按钮和一个文本区域的页面。

现在,你可以向 bookstore_render_booklist 函数添加功能,通过 PHP 获取书籍列表,并使按钮触发页面刷新。

然而,为了获得更流畅的用户体验,你希望使用 JavaScript 和 REST API 异步获取书籍数据并填充书籍列表,而无需等待整个页面刷新。

加载管理 JavaScript

在“WordPress 插件入门”模块中,你学习了如何在插件中加载 JavaScript 文件。

由于此功能被添加到管理仪表盘中,你需要设置一个单独的 wp_enqueue_script 函数调用,并将其挂载到 admin_enqueue_scripts 钩子上,这样 JavaScript 文件就只在管理仪表盘中加载。

首先,在插件目录中创建一个新的 JavaScript 文件,命名为 admin_bookstore.js

然后,将以下代码添加到主插件文件中,以在仪表盘中加载该 JavaScript 文件:

add_action( 'admin_enqueue_scripts', 'bookstore_enqueue_admin_scripts' );
function bookstore_enqueue_admin_scripts() {
    wp_enqueue_script(
        'bookstore-admin',
        plugin_dir_url( __FILE__ ) . 'admin_bookstore.js',
        array(),
        '1.0.0',
        true
    );
}

请注意,这段代码不仅加载了 JavaScript 文件,还指定了一个空的依赖项数组、一个版本号,并通过将最后一个参数设置为 true,指定它应在 HTML 页面的页脚中加载。

你可以在 wp_enqueue_script 函数参考的“参数”部分阅读有关这些参数的更多信息。

你可以通过在 admin_bookstore.js 文件中添加一个简单的 alert,然后刷新管理页面来测试它是否正确加载。

alert( 'Hello, World!' );

一旦你确认它正常工作,就可以从文件中删除该行。

选项 1:使用 Backbone.js 客户端

自从 REST API 被添加到 WordPress 以来,它就包含了一个 Backbone.js REST API JavaScript 客户端,用于直接向 WP REST API 发出请求。

它通过为所有通过 API 暴露的端点提供模型和集合,提供了一个使用 WP REST API 的接口。

为了确保你的 JavaScript 代码能够使用 REST API 客户端,你需要将其添加为已加载 JavaScript 的依赖项。

wp_enqueue_script 的第三个参数是一个包含所有依赖项的数组,你可以将 wp-api 作为依赖项添加到你的 wp_enqueue_script 函数调用中。

add_action( 'admin_enqueue_scripts', 'bookstore_enqueue_admin_scripts' );
function bookstore_enqueue_admin_scripts() {
    wp_enqueue_script(
        'bookstore-admin',
        plugin_dir_url( __FILE__ ) . 'admin_bookstore.js',
        array( 'wp-api' ),
        '1.0.0',
        true
    );
}

这将确保你的插件 JavaScript 代码仅在 REST API JavaScript 客户端加载完成后才加载,从而你可以在插件中使用它。

你需要先为新按钮注册一个点击事件处理器。

const loadBooksByRestButton = document.getElementById( 'bookstore-load-books' );
if ( loadBooksByRestButton ) {
    loadBooksByRestButton.addEventListener( 'click', function () {
        //do somthing
    } );
}

然后,在事件处理函数中,你可以通过全局对象 wp 访问 WP API 客户端,来创建一个新的书籍集合。

    const allBooks = new wp.api.collections.Books();

此时,allBooks 只是一个空集合,因此你需要通过调用集合的 fetch 方法来获取文章。

   allBooks.fetch();

fetch 方法返回一个 Promise,因此你可以链式调用 done 方法来处理响应,并实现一个回调函数来接收 API 请求的响应。

你可以在该回调函数中指定一个 books 参数,用于接收 API 请求的响应。

    allBooks.fetch().done(
        function ( books ) {
            // do something with books
        }
    );

现在,你可以使用类似 forEach 的方法遍历 books 对象,并逐一访问每本书。

    allBooks.fetch().done(
        function ( books ) {
            books.forEach( function ( book ) {
              // do something with book
            } );
        }
    );

最后,你可以将书名和永久链接添加到文本区域中。

首先,你需要在 forEach 循环之前创建一个文本区域的实例,然后在 forEach 循环内部将值追加到文本区域的 value 属性中。

        allBooks.fetch().done(
            function ( books ) {
                const textarea = document.getElementById( 'bookstore-booklist' );
                books.forEach( function ( book ) {
                    textarea.value += book.title.rendered + ',' + book.link + ',\n'
                });
            }
        );

你的最终代码将类似这样。

const loadBooksByRestButton = document.getElementById( 'bookstore-load-books' );
if ( loadBooksByRestButton ) {
    loadBooksByRestButton.addEventListener( 'click', function () {
        const allBooks = new wp.api.collections.Books();
        allBooks.fetch().done(
            function ( books ) {
                const textarea = document.getElementById( 'bookstore-booklist' );
                books.forEach( function ( book ) {
                    textarea.value += book.title.rendered + ',' + book.link + ',\n'
                });
            }
        );
    });
}

切换回自定义的“书籍列表”管理页面,点击“加载书籍”按钮,即可看到书籍列表出现在文本区域中。

选项 2:使用 @wordpress/fetch-api

自 WordPress 5.0 引入区块编辑器以来,@wordpress/fetch-api 包也已可用,用于发起 REST API 请求。

该包是浏览器 fetch API 的封装,提供了一种更现代、更灵活的方式来向 REST API 发起请求。

要使用 fetch API,你可以更新插件的 JavaScript 依赖项,加入 wp-api-fetch

    wp_enqueue_script(
        'bookstyle-script',
        plugins_url() . '/bookstore/admin_bookstore.js',
        array( 'wp-api', 'wp-api-fetch' ),
        '1.0.0',
        true
    );

你可以移除 wp-api 依赖,或者将 wp-api-fetch 作为额外依赖添加。

接下来,在表单的“操作”区域添加一个按钮,用于触发 fetch 请求。

<button id="bookstore-fetch-books">Fetch Books</button>

和之前一样,在你的 bookstore.js 文件中,为新按钮的点击事件设置一个事件监听器,但这次使用 apiFetch 方法来向 REST API 发起请求。

const fetchBooksByRestButton = document.getElementById( 'bookstore-fetch-books' );
if ( fetchBooksByRestButton ) {
    fetchBooksByRestButton.addEventListener( 'click', function () {
        wp.apiFetch( { path: '/wp/v2/books' } ).then( ( books ) => {
            const textarea = document.getElementById( 'bookstore-booklist' );
            books.map( ( book ) => {
                textarea.value += book.title.rendered + ',' + book.link + ',\n'
            });
        } );
    });
}

注意,你将指向书籍端点的路径作为一个对象传递给 wp.apiFetch 函数。这比使用 Backbone.js 客户端更灵活,因为后者需要使用特定的集合来访问书籍。

你可以链式调用 then 方法来处理响应。这与 Backbone 示例中使用 done 方法类似,因为它返回一个 Promise,等待 REST API 请求完成,然后将结果返回给回调函数。

在回调函数内部,你可以访问 books 对象,并使用 map 方法遍历它,将书名和永久链接追加到文本区域。

你还会注意到,这段代码使用了箭头函数语法作为接收响应的回调,这是一种更现代的 JavaScript 函数编写方式。

刷新管理页面,点击“获取书籍”按钮,即可看到书籍列表出现在文本区域中。

选项 3:使用 @wordpress/core-data

如果你正在开发区块,还有一个 core-data 包可用,用于从 REST API 访问数据。

core-data 旨在简化对核心 WordPress 实体的访问和操作。它注册了自己的存储,并提供了许多选择器,可自动从 WordPress REST API 解析数据,同时提供派发动作创建器来操作数据。

core-data 使用了大量 React 功能,因此最适合在区块上下文中使用。

让我们看看如何在区块中使用 core-data 模块从 REST API 获取书籍。

首先,使用你在“区块开发入门”模块中学到的 create-block 工具,创建一个名为 bookstore-block 的新区块。

cd path/to/local/site/wp-content/plugins
npx @wordpress/create-block bookstore-block

这将搭建新区块的脚手架,并生成一些代码供你编辑。

在区块的 edit.js 文件中,从 @wordpress/data 包导入 useSelect 钩子,以及从 @wordpress/core-data 包导入 store

import { useSelect } from '@wordpress/data';
import { store as bookStore } from '@wordpress/core-data';

然后,你可以使用这些来从 REST API 获取书籍。

    const books = useSelect(
        select =>
            select( bookStore ).getEntityRecords( 'postType', 'book' ),
        []
    );

useSelect 是一个钩子,允许你从已注册的选择器中检索数据。

useSelect 接受一个回调函数作为第一个参数,在该函数中你可以使用 bookStore 存储的 getEntityRecords 选择器从 REST API 中检索书籍。这些书籍随后会被存储在 books 变量中。

最后,你可以更新代码,使其在没有返回书籍时返回一个空组件,或者遍历书籍对象并输出书名和链接。

    if ( ! books ) {
        return (
            <div { ...useBlockProps() }></div>
        )
    }

    return (
        <div { ...useBlockProps() }>
            { books.map( ( book ) => (
                <p>
                    <a href={ book.link }>{book.title.rendered}</a>
                </p>
            ) ) }
        </div>
    );

现在,运行区块构建步骤,激活插件,并将书店区块添加到文章或页面中。

你将看到区块输出从 REST API 获取的书名和链接。

不同选项之间的区别

Backbone 客户端是三个选项中最古老的,但它也是与 REST API 集成最紧密的。如果你需要使用 WP REST API 构建管理仪表板页面,它是一个不错的选择,并且远优于使用传统的 admin-ajax.php 端点。

apiFetch 是一个出色的全能解决方案,因为它既可用于管理仪表板页面,也可用于编辑器中的区块。它也是一种更现代的向 REST API 发起请求的方式,并且比 Backbone 客户端更灵活。

core-data 最适合在区块上下文中使用,因为它使用了在区块编辑器上下文之外不可用的 React 功能。

进一步阅读

有关在 WordPress 中使用 REST API 的各种方式的更多信息,请查看 REST API 手册中的 Backbone JavaScript 客户端部分、区块编辑器手册中的 api-fetch 包,以及区块编辑器手册中的 core-data 包。

WordPress REST API

当您为 WordPress 进行开发时,有许多 API 可用于与您的站点数据进行交互。其中最重要的 API 之一就是 REST API。

本课程作为 WordPress REST API 的入门介绍。

您将了解什么是 REST API,以及一些关键的 REST API 概念,例如路由、端点和全局参数,这些将通过您可以在浏览器中执行的一系列示例请求来学习。

您还将了解在哪里可以找到关于 WP REST API 的更多信息。

什么是 WordPress REST API?

WordPress REST API 提供了一个接口,供应用程序与 WordPress 站点进行交互。这些应用程序可以是 WordPress 插件、主题,或需要访问 WordPress 站点数据的自定义应用程序。

WordPress REST API 最著名的实现之一是块编辑器,它是一个通过 REST API 与 WordPress 数据进行交互的 JavaScript 应用程序。

如果您打开浏览器的开发者工具并查看“网络”选项卡,您可以看到当您与块编辑器交互时,向 WordPress REST API 发出的请求。

REST API 是什么意思?

API 代表应用程序编程接口。它是一组允许应用程序相互交互的功能。WordPress 有许多 API,REST API 只是其中之一。

REST 代表表述性状态转移,这是一种软件架构风格,描述了物理上独立的组件之间的统一接口。

其核心是,WordPress REST API 提供了代表文章、页面、分类法以及任何其他自定义数据类型的 REST 端点(URI)。您的代码可以以 JavaScript 对象表示法(即 JSON)的形式向这些端点发送和接收数据,以获取、修改和创建您站点上的内容。

让我们深入了解 REST API 的一些概念,以便更好地理解它们。

路由与端点

WordPress REST API 的上下文中,路由是一个可以映射到不同 HTTP 方法的 URI。

HTTP 方法是您与网络上任何内容交互时发出的请求类型。例如,当您浏览到网络上的一个 URL 时,会向服务器发出一个 GET 请求以请求数据。

当您提交表单时,会发出一个 POST 请求,该请求将提交的表单数据传递给 Web 服务器。

将单个 HTTP 方法映射到路由的过程称为端点。

因此,例如,您会有一个用于获取数据的 GET 端点、一个用于创建数据的 POST 端点以及一个用于删除数据的 DELETE 端点,所有这些都使用相同的路由。

本地开发测试

关于在本地 WordPress 安装上测试 REST API 路由,需要注意的一点是,您可能需要启用除“朴素”之外的固定链接设置。

这是因为 REST API 使用与固定链接相同的 URL 重写功能,将人类可读的路由和端点映射到相关的内部请求。

因此,如果您的本地 WordPress 安装使用的是默认的“朴素”固定链接设置,请将其更改为类似“文章名”的设置。

示例路由与端点

让我们看一些路由和端点的示例。

如果您打开浏览器,并访问 WordPress 站点的 /wp-json/ URI,您将向该 URI 发出一个 GET 请求,该请求会返回一个 JSON 响应。

// 示例代码块,保持原样

某些浏览器内置支持“美化打印”JSON 响应,这将以更易读的格式显示它。

如果您使用 Firefox 查看 JSON 响应,它允许您在不同视图之间切换,以及检查请求头。

根据您的需求,也有浏览器扩展程序,例如 Chrome 的 JSON Formatter 或 Safari 的 JSON Peep。

返回的数据是一个 JSON 响应,显示了哪些路由可用,以及每个路由内有哪些端点可用。

在此示例中,/wp-json/ 是一个路由,当该路由收到 GET 请求时,由显示数据的端点处理。此数据就是所谓的 WordPress REST API 的索引。

相比之下,/wp-json/wp/v2/posts 路由提供了一个返回文章列表的 GET 端点,但也提供了一个 POST 端点。如果您是经过身份验证的用户,并且通过 POST 请求向 /wp-json/wp/v2/posts 路由提交了正确的数据,则该请求由创建新文章的端点处理。

通常,同一个路由(在本例中为 /wp-json/wp/v2/posts)将针对不同的 HTTP 方法有不同的端点,包括用于获取数据的 GET、用于创建数据的 POST 和用于删除数据的 DELETE。

全局参数

WP REST API 包含许多全局参数,这些参数控制 API 如何处理请求/响应处理。这些参数在实际资源之上的层运行,并且对所有资源都可用。

全局参数作为查询字符串参数在 REST API 路由上实现。查询字符串以 ? 开头,后跟一系列由 & 分隔的 key=value 键值对。

看看你之前查看过的 /wp-json/wp/v2/posts 路由,通过在浏览器中请求该路由,从而激活 GET 端点。如你所见,默认情况下会返回文章的所有可用字段。

不过,你可以通过添加 _fields 全局参数来更新路由,然后以逗号分隔的列表形式指定你希望在响应中返回的字段。

wp-json/wp/v2/posts?_fields=author,id,excerpt,title,link

如果你通过刷新浏览器发起第二个 GET 请求,那么只有你请求在响应中返回的字段才会可用。

分页与排序

WP REST API 还支持结果的分页和排序。

分页由 per_pagepageoffset 参数处理。

例如,你可以通过向路由添加 per_page 参数,将 wp-json/wp/v2/posts 路由更新为每页仅返回 5 篇文章。

wp-json/wp/v2/posts?_fields=author,id,excerpt,title,link&per_page=5

如果你通过刷新页面发起新的 GET 请求,则只会返回前 5 篇文章。

还可以使用 orderorder_by 参数对结果进行排序。

例如,你可以将 wp-json/wp/v2/posts 路由更新为按文章标题降序排列。

wp-json/wp/v2/posts?_fields=author,id,excerpt,title,link&per_page=5&orderby=title&order=asc

延伸阅读

WordPress 开发者资源网站有一个专门介绍 REST API 的完整章节,其中包含关于 REST API 关键概念、常见问题、使用和扩展 REST API 等内容的部分。

静态块与动态块

在开发 WordPress 区块时,你需要考虑区块的功能,以及它是否需要根据外部因素而变化。

幸运的是,根据你的需求,可以创建静态或动态区块。

让我们看看静态区块和动态区块之间的区别,如何确定哪种适合你的需求,以及不同的开发方法。

静态区块

如果你一直在按照本模块的课程构建版权日期区块,那么你一直在构建一个静态区块。

由于静态区块的内容是固定的,一旦将其添加到编辑器,save 函数被触发,并且文章或页面被保存,区块的内容将不会改变。

静态区块适用于不需要更改的内容,比如引用或推荐语。

动态区块

然而,如果你考虑版权日期区块的实际需求,理想情况下,如果物理年份发生变化,区块的渲染内容也应该更新。

否则,你需要编辑任何添加了该区块的地方,以触发 save 函数并更新年份。

这就是动态区块的用武之地。

动态区块不通过 save 函数渲染其内容,而是使用 PHP 在包含该区块的文章或页面收到前端请求时渲染其内容。

让我们看看将版权日期区块转变为动态区块需要做些什么。

使版权日期区块动态化

要使版权日期区块动态化,你需要指定一个包含区块渲染逻辑的 PHP 文件或函数。

这可以通过几种方式完成,但最简单的是在 block.json 文件的区块元数据中使用 render 属性。

打开 src 目录中的 block.json 文件,并将以下代码添加到该文件的底部:

"render": "file:./render.php"

这告诉 WordPress 使用名为 render.php 的文件来在前端渲染区块的内容。

然后,你可以在 src 目录中创建一个 render.php 文件,并将渲染逻辑添加到该文件中:

<?php
    $block_props = get_block_wrapper_attributes();
    $starting_year = $attributes['startingYear'];
    $current_year = date( 'Y' );
?>
<p <?php echo $block_props?>>
    Copyright © <?php echo $starting_year?> - <?php echo $current_year; ?>
</p>

你可以使用 get_block_wrapper_attributes 函数来获取区块的包装器属性。这类似于在 JavaScript 中调用 useBlockProps

然后,你可以从 PHP 的 $attributes 数组中获取 startingYear 值。这个 $attributes 变量是暴露给你为区块的 render 元数据属性设置的文件中的三个变量之一,它包含为区块设置的任何属性。

然后,你可以创建 $current_year 变量,它使用 PHP 的 date() 函数始终获取当前年份。这样,当区块被渲染时,它总是获取当前年份。

最后但同样重要的是,输出段落标签,包括所有相关属性和内容。

一旦你设置好了 render.php 文件,你可以删除编辑器中与区块保存过程相关的任何代码,因为你不需要将任何内容保存到文章中。

为此,你可以删除 save.js 文件。

你还可以删除传递给 registerBlockTypesave 属性,以及导入 save 函数的 import 语句。

完成这些更改后,你可以运行构建过程,然后创建一篇文章,并将区块添加到文章中。

你会看到区块在编辑器中仍然按预期渲染。但是,如果你在代码编辑器中查看区块,你会看到保存的版本不包含任何输出。

当你预览它时,内容会被渲染。

然而,如果你模拟年份变化,比如通过更改 PHP 中变量的值,区块的前端渲染将相应更新。

其他资源

有关开发静态区块与动态区块之间差异的更多信息,你可以阅读区块开发基础中的静态或动态渲染区块部分。

区块支持与样式

区块的优势之一,就是能够在每个区块级别控制其外观。

要实现这一点,你可以使用所谓的区块支持功能,并定义区块的样式。

让我们来看看这是如何工作的。

区块支持

区块支持是一种API,允许区块声明对某些常见功能的支持。

例如,大多数区块支持设置对齐方式、背景色、文字颜色、排版等功能。

你可以在区块元数据中定义对这些常见功能的支持,一旦启用,区块在编辑器中就会拥有这些功能。

添加区块支持

要添加对某个功能的支持,你需要在block.json文件的区块元数据的supports属性中定义它。

打开src目录中的block.json文件,更新supports属性,加入align对齐支持。

{
    "supports": {
        "align": true
    }
}

构建过程完成后,创建一篇文章,并将该区块添加到文章中。你会看到该区块现在可以设置对齐方式了。

如你所见,只需启用对齐支持,区块就具备了设置对齐的能力,无需任何额外代码。

启用任何可用的区块支持,只需将它们添加到block.json文件的supports属性中即可,这能为你的用户提供丰富的选项来自定义区块外观。

区块样式

然而,在某些情况下,你可能更愿意自己定义区块的样式。

在“搭建新区块”课程中定义的版权日期区块需求中,我们要求该区块始终具有特定的边框和边框颜色。

这就是style.scsseditor.scss文件的用武之地。这些文件允许你设置特定的区块样式,然后这些样式会在编辑器和前端应用。

这两个文件是语法上很棒的样式表,也称为Sass文件。就像用于创建区块的JSX格式在构建步骤中转换为常规JavaScript一样,Sass文件会转换为常规CSS。这些文件遵循新的SCSS语法,你可以在Sass网站上了解更多信息。幸运的是,你也可以在Sass文件中直接编写纯CSS,这同样有效。

在“搭建新区块”课程中,你了解了这两个文件的用途:

  • style.scss:此文件中的样式会应用于前端和编辑器中的区块
  • editor.scss:此文件中的样式仅应用于编辑器中的区块

如果你需要某个特定样式同时应用于前端和编辑器中的区块,可以将样式添加到style.scss文件中。

打开src目录中的style.scss文件,你会看到以下脚手架代码:

.wp-block-create-block-copyright-date-block {
    background-color: #21759b;
    color: #fff;
    padding: 2px;
}

你可能注意到这个样式并没有应用到当前区块上。这是因为通过useBlockProps应用到区块父容器的类名是根据区块名称自动生成的。

block.json文件中,区块的名称是copyright-date/copyright-date-block,因此生成的类名是wp-block-copyright-date-copyright-date-block

你在style.scss文件中看到的被定位的类名是基于区块的原始名称,所以你需要修改它,使其与生成的类名匹配。

同时,你也可以为区块添加边框和边框颜色。

.wp-block-copyright-date-copyright-date-block {
    background-color: #21759b;
    color: #fff;
    padding: 2px;
    border: 2px solid #000;
}

因为你希望边框始终显示,所以不需要定义任何特定的编辑器样式。这意味着你可以删除editor.scss文件。

你也可以删除block.json文件中的editorStyle属性。

以及edit.js文件中导入editor.scss文件的代码。

这样做可能会破坏你的开发服务器,因此你可能需要重新启动它。

运行构建过程后,创建一篇文章,并将该区块添加到文章中。

你会看到该区块现在具有你在style.scss文件中定义的边框和边框颜色。当你预览区块时,该样式也会在前端应用。

总结

在开发区块时,思考哪些外观元素你希望用户能够编辑,哪些应该始终应用于区块,是很有用的。然后,你可以添加相应的支持,或者将任何特定样式硬编码到相关的样式文件中。

有关区块支持的更多信息,请参阅区块编辑器手册中的详细支持页面。

区块属性

构建块的好处之一是能够允许用户通过块属性来控制块的外观和行为。

让我们学习如何向块添加属性,以及如何向块添加控件,让用户能够更改这些属性。

向块添加属性

属性是块中可由用户控制的特性。例如,对于版权日期块,起始年份就是一个用户可以更改的属性。

要向块添加属性,您需要在块的元数据文件 block.json 中定义它们。

打开 src 目录中的 block.json 文件,并添加以下代码:

{
    "apiVersion": 2,
    "name": "example/copyright-date-block",
    "title": "Copyright Date Block",
    "category": "text",
    "icon": "calendar",
    "description": "显示版权日期",
    "supports": {
        "html": false
    },
    "attributes": {
        "startingYear": {
            "type": "string",
            "default": "2020"
        }
    },
    "editorScript": "file:./index.js",
    "editorStyle": "file:./index.css",
    "style": "file:./style-index.css"
}

JSON 格式的好处之一是,只要使用预期的属性名称,您就可以在现有 JSON 对象中的任何位置添加新属性。在这种情况下,attributes 属性是块注册过程所预期的,因此您可以将其添加到 JSON 对象中的任何位置。

在此示例中,我们向块添加了一个名为 startingYear 的属性。type 属性定义了属性的数据类型,default 属性设置了属性的初始值。

如果您正在运行开发服务器,您可能会注意到在编辑块元数据时它会崩溃。如果发生这种情况,只需重新启动开发服务器即可。

访问块的属性

要在块的编辑组件中访问块的属性,您可以在编辑组件函数中指定一个 props 参数。

编辑组件和保存函数都设置为始终接受这个包含块所有属性的 props 对象。

export default function Edit( props ) {
    // 在这里使用 props
}

然后,您可以使用 props 对象访问块的属性。例如,要访问 startingYear 属性,您可以使用 props.attributes.startingYear

export default function Edit( props ) {
    const blockProps = useBlockProps();
    return (
        <p { ...blockProps }>
            © { props.attributes.startingYear }
        </p>
    );
}

您还可以更新保存函数以包含块的属性。

export default function save( props ) {
    const blockProps = useBlockProps.save();
    return (
        <p { ...blockProps }>
            © { props.attributes.startingYear }
        </p>
    );
}

每次访问起始年份属性时,不必写出 props.attributes.startingYear,您可以使用一种称为解构赋值的语法,先从 props 对象中提取 attributes,然后再从 attributes 中提取 startingYear

export default function Edit( { attributes } ) {
    const blockProps = useBlockProps();
    return (
        <p { ...blockProps }>
            © { attributes.startingYear }
        </p>
    );
}
export default function save( { attributes } ) {
    const blockProps = useBlockProps.save();
    return (
        <p { ...blockProps }>
            © { attributes.startingYear }
        </p>
    );
}

如果您不熟悉对象解构,这是一种从对象中提取属性并将其分配给变量的方法。一开始可能有点奇怪,但一旦习惯了,它就能节省大量时间和代码。

让构建过程完成,然后将块添加到文章或页面中,您会看到块现在显示起始年份属性,并带有您定义的默认值。

块恢复

如果您碰巧在文章或页面中测试块,并在每次更改块属性的默认值或 save 函数时刷新浏览器,有时可能会遇到以下错误:

此块包含意外或无效的内容。

这是因为当块的 save 函数运行时,它会将 save 函数的输出与数据库中已保存的输出进行比较。如果它们不同,就会显示此错误。

如果您打开浏览器开发者工具的“控制台”选项卡,您会看到这被报告为块验证错误。

您可以通过使用“尝试块恢复”按钮来修复此问题,该按钮会重新渲染块并重新保存其输出。

向块添加设置面板

要允许用户更改块的属性,您需要使用块控件。

有两种添加控件的方法:一种是在块工具栏中(当块被选中时出现在块上方),另一种是在设置侧边栏中(也称为检查器,当块被选中时出现在侧边栏中)。

由于 startingYear 属性是一个文本字符串,您可以在块侧边栏中使用 TextControl 来允许用户更改起始年份。

要为您的块向块侧边栏添加控件,您首先需要导入一些内容。

  • 您需要从 @wordpress/block-editor 包中导入 InspectorControls 组件。
  • 您需要从 @wordpress/components 包中导入 PanelBodyTextControl 组件。

首先将这些导入添加到您的 Edit 组件的顶部。

打开 src 目录中的 edit.js 文件,找到导入 useBlockProps 的那一行。

import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';

InspectorControls 组件也可以以相同的方式从 @wordpress/block-editor 包中导入。

import { useBlockProps, InspectorControls } from '@wordpress/block-editor';

PanelBodyTextControl 组件可以通过相同的方式从 @wordpress/components 包中导入。

import { PanelBody, TextControl } from '@wordpress/components';

现在你可以使用这些组件为区块侧边栏添加控件。

首先,在 Edit 组件的输出中添加 InspectorControls 组件。该组件是区块侧边栏中控件的包装容器。

然后添加一个 PanelBody 组件,并为其设置 title 属性。

<InspectorControls>
    <PanelBody title={ __( 'Settings', 'copyright-date-block' ) }>
        Settings
    </PanelBody>
</InspectorControls>

在“构建你的第一个区块”课程中,你应该记得 React 组件只能返回一个父级容器。

目前你还没有一个单一的父级容器,因为你在段落组件旁边添加了 InspectorControls 组件。

这也是你的 IDE 可能会提示代码存在问题的原因。

此时,你有两个选择。

你可以更新 Edit 组件,使其渲染一个父级 div 标签,并将区块属性移到父级 div 上。

<div { ...useBlockProps() }>
    <InspectorControls>
        <PanelBody title={ __( 'Settings', 'copyright-date-block' ) }>
            Testing
        </PanelBody>
    </InspectorControls>
    <p>
        { __(
            'Copyright',
            'copyright-date-block'
        ) }
        © { startingYear } - { currentYear }
    </p>
</div>

或者,你可以使用 React Fragment 将所有内容包裹在一个父级容器中。

<>
    <InspectorControls>
        <PanelBody title='Settings'>
            Testing
        </PanelBody>
    </InspectorControls>
    <p { ...useBlockProps() }>
        { __(
            'Copyright',
            'copyright-date-block'
        ) }
        © { startingYear } - { currentYear }
    </p>
</>

由于该区块的功能实际上只需要段落标签,因此在这种情况下使用 Fragment 是最佳选择。如果你的区块需要更多标记,例如段落上方的标题标签,那么使用 div 选项可能更合理。

构建过程完成后,如果你将区块添加到文章或页面,并启用编辑器设置侧边栏,你将看到添加到区块侧边栏的设置面板。

为了遵循 WordPress 插件开发实践,你可能需要进行一个小更新:使用 __() 函数来翻译 PanelBody 组件的标题。

<PanelBody title={ __( 'Settings', 'copyright-date-block' ) }>

向区块侧边栏添加 TextControl

设置面板就位后,你现在可以添加一个 TextControl 组件,让用户编辑你的属性。

TextControl 组件是一个文本输入字段,允许用户输入字符串。你需要在 TextControl 组件上设置三个属性。前两个是标签和值:

  • label:输入字段上方显示的标签
  • value:输入字段的值

让我们看看这会是怎样的效果:

    <PanelBody title={ __( 'Settings', 'copyright-date-block' ) }>
        <TextControl
            label={ __( 'Starting Year', 'copyright-date-block' ) }
            value={ startingYear }
        />
    </PanelBody>

你需要设置的另一个属性是 onChange 属性。该属性是一个函数,当输入字段的值发生变化时被调用,并接收用户输入的新值。

    <PanelBody title={ __( 'Settings', 'copyright-date-block' ) }>
        <TextControl
            label={ __( 'Starting Year', 'copyright-date-block' ) }
            value={ startingYear }
            onChange={ ( newStartingYear ) => {
                // update startingYear with newValue
            } }
        />
    </PanelBody>

然后,该函数用于更新区块的 startingYear 属性。

这个函数的语法与你之前看到的略有不同。这是一个箭头函数语法的示例,它是 JavaScript 中编写函数的一种更简洁的方式。

为了更新属性,你可以使用传递给 Edit 组件的 props 对象上的另一个属性——setAttributes 函数。该函数用于将区块的任何属性更新为新值。

你可以将 setAttributes 添加到从 props 对象解构的属性列表中,然后使用它来更新 startingYear 属性。

export default function Edit( { attributes, setAttributes } ) {

然后,要更新 startingYear 属性,你需要使用 setAttributes 函数,并将 startingYear 属性的新值传递给它。

    <PanelBody title={ __( 'Settings', 'copyright-date-block' ) }>
        <TextControl
            label={ __( 'Starting Year', 'copyright-date-block' ) }
            value={ startingYear }
            onChange={ ( newStartingYear ) => {
                setAttributes( { startingYear: newStartingYear } );
            } }
        />
    </PanelBody>

等待构建过程完成,然后将区块添加到文章或页面。你会看到现在可以在区块侧边栏中更改起始年份的值,并且区块会实时更新。

你还可以将区块编辑器切换到代码编辑器视图,你会看到起始年份值以 JSON 对象的形式存储在区块包装器上。

最后,如果你预览区块,它会使用 startingYear 属性的新值。

其他资源

如需进一步阅读这些主题,请务必查看区块编辑器手册中的属性指南。