Gutenberg(ブロックエディタ)メモ

WordPressのGutenberg
WordPressのGutenberg

WordPressのGutenbergに関する内容ですが、まだコアに入る前の段階で書いています。

コアに入った後やその後のアップデートでどう変わるかわからないため、その点は留意してください。

最初に

あくまで個人的な認識ですが、まずReactの基本的な部分を学んだ方が断然よいです。

Reactを学ばずとも簡単なものなら作れるため必須とは言い切れないのですが、何かしら実現したい機能を持ったブロックを独自に作りたいと思った時には必須となる可能性が高いでしょう。

個人的には以下の本が参考書として良かったです(比較できるほど読んでいないので最初にあたりを引いただけという感じですが)。

2017年の本なので最新ではありませんが、前半のさらに前半程度をみるだけでGutenbergでも必要なprops(やAttributes)のイメージがぼんやりながら掴めると思います。

Gutenbergに関する記事

この記事を含めて以下のように内容を分けて記述しています。

構造

Gutengergのブロックを作る上での基本的な構造に関して。

ファイルと言語

管理画面で動かすJSファイルと、ブロックの登録やサーバー側での処理を行うPHPファイルの2種類。

JSに関しては、ハンドブック含めてReactのJSX記法とESNextで書かれることが多いため、最低限JSXをトランスパイルできる環境は必須に近いです。

PHPに関しては、Gutenberg専用の記述は多くないようなのでそれほど問題はないと思います。

REST API

metaへのアクセスや書換えをGutenbergのブロックで行う場合にはREST APIが使われます。

functions.phpで無効化している場合は動作しないため、無効化を解除する必要があります。

作る際の形

基本的には、ローカルに開発環境を整えてプラグインの形で作る模様です。

プラグインではなくfunctions.phpやテーマ内のJSで対応することもできますが、趣旨的にもプラグインの方が良いように思います。

運用中のサイトでGutenbergプラグイン(WordPress5.0以降はコアにあるのでプラグインは不要)が有効なら、ローカルでコードを書いてサーバーにファイルをアップする方法でも作成は可能ですが、その場合でもReactの使用とBabelやWebpackを使ってトランスパイルできる環境が最低ラインとなる可能性が高く、いずれにせよGutenbergのブロック制作は環境構築からといえます。

CSS

最終的にブラウザにページが出力される際には、CSSの記述(ブロック自体が持つ見映えに関連する記述)は全て外部CSSファイルにまとめられて、以下のような感じでhead内に出力されます。


<link rel="stylesheet" id="wp-core-blocks-css" href="'https://example.com/wp-includes/css/dist/block-library/style.min.css" type="text/css" media="all">

サイドバーで文字色や背景色を変更できるブロックもありますが、その場合はstyle属性で出力される可能性があります。

CSS(投稿画面)

投稿画面に読み込ませるCSSは、上記ページを参考に以下のように記述することで自動でインライン出力されます。


add_action( 'after_setup_theme', 'nxw_setup_theme' );
function nxw_setup_theme() {
  add_theme_support( 'editor-styles' );
  add_editor_style( get_stylesheet_directory_uri() . '/css/editor-style.css' );
}

( function() {
 window._wpLoadBlockEditor = new Promise( function( resolve ) {
  //省略
 Styles that are reused verbatim in a few places\n *\/\nbody {\n  font-family: \"Noto Serif\", serif;\n  font-size: 16px;\n  line-height: 1.8;\n  color: #191e23; }\n\np {\n  font-size: 16px;\n  line-height: 1.8; }\n\nul,\nol {\n  margin: 0;\n  padding: 0; }\n\nul {\n  list-style-type: disc; }\n\nol {\n  list-style-type: decimal; }\n\nul ul,\nol ul {\n  list-style-type: circle; }\n\n.mce-content-body {\n  line-height: 1.8; }\n"},{"css":"body { font-family: 'Noto Serif JP' }"},{"css":"@charset \"UTF-8\"; h2 { margin-top: 30px; margin-bottom: 20px; padding: 10px; color: #32c1c1;} 
 //省略
 , null ) );
		} );
	} );
} )();

上記下部の.post h2のあたりが投稿画面用CSSの中身ですが、ここが自動で以下のように変換されます。ソースを見てもわかりませんが、開発者ツールで該当箇所を指定すれば確認できます。


