WordPress Gutenberg 编辑器区块开发笔记

前言

自 WordPress 5.0 版本开始,Gutenberg 编辑器成为了 WordPress 的默认编辑器,代替了之前的 Tinymce。新的编辑器将一切区块化,可以快速地以可视化的形式编辑区块,可以取代大部分短代码,并使插入相关内容的体验提升了很多。

最近给 Argon Theme 加上了 Gutenberg 编辑器支持,因为网上关于这一块的中文文档很少,所以在这篇博文记录一些需要注意的地方。

本文以 Argon 主题为例。

开始

Gutenberg 编辑器使用了 React 来实现,在 wp.block 中暴露了一些接口。Github 上已经有了相关的轮子 create-gutenberg-block,不妨使用这个项目搭建脚手架。

npx create-guten-block project-name

create-gutenberg-block 生成项目时是作为插件生成的,将其放在 plugin 文件夹下可以直接作为插件激活。

如果要将其作为主题的一部分,可以删除 plugin.php,并将 src/init.php 中的内容写入主题的 function.php 中,注意路径变动后函数中的路径要修改。

该文件中在博客前台和后台都注册了 style 和 script,可以视情况移除,只保留后台的,而将前台的样式合并进主题的样式,避免文件冗余。同样的,该文件 register_block_type 函数中,也可以将添加到前台的 style 移除。

function.php 中相关代码的例子:

function argon_init_gutenberg_blocks() {
	wp_register_script( //注册编辑器 JS
		'argon-gutenberg-block-js',
		get_bloginfo('template_url') . '/gutenberg/dist/blocks.build.js',
		array('wp-blocks', 'wp-i18n', 'wp-element', 'wp-editor'),
		null,
		true
	);
	wp_register_style( //注册编辑器 CSS
		'argon-gutenberg-block-backend-css',
		get_bloginfo('template_url') . '/gutenberg/dist/blocks.editor.build.css',
		array('wp-edit-blocks'),
		filemtime(get_template_directory() . '/gutenberg/dist/blocks.editor.build.css')
	);
	register_block_type( //注册区块
		'argon/argon-gutenberg-block', array(
			'editor_script' => 'argon-gutenberg-block-js',
			'editor_style'  => 'argon-gutenberg-block-backend-css',
		)
	);
}
add_action('init', 'argon_init_gutenberg_blocks');

这个例子中,Node 项目位于 gutenberg 目录下,构建生成的 js 和 css 位于 gutenberg/dist 目录下。

注册区块分类

在 Argon 主题中,所有 Argon 主题注册的区块都被分到了 Argon 分类中。Wordpress 不会自动创建相关的分类,需要预先注册而不是直接在 js 中设置。Wordpress 采用 block_categories_all Filter 来添加自定义分类[1]

Argon 注册分类的相关代码:

function argon_add_gutenberg_category($block_categories, $editor_context) {
	if (!empty($editor_context->post)){
		array_push(
			$block_categories,
			array(
				'slug'  => 'argon', //分类唯一标识
				'title' => 'Argon', //分类名词
				'icon'  => null, //分类图标 (似乎并没有显示图标的地方)
			)
		);
	}
	return $block_categories;
}
add_filter('block_categories_all', 'argon_add_gutenberg_category', 10, 2);

添加后,Gutenberg 编辑器中出现了 Argon 分类。

注册区块

运行 npm start,文件的修改会被自动监听并实时构建。

进入 src 目录,文件结构如下:

src
│  blocks.js //在此文件中引用汇总区块
│  common.scss //全局 CSS
│  init.php //作为插件时的初始化 PHP,作为主题可以删除了
│
└─block //一个示例区块
        block.js //该区块的 JS
        editor.scss //区块在编辑器中的 SCSS
        style.scss //区块在前台的 SCSS,作为主题可以删除了

示例区块只有简单的固定 HTML,可以参考但意义不大,可以删除了。可以看到,该区块主要从 wp.blocks 中引入了 registerBlockType 函数,并调用其进行注册。

下面以 Argon 中的 alert 区块为例。

Alert 区块编辑界面
Alert 区块设置

