20
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

WordPressAdvent Calendar 2017

Day 23

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

Last updated at Posted at 2017-12-24

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のサンプルでは、registerBlockTypeelはグローバル変数になります)。このような問題を防ぐには、処理の全体を即時関数式で囲む、といったテクニックが必要になります。

郷に入っては郷に従えで、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はそれほど複雑ではありません。

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本体もこのようなスタイルです。)。

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

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開発にはない様々な技術が必要になります。変化をチャンスだと捉えて、色々とチャレンジしていきたいと思います。

20
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?