.editor-styles-wrapper h2{ margin-top: 30px; margin-bottom: 20px; padding: 10px; color: #32c1c1;} 

bodyは自動で.editor-styles-wrapperに置換してくれますが、独自のクラスでは以下のような問題が発生する可能性があります。


<!-- オリジナル -->
.single h2{ color: red;}
.post h2{ color: red;} 

<!-- Gutenbergによる置換後 -->
.editor-styles-wrapper .single h2{ color: red;}
.editor-styles-wrapper .post h2{ color: red;} 

壊れた指定記述部分の確認方法がわからなかっため実際には上記と違うかもしれませんが、少なくとも意図した箇所にCSSは反映されませんでした。

補足

ハンドブックや他サイトにも載っていることと重複しますが、当初悩んだ点を元に記載。

registerBlockTypeの名称

registerBlockTypeに設置する名称に関しては、ハンドブックに記載された以下の通り。

The name for a block is a unique string that identifies a block. Names have to be structured as namespace/block-name, where namespace is the name of your plugin or theme.

/(スラッシュ)は必須で、前半がnamespace、後半がblock-nameの役割を持っています。プラグイン前提の記述なので、namespaceはプラグイン名にするのがわかりやすいです。

この名称はサーバーでの処理と連動させる場合にも使うため、重要な部分でもあります。


// Registering my block with a unique name
registerBlockType( 'my-plugin/book', {} );
アイコン

blocks.registerBlockTypeiconで指定可能なアイコンは、以下のWordPress管理画面で使えるアイコンから選べます。

なお、記述する際にはdashicons-は不要です。
dashicons-buildingであればbuildingだけにします。

attributes

以下のページが項目に触れているので引用。

・title(必須): ブロックのinserter上に表示する名前
・category(必須): inserter上の分類。common formatting layout widgets embedのいずれか
・icon: ブロックのアイコン WordPressのDashiconsの名前を指定するか、独自のsvg要素を指定する
・keywords: 検索時のキーワード
・attributes: ブロックが使用する構造化されたデータ
・transforms: 説明なし。WIPらしい
・useOnce: 記事ごとに1個だけしか使えない場合はtrueにする
・supports: サポート機能を拡張する設定
・anchor(default: false): ページ内リンクできるようにする
・customClassName(default: true): ブロックのラッパー要素にブロック独自のクラス名をつける
・className(default: true): .wp-block-bour-block-name という形式のクラスをつけるか
・supportHTML(default: true): HTMLモードで編集可能か

attributesは重要な設定で、ここに記載することで簡易にedtisaveに同じ値を設定できるようになります。not definedなどのエラーが出る場合には、attributesの設定が抜けていたりスペルミスである場合をまず疑うと初歩的なミスが減ります。

遭遇したエラーなどの問題

以下は遭遇したエラーなどのメモ書き。

「更新に失敗しました」

  • [起きた問題]
    公開や更新を押した際に「更新に失敗しました」のエラーが表示される
  • [原因]
    カスタムフィールドと連動させていたPHPファイルに、投稿画面内にechoする記述があったため。
  • [解決策]
    echoを消す

独自に追加したカスタムフィールドへの入力内容に連動して、記事の公開時と更新時に動作するPHPコードを自作していましたが、Gutenbergに変えると公開や更新を押した際に「更新に失敗しました」のエラーが表示されました。

データ自体は保存されていて、公開や更新もされる模様ですが、投稿画面上ではエラーが出ているという状態。
PHPやJSのエラー表示が確認できなかったため、問題の箇所を特定する手がかりがありませんでした。

別サイトに同手法で追加したカスタムフィールドがあったのでそちらで確認すると問題はありませんでした。
そのためカスタムフィールの内容や追加方法は原因から除外。

連動したPHPファイルの方を疑い、消去法で箇所を特定しました。内容的に疑わしいのがechoでした。

今回は問題の箇所がほぼ使っていない箇所だったため、echoを含めた関連部位を丸ごと削除することで解決しました。

投稿画面を開いたままだとエラーが出る

投稿画面を開いたまま長時間放置しておくと自動保存や更新ができない場合があります。

この時に開発者ツールを開いていれば以下のようなエラーが確認できると思います。


/wp-json/wp/v2/posts/875/autosaves?_locale=user 400 ()
/wp-admin/post.php?post=875&action=edit&meta-box-loader=1&_wpnonce=〇〇&_locale=user 403 ()

保存が出来なくなってはいますがこうなる前の段階で自動保存が完了している可能性が高く、ほとんどの場合は一旦別ページを開くかリロードしてしまえば問題はでないはずです。

サンプルが動かない

  • [起きた問題]
    ハンドブック以外のページに記載されたサンプルが動かない
  • [原因]
    参考にされた記述が古いため、削除された要素やパスが変更された可能性がある
  • [解決策]
    ハンドブックで類似のサンプルを探すか、GitHubなどでremoveを確認したら今現在の書き方を探す

海外含めていくつかのサンプルコードが見つかりますが、そのままではエラーになる場合がよくあります。
理由はいくつも考えられますが、その内に1つに「そのコードが古く、現在は使えない記述である」という原因もあります。

ハンドブックではどこがどうかわったのかわからないので、GitHubの方を確認したり類似のコードを探し探すなど調査する必要があります。

classやfunctionが未定義

開発者ツールで以下のようなエラーがでる場合があります。

react-dom.min.82e21c65.js:110 Error: Minified React error #130; visit…

上記のvisitの後に続くURLにアクセスすると、reactjs.orgのページでもう少し具体的なエラーメッセージが確認できます。
このページでは以下のような記述がでると思います。

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined.

何らかのサンプルコードを試している時にこれが出た場合は、呼び出しのコンポーネントが違う可能性が高いです(2018.5以前あたりに公開された記事を参考にすると発生率が高い)。

例えば、wp.editorから呼ぶはずのRichTextwp.blocksと書いている場合などです。

比較的調べやすい部分なので、とりあえず記述されているコンポーネントでそのまま検索すると簡単に解決できるかもしれません。

Babel6以降のパッケージやプラグイン

参考にする記事が古い(といってもタイミングによっては1年前どころか月単位のズレで起こるのですが…)と使うパッケージやプラグインが別のものに統合や置き換わっている場合がわりとあります。

npm insrtall後に一応教えてくれるますが、今回見つけたものを以下に記載しておきます。

補足
babel-preset-es2105 babel-preset-env ※1
babili babel-preset-minify ※2

※1 babel-preset-env

※2 babel-preset-minify

npm install時のWRANは以下のようになっている。

npm WARN deprecated babili@0.1.4: babili has been renamed to babel-minify. Please update to babel-minify
npm WARN deprecated babel-preset-babili@0.1.4: babili has been renamed to babel-minify. Please update to babel-preset-minify

webpackのエラー

本筋とは関係ありませんが、トランスパイルとファイルの結合に使うのでwebpackでつまづいた点に付いて。

babel(babel-core)は6でwebpackを使いimportのあるファイルでビルドすると、以下のようなエラーがでます(パスや関数名はそれぞれの環境によります)。

ERROR in ./src/index.js
Module not found: Error: Can’t resolve ‘関数名’ in ‘/src’
resolve ‘関数名’ in ‘/src’

色々弄り回すと以下のようなエラーにもなりました(経緯をメモっていなかったので少々あやふやです)。

ERROR in ./src/index.js
Module build failed (from ./node_modules/babel-loader/lib/index.js):
ReferenceError: [BABEL] /src/index.js: Using removed Babel 5 option: foreign.modules – Use the corresponding module transform plugin in the `plugins` option. Check out http://babeljs.io/docs/plugins/#modules

Babel5以前の古い書き方は止めて、pluginsオプションを使えという意味だと思いますが、その時にはまだプラグインを一切記述していなかったので、書けといわれても書く内容がありませんでした。

その後検索で以下のページを発見。

状況が少し違いますが、presetses2015"modules": falseを記載したらという解決策が提示されていました。

今回はbabel-preset-es2105は使っていませんが、babel-preset-envは設定したので、.babelrcに以下を記載して解決できました。


{
  "presets": [
    ['env', {modules: false}]
  ]
}

なお、webpack.config.jsにpresetsを記載すると"modules": falseのない記述状態でも動作しますが、この場合は.babeircが存在しないことが条件でした。

逆に言えば、.babelrcを作成していない状態では問題なく動作したのに、.babelrcを追加した途端にエラーがでるのであれば、上記のような問題が起きている可能性がありえます。

webpackでES5にした際のJSの状態

調べ方が悪いのかあまり目にすることができなかったので、webpackでES5にトランスパイルされた際のJSの状態を一応以下に記載。

importexportを試したかったので、index.jsblock/sample.jsという2つを以下のように適当に作成したとします。


//index.js
import sample from './blocks/sample.js';
console.log(sample());

//block/sample.js
const name = '佐藤';
export default function() {
  return `私は${name}だ。`;
}

これをwebpack v4でBabel6のbabel-preset-envを通してminifyせずにトランスパイルすると以下のようになります。


/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ 		}
/******/ 	};
/******/
/******/ 	// define __esModule on exports
/******/ 	__webpack_require__.r = function(exports) {
/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 		}
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/
/******/ 	// create a fake namespace object
/******/ 	// mode & 1: value is a module id, require it
/******/ 	// mode & 2: merge all properties of value into the ns
/******/ 	// mode & 4: return value when already ns object
/******/ 	// mode & 8|1: behave like require
/******/ 	__webpack_require__.t = function(value, mode) {
/******/ 		if(mode & 1) value = __webpack_require__(value);
/******/ 		if(mode & 8) return value;
/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ 		var ns = Object.create(null);
/******/ 		__webpack_require__.r(ns);
/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ 		return ns;
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({

/***/ "./src/blocks/sample.js":
/*!******************************!*\
  !*** ./src/blocks/sample.js ***!
  \******************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\nvar name = '佐藤';\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (function () {\n  return '\\u79C1\\u306F ' + name + ' \\u3060\\u3002';\n});\n\n//# sourceURL=webpack:///./src/blocks/sample.js?");

/***/ }),

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _blocks_sample_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./blocks/sample.js */ \"./src/blocks/sample.js\");\n\nconsole.log(Object(_blocks_sample_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])());\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });

関連記事

Gutenberg(ブロックエディタ)に関連する記事一覧。

1人がこの記事を評価

役に立ったよという方は上の「記事を評価する」ボタンをクリックしてもらえると嬉しいです。

連投防止のためにCookie使用。SNSへの投稿など他サービスとの連動は一切ありません。

コメント欄