新建一个目录 src/alert,新建 src/alert/alert.jssrc/alert/editor.scss

src/blocks.js 中引用 alert.js

import './alert/alert.js';

alert.js 中,先引入必要的组件库和 CSS:

import './editor.scss'; //编辑器中的 CSS
import {
	RichText,
	InspectorControls
} from "@wordpress/block-editor"; //引入编辑器组件
import {
	ColorPalette,
	TextControl,
	Panel, PanelBody, PanelRow
} from '@wordpress/components'; //引入 UI 组件

const { registerBlockType } = wp.blocks; //从 wp.block 引入 registerBlockType 函数

@wordpress/block-editor 是 Gutenberg 编辑器的组件[2],用来实现富文本编辑等; @wordpress/components 是编辑器中的 UI 组件[3],用来实现右侧栏的颜色选择器、输入框、开关等。

wp.block 是编辑器注册的全局对象,其中的 registerBlockType 函数用来注册区块,下面介绍该函数。

registerBlockType

这个函数是注册区块的主要函数,传入两个参数,第一个参数是区块的唯一标识符,例如 argon/alert。推荐以这样的形式进行命名,同一个主题/插件中,斜杠前面的字符串保持一致,后面的用来区分不同区块。第二个参数是一个对象,用来声明该区块的信息。

下面对第二个参数中的一些参数进行解释,具体详尽的解释可以参考官方文档[4]

title

字符串,必须,该区块的标题,例子中为 argon

description

字符串,可选,该区块的简短介绍

icon

字符串,可选,该区块的图标类名,图标为 Dash Icon 中的图标,例子中为 warning

category

字符串,必须,该区块的分类,在例子中是前面注册的 argon 分类。

keywords

数组,可选,每一项是一个字符串,代表区块的别名,可以用这个数组中的字符串搜索到这个区块。例子中是 ['argon','提示']

attributes

对象,可选,每一项也是一个对象,下面是例子中的该参数:

attributes: {
	color: { //提示的颜色
		type: 'string',
		default: '#7889e8'
	},
	content: { //提示的内容
		type: 'string',
		default: ''
	},
	fa_icon_name: { //提示的图标的类名
		type: 'string',
		default: 'info-circle'
	},
}

每一项对应一个 attribute,attribute 在 WordPress 中的作用是持久存储区块的信息,也可以用来配合做区块的状态管理。其会在保存博文的时候一起被保存,并在下次编辑的时候恢复。

虽然是可选项,但区块有需要用户编辑并保存的信息,就需要将其存储进 attributes 中,所以基本上是必要的。

名称是否必须解释
type必须该属性的类型,有 stringboolintergernumberarrayobject
default可选该属性的默认值
对每一个 attribute 中参数的解释

一般只用到这两个参数。

attribute 还支持自动从给出的选择器中提取值,这样就免去了状态管理的繁琐。但 Argon 中没有使用这一种方式,因为手动管理状态可以在中途对数据进行一些处理和过滤,而自动提取值无法做到。可以参考官方文档

edit

函数,必须。是最主要的函数。区块实际上类似一个 React 组件,这个函数传入一个 prop 参数,返回编辑器中该区块的 JSX。返回的 JSX 会实时渲染在编辑器中。前文中的 attributes 通过 props.attributes 传入。同时,可以通过传入的 props.setAttributes 函数来设置 attributes,这样就实现了数据的双向同步和状态管理。

save

函数,必须。和 edit 相似,save 返回保存时的 JSX,JSX 在渲染后会显示在博客的文章前台,即最终要显示在文章中的 HTML。

例子

下面是一个 edit 函数的例子:

edit: (props) => {
	const onChangeContent = (value) => {
		props.setAttributes({ content: value });
	};

	return (
		<div>
			<div className="alert">
				<RichText
					tagName="span"
					className="alert-inner--text"
					placeholder="内容"
					value={props.attributes.content}
					onChange={onChangeContent}
				/>
			</div>
		</div>
	);
}

该例子经过了简化,省略了区块设置相关的部分。

可以看到,区块和 React 组件如出一辙。

开头的 onChangeContent 函数,在内容被改变的时候 (第 13 行) 会被调用,以更新 attribute

