REST APIとAngularJSを使って、WordPressの投稿を編集できるSPA(Single Page Application)を作る「AngularJSとREST APIでWordPressのSPA化を先取りしてみた!」の続きです。
クライアント
Yeomanを使ってAngularアプリのボイラープレートを制作します。このアプリはngCookiesモジュールを使ってクッキーを取得し、簡単なフォームを表示し、ログインユーザーが正しいと認証されたら、$httpモジュールを使って投稿を取得、編集します。
APIを呼び出すためにAngular WP APIライブラリを入れられることを覚えておいてください。しかし、シンプルにするため、またWordPressにはどのようなものであれ組み込めるということを示すためにデフォルトの$httpサービスを使います。カスタマイズしたAjaxコールも使えます。通常、AngularではリクエストをAPIに送信するための機能は異なったサービス上に置きますが、記事ではシンプルにコントローラーの中で利用します。
クライアント仮想マシンの作成
VagrantcloudのUbuntu 14.04 Server vagrantボックスを使用し、SitePointの記事を見ながらアプリケーションの土台を作成します。
Scotchboxをインストールしたフォルダーに戻ります。私の場合、/boxes/sitepoint-wp-rest-apiです。Angularアプリ仮想マシン用の新規フォルダーを作成します。
cd /boxes
mkdir sitepoint-wp-angular-app
cd sitepoint-wp-angular-app
# this will create a Vagrantfile which we will modify a bit and then run vagrant up
vagrant init ubuntu/trusty64
新しく作成されたVagrantfileを開くとコメント付きの行が表示されます。すべてのコードを削除し、以下のコードに置き換えます。
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.network "private_network", ip: "172.31.255.253"
config.vm.hostname = "app.sitepoint-wp-rest-api.test"
config.vm.synced_folder ".", "/var/www", :mount_options => ["dmode=777", "fmode=666"]
# Optional NFS. Make sure to remove other synced_folder line too
#config.vm.synced_folder ".", "/var/www", :nfs => { :mount_options => ["dmode=777","fmode=666"] }
config.vm.provider "virtualbox" do |v|
v.memory = 1024
v.cpus = 2
end
end
基本的にはWordPressのインストールに使ったのと同じVagrantfileです。しかし、別のIPを使うためには変更が必要です。172.31.255.254はすでに使用されているからです。
使用される元の画像を(Scotchboxではなく公式のubuntu/trusty64で)変更します。大事なことを言い忘れましたが、WordPress用に使っているFQDNにアプリを追加してホスト名を変更します。こうすれば作成したメインドメインとサブドメインの両方で動作するクッキーを設定できます。
sitepoint-wp-angular-appフォルダーからvagrant upを実行します。
vagrantの設定が完了したら、起動した仮想マシンにSSH接続するためにvagrant sshを実行します。こうしてYeomanを土台としたAngularアプリを直接サーバー上に作成できます。次に、grunt接続で開発者のサーバーを実行します。これで仮想マシンで実際にapp.sitepoint-wp-rest-api.testのアプリを利用できます。
仮想マシンが2台なくてもローカルフォルダーからもこの設定ができます。しかし、その場合、ローカルマシンのport80でなにも実行されていないことを確認する必要があり、面倒です。もっと簡単で複数の設定をサポートする方法を以下で紹介します。
GruntとAngularJSの設定
Angularアプリの土台を固めるためにSitePointの記事が使えます。ただし、少し準備が必要です。
ubuntu仮想マシンにSSH接続をした端末に戻ります。ディレクトリを/var/wwwに変更し、npmをインストールします。
cd ~
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
sudo apt-get install -y nodejs
cd /var/www
ここから記事どおりにすれば基本の土台がしっかりしたアプリを取得できます。ローカルネットワーク上ではなく仮想マシン上で記事のコマンドを実行します。最初から始めてgrunt serverが実行されるまで進めてください。
記事で紹介しているコマンドによってはsudoを使って実行する必要があるかもしれませんので注意してください。また、入れ込むべきモジュールを選択するときはangular-cookies.jsやangular-route.jsをチェックし、Bootstrapや必要がないと思われるすべてのものを含めてください。また、bowerを機能させるためにはsudo apt-get install gitも実行する必要があるかもしれません。
grunt serverを実行する前にブラウザーに接続し、検索対象が見つからないとエラーを返すlivereload、または仮想マシンを必ず無効にしてください。/var/www/Gruntfile.jsでconnect:livereloadをconnect:testに変更してください。
最後に、grunt serverを実行する前にアプリのルートにある同一のGruntfile.jsの中でoptionsキーの数行下にある// The actual grunt server settingsについてのコメントを探して、ホスト名を「0.0.0.0」に、ポートを80に変更し、testキーの下にあるポートも変更します。
最終的に、接続設定は以下のようになります。
// /var/www/Gruntfile.js
// ...
// The actual grunt server settings
connect: {
options: {
port: 80,
// Change this to '0.0.0.0' to access the server from outside.
hostname: '0.0.0.0',
livereload: 35729
},
livereload: {
options: {
open: true,
middleware: function (connect) {
return [
connect.static('.tmp'),
connect().use(
'/bower_components',
connect.static('./bower_components')
),
connect().use(
'/app/styles',
connect.static('./app/styles')
),
connect.static(appConfig.app)
];
}
}
},
test: {
options: {
port: 80,
middleware: function (connect) {
return [
connect.static('.tmp'),
connect.static('test'),
connect().use(
'/bower_components',
connect.static('./bower_components')
),
connect.static(appConfig.app)
];
}
}
},
dist: {
options: {
open: true,
base: '<%= yeoman.dist %>'
}
}
},
// ...
/var/wwwフォルダー(またはアプリを入れているフォルダーならどれでも)からsudo grunt serveを実行します。ローカルネットワーク上ではなく仮想マシンでgrunt serveを実行することが重要です。というのは、app.sitepoint.local hostが無効にしたローカルIPで動作する仮想マシンからアプリを入手したいからです。
「http://app.sitepoint-wp-rest.api.test」にアクセスすればフォルダーにあるアプリが実行されているところが表示されます! さらに、SPA上のクッキーには、WordPress Webサイト上にあるのと同一のwordpress_access_tokenが表示されます!
投稿管理機能の追加
次にWordPressの投稿を表示し、ユーザーの権限次第では編集投稿ボタンを表示または隠す基本的な機能をいくつか追加する必要があります。ユーザーが編集できる場合、Saveを押すと投稿タイトルやコンテンツを更新できます。
HTMLから見ていきます。app/views/フォルダーにposts.htmlを追加します。このビューは投稿の一覧や選択した投稿の編集用のフォームを表示するために使用されます。
<!-- app/views/posts.html -->
<div class="container">
<h1>Posts</h1>
<ul>
<li ng-repeat="post in posts">
<a href="{{post.link}}"
target="_blank">{{post.title.rendered}}</a>
<a ng-click="editPost(post.id)"
ng-show="user.capabilities.edit_posts">Edit</a>
</li>
</ul>
<form name="editPostForm" id="editPost" style="display: none;" class="edit">
<label for="title">Title:</label>
<input type="text" id="title" ng-model="post.title">
<label for="content">Content</label>
<input type="text" id="content" ng-model="post.content"/>
<a ng-click="updatePost()">Save</a>
</form>
<p id="responseMessage"></p>
</div>
これはapp/scripts/app.jsファイルです。作成者によるデフォルト設定のいくつかを消去し、投稿ページのルート設定を追加します。
// app/scripts/app.js
angular
.module('publicApp', [
'ngRoute',
'ngCookies'
])
.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/posts.html',
controller: 'PostsCtrl',
controllerAs: 'posts'
})
.otherwise({
redirectTo: '/'
});
});
最後に投稿コントローラを追加する必要があります。app/scripts/controllers/の下に新規ファイルposts.jsを追加し、内部でコピーします。
// app/scripts/controllers/posts.js
angular.module('publicApp')
.controller('PostsCtrl', function ($http, $scope, $cookies, $httpParamSerializerJQLike) {
// ...
});
必要な機能を追加します。まず現在のユーザーIDを取得するためにAPIのエンドポイント、/users/meを取得します。次にそのIDを使ってユーザー権限を取得すればEditボタンを表示するべきか否かが分かります。
// app/scripts/controllers/posts.js
angular.module('publicApp')
.controller('PostsCtrl', function ($http, $scope, $cookies, $httpParamSerializerJQLike) {
var apiUrl = 'http://sitepoint-wp-rest-api.test/wp-json/wp/v2';
// Retrieve user permissions
// First get the current user id
$http({
method: 'GET',
url: apiUrl + '/users/me/?access_token=' + $cookies.get('wordpress_access_token')
}).then(function successCallback(response) {
// second API call to get more details about the current user, e.g. capabilities
$http({
method: 'GET',
url: apiUrl + '/users/' + response.data.id + '/?context=edit&access_token=' + $cookies.get('wordpress_access_token')
}).then(function successCallback(response) {
console.log(response.data);
$scope.user = response.data;
}, function errorCallback(response) {
console.log(response);
});
$scope.user = response.data;
}, function errorCallback(response) {
console.log(response);
});
});
すべての投稿の取得も必要です。
// app/scripts/controllers/posts.js
// ...
// Retrieve all posts
$http({
method: 'GET',
url: apiUrl + '/posts'
}).then(function successCallback(response) {
console.log(response.data);
$scope.posts = response.data;
}, function errorCallback(response) {
console.log(response);
});
// ...
次にEditボタンの機能を追加する必要があります。Editボタンの追加は投稿アレイで頻繁に必要になり、また選択した投稿タイトルやコンテンツを編集のためにフォームへ流し込みます。
// app/scripts/controllers/posts.js
// ...
// Edit post button
$scope.editPost = function(id) {
document.getElementById('editPost').style.display = 'block';
$scope.post.title = '';
$scope.post.content = '';
$scope.post.id = id;
for (var i = 0; i < $scope.posts.length; i++) {
if ($scope.posts[i].id === id) {
$scope.post.title = $scope.posts[i].title.rendered;
$scope.post.content = $scope.posts[i].content.rendered;
}
}
};
// ...
また、POSTリクエストを/postsエンドポイントにトリガーする保存機能も必要です。Saveボタンをクリックするとタイトルやコンテンツの値を変更できる機能です。
// app/scripts/controllers/posts.js
// ...
// Update post
$scope.updatePost = function() {
$http({
url: apiUrl + '/posts/' + $scope.post.id + '/?context=edit&access_token=' + $cookies.get('wordpress_access_token'),
method: "POST",
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
data: $httpParamSerializerJQLike({
title: $scope.post.title,
content: $scope.post.content
})
}).then(function successCallback(response) {
console.log(response);
for (var i = 0; i < $scope.posts.length; i++) {
if ($scope.posts[i].id == $scope.post.id) {
$scope.posts[i] = response.data;
}
}
document.getElementById('editPost').style.display = 'none';
document.getElementById('responseMessage').innerHTML = 'Succesfuly updated post.';
}, function errorCallback(response) {
console.log(response);
});
};
// ...
最終的にposts.jsファイルはここのようになります。
アプリを試しに利用する前に、さらに整理をしなければなりません。app/scripts/controllers/main.jsファイルとapp/views/main.htmlを消去してください。それからindex.htmlに進み、変更を加えます。
<div ng-include="'views/main.html'" ng-controller="MainCtrl"></div>
上のコードを下のコードに変更します。
<div ng-include="'views/posts.html'" ng-controller="PostsCtrl"></div>
また、必ずindex.htmlのng-app値がapp.jsのモジュール名と一致するようにしてください。
作業完了です! これで管理者として(または投稿を編集できるその他の立場の人として)WordPressにログインできます。また、Angularアプリを見れば、1つ1つの投稿の隣にボタンが表示され編集できます。編集ボタンをクリックすればフォームの編集が開き、Saveボタンが表示されます。データは常にWordPress DBに記録されます。
自分自身で試してください。WordPressとSPAの両方で同じように見えるメニューを作成し、この2つの間でシームレスに連携できます。アプリをパスワードで完璧に保護するために、有効なクッキーを持たないユーザーがアクセスしてきてもWordPressログイン画面にリダイレクトされるので安心です。
iframeオプションの利用
先のトークンを共有するためのiframeとlocalStorageオプションを利用するためには、Angularアプリのルートフォルダーに静的HTMLファイルを作成する必要があります。
ファイルの名前をtunnel.htmlとし、以下のマークアップをペーストします。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>The Dark Portal</title>
<script>
function receiveMessage(event)
{
// Do we trust the sender of this message?
if (event.data.prefix !== "unique_key_sitepoint")
return;
for (var key in event.data) {
if (event.data.hasOwnProperty(key) && key != "unique_key_sitepoint") {
localStorage.setItem(key, event.data[key]);
}
}
}
window.addEventListener("message", receiveMessage, false);
</script>
</head>
<body>
<h1>Psst</h1>
</body>
</html>
次に以下のコードをiframeとしてWordPressに挿入します。wp-content/themes/twentysixteen-child/のfunctions.phpに戻り、以下のコードを追加します。
// wp-content/themes/twentysixteen-child/functions.php
// ...
add_action( 'genesis_footer', 'ab_share_token_iframe' );
function ab_share_token_iframe() {
echo '<iframe id="ab-login-iframe" style="display:none"></iframe>';
}
最後に重要なことが、iframeに投稿するWordPressのスクリプトをエンキューする必要があることです。スクリプトをエンキューしてローカライズし、authトークンをフロントエンドに渡してみてください。
新規のフォルダをwp-content/themes/twentysixteen-child/の下に作成し、jsと名前を付け、フォルダ内にこのコンテンツを含むmain.jsファイルを作成します(twentysixteenにはすでにjQueryが追加されています)。
// wp-content/themes/twentysixteen-child/js/main.js
(function($) {
$(document).ready(function() {
var win = document.getElementById('ab-login-iframe');
var payload = {
oauthToken: abData.accessToken,
prefix: "unique_key_sitepoint"
};
win.src = abData.src;
win.onload = function() {
win.contentWindow.postMessage(payload, "*");
}
});
})(jQuery);
次にaccess_tokenを使ってスクリプトを追加、ローカライズします。ここではJS_APP_URLグローバルを使っていることに注意してください。メインのWP設定ファイル、wp-config.phpに進み、以下のコードを追加します(WP_SITEURLの直下に追加できます)。
// wp-config.php
// ...
/** This is new, WP SiteURL and Home globals override */
define('WP_SITEURL', 'http://sitepoint-wp-rest-api.test/');
define('WP_HOME', WP_SITEURL);
define('JS_APP_URL', 'http://app.sitepoint-wp-rest-api.test');
// ...
// wp-content/themes/twentysixteen-child/functions.php
// ...
function ab_localise_scripts() {
$user_id = get_current_user_id();
$auth_token = get_user_meta( $user_id, '_access_token', true);
$data = array(
'accessToken' => $auth_token,
'src' => JS_APP_URL . '/tunnel.html'
);
wp_localize_script( 'customscripts', 'abData', $data );
}
wp_enqueue_script( 'customscripts', get_stylesheet_directory_uri() . '/assets/js/main.js', array('jquery');
ab_localise_scripts();
最後に
以上、紹介した方法を実行すれば、すばやくSPAでAPIを利用できます! この記事ではAngularJSを使いましたが、設定はほかのJSフレームワークでも利用できることを覚えておいてください。
制作中のAngularJSを利用する前に、APIプラグインのベータ版の状況を把握しておくことが重要です。また、SSLに関しても同様です。SSLを把握していないと、access_tokenを公開してしまう可能性があります。公開してしまうとユーザー名とパスワードを送信してしまうのと同じぐらい最悪な事態になります。
(原文:Single Page Apps Using AngularJS and the WordPress REST API)
[翻訳:中村文也]
[編集:Livit]