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 文件,并扩展默认配置。
本例需要添加一个新的 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.phppremium.jspremium.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 };

实现
由于大部分工作已在外部代码库中完成,你只需像使用其他组件一样导入并使用 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


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











暂无评论内容