WordPressのREST API
にはapiFetch()
で簡単にアクセスすることはできます。
ただその対象が投稿タイプ(postsやpagesなど)
やタクソノミー(categoriesやtagsなど)
の場合はコアデータ(@wordpress/core-data
)を使った方がキャッシュ管理やローディング、Undo/Redoなどまでまとめて面倒みてくれるので便利です。
投稿タイプ、例えば投稿(/wp/v2/posts
)やページ(/wp/v2/pages
)などや、
タクソノミー、例えばカテゴリ(/wp/v2/categories
)やタグ(/wp/v2/tags
)などを取得したい場合は
GetEntityRecords()
やGetEntityRecord()
を使うと便利です。
これについてはドキュメント(Gutenberg Dataを使用したアプリの作成
)があるのでそちらを読んでください。
https://ja.wordpress.org/team/handbook/block-editor/how-to-guides/data-basics/
ここではちょっと深堀していきたいと思います。
コアデータ
@wordpress/core-data
のリファレンスを読んでいるといくつかの単語が引っかかります。
state
はReduxの知識があれば無視できます。
kind
は種類のことでpostType
とtaxonomy
とroot
などが確認出来ました。
name
は対象の名前(post
やpage
,tag
,category
などがある)です。
kind
とname
の組み合わせからアクセスするREST API
のベースURLを判断する仕組みのようです。
例えばkind
がpostType
で、name
がpost
なら以下のREST APIにアクセスします。
getEntityRecords('postType', 'post');
↓
/wp/v2/posts
kind
がtaxonomy
で、name
がcategory
なら、
getEntityRecords('taxonomy', 'category');
↓
/wp/v2/categories
他にもいろんなname
が設定してあります。
kind
に属する設定(Config)を取得するにはgetEntitiesConfig(kind)
を使うといようです。
wp.data.select('core').getEntitiesConfig('postType');
wp.data.select('core').getEntitiesConfig('taxonomy');
以下は結果です(postType.post
及びtaxonomy.category
のみ展開してます)。
postType
には「post, page, attachment, nav_menu_item, wp_block, wp_template, wp_template_part, wp_navigation」が確認出来ました。
taxonomy
には「category, post_tag, nav_menu」が確認できます。
投稿
のConfigを見ると、kind
がpostType
, name
がpost
、baseUrl
が/wp/v2/posts
等の情報が得られています。
{kind: 'postType', baseURL: '/wp/v2/posts', baseURLParams: {…}, name: 'post', label: '投稿', …}
読み込み中とキャッシュ
コアデータは一度取得したデータをキャッシュしてます。
getEntityRecords()やgetEntityRecord()の引数(postType
, name
, クエリやIDなど)の組み合わせをキーにキャッシュしてます。
なので同じ引数の関数を二度目以降実行すると、実際にはサーバーにアクセスせずキャッシュを返します。
この引数の組み合わせ
が非常に重要です。
この組み合わせで、現在キャッシュが存在するかをhasEntityRecords()
で取得出来たり、
読み込みが完了したかどうかの状態を知るhasFinishedResolution()
が取得出来ます。
以下のgetEntityRecords()
関数のシグネチャで実験してみます。
getEntityRecords(kind, name, query)
edit.tsx
import React from 'react';
import { useState } from '@wordpress/element';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { Post, store as coreStore} from '@wordpress/core-data';
import KurageExampleBlockProps from './props';
import { useSelect } from '@wordpress/data';
import { decodeEntities } from '@wordpress/html-entities';
import { Button, ButtonGroup, Spinner } from '@wordpress/components';
import './editor.scss';
const CenteringSpinner = () =>
{
return (
<div style={{textAlign: 'center', padding: 10 }}>
<Spinner style={{ width: 100, height: 100 }} />
<p>Loading...</p>
</div>
)
}
export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
const [page, setPage] = useState(1);
const { posts, postsResolved } = useSelect(select => {
// クエリ作成
const query = { page, per_page: 3 };
// ログ
console.log(['postType', 'post', query]);
return {
posts: select(coreStore).getEntityRecords('postType', 'post', query ) as Post[],
// @ts-ignore
postsResolved: select(coreStore).hasFinishedResolution('getEntityRecords', ['postType', 'post', query]) as boolean
}
}, [page]);
return (
<div {...useBlockProps()}>
<ButtonGroup>
<Button variant="primary" onClick={() => setPage(Math.max(1, page - 1))}>前へ</Button>
<Button variant="primary" onClick={() => setPage(page + 1)}>次へ</Button>
</ButtonGroup>
<p>
{page} ページ目
</p>
{
!postsResolved ? <CenteringSpinner />:
!posts.length ? <p>投稿が存在しません</p>:
posts.map(post => (
<p key={post.id}>
{post.id} : {decodeEntities(post.title.rendered)}
</p>
))
}
</div>
);
}
const query = { page, per_page: 3 };
getEntityRecords()
に渡すkindはpostType
, nameはpost
ですが、
queryは一度に取得する最大数(per_page
)を3
に固定、
そして現在のページ(page
)に変化を加えて行きます。
ボタンを押してpage
に変更を加えるようにします。
hasFinishedResolution('getEntityRecords', ['postType', 'post', query])
hasFinishedResolution()
は解決されたら(サーバーから結果が帰ってきたら)trueになります。
つまりアクセス中はfalseになるのでこの結果を使ってローディングを実装します。
第二引数でまとめて組合せを配列指定することに注意してください。
ローディングを分かりやすくするため、サーバーサイドでレスポンスを遅延させます。
適切な箇所にsleep()を仕込んでおきます。
kurage-worker.php
add_action('rest_api_init', function(){
sleep(3);
});
「次へ」ボタンを押すと、3秒ほどローディングが表示される。
「次へ」を押すたびにローディングが表示されますが、
「戻る」を押すとローディングは無く直ぐに結果が表示されるのが確認出来ます。
一度表示したページ(の組合せ)ではローディングは表示されません。
キャッシュが返されるのでPHP側でブレークポイントを張っていてもサーバにアクセスしていないことが確認出来ます。
これ以降は参考程度にしてください。
エンティティの追跡!?
ソースコードを詳しく読んでないので間違ってるかもしれませんが、ちょっと驚いたので書きます。
getEntityRecords()
は投稿のリストを取得しますが、getEntityRecord()
は主キーをもつ単体のレコードを取得します。
このgetEntityRecord()
の動作にちょっと驚きだったので以下のコードを追加します。
edit.tsx
import React from 'react';
import { useState } from '@wordpress/element';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { Post, store as coreStore} from '@wordpress/core-data';
import KurageExampleBlockProps from './props';
import { useSelect } from '@wordpress/data';
import { decodeEntities } from '@wordpress/html-entities';
import { Button, ButtonGroup, Spinner, TextControl, __experimentalVStack as VStack} from '@wordpress/components';
import './editor.scss';
const CenteringSpinner = () =>
{
return (
<div style={{textAlign: 'center', padding: 10 }}>
<Spinner style={{ width: 100, height: 100 }} />
<p>Loading...</p>
</div>
)
}
const ViewPage = ( {id} ) =>
{
const { post, postResolved } = useSelect(select => {
return {
post: select(coreStore).getEntityRecord('postType', 'post', id) as Post,
// @ts-ignore
postResolved: select(coreStore).hasFinishedResolution('getEntityRecord', ['postType', 'post', id]) as boolean
}
}, [id]);
return (
<div style={{border: 'solid 3px green', margin: 15}}>
{
!postResolved ? <CenteringSpinner /> :
!post ? <div>ページが存在しません</div> :
<p>
{post.id} : {decodeEntities(post.title.rendered)}
</p>
}
</div>
)
}
export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
const [page, setPage] = useState(1);
// 追加
const [id, setId] = useState(0);
const [ipt, setIpt] = useState('');
const { posts, postsResolved } = useSelect(select => {
// クエリ作成
const query = { page, per_page: 3 };
// ログ
console.log(['postType', 'post', query]);
return {
posts: select(coreStore).getEntityRecords('postType', 'post', query ) as Post[],
// @ts-ignore
postsResolved: select(coreStore).hasFinishedResolution('getEntityRecords', ['postType', 'post', query]) as boolean
}
}, [page]);
return (
<div {...useBlockProps()}>
<ButtonGroup>
<Button variant="primary" onClick={() => setPage(Math.max(1, page - 1))}>前へ</Button>
<Button variant="primary" onClick={() => setPage(page + 1)}>次へ</Button>
</ButtonGroup>
<p>
{page} ページ目
</p>
<VStack>
{
!postsResolved ? <CenteringSpinner />:
!posts.length ? <p>投稿が存在しません</p>:
posts.map(post => (
// p を Button に変更
<Button variant="primary" key={post.id} onClick={() => setId(post.id)}>
{post.id} : {decodeEntities(post.title.rendered)}
</Button>
))
}
</VStack>
<ViewPage id={id} />
<TextControl value={ipt} onChange={setIpt} />
<Button variant="primary" onClick={() => setId(parseInt(ipt) || 0)}>入力したIDを表示</Button>
</div>
);
}
投稿単体を表示するViewPage
コンポーネントを新たに作りました。
これまでのリスト表示をボタン化しました。
さらにテキストで直接入力出来るようにしました。
ボタンをクリックすると、そのIDをViewPage
で表示します。
↑リストに表示されている3つのボタンのうち何れのボタンを押してもローディングは表示されません。
即座にビューが更新されます。
別の投稿(今回の例ではIDが282の投稿が存在するとする)のIDをテキストコントロールに入力します。
テキストに282
を入力後、入力したIDを表示
ボタンを押します。
getEntityRecords()
で読み込んだリストに入ってないためローディングされます。
その後282
の投稿が表示されます。
↑ 282
の結果
後saveEditedEntityRecord()
を実行した時も、あー、なるほどって思うような動作をしますが、大変なのでこの辺は省略します。
内部の動作
Config
の取得はgetEntitiesConfig()
で出来ますが、では内部はどのような仕組みになっているんでしょう?
投稿タイプは/wp/v2/types?context=view
で取得出来るようです。
Object.entries(
await wp.apiFetch({path: '/wp/v2/types?context=view'})
).map(([key, val]) => `${key}: ${val.rest_base} / ${val.name}`);
タクソノミーは/wp/v2/taxonomies?context=view
で。
Object.entries(
await wp.apiFetch({path: '/wp/v2/taxonomies?context=view'})
).map(([key, val]) => `${key}: ${val.rest_base} / ${val.name}`);
ちょっと試してみましょう
0: post: posts / 投稿
1: page: pages / 固定ページ
2: attachment: media / メディア
3: nav_menu_item: menu-items / ナビゲーションメニューの項目
4: wp_block: blocks / 再利用ブロック
5: wp_template: templates / テンプレート
6: wp_template_part: template-parts / テンプレートパーツ
7: wp_navigation: navigation / ナビゲーションメニュー
0: category: categories / カテゴリー
1: post_tag: tags / タグ
2: nav_menu: menus / ナビゲーションメニュー
ちなみにこれらがどのタイミングで読み込まれているのでしょう!?
てっきりREST API
で/wp/v2/types
を取得しているのかと思い、
実装されているWP_REST_Block_Types_Controller
のコンストラクタにブレークポイントを張ったところ反応なし。
どうやら投稿の表示時(post.php
)がREST API
を回避して間接的に呼び出しているようです。
取得されたデータは以下のようにレンダリングされてました。
ブラウザのDOMを検索すると以下のコードが埋め込まれていました。
REST API
経由ではなくpost.php
を呼び出した時点でapiFetch()
側ででキャッシュさせてたんですね。
さて、ではこのapiFetch()
をどのタイミングで呼び出しているのでしょう!?
インライン記述してあるということはワードプレスのどこかの場所にその記述があるはずです。
探してみました。
block-editor.php
wp_add_inline_script(
'wp-api-fetch',
sprintf(
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
wp_json_encode( $preload_data )
),
'after'
);
ならばapiFetch()
が/wp/v2/types
を呼び出したタイミングで例外を発生させ邪魔させるようにミドルウェアを忍ばせてみます。
wp_add_inline_script(
'wp-api-fetch',
sprintf(
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );'
.'wp.apiFetch.use((op,nx)=>{ if(op.path.indexOf("types") !== -1){ throw 12345; } return nx(op); });'
,
wp_json_encode( $preload_data )
),
'after'
);
path
にtypes
の文字列が含まれたら例外を発生させ、
ブラウザのデバッグからそれを拾い、コールスタックから呼び出し元をたどっていく作戦です。
ただ注意しないといけないのはGutenbergのコードはMinify
化されているので可読性が悪すぎて理解不能です。
そこでMinify
化されてないスクリプトを読み込ませるように以下のようにスクリプトをデバッグモードにします。
PHPの設定ファイル(wp-config.php
)でSCRIPT_DEBUG
を有効にします。
wp-config.php
define('WP_DEBUG', true);
define('SCRIPT_DEBUG', true); # ←これを追加。
実戦ではデバッグモードを無効にするのを忘れないように注意してください。
ブラウザの開発者ツールから例外を補足するようにしてページを更新すると、(throw 12345
)が引っかかった。
コールスタックをさかのぼっていくと見おぼえある箇所を発見。
どうやらEditor
コンポーネントのようでした。
このEditor
コンポーネントは投稿編集ページそのものですね。
Editor
はgetEntityRecord()
セレクタから指定のIDの投稿内容を取得しているんですね。
以下はEditor
のソースコードです。
そしてコアデータはストアで以下のコードが存在します。
コアデータストアは内部でgetOrLoadEntitiesConfig(kind)
を定義してます。
この関数は引数から渡されたkind
のConfig
を取得しキャッシュしてます。
kind
がそれぞれ
postType
の時はloadPostTypeEntities()
をtaxonomy
の時はloadTaxonomyEntities()
を
よみこみます。
例えばloadPostTypeEntities()
は内部で以下を実行しているのが確認出来ます。
const postTypes = await apiFetch( {
path: '/wp/v2/types?context=view',
} );
実はこのgetOrLoadEntitiesConfig()
はgetEntityRecord()
から呼び出されてます。
エディタ(Editor
)がgetEntityRecord()
を使用。
getEntityRecord()
がgetOrLoadEntitiesConfig()
を使用。
/wp/v2/types
が読み込まれ、Config
がキャッシュされる。
getOrLoadEntitiesConfig()
は他にも
- getEntityRecords()
- canUserEditEntityRecord()
- deleteEntityRecord()
- saveEntityRecord()
- saveEditedEntityRecord()
から呼ばれてます。
必要な時に/wp/v2/types
を読み込みキャッシュする。
何ならpost.php
を呼び出した時点でキャッシュしREST API
を回避する。
などかなりの工夫がされてました。
試しにgetEntitiesConfig()
の結果を追加してみました。
wp.apiFetch.use((op,nx)=>{
if (op.path.indexOf("types") !== -1) {
console.log("XXXXXXXXXXX");
}
console.log(wp.data.select("core").getEntitiesConfig("postType"));
console.log(op);
return nx(op);
}
ソースコードを読んだだけでは理解出来なかったのですが、
ようやくエディタが投稿を取得するタイミング、コアデータのREST API
アクセス手法、Config
の設定タイミングなど一通り理解することが出来ました。