チュートリアル: カスタムブロックエディターの構築

このチュートリアルでは @wordpress/block-editor パッケージを使用して「ブロックエディター」のカスタムインスタンスを作成する基礎を順に説明します。

目次

Top ↑

はじめに

Gutenberg のコードには多くのコンポーネントやパッケージが含まれていて複雑ですが、その中心はブロックを管理、編集するツールです。このためエディター上で作業する際、ブロックの編集がどのように行われているのかを 基礎 レベルで理解することは重要です。

このチュートリアルでは理解の助けになるよう、WordPress 内で 完全に機能するカスタムブロックエディター「インスタンス」 の構築手順を説明しながら、同時に主要なパッケージやコンポーネントを紹介します。

この記事を読み終える頃にはブロックエディターの動作原理に対する深い理解と、ブロックエディターインスタンスを作成する際に必要な知識を得ているでしょう。

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 ファイルでしょう。

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

素晴らしい、まさにわたしたちが必要としていたものです ! 実際、この @wordpress/block-editor パッケージが、これからカスタムブロックエディターインスタンスの作成で使用するメインのパッケージです。

しかし、パッケージにコードレベルで取りかかる前に、管理画面内にエディター用のスペースを作成する必要があります。

Top ↑

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

最初のステップとして WordPress の管理画面内にカスタムページを作る必要があります。

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

Top ↑

ページの登録

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

// 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 を確認します。

// 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 にフックします。

まず該当の管理画面ページでのみカスタムコードが動作するよう確認します。コールバック関数の先頭でページの識別子に合致しなければすぐに終了します。

// 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 を使用します。

// 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 デフォルトフォーマットライブラリーの両方も登録する必要があります。

// 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 内での使用のためエクスポーズする必要があります。

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

// 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 ファイルを開きましょう。

ここではまず必要な JS パッケージと CSS スタイルをインポートします。Sass を使用するには デフォルトの @wordpress/scripts Webpack 構成の拡張) が必要なことに注意してください。

// src/index.js

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 を使用してすべての Gutenberg コアブロックを登録
  • 管理画面カスタムページで待っている <div> 内に <Editor> コンポーネントをレンダー
domReady( function () {
    const settings = window.getdaveSbeSettings || {};
    registerCoreBlocks();
    render(
        <Editor settings={ settings } />,
        document.getElementById( 'getdave-sbe-block-editor' )
    );
} );

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

Top ↑

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

上で使用した <Editor> コンポーネントを詳しく見ていきます。

名前からはブロックエディターの実際の中核に思えますが、違います。むしろ、カスタムエディター本体を形づくるコンポーネントを含む ラッパー コンポーネントです。

Top ↑

依存

<Editor> ではまず最初に依存をインポートします。

// 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> コンポーネントを定義できます。

// src/editor.js

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

ここではエディターレイアウトの中核の雛形を自動生成しています。同時に、いくつかの特殊化した コンテキストプロバイダ も作成しており、これらはコンポーネント階層を介して特定の機能を実現します。

詳細に見ていきます。

  • <SlotFillProvider> – コンポーネントツリーを介して 「Slot/Fill」 pattern を利用可能にします。
  • <DropZoneProvider> – ドラッグアンドドロップのためドロップゾーン機能 の使用を有効化します。
  • <Notices> – カスタムコンポーネント。「スナックバー」型通知 (一瞬出てきて、すぐに消える通知) を提供します。core/notices ストアにメッセージがディスパッチされるとレンダーされます。
  • <Header> – エディター UI の先頭に静的なタイトル「Standalone Block Editor」をレンダーします。
  • <BlockEditor> – カスタムブロックエディターコンポーネント。ここが面白い場所です。すぐにもう少し詳しく見ます。
  • <Popover.Slot /> – Slot/Fill の仕組みを使用して <Popover> をレンダーできるスロットをレンダーします。

Top ↑

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

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

// src/editor.js

export default navigateRegions( Editor );

Top ↑

カスタム <BlockEditor>

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

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

src/components/block-editor/index.js を開くと、これまでに見てきた中で、もっとも複雑なコンポーネントであることがわかります。

多くのことが起きていますので、分解して見ていきましょう。

Top ↑

レンダーの理解

まずはじめに <BlockEditor> コンポーネントで何がレンダーされるのかに焦点を当てます。

// 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>
            <div className="editor-styles-wrapper">
                <BlockEditorKeyboardShortcuts />
                <WritingFlow>
                    <ObserveTyping>
                        <BlockList className="getdavesbe-block-editor__block-list" />
                    </ObserveTyping>
                </WritingFlow>
            </div>
        </BlockEditorProvider>
    </div>
);

ここで注目するコンポーネントは <BlockEditorProvider> と <BlockList> です。調べてみましょう。

Top ↑

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

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

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

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

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

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

Top ↑

BlockEditor props

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

内部的には、与えられた registry を withRegistryProvider HOC) 経由でサブスクライブして、ブロック変更イベントをリッスンし、ブロックの変更が永続的かどうか判断し、適切な onChangeonInput ハンドラーをそれぞれ呼び出します。

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

コンポーネントが settings prop を受け取ることを思い出してください。これは先に init.php で JSON としてインライン化したエディター設定を受け取ります。カスタム色や、利用可能な画像サイズ、等々 の機能が構成されます。

Top ↑

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

<BlockEditorProvider> と共にもっとも興味深いコンポーネントが <BlockList> です。

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

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

Top ↑

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

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

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

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

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

