社区新闻

如何使用自定义SlotFill扩展插件

查看官方原文 ↗ 发布于

WordPress 最强大的功能之一是其可扩展性,几乎可以实现任何功能。自 2.0 版本以来,开发者一直使用 Hooks API 来扩展 WordPress 核心。随着 5.0 版本的发布,SlotFill 系统被引入,允许扩展区块编辑器和站点编辑器界面。

如果你不熟悉 SlotFill 系统及其如何用于扩展区块编辑器和站点编辑器,请先阅读 如何通过 SlotFill 系统扩展 WordPress

SlotFill 系统不仅可用于扩展现有 UI,也可用于扩展你的自定义实现——这正是本文的重点。

使用场景

你可能出于多种原因需要扩展。最突出的例子来自插件生态系统,这类扩展为开发者带来了实际收益。

你可以将用途分为内部和外部。

内部使用的例子是“免费增值”定价模式,即提供一个具有基本功能集的插件。然后,如果用户购买许可证,你可以解锁更高级的功能集。

在外部扩展中,你设置一组扩展点,其他开发者可以使用这些点来为基础插件添加功能。

如果你仅使用 PHP 和 Hooks API 来创建这些扩展点,那么这种区别就无关紧要了,因为基于 PHP 的钩子和过滤器在相关源代码加载后即可用。

然而,对于 SlotFill 来说,情况稍微复杂一些。这两种方法的区别在于自定义 SlotFill 是向外暴露的,还是仅在插件代码库内部可用。

那么,让我们来构建一些自定义 SlotFill 并暴露它们。

创建自定义 Slot 和 Fill

一个 SlotFill 包含两个组件:一个 Slot 和一个 Fill。Slot 控制 Fill 的渲染位置,两者通过一个共同的 name 属性连接。

你可以直接从 @wordpress/components 包导入 Slot 和 Fill 组件并手动添加名称,但辅助函数 createSlotFill 会为你完成所有这些工作——只需传递你的 SlotFill 名称。

import { createSlotFill } from '@wordpress/components';
const { Fill, Slot } = createSlotFill( 'BasicCreateSlotFill' );

现在你需要一个它们可以存在于其中的自定义组件。这将帮助你标识你的 SlotFill,避免命名冲突,并以与 WordPress 核心现有 SlotFill 相同的方式工作。

让我们看一个例子。

首先创建一个名为 BasicCreateSlotFill 的新组件(最佳实践:以传递给 createSlotFill 的值命名你的组件)。你的新组件应包含 Fill 组件并包装传递给它的任何子组件。然后将 Slot 组件分配给组件的一个 Slot 属性,并导出整个内容以供使用。

import { createSlotFill } from '@wordpress/components';

const { Fill, Slot } = createSlotFill( 'BasicCreateSlotFill' );

const BasicCreateSlotFill = ( { children } ) => {
    return <Fill>{ children }</Fill>;
};
BasicCreateSlotFill.Slot = Slot;

export default BasicCreateSlotFill;

现在你可以在代码中使用你的自定义 SlotFill 了!第一步是暴露 Slot 属性。

const SettingsScreen = () => (
    <Panel>
        <PanelBody title="Basic" initialOpen={ false }>
            <PanelRow>
                <BasicCreateSlotFill.Slot />
            </PanelRow>
        </PanelBody>
    </Panel>
);

接下来,注册一个插件来定位该 Slot。

registerPlugin( 'custom-slot-fills', {
    render: () => (
        <BasicCreateSlotFill>
            <p>{ `This appears where <BasicCreateSlotFill.Slot/> is rendered` }</p>
        </BasicCreateSlotFill>
    ),
} );

还记得你在构建 BasicSlotFill 组件时将 children 包装在 Fill 组件中吗?children 是你在 registerPlugin 调用中包装在 BasicSlotFill 组件内的任何元素。在上面的例子中,p 标签及其文本内容是子元素,它们将渲染在 <BasicCreateSlotFill.Slot /> 所在的位置。

参考代码库

本文附带一个在 GitHub 上可用的代码仓库。它提供了上述两种用例的一些示例。请花点时间按照设置说明在你的计算机上运行示例。

内部示例:“免费增值”功能

在示例代码就位后,导航到 plugins/freemium-inc 文件夹。此文件夹包含此示例的所有代码,是一个使用 @wordpress/create-block 包搭建的标准 WordPress 插件,包含一个区块。

该区块本身除了输出默认消息外没有其他价值。插件中添加了两个新文件:

  • src/slotfills.js:用于存储我们的自定义 SlotFill
  • webpack.config.js:更新内置构建过程以创建一个包含我们高级功能的单独文件。

理论

