社区新闻

将短代码转换为区块:实用技巧与最佳实践

查看官方原文 ↗ 发布于

我喜欢区块的一个原因是,它们为短代码提供了一个绝佳的“升级路径”。传统上,短代码是插件开发者让用户能在网站任何位置添加特定插件功能的一种方式。但短代码对用户不太友好,而且查找渲染短码所需的相关数据也很麻烦。将现有短代码转换为区块,在所有方面都能提供更好的用户体验。

在本文中,我将分享一些我在最初将短代码转换为区块时希望自己知道的小技巧。

  1. 学习 React
  2. 不是每个短码都应该是块!
  3. 短码转换
  4. 利用自定义组件
  5. 导入封装和组件
  6. 使用 apiFetch 的电源块数据
  7. 使用核心数据的电源块数据
  8. 条件渲染
  9. 了解更多资源

学习 React

在我们深入之前,如果你想构建区块,了解 React 的基本工作原理是非常好的主意。无论你使用 React JSX 路线还是纯 JavaScript 路线,如果你想在框架之上构建,都需要理解 React 的基础知识。

这里,我可以推荐几个资源。Learn WordPress 上提供了区块开发入门:构建你的第一个自定义区块将短代码转换为区块课程,以及本博客上的PHP 开发者区块开发入门指南文章。

并非所有短代码都应成为区块!

让我们明确一点,并非所有短代码都应成为区块。WordPress 短代码系统功能强大且有其用武之地。我当然不认为它会在未来任何时候被移除。事实上,区块编辑器本身也有一个短代码区块。

在许多情况下,短代码仍然比区块更有意义。我最喜欢的一个不需要成为区块的短代码例子是 Make WordPress 博客上使用的 time 短代码。

[time relative]Tuesday 07:00 UTC[/time]

如果你以前从未见过它的使用,这个看似简单的短代码促进了所有 WordPress 贡献者团队的会议。当正确使用时,它会在读者的本地时区渲染时间。这对于全球分布的 WordPress 贡献者团队来说非常有价值,因为它确保他们能够正确规划和安排会议。

目前区块无法实现这种内联渲染。有一些初步尝试在寻找不使用短代码实现这些动态字符串的方法,但尚未可用。

话虽如此,虽然有一天可能将其转换为区块,但我不会这样做,因为对于作者来说,将其作为区块添加并没有真正的好处。

在考虑是否应将短代码转换为区块时,我建议问自己以下问题:

  • 短代码是否允许多种功能?如果短代码只做一件事并且做得很好,可能可以保留为短代码。上面的 time 短代码就是一个很好的例子。
  • 将其转换为区块是否意味着更易于使用?通常,具有大量属性或属性组合以不同方式渲染信息的短代码,是支持区块的最佳目标。
  • 短代码是否渲染复杂的 HTML,如果用户能在编辑器中看到它,会带来巨大差异?通常,这个问题会覆盖前两个问题的答案。一个单用途的短代码或本身易于使用的短代码,如果能在编辑器中渲染其输出使用户更轻松,那么作为区块仍然有意义。

我通常发现,如果对其中任何一个问题可以回答“是”,那么将短代码转换为区块就是有意义的。

最后,我想补充一点,当我说“转换为”时,并不意味着“替换为”。为了向后兼容,你的短代码应该始终存在并工作。特别是如果你有一个用户使用了一段时间的流行产品。你不会希望一次更新突然导致他们所有的内容损坏!

转换短代码

我非常喜欢使用 Create Block,以至于我在 2020 年录制了一个 47 分钟的教程,并将其转化为 Learn WordPress 上的一系列较短教程。如果你从未尝试过将短代码转换为区块,我建议先观看它们,以获取开始所需的基础知识。

如果你的插件有一个只渲染特定 HTML 集的短代码,你可以使用 Create Block 来搭建一个新区块,将相关代码从搭建的区块复制到你的插件中,然后将短代码的 HTML 转换为 JSX,以便在区块的 editsave 函数中返回。

这就是我在 2020 年底开始转换 Seriously Simple Podcasting 插件以支持区块时所遵循的过程。

我创建的第一个区块是基于播放器短代码

播放器短码的初始实现调用了 load_media_player 函数,该函数最初使用 WordPress 内置的 wp_audio_shortcode 函数,并基于 mediaelement.js 渲染 WordPress 中包含的音频播放器。

在插件的 1.19.2 版本中,我们添加了一个功能更丰富的播放器,我们称之为“HTML5 播放器”,它使用了更复杂的标记、用于样式的 CSS 和用于前进后退等功能的 JavaScript

该短代码可以在没有任何属性的情况下添加到启用了播客的文章中,它会为该文章渲染播放器。

将这个短代码转换为区块比我当时还必须做的其他任何事情都更容易实现,所以我首先解决了这个问题。

我使用 Create Block 搭建了一个新插件,然后复制了 package.json 文件和整个 src 目录。

当时,我们使用 grunt 来压缩我们的 CSS 和 JavaScript 资产以供生产使用,所以我必须将搭建的 package.json 与我们已有的内容合并。

然后,我从播放器模板中获取 HTML,逐字复制到区块 edit 函数的返回函数中的 JSX 上。

<div class="castos-player <?php echo $player_mode ?>-mode" 
data-episode="<?php echo $episode_id?>">
    <div class="player">
    <!-- player image -->
        <div class="player__body">
            <!-- player body -->
        </div>
    </div>
</div>
edit: () => {
    return (
        <div class="castos-player <?php echo $player_mode ?>-mode" 
data-episode="<?php echo $episode_id?>">
            <div class="player">
                <!-- player image -->
                <div class="player__body">
                    <!-- player body -->
                </div>
            </div>
        </div>
    );
},

我的下一步是将任何 HTML 属性替换为 DOM 属性(例如将 class="castos-player" 改为 className={castos-player}),并将任何 PHP 变量转换为 JavaScript 变量。我必须以某种方式将像 episode_idplayer_mode 这样的变量传递给区块,我想我可以使用传递给 editsave 函数的 props 对象来实现。

edit: (props) => {
    const episode_id = props.episode_id;
    const player_mode = props.player_mode;
    return (
        <div className="castos-player {player_mode}-mode" data-episode="{episode_id}">
            <div class="player">
                <!-- player image -->
                <div class="player__body">
                    <!-- player body -->
                </div>
            </div>
        </div>
    );
},

这是我早期学到的关于 JSX 的教训之一,它很像 HTML,但并不完全是 HTML。然而,因为它与 HTML 非常相似,所以很容易将现有的 HTML 转换为 JSX,然后进行必要的更改使其工作。

对于第一次迭代,我还需要连接来自短代码的 CSS。当时我使用 enqueue_block_editor_assets 操作来管理,但今天我可能会使用 block.json 元数据中的 style 选项来完成。

这段代码现在已经丢失了,但对于第一次迭代,我只是将变量作为硬编码属性传递给区块的 props,以便我能看到一切正常工作。我稍后会弄清楚如何更新区块代码,使用户能够选择剧集和播放器模式。

一旦我编写了在编辑器中渲染播放器的代码,我确保用相同的代码更新 save 函数,以便区块在前端以相同的方式渲染。

我很高兴地看到一切正常,我的播放器 HTML 在编辑器和前端都得到了渲染,样式完好无损。

然而,这确实使 editsave 函数变得非常庞大!而且我知道它们会变得更大,因为我计划为区块添加额外的功能,比如选择要渲染的剧集。就在这时,我想起了学习过的关于创建自定义 React 组件的知识。

利用自定义组件

React 中的组件本质上是一种避免重复代码的方式。如果你曾经使用过 BlockControls 或 RichText 等东西构建区块,那么你就在使用组件。组件很棒,因为它们允许你编写一次,随处使用。在这种情况下,将 Castos Player HTML 移动到一个组件中是完全有意义的,因为这样我可以编写和管理代码一次,并在区块的编辑和保存函数中重复使用它。