これらがどのように一緒に動作してブロックのリストをレンダーするか簡単に説明します。

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

これらは @wordpress/block-editor パッケージ内でもっとも複雑で、深く関与しているコンポーネントです。エディターがどのように動作しているのかを基本レベルから知りたければ、これらのコンポーネントを調べることをお勧めしますが、ここでは読者の宿題とします。

Top ↑

カスタムブロックエディターのユーティリティコンポーネント

カスタム <BlockEditor> コンポーネントに話しを戻すと、次の「ユーティリティ」コンポーネントにも注意してください。

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

<div className="editor-styles-wrapper">
    <BlockEditorKeyboardShortcuts /> /* 1. */
    <WritingFlow>
        /* 2. */
        <ObserveTyping>
            /* 3. */
            <BlockList className="getdavesbe-block-editor__block-list" />
        </ObserveTyping>
    </WritingFlow>
</div>

これらはエディターインスタンスに対して重要な機能要素を提供します。

  1. <BlockEditorKeyboardShortcuts /> – エディター内でキーボードショートカットを有効化し、使用する。
  2. <WritingFlow> – ブロック間の選択、フォーカス管理、ナビゲーションを処理する。
  3. <ObserveTyping>– エディター内部の isTyping フラグを管理する。さまざまな場所で使用されていて、一番目にするのはキーボードの入力に応じてブロックツールバーを表示、隠す機能。

Top ↑

サイドバーのレビュー

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

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

return (
    <div className="getdavesbe-block-editor">
        <BlockEditorProvider>
            <Sidebar.InspectorFill> /* <-- SIDEBAR */
                <BlockInspector />
            </Sidebar.InspectorFill>
            <div className="editor-styles-wrapper">
                // snip
            </div>
        </BlockEditorProvider>
    </div>
);

これは他と同じように、<BlockInspector> コンポーネントを介した高度なブロック設定の表示に使用されます。

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

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

// src/editor.js
<Notices />
<Header />
<Sidebar /> // <-- ん!?
<BlockEditor settings={ settings } />

src/components/sidebar/index.js を開くと、たしかにコンポーネントが上の <Editor> 内でレンダーされるのがわかります。しかし、その実装は Slot/Fill を利用して Fill (<Sidebar.InspectorFill>) をエクスポーズし、その後、上で見たように <BlockEditor> コンポーネント内で importし、レンダーします。

次に Sidebar.InspectorFill の子として <BlockInspector /> をレンダーします。この結果、離れた場所の DOM 内、すなわち <Sidebar> 内にレンダーされながら、<BlockEditorProvider> の React コンテキスト内で <BlockInspector> を保持できます。

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

ところで <BlockInspector> 自身は実際に <InspectorControls> のために Slot をレンダーします。これで ブロックの edit() 定義の中で <InspectorControls> コンポーネントをレンダーし、Gutenberg のサイドバーに表示できます。このコンポーネントを詳細に調べることを推奨します。

以上で、私たちのカスタム <BlockEditor> のレンダーについてすべて説明しました !

Top ↑

ブロックの永続性

ここまでカスタムブロックエディターを作成する長い旅を続けてきました。しかし、まだ1つ大きな領域が残っています。ブロックの永続性です。永続性はブロックを保存し、ページの更新の  も利用可能 にします。

alt text

このチュートリアルは 実験 ですので、ブロックデータの保存の処理にブラウザーの localStorage API を利用します。しかし実世界のシナリオでは、より信頼できる堅固なシステム、たとえばデータベースを利用することになるでしょう。

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

Top ↑

ブロックの state への保存

src/components/block-editor/index.js を開くと、ブロックを配列として保存するいくつかの state を作成していることに気づきます。

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

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

上で述べたように blocks は、「コントローラー」コンポーネント <BlockEditorProvider> に value prop として渡され、ブロックの初期セットを与えます。同様に updateBlocks セッターは <BlockEditorProvider> の onInput コールバックに紐付けられ、ブロックの state とエディター内でのブロックの変更との同期を保ちます。

Top ↑

ブロックデータの保存

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

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

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

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

DeveloperTools を開き、LocalStorage を見ると、シリアライズされたブロックデータが保存されており、エディター内の変更に応じて更新されることがわかります。以下はこの形式の例です。

<!-- wp:heading -->
<h2>An experiment with a standalone Block Editor in WPAdmin</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 WPAdmin.</p>
<!-- /wp:paragraph -->

Top ↑

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

永続性を実現できたのはよいことですが、ページがリロードされた際にデータを再取得し、エディター内に リストア できなければ使いものになりません。

データへのアクセスはサイドエフェクトであり、自然とこの処理に古い友人(あるいは新しい !?) useEffect フックを使用します。

// 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 オブジェクトに変換し直す。
  • state セッター updateBlocks を呼び出して state 内の blocks 値を更新し、LocalStorage から取り出したブロックを反映する。

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

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

Top ↑

まとめ

ここまでお付き合いいただいた方、おめでとうございます ! ブロックエディター内部がどのように動作しているのかを理解できるようになったとと思います。

加えて、カスタムで機能するブロックエディターを実装する際に必要な動くコード例をレビューしてきました。この情報は特に、Gutenberg が単なる Post の編集からウィジェットやフルサイト編集、さらにその先まで拡張されている今、必ず役に立つはずです。

これまで構築したカスタムで機能するブロックエディターの完全なコードは Github から取得可能 です。ダウンロードし、自分で動かしてみることをお勧めします。実験し、更にその先まで進みましょう !

原文

最終更新日: