【WordPress】カスタムブロックの作り方を書いてみた

## はじめに
WordPress 5.0からGutenbergと呼ばれるブロックエディターがデフォルトのエディターとして採用されました。
そのため、今後はこのブロックエディターによる開発が増えてくると思われます。

個人的には、今までのWysiWygエディターに不満を感じていたわけではありませんが、ブロックエディターという選択肢が増えたことで、**できる事の幅が増えてくる** と思います。
本記事では、このブロックエディターの新規ブロック（以降、カスタムブロック）の作り方を記載して行きます。

## ブロックエディターとは？
ブロックエディターは、名前の通り、HTMLをブロックように積み上げてHTMLを作成していくエディターです。

<img src="https://wordpress.org/gutenberg/files/2018/07/Insert-Block-2-1.gif" width="400">

詳細は、以下を参照ください。

https://ja.wordpress.org/gutenberg/

ブロックエディターの特徴の1つとして、上記リンク先のページにも記載がありますが、 **実際のサイトと同様に表示されるエディター。** であることです。
これから記載するカスタムブロックも、**実際のサイトと同様に表示される**よう作成して行きます。

また、WordPressのブロックエディターは、大きく分けて以下の2種類のカスタムブロックを作成できます。

- 動的ブロック ・・・ 最新の投稿を表示するブロック等の動的にブロックの内容が変化するブロック
- 静的ブロック ・・・ 動的とは反対で、画像アップロードやテキストフィールド等のブロックの内容が変化しないブロック

これから記載するカスタムブロックは**静的ブロック**を作成して行きます。（動的ブロックは別途記載予定）

## 作成するカスタムブロック
以下のようなテキストが書けて、背景色をサイドナビゲーションから選択できるカスタムブロックを作成して行きます。
サンプルブロックは、エディターと画面表示（以降、フロント）にCSSが適応されるブロックと、エディターのみCSSが適応されるブロックの2種類作成します。

![sample-block-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241956/8aaeca37-a384-6a7d-e2fa-4abbc1240d7f.png)

## 環境準備
以下がインストールされている事が前提です。  

- PHP: 7.1以上
- Node: v10.x
- Yarn: v1.9.4

WordPressのブロックエディターはReactで作成されています。
これから作成するカスタムブロックも、ReactのComponentを使って開発して行きます。
しかし、通常のReactを使用するのではなく、WordPress側で用意されているReactを使用します。そのため、Reactをインストールする必要はありません。

以降、ライブラリのインストールです。

### [必須]package.jsonを用意する
以下のような`package.json`を作成します。

```json
{
  "name": "sample-block",
  "version": "1.0.0",
  "description": "Sample Block",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Kodak",
  "license": "GPL-2.0"
}
```

### [任意]webpackを用意する
こちらは任意です。
JavaScriptをwebpackを通してビルドしたい方は、以下をインストールしてください。
今回は、後述する`@wordpress/scripts`を使ってビルドをするので、インストールしません。

```
yarn add -D webpack webpack-cli
```

### [必須]BABELを用意する
以下をインストールします。
カスタムブロックはJSXを使いたいので`@babel/preset-react`もインストールします。

```
yarn add -D @babel/core babel-loader @babel/preset-env @babel/preset-react
```

- `@babel/core` ・・・ BABEL本体
- `babel-loader` ・・・ webpackからBABELを通すためのライブラリ
- `@babel/preset-env` ・・・ 特定の環境（ブラウザ）に合わせた変換をしてくれるライブラリ
- `@babel/preset-react` ・・・ JSXのコンパイル可能にするライブラリ

### [任意]SASSに対応させるためのライブラリを用意する
こちらは任意です。
今回はSASSに対応させたいため、以下をインストールします。

```
yarn add -D node-sass css-loader sass-loader
```

- `node-sass` ・・・ SASS(SCSS)をCSSに変換するライブラリ
- `sass-loader` ・・・ webpackからSASS(SCSS)をCSSに変換するライブラリ
- `css-loader` ・・・ CSSをJavaScriptにバンドルするライブラリ


### [任意]CSSを拡張するためのライブラリを用意する
こちらは任意ですが、インストールしておいたほうが良いです。
ベンダープレフィックスやCSSを別ファイルとして出力してくれます。

