カスタムブロックエディターの構築

Topics

  • 目次
  • はじめに
  • コード構文
  • 何を構築するのか
  • プラグインのセットアップと構造
  • エディターの「コア」
  • カスタムページ「ブロックエディター」の作成
    • ページの登録
    • ターゲット HTML の追加
    • JavaScript と CSS のエンキュー
    • エディター設定のインライン化
  • カスタムブロックエディターの登録とレンダー
  • <Editor> コンポーネントのレビュー
    • 依存
    • Editor のレンダー
    • キーボードナビゲーション
  • カスタム <BlockEditor>
    • <BlockEditorProvider> コンポーネントの理解
      • BlockEditor props
    • <BlockList> コンポーネントの理解
      • BlockList はどのように動作するか ?
    • サイドバーのレビュー
    • ブロックの永続性
      • ステートへのブロックの保存
      • ブロックデータの保存
      • 以前のブロックデータの取得
    • まとめ

WordPress のブロックエディターはパワフルなツールです。様々な方法でコンテンツを作成し、フォーマットできます。ブロックエディター (の一部) は、エディターのコア機能を提供する JavaScript ライブラリ @wordpress/block-editor パッケージによって実装されています。

このパッケージは事実上あらゆるウェブアプリケーション用の独自ブロックエディター作成にも使用できます。つまり WordPress の外でも同じ、ブロックとブロック編集体験を利用できます。

alt text

この柔軟性と相互運用性によりブロックは、複数のアプリケーションに渡ってコンテンツを構築、管理できる強力なツールとなります。また開発者は、ユーザーにとって最適なコンテンツエディターを簡単に作成できます。

このガイドでは、はじめてのカスタムブロックエディターを作成するための、基礎を紹介します。

目次

Top ↑

はじめに

多くのパッケージやコンポーネントが介在する Gutenberg のコードベースは、最初はとっつきにくく感じられるかもしれません。しかしその中核は、ブロックの管理と編集に過ぎません。そのため、エディターで開発したいのであれば、ブロックの編集がどのように行われているのか、基礎レベルで理解することが重要です。

このガイドでは、WordPress 内で完全に機能するカスタムブロックエディターの「インスタンス」を構築する方法を説明します。その過程で、主要なパッケージやコンポーネントを紹介し、ブロックエディターがどのように動作するのかを見ていきます。

この記事を読み終える頃には、ブロックエディターの内部動作を確実に理解し、独自のブロックエディターインスタンスを作成する準備ができていることでしょう。

注意: このガイドで使用しているコードは、付属の WordPress プラグインからダウンロードできます。このプラグインのデモコードは重要な情報源です。

Top ↑

コード構文

このガイドのコードスニペットでは JSX 構文を使用します。そちらの方が好みであれば、プレーンな JavaScript も使えますが、一度 JSX に慣れると、多くの開発者が、JSX の方が読みやすく書きやすいと感じます。このため、ブロックエディターハンドブックのほとんどのコード例では、JSX 構文を使用しています。

Top ↑

何を構築するのか

このガイドを通して、(ほぼ) 完全に機能するブロックエディターインスタンスを作成します。結果は以下のようになります。

スタンドアロンエディターインスタンス。カスタム WordPress 管理ページ内にサンプルのブロックがある。

見た目は似ていますが、このエディターは WordPress で投稿やページを作成する際におなじみの ブロックエディター ではありません。WordPress のカスタム管理ページ内に設置される、完全なカスタムインスタンス「ブロックエディター」です。

このエディターには次の機能があります。

  • すべてのコアブロックを追加、編集可能
  • おなじみのビジュアルスタイルとメイン & サイドバーレイアウト
  • ページリロード間での 基本的な ブロックの永続性

Top ↑

プラグインのセットアップと構造

カスタムエディターは WordPress プラグインとして構築します。簡便のため、プラグインの目的に合わせて「スタンドアロンブロックエディターデモ」と名付けます。

プラグインのファイル構造は以下のようになります。

alt text

