サイトアイコン Capital P

Before Gutenberg – ショートコードで後方互換性

先日RC1がリリースされ、いよいよWordPress 5.0のリリースが近づいてきた。といっても、まだリリース日が確定していないのだが……。今回はプラグインやテーマ作者にとって気になる、互換性の保ちかたについてちょっとしたティップスを紹介しよう。

筆者はいくつかのプラグインをリリースしているのだが、そのうちの一つにHamazonというものがある。これはAmazon, DMM, PHG(itunes)のアフィリエイトコードを差し込むものだ。実装はショートコードだが、Shorcakeに対応しているので、ビジュアルエディタでも使うことができる。

ビジュアルエディターにも対応している。Shorcakeを入れていない場合はただのショートコードを挿入。

さて、これをGutenbergに対応させたいのだが、Gutenbergでショートコードをビジュアルとして表示する手段は提供されていないので、ブロックを作ることになる。しかし、ユーザーの環境は次のようなケースが想定される。

これらすべてをカバーするためには、「ショートコードもGutenbergブロックも両方ある」という状態が望ましい。今回は実際に筆者が行った対応を紹介しよう。

ショートコードとダイナミックブロックの類似

さて、実際にコードに入る前に、ショートコードとダイナミックブロックはとても似ているということを理解しよう。まず、典型的なショートコードAPIの利用方法は次の通り。

add_shortcode( 'my_short_code', function( $attributes = [], $content= '' ) {
    // ここでショートコードの中身を作成
    return $short_code_result;
} );

つづいて、以前紹介したGutenbergのダイナミックブロックは次のようなAPIだ。

register_block_type( 'capital-block/my-block', [
    'render_callback' => function( $attributes, $content = '' ) {
        // ここでブロックの中身を文字列として生成する。
        return $block_result.
    },
] );

双方とも、描画関数が属性のセット $attributes とコンテンツ $contents を受け取っている。このことから、ショートコードとブロックの描画関数はまったく同じでいいということがいえる。この二つはとても似ているのだ。

これで大前提ができた。ショートコードとダイナミックブロックを同一の関数で作成すれば、かなり省エネでGutenberg対応ができそうだ。

Hamazonの場合

さて、Hamazonでの実装は、「アフィリエイト」というボタンを押すとショートコードが挿入されるというものだ。アフィリエイトはその性質上、「APIを叩いて商品を検索し、その中から選ぶ」という仕様になる。ショートコードの場合はショートコードが挿入されるし、Gutenbergの場合はブロックが挿入されれば良いわけだ。実際に挿入されるコードをみてみると……

// ショートコードの場合
[tmkm-amazon asin=4344406850]これは私の処女作です。[/tmkm-amazon]
// Gutenbergの場合
<!-- wp:hamazon/single {"type":"amazon","id":"4344406850"} -->
<p class="wp-block-hamazon-single">これは私の処女作です。</p>
<!-- /wp:hamazon/single -->

といった感じでとても似ている。

トリガーの変更

モーダルウィンドウ呼び出す場合、これまでは media_buttons というフックを利用し、ボタンを追加していた。このボタンをクリックすると、モーダルウィンドが起動する。

media_buttonsを使うとここにボタンが出る。

実際の処理はGithubでみることができる。TinyMCEの細かな動作については、これから必要なくなっていく知識なので細かに説明はしないが、とにかく、このモーダルウィンドで選択を行うとショートコードをJavaScriptで生成して、挿入していたわけだ。

それでは、このモーダルを起動するには、どこでやるのが適切だろうか? それは、ブロックの中である。筆者の場合はダイナミックブロックの描画エリアの上に半透明のレイヤーを被せ、そこに「設定」というボタンを表示するようにした。

ホバーするとボタンが評される。ダイナミックブロックの中は直接編集できないので、これでも構わないだろう。

参考までに、CSSのリンクを紹介しておく。

モーダルの実装

それでは、モーダルの詳細をみてみよう。GutenbergにはモーダルのAPIが存在している。

const { Button, Modal } = wp.components;
const { withState } = wp.compose;

const MyModal = withState( {
    isOpen: false, // モーダルの表示フラグ
} )( ( { isOpen, setState } ) => (
    <div>
        <Button isDefault onClick={ () => setState( { isOpen: true } ) }>クリックでモーダルを開ける</Button>
        { isOpen ?/* ここから下がモーダルの中身。Reactは三項演算子やifを使う */
            <Modal
                title="モーダルのタイトル"
                onRequestClose={ () => setState( { isOpen: false } ) }>
                <Button isDefault onClick={ () => setState( { isOpen: false } ) }>
                    クリックすると閉じる
                </Button>
            </Modal>
            : null }
    </div>
) );

withStateはブロック内でステートを持った要素を作るためのAPIだ。コードが難解なので少し補足をしておこう。まず、謎の三項演算子 { isOpen ? '' : null } だが、ReactはAngularのng-ifやVue.JSのv-ifにあたるものがない。ブレース(波括弧)で括られた中身がJavaScriptとして評価されるので、そこで条件分岐を使ってreturnする。詳しくはReact, JSX で条件分岐(If)やループ(For)を使う方法などを参考にして欲しい(そう、ループもないのだ!)

さらに、プラグインとして利用する場合、APIをimportで読み込む必要はない。すでにwp名前空間に展開されているので、次のようにすればよい。

// サンプルコードではこのように紹介されているが
import { Button, Modal } from '@wordpress/components';
// プラグインとして使う場合はwp名前空間を参照すればよい
const { Button, Modal } = wp.components;

というわけで、このモーダルの中身をHamazonのものに差し替えれば良いのだが、なんと筆者は一年前からこのことを見越してモーダルをReactで実装していたのだ! キューピー3分クッキング的御都合主義(e.g. この鍋が二十分煮たものです)に見えてしまうのだが、本当なのだから仕方がない。このときばかりは自分で自分を抱きしめてやりたくなったものだ。

ちゃんとGithubに証拠が! 実際に対応したJSファイルは2つだけ。

上の例でいえば、modal.jsxをちょっと差し替えたものをブロックのeditメソッドに書けばよい。そのコードがこちら。やや長いので、転載はしない。画像にするとこんな感じ。

めちゃ長くてもうしわけない。

もちろん、たまたまReactで実装しているというケースがそんなにあるとは思えないが、今回で苦労した方は次回以降新しい技術をキャッチアップする投資を想定しておこう。

あとは、ダイナミックブロックの描画関数を再利用することで、ほとんど似たような見栄えが再現できる。

Gutenbergに対応したHamazon

この描画関数は以前あったものをそのまま使っている。Amazon, DMM, Phgという異なるサービスはファサードパターンで実装してあるので、ブロックの描画は抽象メソッドをちょっといじるだけで完了した。実際にはアクセス修飾子を変えたぐらいだ。

それでは、筆者がGutenbergに対応するために行ったコミットへのリンクを貼っておわりにしよう。

まとめ

概念的な説明を駆け足で説明したが、Gutenbergに対応しつつ後方互換性を維持する方法を紹介した。ショートコードを多用している開発者がどれぐらいいるのかわからないが、「ショートコードとブロックは似ている」というポイントをしっかり抑えておいて欲しい。

モバイルバージョンを終了