这里的方法是在区块的 InspectorControls 中暴露一个自定义 SlotFill,然后有条件地加载一个单独的文件,该文件使用 registerPlugin 来添加更多控件(如果“高级”功能已解锁)。

实现

打开 src/slotfills.js 文件并创建一个名为 <PremiumFeatures /> 的自定义 SlotFill。

/**
 * WordPress dependencies
 */
import { createSlotFill } from '@wordpress/components';

/**
 * Create our Slot and Fill components
 */
const { Fill, Slot } = createSlotFill( 'PremiumFeatures' );

const PremiumFeatures = ( { children } ) => <Fill>{ children }</Fill>;

PremiumFeatures.Slot = ( { fillProps } ) => (
    <Slot fillProps={ fillProps }>
        { ( fills ) => {
            return fills.length ? fills : null;
        } }
    </Slot>
);

export default PremiumFeatures;

接下来,通过在 src/edit.js 中添加代码,在区块的 InspectorControls 中暴露 PremiumFeatures.Slot

export default function Edit( props ) {
    const {
        attributes: { makeItFaster },
        setAttributes,
    } = props;
    return (
        <>
            <InspectorControls>
                <PanelBody
                    title={ __( 'Freemium Inc. Settings', 'developer-blog' ) }
                >
                    <CheckboxControl
                        checked={ makeItFaster }
                        label={ __(
                            'Make my site a little faster',
                            'developer-blog'
                        ) }
                        onChange={ () =>
                            setAttributes( { makeItFaster: ! makeItFaster } )
                        }
                    />
                    <PremiumFeatures.Slot fillProps={ { ...props } } />
                </PanelBody>
            </InspectorControls>
            <p { ...useBlockProps() }>
                { __( 'Freemium INC Example', 'developer-blog' ) }
            </p>
        </>
    );
}

请注意,你正在通过 fillProps 属性将所有区块 props 传递给 Slot。这允许你的扩展访问区块可以访问的所有内容,例如其 attributessetAttributes 函数等。如果你的扩展需要更新区块属性或响应这些属性的变化,这一点非常重要。

接下来,创建一个名为 premium/index.js 的单独文件来保存插件的高级项目。

/**
 * WordPress dependencies
 */
import { registerPlugin } from '@wordpress/plugins';
import { __ } from '@wordpress/i18n';
import { CheckboxControl } from '@wordpress/components';
import { useState } from '@wordpress/element';

/**
 * Internal dependencies
 */
import PremiumFeatures from '../src/slotfills';

registerPlugin( 'freemium-inc-premium-items', {
    render: () => {
        return (
            <PremiumFeatures>
                { ( { attributes, setAttributes } ) => {
                    const { tenXMode } = attributes;
                    return (
                        <>
                            <h2>
                                { __( 'Premium Features', 'developer-blog' ) }
                            </h2>
                            <CheckboxControl
                                label={ __(
                                    '🔥🔥Enable 10x mode🔥🔥',
                                    'developer-blog'
                                ) }
                                help={ __(
                                    '10x mode will make your site 10x faster.',
                                    'developer-blog'
                                ) }
                                checked={ tenXMode }
                                onChange={ () =>
                                    setAttributes( { tenXMode: ! tenXMode } )
                                }
                            />
                        </>
                    );
                } }
            </PremiumFeatures>
        );
    },
} );

最后,更新构建过程以包含 premium/index.js 并输出一个我们可以单独加载的文件。由 @wordpress/scripts 包提供的构建过程可以通过在插件根目录添加 webpack.config.js 文件并扩展默认配置来扩展。

在这种情况下,你需要添加一个新的 entry 来告诉 Webpack 它应该处理一个新文件并单独输出它。

// Import the original config from the @wordpress/scripts package.
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );

// Import the helper to find and generate the entry points in the src directory
const { getWebpackEntryPoints } = require( '@wordpress/scripts/utils/config' );

// Add any a new entry point by extending the webpack config.
module.exports = {
    ...defaultConfig,
    entry: {
        ...getWebpackEntryPoints(),
        premium: './premium/index.js',
    },
};

新的 entry 告诉 Webpack 它应该在 ./premium/index.js 中查找一个新文件,并以 premium 作为基础文件名输出它(以及任何相关文件)。现在使用 npm run startnpm run build 重新启动构建过程,让 Webpack 识别配置文件的更改。

查看 build 目录,你应该会看到一些新添加的文件。添加的确切文件取决于你运行的构建命令。

  • premium.asset.php
  • premium.js
  • premium.js.map(仅在使用 start 命令时添加)

现在构建已更新并正常工作,最后一步是有条件地加载 premium.js 文件。这可能会变得非常复杂,但出于本文的目的,代码使用一个设置为 false 的变量。

/**
 * Determine if the plugin has been upgraded and enqueue the assets if so.
 */