ファイルを簡単に紹介します。

  • plugin.php – コメントメタデータの付いた標準的なプラグインの「入り口」ファイル。init.php が必要。
  • init.php – プラグインのメインロジックの初期化を処理する。
  • src/ (ディレクトリ) – JavaScript と CSS ファイルの置き場所。これらのファイルはプラグインによって直接 エンキューされない 。
  • webpack.config.js – カスタム Webpack 構成。@wordpress/scripts npm パッケージよって提供されるデフォルトを拡張し、Sass 経由のカスタム CSS スタイルをサポートする。

上で紹介していない唯一の要素が build/ ディレクトリです。ここには @wordpress/scripts で コンパイルした JS と CSS ファイルが出力されます。ファイルは個別にプラグインがエンキューします。

注意: このチュートリアルを通してコードの先頭にはファイル名を記述したコメントがあります。適宜、参照してください。

ファイル構造を準備できたところで次に、必要なパッケージを見ていきます。

Top ↑

エディターの「コア」

Gutenberg エディターは多くの動作パーツから構成されますが、中核は @wordpress/block-editor パッケージです。その概要は README ファイルにもっとも上手く表現されています。

このモジュールを使用してスタンドアロンのブロックエディターを作成し、使用できます。

完璧です。これがカスタムブロックエディターのインスタンスを作成で使用するメインのパッケージです。しかしその前に、エディターのホームを作成する必要があります。

Top ↑

カスタムページ「ブロックエディター」の作成

まずWordPress の管理画面内に、カスタムブロックエディターのインスタンスを格納する、カスタムページを作成しましょう。

注意: すでに WordPress 管理画面のカスタムページ作成について詳しい方は、この節をスキップしても構いません。

Top ↑

ページの登録

ページの登録には標準の WordPress ヘルパー関数 add_menu_page() を使用して、管理画面カスタムページを登録 します。

// File: init.php

add_menu_page(
    'Standalone Block Editor',         // 表示されるページ名
    'Block Editor',                    // メニューのラベル
    'edit_posts',                      // 必要な権限
    'getdavesbe',                      // ページのフック / スラッグ
    'getdave_sbe_render_block_editor', // ページをレンダーする関数
    'dashicons-welcome-widgets-menus'  // カスタムアイコン
);

getdave_sbe_render_block_editor 関数を使用して、管理画面のコンテンツをレンダーします。なお繰り返しになりますが、各ステップのソースコードは付属プラグインにあります。

Top ↑

ターゲット HTML の追加

ブロックエディターは React で動作するアプリケーションですので、JavaScript がブロックエディターをレンダーするカスタムページ内に HTML を出力する必要があります。

上の手順で参照した getdave_sbe_render_block_editor 関数を使用します。

// File: init.php

function getdave_sbe_render_block_editor() {
	?>
	<div
		id="getdave-sbe-block-editor"
		class="getdave-sbe-block-editor"
	>
		Loading Editor...
	</div>
	<?php
}

この関数は基本的なプレースホルダ HTML を出力します。id 属性 getdave-sbe-block-editor はすぐ後で使います。

Top ↑

JavaScript と CSS のエンキュー

ターゲットのHTMLを配置したら、カスタム管理ページで実行できるように JavaScript と CSS をエンキューします。

これには admin_enqueue_scripts にフックします。

まずカスタムコードが、カスタム管理ページでのみ実行されるようにしなければなりません。そこで、コールバック関数の先頭で、ページがページの識別子と一致しない場合はすぐに終了します。

// File: init.php

function getdave_sbe_block_editor_init( $hook ) {

    // 正しいページでなければ終了。
	if ( 'toplevel_page_getdavesbe' !== $hook ) {
		return;
    }
}

add_action( 'admin_enqueue_scripts', 'getdave_sbe_block_editor_init' );

これでメインの JavaScript ファイルを安全に登録できます。WordPress 標準の wp_enqueue_script() 関数を使用します。

// File: init.php

wp_enqueue_script( $script_handle, $script_url, $script_asset['dependencies'], $script_asset['version'] );

時間とスペースの節約のため、$script_ 変数の代入は省略しました。詳細はここで参照してください。

