使用自定义 SlotFill 扩展插件

WordPress 最强大的特性之一,是几乎可以通过扩展实现任何事情。自 2.0 版起,开发者就使用 Hooks API 扩展 WordPress 核心。5.0 版又引入 SlotFill 系统,让开发者能够扩展区块编辑器和站点编辑器界面。

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

SlotFill 系统不仅能扩展现有界面,还能扩展自定义实现,这正是本文的重点。

使用场景

使用它可能有很多理由。最突出的例子来自插件生态系统,这类扩展已经为开发者带来实际收入。

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

内部使用的例子是“免费增值”定价模式:提供包含基本功能的插件,用户购买许可证后,便可解锁更高级的功能。

对于外部扩展,你需要设置一组扩展点,让其他开发者为基础插件增加功能。

如果仅使用 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 函数与 https://wordpress.org/plugins/ 上的 WordPress 插件无关。它用来注册包含 Fill 的 JavaScript 插件。详情请参阅 如何通过 SlotFill 系统扩展 WordPress。

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

还记得构建 BasicSlotFill 组件时,用 Fill 组件包装 children 吗?你在 BasicSlotFill 组件中包装的任何元素都是 children,这个包装发生在 registerPlugin 调用中。上例中的 p 标签及其文本就是子元素,会渲染在 <BasicCreateSlotFill.Slot /> 所在的位置。

参考代码库

本文的配套代码库可在 GitHub 获取,其中提供了上面两种使用场景的示例。请先按照设置说明,让示例在自己的计算机上运行。

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

准备好示例代码后,进入 plugins/freemium-inc 文件夹。这里包含本例的全部代码,是一个标准的 WordPress 插件,内含通过 @wordpress/create-block 包生成的单一区块。

区块本身除了输出默认消息,并没有实际功能。插件中新增了两个文件:

  • 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;

接下来,将 PremiumFeatures.Slot 添加到 src/edit.js,在区块的 InspectorControls 中公开它。

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。这样,扩展就能访问区块能够访问的所有内容,例如 attributes、setAttributes 函数等。如果扩展需要更新区块属性或响应属性变化,这一点非常重要。

接下来,为插件的高级功能创建一个单独的文件,命名为 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 文件,并扩展默认配置。

《webpack 与 WordPress 软件包如何交互》相关文章链接卡片,来源 WordPress Developer Blog

本例需要添加一个新的 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 start 或 npm 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。幸运的是,使用 @wordpress/scripts 包和自定义 webpack.config.js 文件即可做到。

下例来自 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 };




浏览器控制台显示 window.aql 对象的属性
aql 对象可以通过 window 对象访问。

实现

由于大部分工作已在外部代码库中完成,你只需像使用其他组件一样导入并使用 Slot。

在示例代码库中,进入 plugins/aql-extension 目录查看本例的全部代码。这同样是一个简单的 WordPress 插件,使用自定义 webpack.config.js 将 aql-extension/slotfills/index.js 构建为单独文件,并使用 registerPlugin 注册自定义 Fill:

/**
 * 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 配合工作。这些代码的解释超出本文范围,欢迎自行探索!

Screenshots

Advanced Query Loop 标准控件界面
AQL 标准界面
Advanced Query Loop 标准控件与新增扩展控件
已启用扩展的 AQL

感谢 @greenshady、@marybaum 和 @juanmaguitar 审阅本文。

原文:Extending plugins using custom SlotFills。作者 Ryan Welcher,2023年12月22日,WordPress Developer Blog。本文依据用户明确授权制作中文翻译;未确认单独公开内容许可证,不将软件许可证视为正文及图片许可。正文、标题、图注和替代文本已汉化,代码保持原文。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容