我做的第一件事是将播放器的 JSX 移动到一个 /components/CastosPlayer.js 组件中,并更新变量以作为 props 传入。

class CastosPlayer extends Component {
    render() {
        return (
            <div className="{this.props.className} {this.props.player_mode}-mode" data-episode="{this.props.episode_id}">
                <div class="player">
                    <!-- player image -->
                    <div class="player__body">
                        <!-- player body -->
                    </div>
                </div>
            </div>
        );
    }
}
export default EditCastosPlayer;

然后我将组件导入到主区块 index.js 文件中

import CastosPlayer from "./components/CastosPlayer";

最后,我使用 JSX 语法在我的编辑函数中实现组件,将变量作为 props 传递给组件。

edit: (props) => {
    const episode_id = props.episode_id;
    const player_mode = props.player_mode;
    const className = props.className;
    return (
        <CastosPlayer
            className="{className}
            episodeId={episode_id}
            playerMode={player_mode}
        />
    );
},

如果我没记错的话,当时我还必须将 className 变量传递给组件。

然而,随着 edit 函数功能的增长,我开始意识到拥有一个单独的 EditCastosPlayer 组件也是有意义的。所以我正是这样做的:

import CastosPlayer from "./CastosPlayer";

class EditCastosPlayer extends Component {
    constructor({className, episodeId, playerMode}) {
        super(...arguments);
        this.state = {
            className: className,
            episodeId: episodeId,
            playerMode: playerMode,
        };
    }
    render() {
        return (
            <CastosPlayer
                className={this.state.className}
                episodeId={this.state.episodeId}
                playerMode={this.state.playerMode}
            />
        );
    }
}
export default EditCastosPlayer;

我使用 this.state 对象来存储传递给组件的任何特定变量。我不确定这是否是正确的方法,但它奏效了。

然后我需要将 EditPlayer 组件导入到主区块文件中:

import EditPlayer from './components/EditPlayer';

我最终的区块编辑函数看起来相当整洁。

edit: EditPlayer,

我发现自定义组件非常有用,以至于我最终为几乎所有东西都创建了自定义组件,并且当我看到将要重复或可以通过分离到自己的组件来简化的代码时,我今天仍然这样做。

导入包和组件

在上面的代码示例中,你会看到我使用 import 关键字来导入特定组件。

如果你碰巧在浏览代码仓库,在某些地方我像这样从 WordPress 包中导入东西:

const {Component} = wp.element;

然而在其他地方,我这样导入。

import {Component} from "@wordpress/element";

这纯粹是因为我没有完全理解在 JavaScript 中导入代码的工作原理。在第一个例子中,我从全局 wp 包中导入 Component 类,只要区块编辑器处于活动状态,该包就存在。在第二个例子中,我从 @wordpress/element 包中导入,这是插件的一个节点依赖项。无论哪种方式,当代码被转译时,结果都是一样的。然而,由于我依赖 JSX 和 npm build 步骤来转译代码,我应该始终使用第二种方法。

使用 apiFetch 为区块数据提供动力

当然,随着区块功能的增长,我需要从 WordPress REST API 获取数据。因为我们希望允许用户选择要渲染的剧集,所以我需要获取所有剧集的列表。我使用 apiFetch 包来实现这一点。

使用 apiFetch 的好处在于它专为使用 WordPress REST API 而设计,所以我所要做的就是将其导入到我的代码中,然后传递一个剧集端点,它就会返回数据。

import apiFetch from '@wordpress/api-fetch';
const populateEpisodes = () => {
    let fetchPost = 'ssp/v1/episodes';
    apiFetch({path: fetchPost}).then(posts => {
        // do something with the returned posts
    });
}

使用 core-data 为区块数据提供动力

直到最近我才知道 core-data 包的存在。它是一个允许你从 WordPress REST API 获取数据而无需使用 apiFetch 的包。

使用 apiFetch 和 core-data 包之间的区别在于,core-data 更类似于使用 WordPress 的内置函数来获取数据。