スクリプトの依存関係の3番目の引数 $script_asset['dependencies'] に注意してください。この依存関係は
wordpress/dependency-extraction-webpack-plugin を使用して動的に生成されます。これで WordPress 提供のスクリプトが、ビルドされたバンドルに含まれないことが保証されます

またデフォルトの美しいスタイルを利用するには、カスタム CSS スタイルと、WordPress デフォルトの書式設定ライブラリの両方を登録する必要があります。

// File: init.php

// デフォルトのエディタースタイルをエンキュー
wp_enqueue_style( 'wp-format-library' );

// カスタムスタイルをエンキュー
wp_enqueue_style(
    'getdave-sbe-styles',                       // ハンドル
    plugins_url( 'build/index.css', __FILE__ ), // ブロックエディター CSS
    array( 'wp-edit-blocks' ),                  // この下に CSS を含むための依存
    filemtime( __DIR__ . '/build/index.css' )
);

Top ↑

エディター設定のインライン化

wordpress/block-editor パッケージを見ると、エディターのデフォルト設定の構成に settingsオブジェクトを受け取ります。これらはサーバーサイドで利用できるため、JavaScript 内で使用できるようにエクスポーズする必要があります。

それには、グローバルな window.getdaveSbeSettings オブジェクトに割り当てられた設定オブジェクトを JSON としてインライン化します。

// File: init.php

// カスタムエディター設定を取得。
$settings = getdave_sbe_get_block_editor_settings();

// すべての設定をインライン化。
wp_add_inline_script( $script_handle, 'window.getdaveSbeSettings = ' . wp_json_encode( $settings ) . ';' );

Top ↑

カスタムブロックエディターの登録とレンダー

上の PHP で管理ページが作成できたので、ようやく JavaScript を使用してページの HTML の中にブロックエディターをレンダーできます。

まずメインの src/index.js ファイルを開きます。次に、必要な JavaScript パッケージを取り込み、CSS スタイルをインポートします。Sass を使用するには、デフォルトの @wordpress/scripts Webpack 構成を 拡張 する必要があることに注意してください。

// File: src/index.js

// WordPress の依存。
import domReady from '@wordpress/dom-ready';
import { render } from '@wordpress/element';
import { registerCoreBlocks } from '@wordpress/block-library';

// 内部の依存。
import Editor from './editor';
import './styles.scss';

次に、DOM の準備ができた後で、以下を行う関数を実行する必要があります。

  • window.getdaveSbeSettings からエディターの設定を取得する (以前、PHPでインライン化された)。
  • RegisterCoreBlocks を使用してすべての WordPress のコアブロックを登録する。
  • カスタム管理ページで待機中の <div> 内に <Editor> コンポーネントをレンダーする。
domReady( function () {
	const root = createRoot( document.getElementById( 'getdave-sbe-block-editor' ) );
	const settings = window.getdaveSbeSettings || {};
	registerCoreBlocks();
	root.render(
		<Editor settings={ settings } />
	);
} );

注意: 不要な JS グローバルを作成しなくても PHP からエディターをレンダーできます。例として、Gutenberg プラグイン内の Edit Site パッケージを参照してください。

Top ↑

<Editor> コンポーネントのレビュー

上のコードで使用された <Editor> コンポーネントを見ていきます。このコンポーネントは関連するプラグインの src/editor.js 内にあります。

名前とは裏腹に、これはブロックエディターの実際のコアではありません。むしろ、カスタムエディターの本体を形成するコンポーネントを含む ラッパー コンポーネントです。

Top ↑

依存

<Editor> 内部ではまず、いくつかの依存関係を取り込みます。

// File: src/editor.js

import Notices from 'components/notices';
import Header from 'components/header';
import Sidebar from 'components/sidebar';
import BlockEditor from 'components/block-editor';

これらの中で最も重要なものが内部コンポーネントの BlockEditor と Sidebar です。すぐ後で説明します。

残りのコンポーネントは、ほぼエディターのレイアウトと周辺のユーザーインターフェース (UI) を形成する、静的な要素で構成されます。これらの要素には、ヘッダーや通知エリア等があります。

Top ↑

