先决条件
阅读本文需要您至少要掌握 React 、WordPress、JavaScript、PHP的相关知识,同时您如果进行实践需要您有Node相关环境。
研究对象
相信大家看过这款精美的 WordPress 主题,出自大佬:“solstice23”之手。这款主题是开源的,目前有1.6k的 Start 。作者是在今年7月份对古腾堡进行适配。
Argon主题 github 开源地址:https://github.com/solstice23/argon-theme
古腾堡模块在gutenberg目录下,拿到手后要先看他的 package.json 用了什么,而不是上去直奔 JavaScript 。
所用依赖
"dependencies": {
"@wordpress/compose": "^4.2.0",
"@wordpress/data": "^5.2.0",
"cgb-scripts": "1.23.1",
"font-awesome": "^4.7.0",
"is-whitespace-character": "^2.0.0"
}
- @wordpress/components 它是wordpress的通用组件库,通过使用这个主题或查看的古腾堡模块目录我们知道作者做的是有折叠模块的,那折叠模块有设置,可以看到这里有个普通的 Switchbutton (WordPress中叫ToggleControl)。那这个切换按钮就是来自 @wordpress/components ,如果您用过 BootStrap 你就知道第一个说的是干嘛的了。
官方组件库的示例:https://wordpress.github.io/gutenberg/?path=/docs/components-togglecontrol--default
使用 ToggleControl 代码示例:
import { ToggleControl } from '@wordpress/components';
import { useState } from '@wordpress/element';
const MyToggleControl = () => {
const [ hasFixedBackground, setHasFixedBackground ] = useState( false );
return (
<ToggleControl
label="Fixed Background"
help={
hasFixedBackground
? 'Has fixed background.'
: 'No fixed background.'
}
checked={ hasFixedBackground }
onChange={ () => {
setHasFixedBackground( ( state ) => ! state );
} }
/>
);
};
2. @wordpress/data 它就是 WordPress 的数据管理模块,建立在 Redux 的基础上(因为 wordpress 后台用的有React), WordPress 用到了它,并且有自己的特性。它的作用和 Vuex 一样用来统一管理数据。
使用 @wordpress/data 代码示例 :
import apiFetch from '@wordpress/api-fetch';
import { createReduxStore, register } from '@wordpress/data';
const DEFAULT_STATE = {
prices: {},
discountPercent: 0,
};
const actions = {
setPrice( item, price ) {
return {
type: 'SET_PRICE',
item,
price,
};
},
startSale( discountPercent ) {
return {
type: 'START_SALE',
discountPercent,
};
},
fetchFromAPI( path ) {
return {
type: 'FETCH_FROM_API',
path,
};
},
};
const store = createReduxStore( 'my-shop', {
reducer( state = DEFAULT_STATE, action ) {
switch ( action.type ) {
case 'SET_PRICE':
return {
...state,
prices: {
...state.prices,
[ action.item ]: action.price,
},
};
case 'START_SALE':
return {
...state,
discountPercent: action.discountPercent,
};
}
return state;
},
actions,
selectors: {
getPrice( state, item ) {
const { prices, discountPercent } = state;
const price = prices[ item ];
return price * ( 1 - 0.01 * discountPercent );
},
},
controls: {
FETCH_FROM_API( action ) {
return apiFetch( { path: action.path } );
},
},
resolvers: {
*getPrice( item ) {
const path = '/wp/v2/prices/' + item;
const price = yield actions.fetchFromAPI( path );
return actions.setPrice( item, price );
},
},
} );
register( store );
3. cgb-scripts 这个是古腾堡块的开发工具包,Argon 主题作者用的就是它进行块开发,当然这个工具也是开源的。使用命令行进行创建:
npx create-guten-block my-block
然后你就能看到和 Argon 主题一样的古腾堡目录结构。
cgb-scripts github 开源地址: https://github.com/ahmadawais/create-guten-block
4. font-awesome 这个大家都不陌生了,开源的图标库。
5. is-whitespace-character 他用于检查字符是否为空白字符,同时它也是开源的。
is-whitespace-character github 开源地址: https://github.com/wooorm/is-whitespace-character#readme
那这个 Argon 主题作者在哪里用到了呢?打开他写的古腾堡警告组件 admonition 模块,看87行代码。
{!(isWhitespaceCharacter(props.attributes.fa_icon_name) || props.attributes.fa_icon_name == "") &&
<span>
<i className={`fa fa-${props.attributes.fa_icon_name}`}></i>
</span>
}
这段代码逻辑很简单,如果用户自定义的图标名称输入的是空格则不进行图标处理(第一个位置), 反之不成立说明用户输入的第一个位置不是空格,就取用户输入的(那如果第一位不是空格,但后面有,则不用处理)。
注意点
Argon 主题作者虽然在 package.json 引入了 font-awesome ,但它没有在古腾堡模块内使用,而是从 assets\vendor 取用。所用依赖部分除了 cgb-scripts ,其他为作者自己去手动安装的。
复刻目标
本文复刻 Argon 主题的古腾堡提示模块
准备工作
使用官方脚手架创建基础块目录,在主题目录下执行命令,一路回车就行。
npx @wordpress/create-block
安装好后自己在 npm 装下 font-awesome 、is-whitespace-character 。
在 src 下我们建立一个 alert 目录,在里面分别建立:index.js 、index.css 。
JavaScript复制过来要进行修改,css则不修改,不要直接拿来用!原作者没有完全按需引用,有些没有用到。
index.js
import './index.css';
import { __ } from '@wordpress/i18n';
import {
RichText,
InspectorControls} from "@wordpress/block-editor";
import {
ColorPalette,
TextControl,
PanelBody, PanelRow
} from '@wordpress/components';
import { isWhitespaceCharacter } from 'is-whitespace-character'
import { registerBlockType } from '@wordpress/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>{__('颜色', 'argon')}</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>{__('图标', 'argon')}</PanelRow>
<TextControl
value={props.attributes.fa_icon_name}
onChange={onIconChange}
/>
<p className="help-text">
{__('Font Awesome 中的图标名,留空则不显示', 'argon')}
<a href="https://fontawesome.com/v4.7.0/icons/" target="_blank">{__('浏览图标', 'argon')}</a>
</p>
</fieldset>
</div>
</PanelRow>
</PanelBody>
</InspectorControls>
</div>
);
},
save: (props) => {
return (
<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>
}
<span className="alert-inner--text" dangerouslySetInnerHTML={{ __html: props.attributes.content }}></span>
</div>
);
},
});
index.css
.alert {
font-size: 0.875rem;
padding: 1rem 1.5rem;
border: 0;
border-radius: 0.25rem;
}
.alert .alert-inner--icon {
font-size: 1.25rem;
display: inline-block;
margin-right: 1.25rem;
vertical-align: middle;
}
.alert .alert-inner--icon i.ni {
position: relative;
top: 1px;
}
.alert .alert-inner--text {
display: inline-block;
vertical-align: middle;
}
.alert:not(.alert-secondary) {
color: #fff;
}
[class*="alert-"] .alert-link {
color: #fff;
border-bottom: 1px dotted rgba(255, 255, 255, 0.5);
}
.alert-heading {
font-size: 1.5rem;
font-weight: 600;
margin-top: 0.15rem;
}
.alert-dismissible .close {
top: 50%;
right: 1.5rem;
padding: 0;
transform: translateY(-50%);
opacity: 1;
color: rgba(255, 255, 255, 0.6);
}
.alert-dismissible .close:focus,
.alert-dismissible .close:hover {
opacity: 1 !important;
color: rgba(255, 255, 255, 0.9);
}
@media (max-width: 575.98px) {
.alert-dismissible .close {
top: 1rem;
right: 0.5rem;
}
}
.alert-dismissible .close > span:not(.sr-only) {
font-size: 1.5rem;
color: rgba(255, 255, 255, 0.6);
background-color: transparent;
}
.alert-dismissible .close:focus > span:not(.sr-only),
.alert-dismissible .close:hover > span:not(.sr-only) {
color: rgba(255, 255, 255, 0.9);
background-color: transparent;
}
.alert {
position: relative;
margin-bottom: 1rem;
padding: 1rem 1.5rem;
border: 0.0625rem solid transparent;
border-radius: 0.25rem;
}
.alert-heading {
color: inherit;
}
.alert-link {
font-weight: 600;
}
.alert-dismissible {
padding-right: 4.5rem;
}
.alert-dismissible .close {
position: absolute;
top: 0;
right: 0;
padding: 1rem 1.5rem;
color: inherit;
}
.alert-primary {
color: #fff;
border-color: #7889e8;
background-color: #7889e8;
}
.alert-primary hr {
border-top-color: #6276e4;
}
.alert-primary .alert-link {
color: #324cdd;
}
.alert-secondary {
color: #212529;
border-color: #f6f7f8;
background-color: #f6f7f8;
}
.alert-secondary hr {
border-top-color: #e8eaed;
}
.alert-secondary .alert-link {
color: #d6dae2;
}
.alert-success {
color: #fff;
border-color: #4fd69c;
background-color: #4fd69c;
}
.alert-success hr {
border-top-color: #3ad190;
}
.alert-success .alert-link {
color: #24a46d;
}
.alert-info {
color: #fff;
border-color: #37d5f2;
background-color: #37d5f2;
}
.alert-info hr {
border-top-color: #1fd0f0;
}
.alert-info .alert-link {
color: #0da5c0;
}
.alert-warning {
color: #fff;
border-color: #fc7c5f;
background-color: #fc7c5f;
}
.alert-warning hr {
border-top-color: #fc6846;
}
.alert-warning .alert-link {
color: #fa3a0e;
}
.alert-danger {
color: #fff;
border-color: #f75676;
background-color: #f75676;
}
.alert-danger hr {
border-top-color: #f63e62;
}
.alert-danger .alert-link {
color: #ec0c38;
}
.alert-light {
color: #fff;
border-color: #bac1c8;
background-color: #bac1c8;
}
.alert-light hr {
border-top-color: #acb4bd;
}
.alert-light .alert-link {
color: #919ca6;
}
.alert-dark {
color: #fff;
border-color: #45484b;
background-color: #45484b;
}
.alert-dark hr {
border-top-color: #393b3e;
}
.alert-dark .alert-link {
color: #0a0c0d;
}
.alert-default {
color: #fff;
border-color: #3c4d69;
background-color: #3c4d69;
}
.alert-default hr {
border-top-color: #334159;
}
.alert-default .alert-link {
color: #0b1526;
}
.alert-white {
color: #212529;
border-color: #fff;
background-color: #fff;
}
.alert-white hr {
border-top-color: #f2f2f2;
}
.alert-white .alert-link {
color: #e6e6e6;
}
.alert-neutral {
color: #212529;
border-color: #fff;
background-color: #fff;
}
.alert-neutral hr {
border-top-color: #f2f2f2;
}
.alert-neutral .alert-link {
color: #e6e6e6;
}
.alert-darker {
color: #fff;
border-color: #292929;
background-color: #292929;
}
.alert-darker hr {
border-top-color: #1c1c1c;
}
.alert-darker .alert-link {
color: #000;
}
.help-text{
margin-top: -8px !important;
font-size: 12px;
font-style: normal;
color: rgb(117, 117, 117);
}
functions.php
<?php
function init_gutenberg_blocks() {
wp_register_script(
'demo-gutenberg-block-js',
get_template_directory_uri().'/esnext-example/build/index.js',
array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-editor'),
null,
true
);
wp_register_style(
'demo-gutenberg-block-backend-css',
get_template_directory_uri().'/esnext-example/build/index.css',
array('wp-edit-blocks'),
get_template_directory() . '/esnext-example/build/style-index.css'
);
register_block_type(
'esnext-example/demo-gutenberg-block', array(
'editor_script' => 'demo-gutenberg-block-js',
'editor_style' => 'demo-gutenberg-block-backend-css',
));
}
add_action('init', 'init_gutenberg_blocks');
这里引入都是打包后的模块,不要急,后面我就说打包。
我这种做法是手动建立,跳过了官方说的插件激活那种步骤。最后使用 register_block_type 注册块,也就是说我的资源只在后台加载,前端只需要放有对应块样式就能进行显示。
准备工作好了后,我们找到 src 下的 index 引入我们写的警告模块。
import 'font-awesome/css/font-awesome.min.css'
import './alert'
/**
* Registers a new block provided a unique name and an object defining its behavior.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
*/
import { registerBlockType } from '@wordpress/blocks';
/**
* Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files.
* All files containing `style` keyword are bundled together. The code used
* gets applied both to the front of your site and to the editor.
*
* @see https://www.npmjs.com/package/@wordpress/scripts#using-css
*/
import './style.scss';
/**
* Internal dependencies
*/
import Edit from './edit';
import save from './save';
/**
* Every block starts by registering a new block type definition.
*
* @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/
*/
registerBlockType( 'create-block/esnext-example', {
title: '测试一下',
testValue: null,
/**
* @see ./edit.js
*/
edit: Edit,
/**
* @see ./save.js
*/
save,
} );
在脚手架生成的文件下执行打包(我生成的目录名叫esnext-example就在这个目录下执行)。
打包开发环境:
npm run build
打包生产环境
npm run format
其他命令您可以参照这个即可:https://www.npmjs.com/package/@wordpress/create-block
如果您是IDEA、PHPStorm即 Jetbrains 系列编辑器直接点击这个也能打包。
这里就是打包后输出的目录