Gutenbergプラグイン開発のためのモダンJavaScript入門

[WordPress Advent Calendar 2017](https://qiita.com/advent-calendar/2017/wordpress) 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で見比べてみましょう。

```js
// 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() {
		// 省略
	}
} );
```

```js
// 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](https://reactjs.org/docs/react-api.html#createelement)メソッドのラッパーです。`React.createElement`は第1引数にHTML要素の名前、第2引数にclassやstyle等の属性(props)、第3引数に子要素の配列を取ります。

シンプルな要素の描画であればES5のような記法でも十分対応できますが、複雑なUIを実装する際は実装難易度が高くなり、メンテナンス性も低下する恐れがあります。`React.createElement`と等価で、より読みやすい記法が、下のサンプルで用いている[JSX](https://reactjs.org/docs/introducing-jsx.html)です。

また、グローバル変数を定義してしまうと、他のプラグインにも影響を与えてしまう恐れがあります（↑のES5のサンプルでは、`registerBlockType`と`el`はグローバル変数になります）。このような問題を防ぐには、処理の全体を[即時関数式](https://developer.mozilla.org/en-US/docs/Glossary/IIFE)で囲む、といったテクニックが必要になります。

郷に入っては郷に従えで、ESNext + JSXのようなモダンな開発環境を使った方が、Gutenbergプラグイン作成の際には効率的でしょう。

## Gutenbergプラグインのプロジェクトテンプレート

最近、Gutenbergプラグインを作っては捨ててしていて面倒に感じてきたので、セットアップ済みのプロジェクトを作成しました。下記リポジトリからダウンロードできます。
https://github.com/ryo-utsunomiya/gutenberg-plugin-template

このプロジェクトでは、以下の環境をセットアップしています。

- [webpack](https://webpack.js.org/) + [babel](https://babeljs.io/): JSX + ESNextのビルド
- [eslint](https://eslint.org/): JavaScriptのコードスタイルチェック & 修正
- [PHP_CodeSniffer](https://github.com/squizlabs/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](https://www.npmjs.com/)を使うのが一般的です。Node.jsをインストールするとついてくるので、JavaScriptエンジニアには馴染み深いツールといえるでしょう。`package.json`という設定ファイルにライブラリ名とバージョンを書いて`npm install`コマンドを実行すれば、指定したライブラリを`node_modules`ディレクトリにインストールしてくれます。

ちなみに、Gutenbergのテンプレートでは、以下のライブラリを指定しています。

```json
{
  "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は未実装）。

```json
{
  "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](https://babeljs.io/)はJavaScriptのトランスパイラで、ESNext => ES5の変換によく用いられます。また、Babelはプラグイン機構を有しており、[JSXを通常のJavaScriptに変換するプラグイン](https://www.npmjs.com/package/babel-plugin-transform-react-jsx)が存在します。

Babelを使うと、以下のような変換を行えます。

```js
// ESNext + JSX
const hw = <h1>Hello World</h1>;
```

```js
// 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ライブラリとして最小限の設定は以下のようになります。

```json
{
  "presets": [
    [
      "env",
      {
        "modules": false
      }
    ]
  ],
  "plugins": [
    [
      "transform-react-jsx",
      {
        "pragma": "wp.element.createElement"
      }
    ]
  ]
}
```

Babelにはpresetと呼ばれる「プラグインのセット」が存在します。その中で、JavaScriptのバージョンアップに合わせて内容が更新されるのが[babel-preset-env](https://github.com/babel/babel/tree/master/packages/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](https://webpack.js.org/)を使うことで、以下のような仕組みが実現できます。

- トランスパイル後のJSをひとまとめにする
- ESNextのモジュール機構を使えるようにする

ソースコードをひとまとめにすることを「バンドル」と呼び、パフォーマンス上の恩恵が得られる等のメリットがあります。

それ以上に重要なのが、webpackを使うことでブラウザでもモジュール機構が実現できることです。ブラウザにおけるモジュール機構はChrome等の最新版でもまだ実装完了していない機能ですが、webpackはソースをひとまとめにするため、依存関係の解決もwebpackのレイヤーで行うことができます。

大規模なプロジェクトではwebpackの設定は複雑化していく傾向にあり、実際Gutenbergのwebpack.config.jsはかなり複雑です。

しかし、最小限の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](https://eslint.org/)というライブラリを使っています。

これを使うと、以下のようなコードのコーディングスタイル違反をすぐに検知できます。

```js
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`をつけてローカル変数にしましょう。

```js
const registerBlockType = wp.blocks.registerBlockType;

registerBlockType( 'sample/sample', {
	title: 'Sample',
} );
```

これでGutenbergプロジェクトのコーディング規約に則したコードになりました。

## Block Typeの定義

ここまで開発環境がどうなっているか解説しました。ここまでを理解していれば、Gutenbergテンプレートで開発を始めることができるでしょう。最後に、複数のブロックを含む場合の書き方のパターンの提案をします。

具体的に言うと、ブロックの定義は各ファイルに分割しておき、index.jsでプラグインのブロックをまとめて`registerBlockType`するのがおすすめです（Gutenberg本体もこのようなスタイルです。）。

以下のように、ブロックの定義は単一ファイルで完結させます。

```blocks/sample.js
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では、ブロックの登録を行うだけで、それ以外のことはしません。

```index.js
import Sample from './blocks/sample.js';

const { registerBlockType } = wp.blocks;

registerBlockType( Sample.name, Sample );
```

このようにしておくと、ブロックの数が増えたり、複雑な機能を持ったブロックを開発する際にも、メンテナンス性を保つことができます。

## まとめ

Gutenbergプラグインには、従来のWordPress開発にはない様々な技術が必要になります。変化をチャンスだと捉えて、色々とチャレンジしていきたいと思います。