Editor のレンダー

これらのコンポーネントを利用することで、<Editor> コンポーネントを定義できます。

// File: src/editor.js

function Editor( { settings } ) {
	return (
		<DropZoneProvider>
			<div className="getdavesbe-block-editor-layout">
				<Notices />
				<Header />
				<Sidebar />
				<BlockEditor settings={ settings } />
			</div>
		</DropZoneProvider>
	);
}

このプロセスでは、エディターのレイアウトの核となる部分を自動生成します。同時にいくつかの特殊なコンテキストプロバイダーも出力し、コンポーネントの階層全体で利用可能な特定の機能を作成します。

詳しく見ていきます。

  • <DropZoneProvider> – ドラッグアンドドロップのためドロップゾーン機能 の使用を有効化します。
  • <Notices> – 「スナックバー」型通知 (一瞬出てきて、すぐに消える通知) を提供します。core/notices ストアにメッセージがディスパッチされるとレンダーされます。
  • <Header> – エディター UI の先頭に静的なタイトル「Standalone Block Editor」をレンダーします。
  • <BlockEditor> – カスタムブロックエディターコンポーネント。

Top ↑

キーボードナビゲーション

基本的なコンポーネント構造が整ったため、あとはすべてを
navigateRegions HOCでラップして、レイアウト内の異なる「リージョン」間のキーボードナビゲーションを提供します。

// File: src/editor.js

export default navigateRegions( Editor );

Top ↑

カスタム <BlockEditor>

これでコアとなるレイアウトとコンポーネントが揃いました。次は、いよいよブロックエディターそのもののカスタム実装を探索します。

このためのコンポーネントが <BlockEditor>であり、こここそが魔法が起きる場所です。

src/components/block-editor/index.js を開くと、これまでに見てきた中でもっとも複雑なコンポーネントであることがわかります。多くのことが起きていますので、まず <BlockEditor> コンポーネントによってレンダーされるものに集中します

// File: src/components/block-editor/index.js

return (
	<div className="getdavesbe-block-editor">
		<BlockEditorProvider
			value={ blocks }
			onInput={ updateBlocks }
			onChange={ persistBlocks }
			settings={ settings }
		>
			<Sidebar.InspectorFill>
				<BlockInspector />
			</Sidebar.InspectorFill>
			<BlockCanvas height="400px" />
		</BlockEditorProvider>
	</div>
);

キーとなるコンポーネントが <BlockEditorProvider> と <BlockList> です。調べていきましょう。

Top ↑

<BlockEditorProvider> コンポーネントの理解

<BlockEditorProvider>は階層の中で最も重要なコンポーネントの1つです。新しいブロックエディターのために、新しいブロック編集コンテキストを確立します。

結果として、これがこのプロジェクトの最終ゴールの 基礎 となります。

<BlockEditorProvider> の子コンポーネントはブロックエディターの UI を構成します。これらのコンポーネントはContext を介してデータにアクセスし、エディター内でのブロックとその動作の レンダー および 管理 を可能にします。

// File: src/components/block-editor/index.js

<BlockEditorProvider
	value={ blocks }           // ブロックオブジェクトの配列
	onInput={ updateBlocks }   // ブロック更新を管理するハンドラ
	onChange={ persistBlocks } // ブロック更新 / 永続化を管理するハンドラ
	settings={ settings }      // エディター settings オブジェクト
/>

Top ↑

BlockEditor props

上で見るように <BlockEditorProvider>は (パースされた) ブロックオブジェクトの配列を value プロパティとして受け取り、エディター内で変更が検出されると、(新しいブロックを引数として渡して) onChange ハンドラプロパティと onInput ハンドラプロパティを呼び出します。

内部的には、与えられた registry を withRegistryProvider HOC を介してサブスクライブし、ブロック変更イベントをリッスンし、変更されたブロックが永続的かどうかを判断し、それに応じて適切な onChange|Input ハンドラを呼び出します。

このシンプルなプロジェクトの目的のため、これらの機能により以下が可能です。