function maybe_add_premium_features() {

    // This can be done any number of ways.
    $user_has_upgraded = false;

    $premium_assets_file = plugin_dir_path( __FILE__ ) . 'build/premium.asset.php';
    if ( $user_has_upgraded && file_exists( $premium_assets_file ) ) {
        $assets = include $premium_assets_file;
        wp_enqueue_script(
            'freemium-inc-premium',
            plugin_dir_url( __FILE__ ) . 'build/premium.js',
            $assets['dependencies'],
            $assets['version'],
            true
        );
    }
}
add_action( 'enqueue_block_editor_assets', 'maybe_add_premium_features' );

插入区块,然后在代码中将 $user_has_upgraded 设置为 true 以查看高级设置。

外部示例:扩展 Advanced Query Loop

创建外部示例需要一个暴露 SlotFill 以供使用的现有代码库。在本教程中,你将扩展我的 Advanced Query Loop (AQL) 插件。你可以在 GitHub 仓库中查看 AQL 的所有代码,包括 SlotFill 的文档。

理论

外部代码库对扩展者不可用,因此必须采取额外步骤来暴露 Slot 以供使用。幸运的是,这可以通过使用带有自定义 webpack.config.js 文件的 @wordpress/scripts 包来完成。

使用下面取自 AQL 的示例,你可以通过向 output 属性添加 library,创建一个附加到 window 对象的新 JavaScript 对象来存储 Slot(或任何需要的项目)。

// Import the original config from the @wordpress/scripts package.
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );

// Import the helper to find and generate the entry points in the src directory
const { getWebpackEntryPoints } = require( '@wordpress/scripts/utils/config' );

// Add any a new entry point by extending the webpack config.
module.exports = {
    ...defaultConfig,
    entry: {
        ...getWebpackEntryPoints(),
        variations: './src/variations/index.js',
    },
    output: {
        ...defaultConfig.output,
        library: [ 'aql' ],
    },
};

现在,从 /src/variations/index.js 导出的任何项目都将暴露在全局 aql 对象中。

/**
 * WordPress dependencies
 */
import { registerBlockVariation } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
/**
 * Internal dependencies
 */
import './controls';
import AQLIcon from '../components/icons';
import AQLControls from '../slots/aql-controls';
import AQLControlsInheritedQuery from '../slots/aql-controls-inherited-query';
const AQL = 'advanced-query-loop';

registerBlockVariation( 'core/query', {
    name: AQL,
    title: __( 'Advanced Query Loop', 'advanced-query-loop' ),
    description: __( 'Create advanced queries', 'advanced-query-loop' ),
    icon: AQLIcon,
    isActive: [ 'namespace' ],
    attributes: {
        namespace: AQL,
    },
    scope: [ 'inserter', 'transform' ],
} );

export { AQL, AQLControls, AQLControlsInheritedQuery };

实现

由于大部分工作是在外部代码库中完成的,你只需要像使用任何其他 Slot 一样导入和使用它们。

在示例仓库中,导航到 plugins/aql-extension 目录以查看此示例的所有代码。这又是一个简单的 WordPress 插件,带有一个自定义的 webpack.config.js,用于构建存储在 aql-extension/slotfills/index.js 中的单个文件,其中自定义 Fill 使用 registerPlugin 注册:

/**
 * WordPress dependencies
 */
const { AQLControls, AQLControlsInheritedQuery } = window.aql;
import { registerPlugin } from '@wordpress/plugins';
import { ToggleControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

const LoggedInUserControl = ( { attributes, setAttributes } ) => {
    const { query: { authorContent = false } = {} } = attributes;
    return (
        <>
            <ToggleControl
                label={ __( 'Show content for logged in user only' ) }
                checked={ authorContent === true }
                onChange={ () => {
                    setAttributes( {
                        query: {
                            ...attributes.query,
                            authorContent: ! authorContent,
                        },
                    } );
                } }
            />
        </>
    );
};

registerPlugin( 'aql-extension', {
    render: () => {
        return (
            <>
                <AQLControls>
                    { ( props ) => <LoggedInUserControl { ...props } /> }
                </AQLControls>
                <AQLControlsInheritedQuery>
                    { ( props ) => <LoggedInUserControl { ...props } /> }
                </AQLControlsInheritedQuery>
            </>
        );
    },
} );

Advanced Query Loop 插件暴露了两个 Slot:一个用于查询被继承时,另一个用于未被继承时。此代码将新控件添加到两者中。

从 SlotFill 的角度来看,这是一个完整的示例,你现在将在“高级查询设置”选项卡下获得一个新的控件,用于仅显示登录用户的文章。

此示例中有一些额外的 PHP 代码使其能与 AQL 一起工作。虽然解释该代码超出了本文的范围,但欢迎你探索它!