「Google Feed API」を使わず、XMLHttpRequestでクロスドメインのRSSを取得して最新記事一覧を表示させるスクリプトを書いたんですが、その際にわからなかったことをまとめておきます。JavaScriptに関しては下に書きますが、jQueryは使っていません。
RSS取得後のネームスペース付きXMLからデータを抽出する方法はクロスドメインとは別の問題なので、他のページ「querySelectorでIEでもそれ以外でもネームスペース付のXMLからデータを取得する方法」にまとめています。
クロスドメインのRSSを取得するサーバー構築
需要があるかどうかは別にして、単純に自ドメインのRSSを取り込むだけなら通常通りのやり方で問題ないんですが、クロスドメインになるとアクセス拒否されます。そういった問題に対する手段として「Google Feed API」がありますけど、先日一時的に「Google Feed API」が使えないという騒動になりました。外部サービスに頼るとそのサービスが止まった時に困るという事で、自前で外部RSSを中継するサーバーを立てました。
中継サーバーは数行のスクリプトで出来るんですが、PHPの場合なら次のようになると思います。
<?php // GETで ?rss=URL という形でリクエストされる場合 // GETのパラメーターにrssがなかったら // エラーステータスを返して終了 if(!isset($_GET['rss'])){ header('HTTP/1.0 400 Bad Request'); exit(); } $url = rawurldecode($_GET['rss']); // $_GET['rss']の文字列がURLのパターンにマッチしてなかったら // エラーステータスを返して終了 if(!preg_match('|^(\S+?:/)?/\S+?\.\S+?/\S+$|', $url)){ header('HTTP/1.0 400 Bad Request'); exit(); } $host = parse_url($url, PHP_URL_HOST); // 指定のアドレスが存在しない場合 // エラーステータスを返して終了 if(@gethostbyname($host) == $host){ header('HTTP/1.0 400 Bad Request'); exit(); } // 指定されたアドレスのデータを取得 $rss = @file_get_contents($url); // 注) // file_get_contents自体のトラブルで通信不能でも // 通信出来てて200以外の場合でもfalseが返る。 // なので下はレスポンスヘッダを調べている // レスポンスヘッダがなかったら通信できていない // エラーステータス(500)を返して終了 if(!isset($http_response_header)){ header('HTTP/1.0 500 Internal Server Error'); exit(); } $response = array_shift($http_response_header); // レスポンスヘッダが200 OKでないなら // そのステータス(おそらく500以外)を返して終了 if(false === strpos($response, '200')){ header($response); exit(); } $content_type = null; // レスポンスヘッダのContent-Typeを調べる foreach($http_response_header as $line){ if(false !== strpos(strtolower($line), 'content-type:')){ if(false !== strpos($line, 'xml')){ $content_type = $line; } break; } } // 取得したファイルがxmlでない(RSSでない)なら // エラーステータスを返して終了 if(!$content_type){ header('HTTP/1.0 400 Bad Request'); exit(); } // 取得したRSS(と思われるxml)をそのまま送信 header('HTTP/1.0 200 OK'); header($content_type); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET'); print($rss); /* PHPのみの単体ファイルなので終了タグ ?> は不要 PHPマニュアル http://php.net/manual/ja/language.basic-syntax.phptags.php より 「ファイル全体が純粋な PHP コードである場合は、ファイルの最後の終了タグは省略するのがおすすめです。」 */
もし特定ドメインからのアクセスしか許可しないなら、file_get_contents の結果がxmlなのかどうかをチェックせずにそのまま返してもいいのかも知れませんが、今回はアクセス制限をしないので悪用防止でxml以外は返さないようにしています。
ここで重要なのは赤文字の2行です。これがないとクロスドメイン問題が解消されません。
注)このスクリプトを置くURLと同じドメインからのアクセスなら赤文字2行がなくても通信できます。そうではなく、他のドメインとも通信する場合には必要です。例えばこのスクリプトにアクセスして特定サイトの最新記事一覧を取得するブログパーツを配布する場合などは、どのURLのブログからアクセスが来るかわかりません。そういう時には赤文字の2行が必要です。
HTTP access control
HTTP access control (CORS) | MDN
上のサイトに詳しく書いてあるんですが、XMLHttpRequestでクロスドメイン間通信を行うには、サーバー側でそれを許可する旨のヘッダーを送信しないといけません。そのための仕様が「Cross-Origin Resource Sharing (CORS) 」というらしくて、XMLHttpRequest以外にもWebフォントなどを特定ドメインだけに限定して許可したりできます。
今回は説明を簡単にするためにPHPのheader関数で送信していますが、.htaccessに書くことができます。
上の2行を.htaccessに書く場合はこうなります。
Header append Access-Control-Allow-Origin: * Header append Access-Control-Allow-Methods: GET
Access-Control-Allow-Origin
Access-Control-Allow-Originには、アクセスを許可するURIを指定します。
たとえば、http://example.com にだけアクセスを許すのであれば、次のように書きます。
Access-Control-Allow-Origin: http://example.com
アクセスを許可するサイトを複数指定することも出来なくはないのですが、工夫が必要みたいです。
参考ページ「Access-Control-Allow-Originヘッダで複数のオリジンドメインを許可する方法 – ぷれすとぶろぐ」
アドレスを指定せず、すべてのURIに解放するのであれば、上のようにワイルドカード * を使います。次の場合は、すべてのURIからのアクセスを許可することになります
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods
Access-Control-Allow-Methodsには、アクセスに許可するメソッドを指定します。
GET通信のみ許可する場合は次のようになります。
Access-Control-Allow-Methods: GET
Access-Control- で始まるヘッダは他にもあるんですが、この2つの指定だけでもXMLHttpRequestでの通信は出来るようになります。
JavaScript側の対応
サーバー側で上記の設定をしていれば、JavaScript側では普通にXMLHttpRequest通信をするだけです。
WindowsのInternetExplorerだけは、XMLHttpRequest ではなく XDomainRequest でないとクロスドメイン間通信は出来ないと書いてあるサイトが多かったのですが今は大丈夫みたいです。
というかInternetExplorer11(バージョン: 11.0.9600.18124)では window.XDomainRequest が見つかりませんでした。次のようにして試してみたらIE11でもそれ以外でも2が表示されます。
if(window.XDomainRequest){ alert(1); }else if(window.XMLHttpRequest){ alert(2); }
InternetExplorer11でも window.XMLHttpRequest が返ってくるんですがクロスドメイン間通信は出来ました。ただ、古いバージョンのIEではダメでしょうから、結論としてJavaScriptのXMLHttpRequestでRSSを取得するには下のようになります。
ただし、XMLHttpRequest Level2に対応していないブラウザ(クロスドメイン間通信自体が出来ないブラウザ)ではcallback関数は呼ばれませし、エラーメッセージも表示しません。
私は「サイドバーに最新記事一覧を表示させるブログパーツ」に使ったので、未対応ブラウザではエラー表示をするよりも、パーツのタイトルも含めて一切何も表示させない方がユーザーが混乱しないと思ったので。
jQueryを使わずXMLHttpRequestでRSSを取得するサンプル
/* 取得したいRSSが http://example.com/index.rdf で 先ほどのサーバーが http://hoge.dwm.me/ の場合 */ function callback(){ /* ここに取得したRSSの処理を記述 */ } var rss_url = 'http://example.com/index.rdf'; var url = 'http://hoge.dwm.me/?rss=' + encodeURIComponent(rss_url); if(window.XDomainRequest){ var xhr = new XDomainRequest(); xhr.onload = function(){callback();}; xhr.onerror = function(){} xhr.open('GET', url, true); xhr.send(''); }else if(window.XMLHttpRequest){ var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(){ if(this.readyState == 4 && this.status == 200){ callback(); } }; xhr.open('GET', url, true); xhr.send(''); }
これでRSSのxml自体は取得できます。後は取得したデータを処理するcallback関数の中身を書くだけです。
RSSはネームスペース付きXMLなのでquerySelectorでうまくデータが取得できなかったんですが、JavaScriptでネームスペース付きXMLからデータを取得する方法がわかれば解決します。
それに関してはXMLHttpRequestでのクロスドメイン通信とは違う話なので別のページ「querySelectorでIEでもそれ以外でもネームスペース付のXMLからデータを取得する方法」にまとめました。必要ならそちらをご覧ください。取得したRSSから各ページの最初の画像も抽出して一覧表示するサンプルを掲載しています。
取得するURLが同ドメインかクロスドメインか判断して取得先を振り分けるJavaScript
取得するRSSのURLが固定なら問題ないですが、場合によって同じドメインのRSSを取りに行ったり別ドメインのRSSを取りに行ったりする場合、同じドメインなのに上の自作サーバーを経由するのはムダです。
取得するURLに自ドメインと別ドメインが混在する場合は、JavaScript側でアクセスする先を切り替えるようにします。次はその一例です。
function callback(rss){
/* ここに取得したRSSの処理を記述 */
}
function get_rss(url){
var rss_server = 'ここに自作RSS中継サーバーのURL';
var pattern = /^(\S+?:\/)?\/([^\/\s]+).*$/;
var server_domain = rss_server.replace(pattern, '$2');
var location = window.location.href.replace(pattern, '$2');
/*
指定されたRSSのドメインが閲覧中のページのドメインと
同じならそのままRSSが取得できるので
違う場合のみ中継サーバーを使う。
*/
if(server_domain != location){
url = rss_server + '?rss=' + encodeURIComponent(url);
}
if(window.XDomainRequest){
var xhr = new XDomainRequest();
xhr.onload = function(){callback(this.responseXML);};
xhr.onerror = function(){}
xhr.open('GET', url, true);
xhr.send('');
}else if(window.XMLHttpRequest){
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(this.readyState == 4 && this.status == 200){
callback(this.responseXML);
}
};
xhr.open('GET', url, true);
xhr.send('');
}
}
get_rss('http://example.com/index.rdf');
取得したデータのContent-Typeを調べるにはgetResponseHeaderを使う
今回のスクリプトでは使っていませんが、もし取得したデータのレスポンスヘッダを見るにはgetResponseHeaderメソッドを使います。Content-Typeを取得したい場合は次のようにします。
var content_type = xhr.getResponseHeader('Content-Type');