コンポーネントが settings プロパティを受け取ることも思い出してください。このプロパティに、先ほど init.php 内で JSON としてインライン化したエディタ設定を追加します。これらの設定を使用して、カスタム色、使用可能な画像サイズ、その他の機能を設定できます。

Top ↑

<BlockList> コンポーネントの理解

<BlockEditorProvider> と並んで次に興味深いコンポーネントが <BlockList> です。

最も重要なコンポーネントのひとつであり、エディター内でのブロックのリストのレンダーを担います。

<BlockEditorProvider> の子として配置されているため、エディター内の現行ブロックのすべてのステート情報にフルアクセスできます。

Top ↑

BlockList はどのように動作するか ?

ブロックリストのレンダーに、<BlockList> は内部でいくつかの他の低レベルコンポーネントに依存しています。

これらのコンポーネントの階層は おおよそ 次のとおりです。

// 説明を目的とした擬似コード

<BlockList>
	/* rootClientId からブロックのリストをレンダーする。 */
	<BlockListBlock>
		/* BlockList から単一ブロックをレンダーする。 */
		<BlockEdit>
			/* ブロックの標準の編集可能領域をレンダーする。 */
			<Component /> /* edit() 実装で定義されたようにブロック UI をレンダーする。
			*/
		</BlockEdit>
	</BlockListBlock>
</BlockList>

これらが一緒に動作して、ブロックリストをレンダーする様子を簡単に説明します。

  • <BlockList> はすべてのブロックの clientId をループし、それぞれを <BlockListBlock /> でレンダーします。
  • 次に <BlockListBlock /> は、個々のブロックを自身のサブコンポーネント <BlockEdit> でレンダーします。
  • 最後に ブロック自身が、Component プレースホルダーコンポーネントを使用してレンダーされます。

@wordpress/block-editor パッケージのコンポーネントは、最も複雑で、中核に存在します。エディターがどのように機能するのかを基本的なレベルで把握したいのであれば、これらのコンポーネントの理解は非常に重要です。コンポーネントの学習を強く推奨します。

Top ↑

サイドバーのレビュー

また、<BlockEditor> のレンダー内には、<Sidebar> コンポーネントがあります。

// File: src/components/block-editor/index.js

return (
    <div className="getdavesbe-block-editor">
        <BlockEditorProvider>
            <Sidebar.InspectorFill> /* <-- SIDEBAR */
                <BlockInspector />
            </Sidebar.InspectorFill>
            <BlockCanvas height="400px" />
        </BlockEditorProvider>
    </div>
);

これは (ある部分)、<BlockInspector> コンポーネントを介した高度なブロック設定の表示に使用されます。

<Sidebar.InspectorFill>
	<BlockInspector />
</Sidebar.InspectorFill>

しかし、注意深い読者であれば、すでに <Editor> (src/editor.js) コンポーネントのレイアウト内の <Sidebar> コンポーネントの存在に気づいたかもしれません。

// File: src/editor.js
<Notices />
<Header />
<Sidebar /> // <-- これは何 ?
<BlockEditor settings={ settings } />


src/components/sidebar/index.js ファイルを開くと、確かにこれが上の <Editor> 内でレンダーされているコンポーネントだとわかります。しかし、この実装では Slot/Fill を利用して Fill (<Sidebar.InspectorFill>) をエスポーズし、その後、<BlockEditor>コンポーネントの内部でレンダーします (上の説明を参照)。

これで、<BlockInspector /> を Sidebar.InspectorFill の子としてレンダーできます。これにより、<BlockEditorProvider> の React コンテキスト内に <BlockInspector> を保持しながら、別の場所 (つまり <Sidebar> 内) で DOM にレンダーできます。

過度に複雑に見えるかもしれませんが、<BlockInspector> が現在のブロックの情報にアクセスするには必要です。Slot/Fill がなければ、この設定の実現は非常に難しかったと思います。

これでカスタム <BlockEditor> のレンダーは完了です。

なお、<BlockInspector> 自体は、実際には <InspectorControls> 用の Slot をレンダーします。これでブロックの edit() 定義の中で <InspectorControls> コンポーネントをレンダーし、エディターのサイドバー内に表示できます。このコンポーネントを詳細に調べることを推奨します。