```
yarn add -D postcss-loader autoprefixer mini-css-extract-plugin optimize-css-assets-webpack-plugin terser-webpack-plugin
```

- `postcss-loader` ・・・ ベンダープレフィックスをCSSに追加するため＆ネストしたCSSを解析してくれるライブラリ
- `autoprefixer` ・・・ ベンダープレフィックスをCSSに追加（`postcss-loader`のプラグイン）
- `mini-css-extract-plugin` ・・・ CSSを別ファイルとして出力するためのライブラリ
- `optimize-css-assets-webpack-plugin` ・・・ 作成するCSSを圧縮するためのライブラリ
- `terser-webpack-plugin` ・・・ CSSを圧縮する際、さらにCSSを縮小してくれる（邪魔なコメントやscourceMapを削除してくれる）ライブラリ


### [必須]ビルドフォルダーにゴミファイルを残さないためのライブラリを用意する
ビルドフォルダーにゴミファイルを残さないようにするため、これはインストールしておきましょう。

```
yarn add -D clean-webpack-plugin
```

- `clean-webpack-plugin` ・・・ ビルドする度に、ビルドフォルダーを削除するライブラリ


### [必須]カスタムブロックを作るライブラリを用意する
カスタムブロックを作成するためのライブラリです。

```
yarn add -D @wordpress/browserslist-config @wordpress/scripts @wordpress/blocks @wordpress/dependency-extraction-webpack-plugin
```

- `@wordpress/browserslist-config` ・・・ WordPressのブロックエディターが動くブラウザリストのライブラリ
- `@wordpress/scripts` ・・・ WordPressのブロックエディター用のwebpackが詰め込まれたライブラリ。これを使用しない場合は、webpackを別途インストールする必要がある。
- `@wordpress/blocks` ・・・ WordPressでカスタムブロックを作るためのライブラリ。
- `@wordpress/dependency-extraction-webpack-plugin` ・・・ ライブラリの依存関係を自動で解決するassets.phpファイルを生成してくれるライブラリ

### [任意]作成するカスタムブロックに合わせてライブラリを用意する
作りたいカスタムブロックに合わせて、ライブラリをインストールします。
今回は、`@wordpress/block-editor`と`@wordpress/components`を使用します。

```
yarn add -D @wordpress/block-editor @wordpress/components
```

各ライブラリの詳細は、公式サイトを参照してください。

https://developer.wordpress.org/block-editor/packages/

## 事前準備

<details><summary>package.json（折りたたんでいます）</summary><div>

```json
{
  "name": "sample-block",
  "version": "1.0.0",
  "description": "Sample Block",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "wp-scripts build --mode=development --config webpack.config.js --watch",
    "build": "wp-scripts build --mode=production --config webpack.config.js"
  },
  "author": "Kodak",
  "license": "GPL-2.0",
  "devDependencies": {
    "@babel/core": "^7.8.7",
    "@babel/preset-env": "^7.8.7",
    "@babel/preset-react": "^7.8.3",
    "@wordpress/block-editor": "^3.7.5",
    "@wordpress/blocks": "^6.12.1",
    "@wordpress/browserslist-config": "^2.6.0",
    "@wordpress/components": "^9.2.4",
    "@wordpress/dependency-extraction-webpack-plugin": "^2.4.0",
    "@wordpress/scripts": "^7.1.3",
    "autoprefixer": "^9.7.4",
    "babel-loader": "^8.0.6",
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^3.4.2",
    "mini-css-extract-plugin": "0.6.0",
    "node-sass": "^4.13.1",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "postcss-loader": "^3.0.0",
    "sass-loader": "^8.0.2",
    "terser-webpack-plugin": "^2.3.5"
  },
  "browserslist": [
    "extends @wordpress/browserslist-config"
  ]
}
```

</div></details>

WordPress用のwebpack（wp-scripts）を使用して、ビルドをするため、以下のように定義します。

```json
  "scripts": {
    "start": "wp-scripts build --mode=development --config webpack.config.js --watch",
    "build": "wp-scripts build --mode=production --config webpack.config.js"
  },
```

ブラウザリストはextendsを使用して読み込みます。

```json
  "browserslist": [
    "extends @wordpress/browserslist-config"
  ]
```

