WordPressを劇的に高速化、1秒以内に表示されるフロントエンドの構築方法 -Zero-latency WordPress Front-end
Post on:2019年2月15日
sponsorsr
サーバーサイドのレンダリング(SSR)を使用して、数分の1秒以内にページが高速に表示されるWordPressのフロントエンドを構築するテクニックを紹介します。
バックエンドのキャッシュと組み合わせることで、非常に高速になり、しかも安価にWordPressサイトを構築できます。

Zero-latency WordPress Front-end -GitHub
下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様のライセンスを元に翻訳しています。
- デモページ
 - サーバーサイドのレンダリング(SSR)
 - バックエンドのサービス
 - 非キャッシュページのアクセス
 - キャッシュページのアクセス
 - キャッシュのパージ
 - 始めてみよう
 - Nginxの設定
 - バックエンドのJavaScript
 - フロントエンドのJavaScript
 - Cordova
 - 終わりに
 
デモページ
サンプルのコードで何ができるのかを示すために、3つのデモページを用意しました。
2つのデモページは、Amazon EC2 A1 インスタンスにホストされています。Graviton CPUのシングルコアと2GのRAMです。十分すぎるほどのスペックと言えるでしょう。
pfj.trambar.ioは同じサーバー上で実行されているテスト用のWordPressからデータを取得しており、ダミーテキストを使用しています。管理画面には、アカウント(bdickus)、パスワード(incontinentia)でログインできます。新しい記事を公開すると、キャッシュが削除されます。記事は約30秒後に自動的にフロントページに表示されます(更新ボタンを押す必要はありません)。
Nginxのキャッシュをここで見ることができます。
et.trambar.ioとrwt.trambar.ioは、それぞれExtremeTechとReal World Techからデータを取得しています。これらは、サンプルコードが実際のコンテンツとどのように一致しているかを理解しやすくするためのものです。WordPressインスタンスからキャッシュパージコマンドを受け取らないので内容が古くなっている可能性があります。
サーバーサイドのレンダリング(SSR)
Isomorphic ReactコンポーネントはWebブラウザ上だけでなく、Webサーバー上でもレンダリングすることができます。サーバーサイドのレンダリング(SSR)の主な目的の1つは、検索エンジン最適化です。もう1つは、JavaScriptのローディング時間を隠すことです。ローディング時にスピナーやプログレスバーを表示するのではなく、フロントエンドをサーバーにレンダリングしてHTMLをブラウザに送信します。実質的には、ローディング画面としてフロントエンド自身の外観を使用しています。
下記のgifアニメは、SSRによってページがどのように機能するかを示したものです。

SSRでページがどのように表示されるか
SSRのHTMLはJavaScriptにサポートされていませんが、機能的なハイパーリンクを持っています。JavaScriptの読み込みが完了する前にビジターがリンクをクリックすると、別のSSRページに移動します。サーバーはコードとデータの両方に即座にアクセスできるため、このページを非常に迅速に生成できます。ページがすでにサーバー側のキャッシュに存在している可能性もありますが、その場合はさらに早く表示されます。
バックエンドのサービス
バックエンドはWordPress自身と、Nginx、Node.jsの3つのサービスから成り立っています。下記の図では、さまざまなタイプのデータがどのように移動するかを示しています。

バックエンドの構成
NginxがWordPressから直接JSONデータを取得しないことに注意してください。代わりに、データは最初にNodeを通過します。迂回しているのは、主にWordPressがJSONレスポンスに電子タグを付けないためです。電子タグがないと、ブラウザはキャッシュの検証を実行できません(つまり、条件付きリクエスト→304 not modified)。Nodeを介してデータを渡すと、不要なフィールドを削除することもできます。最終的に、Nginxに送信する前にデータを圧縮できます。サイズを小さくすると、より多くのコンテンツがキャッシュに収まります。また、Nginxが同じデータを何度もgzipする必要もなくなります。
フロントエンドのコードを実行すると、NodeはNginxにJSONデータをリクエストします。もしそのデータがキャッシュに見つからない場合、Nodeはそれ自身のリクエストを処理することになります。この往復により、NginxはJSONデータをキャッシュします。ブラウザはすぐに同じデータをリクエストするようになるので(同じフロントエンドのコードが実行されるので)、それが起こることをわたし達は望みます。
非キャッシュページのアクセス
下記のgifアニメは、ブラウザがページをリクエストし、Nginxのキャッシュが空の場合に何が起こるかを示しています。