Top ↑

ブロックの永続性

ここまでカスタムブロックエディターを作成する長い旅を続けてきました。しかし、まだ触れなければならない大きなエリアが1つ残っています。ブロックの永続性です。言い換えれば、ブロックの保存と、ページの更新の  での利用を実現します。

alt text

このガイドはあくまで「実験」ですので、ブロックデータの保存に処理に、ブラウザの localStorage API を利用します。実際のシナリオでは、より信頼性が高く堅牢なシステム (データベースなど) を選択することになるでしょう。

ブロックを保存する処理を見ていきます。

Top ↑

ステートへのブロックの保存

src/components/block-editor/index.js ファイルを見ると、ブロックを配列として保存するためのステートが作成されていることに気づきます。

// File: src/components/block-editor/index.js

const [ blocks, updateBlocks ] = useState( [] );

前述したように blocks は、「コントロールされた」コンポーネント <BlockEditorProvider> に value prop として渡され、ブロックの初期セットを与えます。同様に updateBlocks セッターは <BlockEditorProvider> の onInput コールバックにフックされ、ブロックのステートと、エディター内でのブロックの変更との同期が保たれることを保証します。

Top ↑

ブロックデータの保存

ここで注意を onChange ハンドラーに向けると、以下のように定義される関数 persistBlocks() に紐付いていることがわかります。

// File: src/components/block-editor/index.js

function persistBlocks( newBlocks ) {
	updateBlocks( newBlocks );
	window.localStorage.setItem( 'getdavesbeBlocks', serialize( newBlocks ) );
}

この関数は「コミットされた」ブロックの変更の配列を受け取り、ステートセッター updateBlocks を呼び出します。またそのブロックを LocalStorage 内にキー getdavesbeBlocks で格納します。この実現のために、ブロックデータは Gutenberg 「ブロックグラマー」 形式でシリアライズされ、文字列として安全に保存されます。

開発者ツールを開いて LocalStorage を調べると、シリアライズされたブロックデータが保存され、エディター内で変更が発生すると更新されることがわかります。以下はこの形式の例です。

<!-- wp:heading -->
<h2>An experiment with a standalone Block Editor in the WordPress admin</h2>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>This is an experiment to discover how easy (or otherwise) it is to create a standalone instance of the Block Editor in the WordPress admin.</p>
<!-- /wp:paragraph -->

Top ↑

以前のブロックデータの取得

永続化自体はこれで良いのですが、ページをリロードするたびにエディター内でデータが取得され、リストア されて初めて有用になります。

データへのアクセスは副作用ですので、これを処理するには useEffect フックを使用する必要があります。

// File: src/components/block-editor/index.js

useEffect( () => {
	const storedBlocks = window.localStorage.getItem( 'getdavesbeBlocks' );

	if ( storedBlocks && storedBlocks.length ) {
		updateBlocks( () => parse( storedBlocks ) );
		createInfoNotice( 'Blocks loaded', {
			type: 'snackbar',
			isDismissible: true,
		} );
	}
}, [] );

このハンドラは、

  • ローカルストレージからシリアライズされたブロックデータを取得する。
  • parse() ユーティリティを使用してシリアライズされたブロックを JavaScript オブジェクトに変換し直す。
  • ステートセッター updateBlocks を呼び出して、ステート内の blocks 値を更新し、LocalStorage から取り出したブロックを反映する。

この操作の結果、コントロールされた <BlockEditorProvider> コンポーネントは LocalStorage からリストアされたブロックで更新され、エディターにブロックが表示されます。

最後に、通知を生成します。ブロックがリストアされたことを示すために、<Notice> コンポーネントに「スナックバー」通知として表示します。

Top ↑

まとめ

このガイドの完了、おめでとうございます。これでブロックエディター内部がどのように動作しているのかをよく理解できたと思います。

ここで構築したカスタムブロックエディターの完全なコードは、GitHub から利用可能です。ダウンロードし、自分で動かしてみてください。実験し、更にその先まで進んでみてください。

原文

最終更新日: