WordPress Advent Calendar 2017 23日目に空きが生じたようなので、埋め草を投下します。
本記事では、Gutenbergプラグインを開発するためにはどのような知識が必要か、概観することを目標にします。それぞれの技術については、解説記事がいくらでもあるので深掘りはしません。
本記事のターゲットは以下のような読者です。
- WordPressプラグインを作ったことがある(または、作り方を知っている)
- JavaScriptの基本的な知識がある
- 変数定義に
var
をつけないといけない理由がわかる - 現行のブラウザ(IE11含む)で動作するのはES5であることを知っている
- ES2015(またはESNext)という仕様があることを知っている
- React.js、Babel、webpackといったキーワードを聞いたことがある
- 変数定義に
GutenbergとモダンJavaScript
WordPressの新エディタであるGutenbergには、新しめのフロントエンド技術が使われています。
- React.js
- オプション:JSX
- ESNext(ES2015+)
- Babel
- webpack
このうち、最低限理解する必要があるのはReact.jsで、他はオプションです。
しかし、ES5でのGutenbergプラグイン開発は、つらいと思います。簡単なブロック定義のコードを、ES5とESNext + JSXで見比べてみましょう。
// ES5
var registerBlockType = wp.blocks.registerBlockType;
var el = wp.element.createElement;
registerBlockType( 'sample/sample', {
name: 'sample/sample',
title: 'Sample',
icon: 'universal-access-alt',
category: 'common',
edit: function() {
return el(
'div',
{ className: 'container' },
[
el( 'h1', null, 'Header1' ),
el( 'p', null, 'Paragraph' ),
],
);
},
save: function() {
// 省略
}
} );
// ESNext + JSX
const { registerBlockType } = wp.blocks;
registerBlockType( 'sample/sample', {
name: 'sample/sample',
title: 'Sample',
icon: 'universal-access-alt',
category: 'common',
edit: () => {
return (
<div className="container">
<h1>Header1</h1>
<p>Paragraph</p>
</div>
);
},
save: () => {
// 省略
},
} );
注目してほしいのはedit
関数です。この中では、wp.element.createElement
メソッドを使っています。このメソッドはReact.jsのcreateElementメソッドのラッパーです。React.createElement
は第1引数にHTML要素の名前、第2引数にclassやstyle等の属性(props)、第3引数に子要素の配列を取ります。
シンプルな要素の描画であればES5のような記法でも十分対応できますが、複雑なUIを実装する際は実装難易度が高くなり、メンテナンス性も低下する恐れがあります。React.createElement
と等価で、より読みやすい記法が、下のサンプルで用いているJSXです。
また、グローバル変数を定義してしまうと、他のプラグインにも影響を与えてしまう恐れがあります(↑のES5のサンプルでは、registerBlockType
とel
はグローバル変数になります)。このような問題を防ぐには、処理の全体を即時関数式で囲む、といったテクニックが必要になります。
郷に入っては郷に従えで、ESNext + JSXのようなモダンな開発環境を使った方が、Gutenbergプラグイン作成の際には効率的でしょう。
Gutenbergプラグインのプロジェクトテンプレート
最近、Gutenbergプラグインを作っては捨ててしていて面倒に感じてきたので、セットアップ済みのプロジェクトを作成しました。下記リポジトリからダウンロードできます。
https://github.com/ryo-utsunomiya/gutenberg-plugin-template
このプロジェクトでは、以下の環境をセットアップしています。
- webpack + babel: JSX + ESNextのビルド
- eslint: JavaScriptのコードスタイルチェック & 修正
- PHP_CodeSniffer: PHPのコードスタイルチェック & 修正
ディレクトリとファイルの構成は以下のようになります(build, node_modules, vendorディレクトリは自動生成されるので、テンプレートには含まれません)。
.
├── .babelrc # JSのビルド設定
├── .editorconfig # エディタの設定(改行文字、文字コード等)の共通化用設定
├── .eslintignore # JSのコーディングスタイルチェック対象から除外するファイルの設定
├── .eslintrc.json # JSのコーディングスタイル設定
├── .gitignore # git管理に含めないファイルの設定
├── README.md # GitHub用のREADME
├── blocks # Gutenbergのブロックを置く場所
├── build # ビルド後のJSファイルの書き出し先
├── composer.json # PHPライブラリの管理
├── composer.lock # インストール済みPHPライブラリのバージョン管理
├── index.js # JSアプリのエントリーポイント
├── index.php # WordPressプラグインのエントリーポイント
├── node_modules # JSライブラリの格納場所
├── package-lock.json # インストール済みJSライブラリのバージョンを管理(Node 8.x以上向け)
├── package.json # JSライブラリの管理、ビルドスクリプトの記述
├── phpcs.xml # PHPのコーディングスタイル設定
├── readme.txt # WordPressプラグインとしてのreadme
├── vendor # PHPライブラリの格納場所
├── webpack.config.js # JSのビルド設定
└── yarn.lock # インストール済みJSライブラリのバージョンを管理(yarn向け)
この開発環境は、Gutenberg公式の開発環境にできるだけ揃える、という方針で構築しています。
たとえば、ビルド後のJSファイルの書き出し先は、Gutenbergではbuildディレクトリになっているため、それに倣っています。
ライブラリ管理: npmとyarn
JavaScriptライブラリの管理には、npmを使うのが一般的です。Node.jsをインストールするとついてくるので、JavaScriptエンジニアには馴染み深いツールといえるでしょう。package.json
という設定ファイルにライブラリ名とバージョンを書いてnpm install
コマンドを実行すれば、指定したライブラリをnode_modules
ディレクトリにインストールしてくれます。
ちなみに、Gutenbergのテンプレートでは、以下のライブラリを指定しています。
{
"dependencies": {},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-eslint": "^8.0.3",
"babel-loader": "^7.1.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-react-jsx": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1",
"cross-env": "^5.1.3",
"eslint": "^4.13.1",
"eslint-config-wordpress": "^2.0.0",
"eslint-plugin-jest": "^21.5.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.5.1",
"webpack": "^3.10.0"
},
}
dependenciesはアプリケーションとして動作するのに必要なライブラリ、devDependenciesは開発時にのみ必要なライブラリです。GutenbergのAPIは、グローバルに定義されているwp
オブジェクトからアクセスできます。また、React、jQuery、moment.js、lodash、TinyMCEといったGutenberg自身が使用しているライブラリは、グローバルに露出しています。そのため、簡単なUIを作るだけならdependenciesは空でOKです。devDependenciesには、ビルドとコードスタイルチェック用のライブラリを指定しています。
package.json
にはライブラリの管理の他に、もう一つ役割があります。それが簡単なスクリプトの定義です。Gutenbergテンプレートでは以下のコマンドを定義しています(testは未実装)。
{
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"dev": "cross-env BABEL_ENV=default webpack --watch",
"lint": "eslint .",
"test": "echo \"Error: no test specified\" && exit 1"
},
}
ここで定義したbuild
スクリプトは、npm run build
というコマンドで呼び出せます。開発時に頻繁に呼び出すコマンドはpackage.json
に書いておくと便利です。
Babelによるトランスパイル
ES5 => ESNextのように、同じJavaScript言語の範囲内で変換を行うことをトランスパイルと呼びます。BabelはJavaScriptのトランスパイラで、ESNext => ES5の変換によく用いられます。また、Babelはプラグイン機構を有しており、JSXを通常のJavaScriptに変換するプラグインが存在します。
Babelを使うと、以下のような変換を行えます。
// ESNext + JSX
const hw = <h1>Hello World</h1>;
// ES5
var hw = wp.element.createElement("h1", null, "Hello World");
const
=> var
はES5 => ESNext、<h1>Hello World</h1>
=> wp.element.createElement("h1", null, "Hello World")
はJSX => JavaScriptの変換です。
Babelのビルド設定は.babelrc
というファイルにJSON形式で記述します。Gutenbergライブラリとして最小限の設定は以下のようになります。
{
"presets": [
[
"env",
{
"modules": false
}
]
],
"plugins": [
[
"transform-react-jsx",
{
"pragma": "wp.element.createElement"
}
]
]
}
Babelにはpresetと呼ばれる「プラグインのセット」が存在します。その中で、JavaScriptのバージョンアップに合わせて内容が更新されるのがbabel-preset-envです。古い記事だとbabel-preset-es2015を使ってたりしますが、どうせESNextを使うならその時々の最新バージョン(=env)を使った方が良いです。
"modules": false
はBabelによるモジュール機構のトランスパイルを無効化します。これは、import 'react';
をrequire('react')
に変換するようなトランスパイルのことです。Node.jsで動作させるコードをトランスパイルする際に使用しますが、この開発環境では不要です(モジュールローダーは後述するwebpackが担います)。
また、"transform-react-jsx"
の設定で、"pragma": "wp.element.createElement"
を設定しています。JSX => JavaScriptのトランスパイルはデフォルトではReact.createElement
を使いますが、pragmaの設定を行うことでGutenbergのcreateElement
メソッドを使うようにしています。
webpackによるバンドルとモジュール
webpackを使うことで、以下のような仕組みが実現できます。
- トランスパイル後のJSをひとまとめにする
- ESNextのモジュール機構を使えるようにする
ソースコードをひとまとめにすることを「バンドル」と呼び、パフォーマンス上の恩恵が得られる等のメリットがあります。
それ以上に重要なのが、webpackを使うことでブラウザでもモジュール機構が実現できることです。ブラウザにおけるモジュール機構はChrome等の最新版でもまだ実装完了していない機能ですが、webpackはソースをひとまとめにするため、依存関係の解決もwebpackのレイヤーで行うことができます。
大規模なプロジェクトではwebpackの設定は複雑化していく傾向にあり、実際Gutenbergのwebpack.config.jsはかなり複雑です。
しかし、最小限のwebpack.config.jsはそれほど複雑ではありません。
module.exports = {
entry: './index.js',
output: {
path: __dirname,
filename: 'build/index.js',
},
module: {
loaders: [
{
test: /.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
],
},
};
ビルドの起点となるファイル(entry)と出力先(output)、そして使用するローダーを指定するだけです(webpackでは、JavaScriptソースコードの変換を行うプラグインのことをローダーと呼びます)。
"loaders"
では、/.jsx?$/
という正規表現にマッチするファイル名のファイルに対してbabel-loaderというwebpack向けのBabelプラグインで処理を行う設定を行っています。注意が必要なのはnode_modulesを除外することくらいでしょうか。
ESLint
コーディングスタイルを統一しておくと、長期的にコードの品質が保ちやすくなります。
コーディング規約は何でも良いですが、Gutenbergと揃えておくのが無難かなと思います。しかし、GutenbergのJavaScriptコーディング規約は、WordPress本体がそうであるように、スペースの空け方などが独特です。自分で覚えるのは大変なので、プログラムの助けを借りましょう。
Gutenbergはコードスタイルのチェックと修正にESLintというライブラリを使っています。
これを使うと、以下のようなコードのコーディングスタイル違反をすぐに検知できます。
registerBlockType = wp.blocks.registerBlockType;
registerBlockType('sample/sample', {
title: 'Sample',
});
このコードに対してnpm run lint
コマンドでチェックを行うと、以下のようにエラーが表示されます。
1:1 error 'registerBlockType' is not defined no-undef
3:1 error 'registerBlockType' is not defined no-undef
3:18 error There must be a space inside this paren space-in-parens
5:2 error There must be a space inside this paren space-in-parens
✖ 4 problems (4 errors, 0 warnings)
2 errors, 0 warnings potentially fixable with the `--fix` option.
内容としては、(1) registerBlockType
が未定義である (2) スペースの空け方がダメ の2点です。このうち、スペースの方はnpm run lint -- --fix
コマンドを使うと自動的に修正できます。未定義変数の方は機械的には直せないので、const
をつけてローカル変数にしましょう。
const registerBlockType = wp.blocks.registerBlockType;
registerBlockType( 'sample/sample', {
title: 'Sample',
} );
これでGutenbergプロジェクトのコーディング規約に則したコードになりました。
Block Typeの定義
ここまで開発環境がどうなっているか解説しました。ここまでを理解していれば、Gutenbergテンプレートで開発を始めることができるでしょう。最後に、複数のブロックを含む場合の書き方のパターンの提案をします。
具体的に言うと、ブロックの定義は各ファイルに分割しておき、index.jsでプラグインのブロックをまとめてregisterBlockType
するのがおすすめです(Gutenberg本体もこのようなスタイルです。)。
以下のように、ブロックの定義は単一ファイルで完結させます。
export default {
name: 'sample/sample',
title: 'Sample',
icon: 'universal-access-alt',
category: 'common',
edit: () => <h1>Edit mode</h1>,
save: () => <h1>Saved content</h1>,
};
index.jsでは、ブロックの登録を行うだけで、それ以外のことはしません。
import Sample from './blocks/sample.js';
const { registerBlockType } = wp.blocks;
registerBlockType( Sample.name, Sample );
このようにしておくと、ブロックの数が増えたり、複雑な機能を持ったブロックを開発する際にも、メンテナンス性を保つことができます。
まとめ
Gutenbergプラグインには、従来のWordPress開発にはない様々な技術が必要になります。変化をチャンスだと捉えて、色々とチャレンジしていきたいと思います。