注）[mini-css-extract-pluginは、chunkFilenameを使用しているとエラーが出たのでバージョンを0.6.0にしています。](https://github.com/webpack-contrib/mini-css-extract-plugin/issues/417)

<details><summary>webpack.config.js（折りたたんでいます）</summary><div>

```js
const autoprefixer                      = require('autoprefixer');
const MiniCSSExtractPlugin              = require('mini-css-extract-plugin');
const OptimizeCSSAssetsWebpackPlugin    = require('optimize-css-assets-webpack-plugin');
const TerserWebpackPlugin               = require('terser-webpack-plugin');
const { CleanWebpackPlugin }            = require('clean-webpack-plugin');
const DependencyExtractionWebpackPlugin = require('@wordpress/dependency-extraction-webpack-plugin');

module.exports = (env, argv) => {
				// mode('development'と'production')によって動作を切り替える関数
        function isDevelopment() {
            return argv.mode === 'development'
        }

        var config = {
        entry: {
						editor: './src/editor.js',
						script: './src/script.js',
        },
        output: {
            filename: "[name].js"
        },
        optimization: {
            minimizer: [
								// sourceMapをmodeによって削除する
                new TerserWebpackPlugin({
                    sourceMap: isDevelopment()
								}),
								// CSSを圧縮する
                new OptimizeCSSAssetsWebpackPlugin(
                    {
                        cssProcessorOptions: {
                            map: {
                                inline: false,
                                annotation: true
                            }
                        }
                    }
                )
            ]
        },
        plugins: [
						// assets.phpファイルを出力する
						new DependencyExtractionWebpackPlugin(),
						// ビルドの度にビルドフォルダーを削除する
						new CleanWebpackPlugin(),
						// CSSファイルを別ファイルで出力する
            new MiniCSSExtractPlugin({
							chunkFilename: "[id].css",
							filename: chunkData => {
									return chunkData.chunk.name === "script" ? 'style.css' : '[name].css';
							}
						})
				],
				// source-mapを使用する
        devtool: 'source-map',
        module: {
            rules: [
                {
										// JavaScriptのローダーの設定
                    test: /\.js$/,
                    exclude: /node_modules/,
                    use: {
												// BABELを使用する
                        loader: 'babel-loader',
                        options: {
                            presets: [
                                '@babel/preset-env',
                                [
                                    '@babel/preset-react',
                                    {
																				// Reactで使用する関数をBABELをトランスパイルします
																				// 通常は"React.createElement.xxxx"を記述しますが、WordPressのブロックエディターはWordPress用のReactを使用するため、以下のように記述します
                                        "pragma": "wp.element.createElement",
                                        "pragmaFrag": "wp.element.Fragment",
                                        "development": isDevelopment()
                                    }
                                ]
                            ]
                        }
                    }
                },
                {
										// SASSのローダーの設定
                    test: /\.(sa|sc|c)ss$/,
                    use: [
												// CSSを別ファイル化するためのローダーを指定
                        MiniCSSExtractPlugin.loader,
                        'css-loader',
                        {
														// postcss-loaderのプラグインを使って、ベンダープレフィックスをCSSに追加
                            loader: 'postcss-loader',
                            options: {
                                plugins: [
                                    autoprefixer()
                                ]
                            }
                        },
                        'sass-loader'
                    ]
                }
            ]
        },
    };
    return config;
}
```
</div></details>


Reactで使用する関数をBABELをトランスパイルします。
通常は"React.createElement.xxxx"を記述しますが、WordPressのブロックエディターはWordPress用のReactを使用するため、以下のように記述します。

```json
'@babel/preset-react',
{
    "pragma": "wp.element.createElement",
    "pragmaFrag": "wp.element.Fragment",
}
```

WordPress用のwebpack（wp-scripts、dependency-extraction-webpack-plugin）を使用して、ビルドをしない方は、WordPressの依存関係を解決するために以下のような記述が必要なので注意してください。

ご参考）https://developer.wordpress.org/block-editor/packages/packages-dependency-extraction-webpack-plugin/

```json
externals: {
    "@wordpress/blocks": ["wp", "blocks"],
    "@wordpress/editor": ["wp", "editor"],
    "@wordpress/components": ["wp", "components"],
    "@wordpress/element": ["wp", "element"],
}
```

## プラグインとしてカスタムブロックを開発
カスタムブロックを作成する流れを図にすると以下のような感じです。

<img width="400" alt="sample-block-make.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241956/40340ca5-eb8a-9ab0-ffec-aee1e08fce72.png">


どこから作成しても問題ありませんが、カスタムブロックJSメインコードとカスタムブロックスタイルコードを作成して、最後にカスタムブロック登録コードを作るのが良いと思います。

<details><summary>firstblock/editor.js（折りたたんでいます）</summary><div>

```js
import './editor.scss';
import { registerBlockType } from "@wordpress/blocks";
import { RichText, InspectorControls } from "@wordpress/editor";
import { PanelBody, ColorPalette } from "@wordpress/components";

registerBlockType('sample-block/firstblock', {
	title: 'サンプルブロック(editor)',

	icon: 'wordpress-alt',

	category: 'common',

	example: {},

	attributes: {
		content: {
			type: 'string',
			source: 'html',
			selector: 'p'
		},
		colorPrefix: {
			type: 'string',
			default: ''
		}
	},

	edit( { className, attributes, setAttributes } ) {
		const { content, colorPrefix } = attributes;

		const changeBackGroundColor = (backGroundColor) => {
			let color_prefix = '';

			switch ( backGroundColor ) {
				case 'blue':
					color_prefix = '--blue' ;
					break;
				case 'red':
					color_prefix = '--red';
					break;
				case 'green':
					color_prefix = '--green';
					break;
				case 'yellow':
					color_prefix = '--yellow';
					break;
				default:
					color_prefix = '';
					break;
			}
			setAttributes({colorPrefix: color_prefix})
		}

		return (
			<div className={ className }>
				<InspectorControls>
						<PanelBody title='背景カラー設定'>
								<ColorPalette
									colors={[
										{name: 'ブルー', color: 'blue'},
										{name: 'レッド', color: 'red'},
										{name: 'グリーン', color: 'green'},
										{name: 'イエロー', color: 'yellow'},
									]}
									disableCustomColors='true'
									onChange={ changeBackGroundColor }
								/>
						</PanelBody>
				</InspectorControls>
				<RichText
					className={`wp-block-sample-block-firstblock__content${colorPrefix}`}
					tagName='p'
					onChange={ ( content ) => setAttributes( { content: content } ) }
					value={ content }
				/>
			</div>
		);
	},

	save( { attributes } ) {
		const { content, colorPrefix } = attributes;

		return (
			<div>
				{ content && (
					<RichText.Content
						className={`wp-block-sample-block-firstblock__content${colorPrefix}`}
						tagName='p'
						value={ content }
					/>
				)}
			</div>
		);
	}
});
```
</div></details>

<details><summary>firstblock/editor.scss（折りたたんでいます）</summary><div>

```scss
.wp-block-sample-block-firstblock {
  &__content {
    color: black;
    background-color:	white;
  }

  &__content--red {
    color: white;
    background-color:	red;
  }

  &__content--green {
    color: white;
    background-color:	green;
  }

  &__content--blue {
    color: white;
    background-color:	blue;
  }

  &__content--yellow {
    color: red;
    background-color:	yellow;
  }
}
```
</div></details>

`edit`関数がエディター表示の部分、`save`関数がフロントを表示する部分になります。
`attributes`は、主にエディターで設定した値（`edit`関数で設定した値）をフロントへ渡す値（`save`関数へ渡す値）を定義します。
上記だと、RichText書かれたValue値やColorPaletteで選択したクラス名のプレフィックスなどです。

サイドナビゲーションは、InspectorControls、PanelBodyなどを組み合わせれば簡単に実装できます。


<details><summary>plugin.php（折りたたんでいます）</summary><div>

```php
<?php
/**
 * Plugin Name: Sample-Block
 * Plugin URI: *
 * Description: Sample用カスタムブロック.
 * Version: 1.0
 * Author: Kodak
 * Author URI: *
 * License: GPL2
 *
 * @package sample-block
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * カスタムブロックの登録.
 */
function sample_block_register() {
	// スクリプトファイルをWordPressに登録する.
	$editor_asset_file = include( plugin_dir_path( __FILE__ ) . 'dist/editor.asset.php' );
	$script_asset_file = include( plugin_dir_path( __FILE__ ) . 'dist/script.asset.php' );

	// editor-script用のJSファイル.
	wp_register_script(
		'sample-block-editor-script',
		plugins_url( 'dist/editor.js', __FILE__ ),
		$editor_asset_file['dependencies'],
		$editor_asset_file['version']
	);

	// editor-style用のCSSファイル.
	wp_register_style(
		'sample-block-editor-style',
		plugins_url( 'dist/editor.css', __FILE__ ),
		[ 'wp-edit-blocks' ],
		filemtime( plugin_dir_path( __FILE__ ) . 'dist/editor.css' )
	);

	// script用のJSファイル.
	wp_register_script(
		'sample-block-script',
		plugins_url( 'dist/script.js', __FILE__ ),
		$script_asset_file['dependencies'],
		$script_asset_file['version']
	);

	// style用のCSSファイル.
	wp_register_style(
		'sample-block-style',
		plugins_url( 'dist/style.css', __FILE__ ),
		[ 'wp-edit-blocks' ],
		filemtime( plugin_dir_path( __FILE__ ) . 'dist/style.css' )
	);

	// カスタムブロックの登録(editor).
	register_block_type(
		'sample-block/firstblock',
		[
			'editor_script' => 'sample-block-editor-script', // エディター画面の時のみスクリプトを読み込む.
			'editor_style'  => 'sample-block-editor-style', // エディター画面の時のみスタイルを読み込む.
		]
	);

	// カスタムブロックの登録.
	register_block_type(
		'sample-block/secondblock',
		[
			'script' => 'sample-block-script', // エディター・フロントの両画面でスクリプトを読み込む.
			'style'  => 'sample-block-style', // エディター・フロントの両画面でスタイルを読み込む.
		]
	);
}
add_action( 'enqueue_block_editor_assets', 'sample_block_register' );
add_action( 'enqueue_block_assets', 'sample_block_register', 1 );
```
</div></details>

WordPress用のwebpack（wp-scripts、dependency-extraction-webpack-plugin）を使用して、ビルドした場合、スクリプトファイル（xxxx.asset.php）が作られます。
ライブラリの依存関係やバージョン情報が書かれているので、それらを使ってJSファイルを登録します。

カスタムブロックの登録は、`editor_script`、`editor_style`、`script`、`style`の4種類があります。
4つの違いは以下の通りです。

- `editor_script` ・・・ エディター画面の時のみスクリプトを読み込む
- `editor_style` ・・・ エディター画面の時のみスタイルを読み込む
- `script` ・・・ エディター・フロントの両画面でスクリプトを読み込む
- `style` ・・・ エディター・フロントの両画面でスタイルを読み込む

例えば、本記事の最初に見せたブロックをプレビューしてフロントから見てみると以下のように表示されます。

![sample-block-preview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241956/3a44e5ac-dc27-8f56-90cd-e41ea6a9a993.png)


上：エディターとフロントにCSSが適応されるブロック（`script`、`style`）を使用した場合
下：エディターのみCSSが適応されるブロック（`editor_script`、`editor_style`）を使用した場合

上の方は、エディターで指定した背景色（赤色）がフロントに適用されていることがわかります。
対して、下の方はエディターで指定した背景色（緑色）がフロントには反映されていないことがわかります。

最後にHookですが、`editor_script`、`editor_style`を使用する場合は`enqueue_block_editor_assets`を使いましょう。
`script`、`style`を使用する場合は`enqueue_block_assets`を使いましょう。
ちゃんとブロックエディター用のHookが用意されているので、適切なHookを使用するようにしましょう。間違っても`init`は使用しないようにしましょう。

## さいごに
というわけで、長々とカスタムブロックの作り方を書いてみましたが、いかがだったでしょうか。
個人的に一番苦労したのはwebpackの設定です。
私自身バックエンドのエンジニアなので、webpackの設定方法を理解するのにえらい時間が掛かりました・・・
カスタムブロックはモダンな作りになっていて面白いのですが、学習コストが高いのが辛いところですね。

今度は、動的ブロックの作成にも挑戦したいと思います。