Wordpressの次世代エディタGutenbergを読む


WordPress5.0で導入されそうなエディタのGutenberg。React+Reduxで作られてると聞いたのでサラッと読んでみました。

https://github.com/WordPress/gutenberg

![localhost_9004_wp-admin_post_php_post_1_action_edit.png](https://qiita-image-store.s3.amazonaws.com/0/24124/e114c811-4622-4bd0-d344-78b45a7d767a.png)

## WordPressからReactAppの呼び出し
WP REST APIで該当記事のデータをJSONで取得して、それを`wp_add_inline_script`などでHTMLに出力

https://github.com/WordPress/gutenberg/blob/f598c96810b682491fdfd3567c59378c5b65265f/lib/client-assets.php#L623-L648

```php:client-assets.php
$post_to_edit = gutenberg_get_post_to_edit($post)

function gutenberg_get_post_to_edit( $post_id ) {
	$post = get_post( $post_id );
	if ( ! $post ) {
		return new WP_Error( 'post_not_found', __( 'Post not found.', 'gutenberg' ) );
	}
	$post_type_object = get_post_type_object( $post->post_type );
	if ( ! $post_type_object ) {
		return new WP_Error( 'unrecognized_post_type', __( 'Unrecognized post type.', 'gutenberg' ) );
	}
	if ( ! current_user_can( 'edit_post', $post->ID ) ) {
		return new WP_Error( 'unauthorized_post_type', __( 'Unauthorized post type.', 'gutenberg' ) );
	}
	$request = new WP_REST_Request(
		'GET',
		sprintf( '/wp/v2/%s/%d', ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name, $post->ID )
	);
	$request->set_param( 'context', 'edit' );
	$response = rest_do_request( $request );
	if ( $response->is_error() ) {
		return $response->as_error();
	}
	return rest_get_server()->response_to_data( $response, false );
}
```
window空間内にデータを突っ込む。wp_add_inline_scriptでinlineのJSとして出力している。これはWPプラグインなどで良く見られる方法。
window空間にinitialStateを構築するデータを突っ込むのも定番だと思います。

https://github.com/WordPress/gutenberg/blob/f598c96810b682491fdfd3567c59378c5b65265f/lib/client-assets.php#L812-L816

```php:client-assets.php
	wp_add_inline_script(
		'wp-edit-post',
		'window._wpGutenbergPost = ' . wp_json_encode( $post_to_edit ) . ';'
	);
```

取得したデータを引数にして`initializeEditor`を実行。この関数がReactDOM.renderを呼び出している。

https://github.com/WordPress/gutenberg/blob/master/lib/client-assets.php#L894-L898

```js:client-assets.php
window._wpLoadGutenbergEditor = wp.api.init().then( function() {
  wp.blocks.registerCoreBlocks();
  return wp[ 'edit-post' ].initializeEditor( 'editor', window._wpGutenbergPost, editorSettings );
})
```

initializeEditorはReactDOMを呼び出す感じのメソッドで`edit-post/index.js`に定義されている様子。
この先はWebpackを使ってコンパイルされているJS。

window._wpGutenbergPostの中身を見るとcontentでHTMLをそのまま渡していることが分かります。

WP REST APIのpost属性に幾つかエディタに_linksなどの属性が付与されているっぽいです。
http://ja.wp-api.org/reference/posts/


```json:window._wpGutenbergPost
{
   "id":1,
   "date":"2018-02-11T02:23:44",
   "date_gmt":"2018-02-10T17:23:44",
   "guid":{
      "rendered":"http://localhost:9004/?p=1",
      "raw":"http://localhost:9004/?p=1"
   },
   "modified":"2018-02-11T12:53:34",
   "modified_gmt":"2018-02-11T03:53:34",
   "password":"",
   "slug":"hello-world",
   "status":"draft",
   "type":"post",
   "link":"http://localhost:9004/?p=1",
   "title":{
      "raw":"Hello world!",
      "rendered":"Hello world!"
   },
   "content":{
      "raw":"<p><strong>WordPress</strong> へようこそ。これは最初の投稿です。編集もしくは削除してブログを始めてください !</p>\n",
      "rendered":"<p><strong>WordPress</strong> へようこそ。これは最初の投稿です。編集もしくは削除してブログを始めてください !</p>\n",
      "protected":false
   },
   "excerpt":{
      "raw":"",
      "rendered":"<p>WordPress へようこそ。これは最初の投稿です。編集もしくは削除してブログを始めてください !</p>\n",
      "protected":false
   },
   "author":1,
   "featured_media":0,
   "comment_status":"closed",
   "ping_status":"closed",
   "sticky":false,
   "template":"",
   "format":"standard",
   "meta":[

   ],
   "categories":[
      1
   ],
   "tags":[

   ],
   "revisions":{
      "count":14,
      "last_id":18
   },
   "_links":{
      "self":[
         {
            "href":"http://localhost:9004/index.php?rest_route=/wp/v2/posts/1"
         }
      ],
      "collection":[
         {
            "href":"http://localhost:9004/index.php?rest_route=/wp/v2/posts"
         }
      ],
      "about":[
         {
            "href":"http://localhost:9004/index.php?rest_route=/wp/v2/types/post"
         }
      ],
      "author":[
         {
            "embeddable":true,
            "href":"http://localhost:9004/index.php?rest_route=/wp/v2/users/1"
         }
      ],
      "replies":[
         {
            "embeddable":true,
            "href":"http://localhost:9004/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1"
         }
      ],
      "version-history":[
         {
            "href":"http://localhost:9004/index.php?rest_route=/wp/v2/posts/1/revisions"
         }
      ],
      "wp:attachment":[
         {
            "href":"http://localhost:9004/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1"
         }
      ],
      "wp:term":[
         {
            "taxonomy":"category",
            "embeddable":true,
            "href":"http://localhost:9004/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1"
         },
         {
            "taxonomy":"post_tag",
            "embeddable":true,
            "href":"http://localhost:9004/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1"
         }
      ],
      "curies":[
         {
            "name":"wp",
            "href":"https://api.w.org/{rel}",
            "templated":true
         }
      ]
   }
}
```

## editorを読んでいく
### gutenbergのディレクトリ構成
以下の4つを抑えておくと読みやすいです。

```
edit-post - 管理画面上にmountされるReactSPA。
editor - edit-post内で呼び出されるエディタ
blocks - エディタ内で使用されるblock（後述）のコンポーネント群。
lib - Wordpressプラグインとして動くphpのコード
```

### マウントとHTMLのパース

initializeEditorは`edit-post/index.js`に定義されているので見てみます。
このedit-postディレクトリはWPの管理画面上にマウントされるReactSPAのディレクトリになっています。

initializeEditorを読むとよくあるReactのマウントを行うエントリーポイント的なメソッドになっていることがわかります。
ここのEditorProviderというのがeditorのstoreにpostを突っ込むactionをdispatchしてくれます。

```js:edit-post/index.js
export function initializeEditor( id, post, settings ) {
	const target = document.getElementById( id );
	const reboot = reinitializeEditor.bind( null, target, settings );
	const ReduxProvider = createProvider( 'edit-post' );

	const provider = render(
		<EditorProvider settings={ settings } post={ post }>
			<ErrorBoundary onError={ reboot }>
				<ReduxProvider store={ store }>
					<Layout />
				</ReduxProvider>
			</ErrorBoundary>
		</EditorProvider>,
		target
	);

	return {
		initializeMetaBoxes: provider.initializeMetaBoxes,
	};
}
```

storeにpostを突っ込むというところで気になるのが**「どのような形でstoreに保存しているか？」**というところです。
保存する途中で `resetBlocks( parse( post.content.raw ) )`という気になるactionが発行されていました。

parseでは以下のようなObjectが得られます。

```js
  {
    uid: '22ae2359-031a-47a1-935b-7284404af3d3',
    name: 'core/freeform',
    isValid: true,
    attributes: {
      content: '<p><strong>WordPress</strong> へようこそ。これは最初の投稿です。編集もしくは削除してブログを始めてください !</p>'
    },
    originalContent: '<p><strong>WordPress</strong> へようこそ。これは最初の投稿です。編集もしくは削除してブログを始めてください !</p>'
  }
```

これをstoreに格納する際にはuidでフラットなObjectになおして格納を行っています。
正規化してstoreに入れようというのは単一のオブジェクトであることを保証するために[Docs](https://redux.js.org/docs/recipes/reducers/NormalizingStateShape.html)でも推奨されている方法ですね。

blocksByUidに実際のオブジェクトを、blockOrderで使用している順番にblockのuidを格納している感じでした。

```js:store
  {
    blocksByUid: {
      '22ae2359-031a-47a1-935b-7284404af3d3': {
        uid: '22ae2359-031a-47a1-935b-7284404af3d3',
        name: 'core/freeform',
        isValid: true,
        attributes: {
          content: '<p><strong>WordPress</strong> へようこそ。これは最初の投稿です。編集もしくは削除してブログを始めてください !</p>'
        },
        originalContent: '<p><strong>WordPress</strong> へようこそ。これは最初の投稿です。編集もしくは削除してブログを始めてください !</p>'
      }
    },
    blockOrder: [
      '22ae2359-031a-47a1-935b-7284404af3d3'
    ]
  }
```

とりあえず初期化処理としてReactをマウントしてHTMLをJS Objectとして表示する箇所まで読むことが出来ました。
blockというObjectをReactに読ませて対応するコンポーネントを表示していそうって予測がたちました。

### blockの表示
とりあえず上から読んでいくとBlockList(editor/components/block-list/index.js)というコンポーネントがreduxとconnectする際にblocksをstoreから引っ張っていることがわかります。

ここからは駆け足。

https://github.com/WordPress/gutenberg/blob/45074119c4c94f6c53c119bbafe33eecf5ec0410/editor/components/block-list/index.js

```rust:editor/components/block-list/index.js
render() {
	return map( layouts, ( layout ) => {
		// When rendering grouped layouts, filter to blocks assigned to layout.
		const layoutBlocks = isGroupedByLayout ?
			filter( blocks, ( block ) => (
				get( block, [ 'attributes', 'layout' ] ) === layout.name
			) ) :
			blocks;

		return (
			<BlockListLayout
				key={ layout.name }
				layout={ layout.name }
				isGroupedByLayout={ isGroupedByLayout }
				blocks={ layoutBlocks }
				renderBlockMenu={ renderBlockMenu }
				rootUID={ rootUID }
				showContextualToolbar={ showContextualToolbar }
			/>
		);
	} );
}
```

```rust:editor/components/block-list/layout.js
class BlockListLayout {
render() {
	<BlockEdit
		name={ blockName }
		isSelected={ isSelected }
		attributes={ block.attributes }
		setAttributes={ this.setAttributes }
		insertBlocksAfter={ isLocked ? undefined : this.insertBlocksAfter }
		onReplace={ isLocked ? undefined : onReplace }
		mergeBlocks={ isLocked ? undefined : this.mergeBlocks }
		id={ block.uid }
		isSelectionEnabled={ this.props.isSelectionEnabled }
		toggleSelection={ this.props.toggleSelection }
	/>
}
```

```rust:blocks/block-edit/index
export function BlockEdit( props ) {
	const blockType = getBlockType( name );
	const Edit = blockType.edit || blockType.save;

	return (
		<Edit
			{ ...props }
			className={ className }
			focus={ props.isSelected ? {} : false }
			setFocus={ noop }
		/>
	);
}
```

ここでblockTypeという要素が出てきます。getBlockTypeの中が気になるので見ていきます。

```js:blocks/api/registration.js
export function getBlockType( name ) {
	return blocks[ name ];
}
```

ここのblocksの中身を見てみましょう。globalに露出しているので、ChromeのDevToolsで直接見てみました。

<img width="1008" alt="スクリーンショット 2018-02-11 13.56.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/24124/dacac62c-5545-34b3-8a90-f7fd8a0d1c46.png">

blockのリストっぽい配列が得られました。一つ`core/freeform`に対応するblockTypeを見ると以下の構造になっています。
ここのeditにはReactComponentが入っています。

```json
{
   "name":"core/freeform",
   "title":"Classic",
   "desription":"The classic editor, in block form.",
   "icon":"editor-kitchensink",
   "category":"formatting",
   "edit": function(),
   "attributes":{
      "content":{
         "type":"string",
         "source":"html"
      },
      "className":{
         "type":"string"
      }
   }
}
```

これでHTML=>JS Objectに変換。JS Objectにはblockの配列が入っていて、各blockに対して対応するblockTypeから表示に使用するReactComponentを呼びだしていることがわかりました。

## 所感
この設計はLPをドラッグ・アンド・ドロップで作成できるよ！ってサービスによくある設計だと思いました。

このBlockによって描写する仕組みに加えてエディタ特有のセレクション（カーソルの位置）やinline styleとかがついていきます。

なおリッチテキストが使える部分に関しては[TinyMCE](https://www.tinymce.com/)が入っており、エディタの難しい部分に関してはおまかせしているようでした。
今のselectionがどのブロックにあって、ブロックをまたいだ時にどういう扱いをするのかみたいなところが独特に見えました。

ちなみにblocksはユーザーがカスタマイズして追加出来る仕組みがあるので、こういった構造になっているのだと思います。
エディタ部分以外での学びがおもったより大きかった