キャッシュされていないページの場合
キャッシュページのアクセス
下記のgifアニメは、コンテンツ(HTMLとJSONの両方)がキャッシュされた後のリクエストの処理方法を示しています。

キャッシュされているページの場合
キャッシュのパージ
下記のgifアニメは、新しい記事がWordPressで公開されるとどうなるかを示しています。

新しい記事が公開された場合
始めてみよう
このデモはDockerアプリとして提供されています。DockerとDocker Composeがコンピュータにインストールされていない場合はインストールしてください。WindowsとmacOSでは、ポート8000を有効にする必要があります。
コマンドプロンプトで、npm installまたはnpm ciを実行します。すべてのライブラリがダウンロードされたら、npm run start-serverを実行します。DockerはDocker Hubから4つの公式イメージ(WordPress、MariaDB、Nginx、Node.js)をダウンロードします。
サービスが起動して実行されたら、http://localhost:8000/wp-admin/にアクセスします。すると、WordPressのインストールページが表示されます。テストサイトに関する情報を入力して、管理者用のアカウントを作成します。その後ログインして、設定のパーマリンク設定に移動します。共通設定からURLスキーマをどれか1つ選択してください。
次に、プラグインの新規追加に移動します。Proxy Cache Purgeをインストールし、有効化します。サイドバーに表示されるので、クリックした後、下部のカスタムIPを172.129.0.3に設定します。これはわたし達のNode.jsサービスのアドレスです。
ブラウザの別タブを開き、http://localhost:8000/にアクセスすると、テストサイトが表示されます。

テストサイトの表示
テストサイトが無事表示されているのを確認した後、管理画面に戻り、記事を新規に追加します。約30秒後にその記事は自動的にトップページに表示されます。

記事を新規に追加
コードがデバッグモードで実行されていることを確認するには、npm run watchを実行します。クライアント側のコードは変更があるたびに再構築されます。
テストサイトにダミーデータを入力するには、FakerPressプラグインをインストールしてください。
テストサーバーをシャットダウンするには、npm run stop-serverを実行します。Dockerボリュームを削除するには、npm run remove-serverを実行します。
WordPressを実行している本番用サイトがある場合は、フロントエンドのデモでその内容がどのように見えるかを確認できます(ただし、RESTインターフェースが公開され、パーマリンクが有効になっている場合)。docker-compose-remote.ymlを開き、環境変数WORDPRESS_HOSTをサイトのアドレスに変更します。そして、npm run start-server-remoteを実行します。
Nginxの設定
Nginxの設定ファイルを確認してみましょう。最初の2行は、キャッシュのパス、キャッシュの最大値(1GB)、非アクティブのエントリの保持期間(7日)を設定しています。
| 
					 1 2  | 
						proxy_cache_path /var/cache/nginx/data keys_zone=data:10m max_size=1g inactive=7d; proxy_temp_path /var/cache/nginx/tmp;  | 
					