例如,这是使用 core-data 复制 WordPress get_users() 函数以从 WordPress 站点返回用户列表的代码。

import {useSelect} from "@wordpress/data";

const users = useSelect( ( select ) => {
        return select( 'core' ).getUsers();
}, [] );

如果你只需要查询核心 WordPress 数据,如用户、文章等,这非常酷。

条件渲染

我学到的关于 React 为何如此强大的另一件很酷的事情是,能够根据布尔变量的值有条件地渲染不同的东西。

例如,当用户将 Castos Player 区块添加到他们的文章或页面时,他们应该首先看到一个选择框,其中填充了通过上述 apiFetch 调用获取的所有剧集。

<div className={this.state.className}>
    Select podcast Episode
    <select ref={this.episodeRef}>
        {this.state.episodes.map((item, key) =>
            <option value={item.id}>{item.title}</option>
        )}
    </select>
    <button onClick={activateEpisode}>Go</button>
</div>

然而,一旦他们选择了剧集并点击按钮触发 activateEpisode 函数,选择框应该消失,并且应该渲染 Castos Player。

所以,我在 EditPlayer 组件构造函数中创建了一个名为 editing 的布尔变量,并默认将其设置为 true

class EditPlayer extends Component {
    constructor({className}) {
        super(...arguments);
        let editing = true;

然后,在函数中,选中剧集并加载相关剧集数据后,我会将activateEpisode设置为 false

最后,我会用条件句包裹组件的返回,然后根据 的值返回 select 代码,或者返回 CastosPlayer 组件。editing

if (editing) {
    return (
        <div className={this.state.className}>
            Select podcast Episode
            <select ref={this.episodeRef}>
                {this.state.episodes.map((item, key) =>
                    <option value={item.id}>{item.title}</option>
                )}
            </select>
            <button onClick={activateEpisode}>Go</button>
        </div>
    );
} else {
    return [
        controls, (
            <CastosPlayer className={this.state.className}
                          episodeImage={this.state.episode.episodeImage}
                          episodeFileUrl={this.state.episode.episodeFileUrl}
                          episodeTitle={this.state.episode.episodeTitle}
                          episodeDuration={this.state.episode.episodeDuration}
                          episodeDownloadUrl={this.state.episode.episodeDownloadUrl}
            />
        )];
}

了解更多资源

如果说我能自己搞明白这一切,那我就是在撒谎。除了完成初学者React课程外,我发现各种资源非常有帮助。

每当有人问我在哪里寻求帮助时,我首先建议的是找一位有经验的贡献者依靠。我很感激WordPress社区中许多开发者朋友,他们愿意帮助我解答具体问题。许多古腾堡贡献者是全职赞助来参与古腾堡的开发,所以我建议你直接通过 Make WordPress Slack 联系其中一位。我相信他们会很乐意帮忙的。

GitHub上的Gutenberg仓库讨论区也是提问的好地方,WordPress开发栈Exchange也是很好的地方。

你也非常欢迎直接联系我。我很乐意尽力帮忙,如果我没有答案,我会尽力帮你找到合适的人。欢迎在这篇帖子下评论,我会尽力帮忙,或者引导你找到能帮忙的人。

我还推荐阅读《块编辑器手册》。它是了解构成古腾堡的各种组成部分和包裹的绝佳资源。但要花时间仔细阅读,然后在不理解的地方再提问。我发现最有帮助的是先用手册里的信息解决当前的问题,然后当我直接联系相关人员时,附上我的代码和手册链接,让他们看到我尝试过什么,然后帮我找出缺失的地方。这比只问一个笼统的问题而没有任何背景要有帮助得多。

Learn WordPress网站也是学习WordPress开发的好资源。除了我在这篇文章中链接的教程和课程外,通常还会举办关于块开发的在线研讨会

最后但同样重要的是,看看其他区块代码。在WordPress插件仓库中找到实现过模块的插件,深入了解它们做了什么,以及如何解决问题。

祝你编程愉快!