这里调用了 Gutenberg 编辑器的 RichText 组件,可以实现富文本编辑。tagName 参数使其作为 span 被编辑。

例子 2

下面是未简化的例子

edit: (props) => {
	const onChangeContent = (value) => {
		props.setAttributes({ content: value });
	};
	const onChangeColor = (value) => {
		props.setAttributes({ color: (value || "#7889e8") });
	}
	const onIconChange = (value) => {
		props.setAttributes({ fa_icon_name: value });
	}

	return (
		<div>
			<div className="alert" style={{ backgroundColor: props.attributes.color }}>
				{props.attributes.fa_icon_name != "" &&
					<span className="alert-inner--icon">
						<i className={`fa fa-${props.attributes.fa_icon_name}`}></i>
					</span>
				}
				<RichText
					tagName="span"
					className="alert-inner--text"
					placeholder="内容"
					value={props.attributes.content}
					onChange={onChangeContent}
				/>
			</div>
			<InspectorControls key="setting">
				<PanelBody title="区块设置" icon={"more"} initialOpen={true}>
					<PanelRow>
						<fieldset>
							<PanelRow>颜色</PanelRow>
							<ColorPalette
								onChange={onChangeColor}
								colors={[
									{ name: 'argon', color: '#7889e8' },
									{ name: 'green', color: '#4fd69c' },
									{ name: 'red', color: '#f75676' },
									{ name: 'blue', color: '#37d5f2' },
									{ name: 'orange', color: '#fc7c5f' },
									{ name: 'pink', color: '#fa7298' },
									{ name: 'black', color: '#3c4d69' },
								]}
								value={props.attributes.color}
							/>
						</fieldset>
						<fieldset>
							<PanelRow>图标</PanelRow>
							<TextControl
								value={props.attributes.fa_icon_name}
								onChange={onIconChange}
							/>
							<p className="help-text">
								Font Awesome 中的图标名,留空则不显示&nbsp;
								<a href="https://fontawesome.com/v4.7.0/icons/" target="_blank">浏览图标</a>
							</p>
						</fieldset>
					</PanelRow>
				</PanelBody>
			</InspectorControls>
		</div>
	);
}

实际上并没有复杂多少。InspectorControls 是编辑器的 UI 组件,会出现在右侧的区块设置栏。PanelBodyPanelRow、前者是一个可折叠块,后者是一个容器,使设置项有 padding,和其他 UI 统一。

ColorPalette 是 WordPress 的颜色选择器组件,TextControl 是输入框组件。这些组件都是我们之前从 @wordpress/components 中导入的,省去了不少手写组件的繁琐。

同样地,选项面板中的修改也被 onChangeColoronIconChange 函数同步到了 attributes 并同步到了区块的 UI 中。

完整的例子

下面是整个 alert.js 文件:

import './editor.scss';
import {
	RichText,
	InspectorControls,
} from "@wordpress/block-editor";
import {
	ColorPalette,
	TextControl,
	Panel, PanelBody, PanelRow
} from '@wordpress/components';

const { registerBlockType } = wp.blocks;