proxy_cache_pathはレベルなしで指定されるので、ファイルはフラットなディレクトリ構造に格納されます。これにより、キャッシュをスキャンしやすくなります。 proxy_temp_pathはキャッシュと同じボリューム上の場所に設定されているため、Nginxは名前の変更操作でファイルをそこに移動できます。
次のセクションでは、WordPress管理者ページのプロキシを設定します
| 
					 1 2 3 4 5 6 7 8 9 10  | 
						location ~ ^/wp-* {     proxy_pass http://wordpress;     proxy_set_header Host $http_host;     proxy_set_header X-Real-IP $remote_addr;     proxy_set_header X-Forwarded-For $remote_addr;     proxy_set_header X-Forwarded-Host $server_name;     proxy_set_header X-Forwarded-Proto $scheme;     proxy_pass_header Set-Cookie;     proxy_redirect off; }  | 
					
次のセクションでは、NginxとNodeとのやりとりを制御します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14  | 
						location / {     proxy_pass http://node;     proxy_set_header Host $http_host;     proxy_cache data;     proxy_cache_key $uri$is_args$args;     proxy_cache_min_uses 1;     proxy_cache_valid 400 404 1m;     proxy_ignore_headers Vary;     add_header Access-Control-Allow-Origin *;     add_header Access-Control-Expose-Headers X-WP-Total;     add_header X-Cache-Date $upstream_http_date;     add_header X-Cache-Status $upstream_cache_status; }  | 
					
先ほどproxy_cacheディレクティブで定義したキャッシュのゾーンを選択します。proxy_cache_keyを使って、キャッシュのキーを設定します。パスとクエリ文字列のMD5ハッシュは、キャッシュされた各サーバー応答を保存するために使用される名前になります。proxy_cache_min_usesディレクティブを使用して、Nginxに最初のリクエストでキャッシュを開始するように指示します。proxy_cache_validディレクティブを使用して、Nginxに1分間エラー応答をキャッシュするように依頼します。
proxy_ignore_headersディレクティブは、同じURLへのリクエストが異なるAccept-Encodingヘッダを持っている場合にNginxが別々のキャッシュエントリを作成しないようにするためのものです(例えば、追加の圧縮方法)。
add_headerを使用して追加された最初の2つのヘッダは、CORSを有効にするためのものです。最後の2つのX-Cache-*ヘッダはデバッグ用です。ブラウザのデベロッパーツールを使用してリクエストを調べると、リクエストによってキャッシュヒットが発生したかどうかを確認できます。

デベロッパーツールで確認
バックエンドのJavaScript
HTMLページの生成
Expressハンドラ(index.js)は、NginxがHTMLページを要求したときに呼び出されます。ページのナビゲーションはクライアントサイドで処理されるので、これはめったに起こらないはずです。ビジタ-のほとんどはルートページからサイトに入るでしょう、そして必然的にキャッシュされます。
ハンドラは、リモートエージェントが検索エンジンのスパイダーであるか検出し、それに応じてリクエストを処理します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20  | 
						async function handlePageRequest(req, res, next) {     try {         let path = req.url;         let noJS = (req.query.js === '0');         let target = (req.isSpider() || noJS) ? 'seo' : 'hydrate';         let page = await PageRenderer.generate(path, target);         if (target === 'seo') {             // not caching content generated for SEO             res.set({ 'X-Accel-Expires': 0 });         } else {             res.set({ 'Cache-Control': CACHE_CONTROL });             // remember the URLs used by the page             pageDependencies[path] = page.sourceURLs;         }         res.type('html').send(page.html);     } catch (err) {         next(err);     } }  | 
					
PageRenderer.generate()(page-renderer.js)は、Isomorphic Reactコードを使用してページを生成します。Fetch APIはNode.jsには存在しないため、データソースに互換性のある機能を提供する必要があります。これを利用して、フロントエンドがアクセスするURLのリストを取得します。後で、このリストを使用して、キャッシュされたページが古くなったかどうかを判断します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25  | 
						async function generate(path, target) {     console.log(`Regenerating page: ${path}`);     // retrieve cached JSON through Nginx     let host = NGINX_HOST;     // create a fetch() that remembers the URLs used     let sourceURLs = [];     let fetch = (url, options) => {         if (url.startsWith(host)) {             sourceURLs.push(url.substr(host.length));             options = addHostHeader(options);         }         return CrossFetch(url, options);     };     let options = { host, path, target, fetch };     let rootNode = await FrontEnd.render(options);     let appHTML = ReactDOMServer.renderToString(rootNode);     let htmlTemplate = await FS.readFileAsync(HTML_TEMPLATE, 'utf-8');     let html = htmlTemplate.replace(`<!--REACT-->`, appHTML);     if (target === 'hydrate') {         // add <noscript> tag to redirect to SEO version         let meta = `<meta http-equiv=refresh content="0; url=?js=0">`;         html += `<noscript>${meta}</noscript>`;     }     return { path, target, sourceURLs, html }; }  | 
					
FrontEnd.render()は、プレーンなHTMLの子要素を含むReactElementを返します。React DOMサーバーを使って、それを実際のHTMLテキストに変換します。そして、HTMLテンプレートに貼り付けます。そこでは、ルートReactコンポーネントをホストする要素の中にHTMLのコメントがあります。
FrontEnd.render()は、フロントエンドのBootstrapのコードによってエクスポートされた関数です。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24  | 
						async function serverSideRender(options) {     let basePath = process.env.BASE_PATH;     let dataSource = new WordpressDataSource({         baseURL: options.host + basePath + 'json',         fetchFunc: options.fetch,     });     dataSource.activate();     let routeManager = new RouteManager({         routes,         basePath,     });     routeManager.addEventListener('beforechange', (evt) => {         let route = new Route(routeManager, dataSource);         evt.postponeDefault(route.setParameters(evt, false));     });     routeManager.activate();     await routeManager.start(options.path);     let ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: options.target });     return harvest(ssrElement); } exports.render = serverSideRender;  | 
					
コードはデータソースとルートマネージャを起動します。これらを使用して、ルートのReact要素<FrontEnd />を作成します。関数harvest()は、プレーンなHTML要素になるまでコンポーネントツリーを再帰的にレンダリングします。

コンポーネントツリーを再帰的にレンダリング
フロントエンドはRelaksの助けを借りて構築されています。RelaksはReactコンポーネントのrenderメソッド内で非同期呼び出しを可能にするライブラリです。データの取得はレンダリングサイクルの一部として行われます。このモデルはSSRを非常に簡単にします。ページをレンダリングするには、そのすべてのコンポーネントのrenderメソッドを呼び出して、それらが終了するのを待ちます。
JSONデータの検索
下記のハンドラは、NginxがJSONファイルをリクエストした時(キャッシュミスが発生した時)に呼び出されます。非常にシンプルで、URLのプレフィックスを/json/から/wp-json/に変更し、HTTPヘッダをいくつか設定するだけです。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15  | 
						async function handleJSONRequest(req, res, next) {     try {         // exclude asterisk         let root = req.route.path.substr(0, req.route.path.length - 1);         let path = `/wp-json/${req.url.substr(root.length)}`;         let json = await JSONRetriever.fetch(path);         if (json.total) {             res.set({ 'X-WP-Total': json.total });         }         res.set({ 'Cache-Control': CACHE_CONTROL });         res.send(json.text);     } catch (err) {         next(err);     } }  | 
					
JSONRetriever.fetch()json-retrievever.js)は、WordPressからJSONデータをダウンロードし、不正なプラグインを処理するためにエラー訂正を実行します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26  | 
						async function fetch(path) {     console.log(`Retrieving data: ${path}`);     let url = `${WORDPRESS_HOST}${path}`;     let res = await CrossFetch(url);     let resText = await res.text();     let object;     try {         object = JSON.parse(resText);     } catch (err) {         // remove any error msg that got dumped into the output stream         if (res.status === 200) {             resText = resText.replace(/^[^\{\[]+/, '');             object = JSON.parse(resText);         }     }     if (res.status >= 400) {         let msg = (object && object.message) ? object.message : resText;         let err = new Error(msg);         err.status = res.status;         throw err;     }     let total = parseInt(res.headers.get('X-WP-Total'));     removeSuperfluousProps(path, object);     let text = JSON.stringify(object);     return { path, text, total }; }  | 
					
不要なフィールドは、JSONオブジェクトが再度文字列化される前に取り除かれます。
パージリクエスト処理
新しい記事がWordPressで公開されると、Proxy Cache PurgeはPURGEリクエストを送信します。Nodeがこれらのリクエストを受け取るようにシステムを構成しました。パージを実行する前に、リクエストが本当にWordPressからのものかどうかを確認します。それはURLかワイルドカード表現を与えるかもしれません。
プラグインがキャッシュ全体を消去したい場合と、単一のJSONオブジェクトを消去したい場合の2つのシナリオについて説明します。後者の場合は、影響を受ける可能性のあるすべてのクエリを削除します
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47  | 
						async function handlePurgeRequest(req, res) {     // verify that require is coming from WordPress     let remoteIP = req.connection.remoteAddress;     res.end();     let wordpressIP = await dnsCache.lookupAsync(WORDPRESS_HOST.replace(/^https?:\/\//, ''));     if (remoteIP !== `::ffff:${wordpressIP}`) {         return;     }     let url = req.url;     let method = req.headers['x-purge-method'];     if (method === 'regex' && url === '/.*') {         pageDependencies = {};         await NginxCache.purge(/.*/);         await PageRenderer.prefetch('/');     } else if (method === 'default') {         // look for URLs that looks like /wp-json/wp/v2/pages/4/         let m = /^\/wp\-json\/(\w+\/\w+\/\w+)\/(\d+)\/$/.exec(url);         if (!m) {             return;         }         // purge matching JSON files         let folderPath = m[1];         let pattern = new RegExp(`^/json/${folderPath}.*`);         await NginxCache.purge(pattern);         // purge the timestamp so CSR code knows something has changed         await NginxCache.purge('/.mtime');         // look for pages that made use of the purged JSONs         for (let [ path, sourceURLs ] of Object.entries(pageDependencies)) {             let affected = sourceURLs.some((sourceURL) => {                 return pattern.test(sourceURL);             });             if (affected) {                 // purge the cached page                 await NginxCache.purge(path);                 delete pageDependencies[path];                 if (path === '/') {                     await PageRenderer.prefetch('/');                 }             }         }     } }  | 
					
例えば、PURGE /wp-json/wp/v2/posts/100/を受け取ったときは、/json/wp/v2/posts.*のパージを実行します。このアプローチはかなり保守的です。必要がない時は、エントリーはパージされます。データはかなり速くリロードされるので、それほどひどくありません。電子タグはコンテンツに基づいているため、実際には変更が行われていない場合は同じ電子タグになります。バックエンドのキャッシュミスがあっても、Nginxはブラウザに304 not modifiedを送ります。
JSONデータを消去した後、/.mtimeタイムスタンプのファイルを消去します。これは、データクエリを再実行する時が来たというブラウザへの合図として機能します。
それから、パージされたデータを利用し、以前に生成されたHTMLファイルをパージします。handlePageRequest()でソースURLのリストを保存した方法を思い出してください。
キャッシュのパージをサポートしているのは、Nginx Plus(Nginxの有料版)だけです。NginxCache.purge()(nginx-cache.js)は基本的にそのための回避策です。コードはそれほど効率的ではありませんが、機能は果たします。うまくいけば将来、キャッシュのパージはNginxの無料版で利用可能になるかもしれません。
タイムスタンプ処理
タイムスタンプのリクエスト処理は非常に簡単です。
| 
					 1 2 3 4 5 6 7 8 9 10  | 
						async function handleTimestampRequest(req, res, next) {     try {         let now = new Date;         let ts = now.toISOString();         res.set({ 'Cache-Control': CACHE_CONTROL });         res.type('text').send(ts);     } catch (err) {         next(err);     } }  | 
					
フロントエンドのJavaScript
DOM hydration
下記の関数(main.js)はフロントエンドのブートストラップを担当します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52  | 
						async function initialize(evt) {     // create data source     let host = process.env.DATA_HOST || `${location.protocol}//${location.host}`;     let basePath = process.env.BASE_PATH;     let dataSource = new WordpressDataSource({         baseURL: host + basePath + 'json',     });     dataSource.activate();     // create route manager     let routeManager = new RouteManager({         routes,         basePath,         useHashFallback: (location.protocol !== 'http:' && location.protocol !== 'https:'),     });     routeManager.addEventListener('beforechange', (evt) => {         let route = new Route(routeManager, dataSource);         evt.postponeDefault(route.setParameters(evt, true));     });     routeManager.activate();     await routeManager.start();     let container = document.getElementById('react-container');     if (!process.env.DATA_HOST) {         // there is SSR support when we're fetching data from the same host         // as the HTML page         let ssrElement = createElement(FrontEnd, { dataSource, routeManager, ssr: 'hydrate' });         let seeds = await harvest(ssrElement, { seeds: true });         plant(seeds);         hydrate(ssrElement, container);     }     let csrElement = createElement(FrontEnd, { dataSource, routeManager });     render(csrElement, container);     // check for changes periodically     let mtimeURL = host + basePath + '.mtime';     let mtimeLast;     for (;;) {         try {             let res = await fetch(mtimeURL);             let mtime = await res.text();             if (mtime !== mtimeLast) {                 if (mtimeLast) {                     dataSource.invalidate();                 }                 mtimeLast = mtime;             }         } catch (err) {         }         await delay(30 * 1000);     } }  | 
					
このコードはデータソースとルートマネージャを作成します。SSRが採用されている場合は、すでにページ内にあるDOM要素を「hydrate」します。最初に、サーバーで行われたのと同じ一連のアクションを実行します。そうすることで、ビジターがSSR HTMLを見ている間に、後でCSRに必要となるデータが取り込まれます。harvest()に{seeds:true}を渡すと、リスト内の非同期のRelaksコンポーネントのコンテンツを返すようになります。「seeds」がRelaksに置かれ、非同期コンポーネントがその初期の外観を同期的に返すことができます。このステップがないと、非同期レンダリングに必要なわずかな遅延によって、hydrationプロセス中にミスマッチが発生します。
DOMがhydrateされると、2つ目の<FrontEnd />要素をレンダリングすることでCSRへの移行を完了します。今回はssrを使用しません。
その後、30秒ごとにコンテンツが更新するため、サーバーは無限ループに入ります。
ルーティング
フロントエンドでWordPressのパーマリンクを正しく処理することを望みます。単純なパターンマッチに頼ることはできないので、これはページのルーティングを少し複雑にします。/hello-world/のようなURLは、ページ、投稿、タグのリストを指す可能性があります。これはすべてスラッグの割り当てに依存します。正しいルートを見つけるためには、常にサーバーからの情報が必要となります。
relaks-route-managerはこのシナリオを考慮して設計されていません。ただし、ルート変更の前に非同期操作を実行することは意味があります。beforechangeイベントを発行する時、約束が満たされるまで(変更を許可しています)デフォルトのアクションを延期するためにevt.postponeDefault()を呼び出すことができます。
| 
					 1 2 3 4  | 
						routeManager.addEventListener('beforechange', (evt) => {     let route = new Route(routeManager, dataSource);     evt.postponeDefault(route.setParameters(evt, true)); });  | 
					
route.setParameters()(routing.js)は基本的にデフォルトのパラメータ抽出メカニズムを置き換えます。ルーティングのテーブルは下記のようになります。
| 
					 1 2 3  | 
						let routes = {     'page': { path: '*' }, };  | 
					
これは、どのURLにも一致します。
route.setParameters()自体がroute.getParameters()を呼び出して、パラメータを取得します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14  | 
						async setParameters(evt, fallbackToRoot) {     let params = await this.getParameters(evt.path, evt.query);     if (params) {         params.module = require(`pages/${params.pageType}-page`);         _.assign(evt.params, params);     } else {         if (fallbackToRoot) {             await this.routeManager.change('/');             return false;         } else {             throw new RelaksRouteManagerError(404, 'Route not found');         }     } }  | 
					
重要なパラメータはpageTypeです。これは、ページコンポーネントの1つをロードするために使用されます。
一見すると、route.getParameters()(routing.js)は非効率的に見えるかもしれません。URLがページを指しているかどうかを確認するには、すべてのページを取得し、そのうちの1つにそのURLがあるかどうかを確認します。
| 
					 1 2 3 4 5  | 
						let allPages = await wp.fetchPages(); let page = _.find(allPages, matchLink); if (page) {    return { pageType: 'page', pageSlug: page.slug, siteURL }; }  | 
					
カテゴリでも同じチェックをします。
| 
					 1 2 3 4 5  | 
						let allCategories = await wp.fetchCategories(); let category = _.find(allCategories, matchLink); if (category) {     return { pageType: 'category', categorySlug: category.slug, siteURL }; }  | 
					
ほとんどの場合、問題のデータはすでにキャッシュされているでしょう。トップのナビはページをロードし、サイドのナビはカテゴリ(トップのタグも)をロードします。ルートを解決するために実際のデータ転送は必要ありません。コールドスタートでは、このプロセスはやや遅くなります。しかし、SSRのメカニズムはこの遅れを覆い隠します。ビジターは、気にならないと思います。もちろん、すべてのページが用意されているので、ビジターがナビゲーションバーをクリックするとすぐにページがポップアップします。
route.getObjectURL()(routing.js)は、オブジェクト(投稿、ページ、カテゴリなど)へのURLを取得するために使用されます。この方法は、オブジェクトのパーマリンクからサイトのURLを削除するだけです。
| 
					 1 2 3 4 5 6 7 8 9  | 
						getObjectURL(object) {     let { siteURL } = this.params;     let link = object.link;     if (!_.startsWith(link, siteURL)) {         throw new Error(`Object URL does not match site URL`);     }     let path = link.substr(siteURL.length);     return this.composeURL({ path }); }  | 
					
投稿にリンクするには、その投稿を事前にダウンロードする必要があります。記事をクリックすると、すぐに記事が表示されます。
カテゴリとタグへのリンクは、明示的な先読みを実行します。
| 
					 1 2 3 4 5  | 
						prefetchObjectURL(object) {     let url = this.getObjectURL(object);     setTimeout(() => { this.loadPageData(url) }, 50);     return url; }  | 
					
最初の10件の投稿は常に取得されるため、ビジターはクリック後すぐに何かを見ることができます。
Welcomeページ
WelcomePage(welcome-page.jsx)は非同期コンポーネントです。renderAsync()メソッドは、投稿のリストを取得し、WelcomePageSyncに渡して、実際のユーザインタフェースをレンダリングします。
| 
					 1 2 3 4 5 6 7 8 9  | 
						async renderAsync(meanwhile) {     let { wp, route } = this.props;     let props = { route };     meanwhile.show(<WelcomePageSync {...props} />)     props.posts = await wp.fetchPosts();     meanwhile.show(<WelcomePageSync {...props} />)     props.medias = await wp.fetchFeaturedMedias(props.posts, 10);     return <WelcomePageSync {...props} />; }  | 
					
WelcomePageSyncは、投稿リストのレンダリングタスクをPostListに委任します。
| 
					 1 2 3 4 5 6 7 8  | 
						render() {     let { route, posts, medias } = this.props;     return (         <div className="page">             <PostList route={route} posts={posts} medias={medias} minimum={40} />         </div>     ); }  | 
					
投稿リスト
PostList(post-list.jsx)のレンダリング方法は、特別なことは何もしません。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16  | 
						render() {     let { route, posts, medias } = this.props;     if (!posts) {         return null;     }     return (         <div className="posts">         {             posts.map((post) => {                 let media = _.find(medias, { id: post.featured_media });                 return <PostListView route={route} post={post} media={media} key={post.id} />             })         }         </div>     ); }  | 
					
コンポーネントに関して注目すべき唯一の点は、スクロール時にデータロードを実行するということです。
| 
					 1 2 3 4 5 6 7 8 9  | 
						handleScroll = (evt) => {     let { posts, maximum } = this.props;     let { scrollTop, scrollHeight } = document.body.parentNode;     if (scrollTop > scrollHeight * 0.5) {         if (posts && posts.length < maximum) {             posts.more();         }     } }  | 
					
Cordova
これはボーナス セクションです。
あなたがCordovaで、cheapskateモバイルアプリを作成する方法を示します。
始めるには、Android StudioまたはXcodeをインストールしてください。その後、コマンドラインでnpm install -g cordova-cliを実行します。relaks-wordpress-example/cordova/sample-appに移動し、cordova prepare andoirdまたはcordova prepare iosを実行します。Android StudioまたはXcodeで新しく作成したプロジェクトを開きます。relaks-wordpress-example/cordova/sample-app/platform/[android | ios]にそれを見つけるでしょう。
リポジトリのCordovaコードはhttps://et.trambar.ioからデータを取得します。場所を変更するには、環境変数CORDOVA_DATA_HOSTを目的のアドレスに設定し、npm run buildを実行します。
終わりに
ここで紹介した方法が、あなたに新しいインスピレーションを与えることを願っています。WordPressは古いソフトウェアですが、コードに少し手を加えることで、エンドユーザーのエクスペリエンスを大幅に向上させることができます。
デモのシステムは、最初のロードが速く感じるでしょう。ページ間の遷移も速く感じます。さらに重要なことは、このシステムの運用コストが安いということです。
このコンセプトは、WordPress固有のものではありません。特にサーバーサイドレンダリング(SSR)は、単一ページのWebアプリケーションにとって非常に便利なテクニックです。ロード時間への悪影響を気にすることなく、JavaScriptライブラリを使用してプロジェクトを強化できます。
sponsors