registerBlockType('argon/alert', {
	title: '提示',
	icon: 'warning',
	category: 'argon',
	keywords: [
		'argon',
		'提示'
	],
	attributes: {
		color: {
			type: 'string',
			default: '#7889e8'
		},
		content: {
			type: 'string',
			default: ''
		},
		fa_icon_name: {
			type: 'string',
			default: 'info-circle'
		},
	},
	edit: (props) => {
		const onChangeContent = (value) => {
			props.setAttributes({ content: value });
		};
		const onChangeColor = (value) => {
			props.setAttributes({ color: (value || "#7889e8") });
		}
		const onIconChange = (value) => {
			props.setAttributes({ fa_icon_name: value });
		}

		return (
			<div>
				<div className="alert" style={{ backgroundColor: props.attributes.color }}>
					{!(isWhitespaceCharacter(props.attributes.fa_icon_name) || props.attributes.fa_icon_name == "") &&
						<span className="alert-inner--icon">
							<i className={`fa fa-${props.attributes.fa_icon_name}`}></i>
						</span>
					}
					<RichText
						tagName="span"
						className="alert-inner--text"
						placeholder="内容"
						value={props.attributes.content}
						onChange={onChangeContent}
					/>
				</div>
				<InspectorControls key="setting">
					<PanelBody title="区块设置" icon={"more"} initialOpen={true}>
						<PanelRow>
							<div id="gutenpride-controls">
								<fieldset>
									<PanelRow>颜色</PanelRow>
									<ColorPalette
										onChange={onChangeColor}
										colors={[
											{ name: 'argon', color: '#7889e8' },
											{ name: 'green', color: '#4fd69c' },
											{ name: 'red', color: '#f75676' },
											{ name: 'blue', color: '#37d5f2' },
											{ name: 'orange', color: '#fc7c5f' },
											{ name: 'pink', color: '#fa7298' },
											{ name: 'black', color: '#3c4d69' },
										]}
										value={props.attributes.color}
									/>
								</fieldset>
								<fieldset>
									<PanelRow>图标</PanelRow>
									<TextControl
										value={props.attributes.fa_icon_name}
										onChange={onIconChange}
									/>
									<p className="help-text">
										Font Awesome 中的图标名,留空则不显示&nbsp;
										<a href="https://fontawesome.com/v4.7.0/icons/" target="_blank">浏览图标</a>
									</p>
								</fieldset>
							</div>
						</PanelRow>
					</PanelBody>
				</InspectorControls>
			</div>
		);
	},
	save: (props) => {
		return (
			<div className="alert" style={{ backgroundColor: props.attributes.color }}>
				{props.attributes.fa_icon_name != "" &&
					<span className="alert-inner--icon">
						<i className={`fa fa-${props.attributes.fa_icon_name}`}></i>
					</span>
				}
				<span className="alert-inner--text" dangerouslySetInnerHTML={{ __html: props.attributes.content }}></span>
			</div>
		);
	},
});

save 函数中,因为不需要动态编辑,所以 RichText 组件被普通的 span 取代,onChange 监听回调也不需要了。

需要注意的是,设置区块内容需要使用 dangerouslySetInnerHTML 来设置,否则编辑器中的富文本会被展示为 HTML 源码。因为所有字符串都是在后台编辑器中产生,所以可以信任。

CSS

editor.scss 中写好样式即可。这个过程应该是伴随着写 edit 函数的。

End

npm run build 进行构建,至此,一个编辑器区块已经开发完成。

Alert 区块效果

Extra

注册不同的样式变体

编辑一些区块时可以在左上角看到,一些区块有不止一个样式,例如表格有 “默认” 和 “条纹” 两种。要注册不同的样式,只需要在 registerBlockType 的第二个参数中传入 styles 数组,每一项是一个对象,有如下常用参数:

名称是否必需解释
name必需字符串,该变体的唯一标识符,也是该变体的 CSS 类名
label必需字符串,该变体的显示名称
isDefault可选布尔值,该变体是否作为默认选择

需要注意的是 isDefault 参数,如果所有数组项都没有它,那么将会出现一个不在变体列表中的 “默认样式”。如果设置了此项,这个 “默认样式” 即不会出现。

当切换样式变体时,实际上是切换了这个区块的额外 CSS 类名。在右侧栏中的区块高级设置中可以看见。所以给不同样式变体写 CSS 即对每个类名写 CSS。类名也会传到 props.attributes.classNameprops.className 中,可以在组件中进行处理。

很迷的是,即使你设置了 isDafault 为 true,新建区块时,Wordpress 却不会默认设置上这个变体的类名,所以需要进行手动判断。

在区块中维护可变长度列表

和在 React 组件中维护可变长度列表一样,将 attribute 的类型设为数组,对这个数组进行操作即可。

作者: solstice23
本文采用 CC BY-NC-SA 4.0 协议

评论

  1. DrBlack
    Windows Chrome
    4周前
    2021-8-29 18:39:10

    tqlwww贴贴

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: Telegram @AmashiroNatsukiEars_NoWord Sticker
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
AmashiroNatsukiEars
小恐龙
花!
上一篇