querySelectorでIEでもそれ以外でもネームスペース付きのXMLからデータを取得する方法

document.evaluateはIEで動かないんですが、RSSのようにネームスペース付きXMLファイルにIEでもそれ以外のブラウザでもquerySelectorを使った同一ソースでアクセスする方法をまとめました。jQueryを使わなくても大した長さにはなりません。

前のページ「Google Feed APIを使わずXMLHttpRequestでクロスドメインのRSSを取得する方法 まとめ」でJavaScriptのXMLHttpRequestを使ってRSSを取得するまで書きました。取得したRSSはネームスペース付きのXMLなので、どうやってデータにアクセスすればいいのかわかりませんでしたが、このページではそれを解決する方法をまとめています。jQueryは使っていません。

document.evaluateはIEで動かない

最近はIEで見ても、その他のブラウザで見ても同じに見えるので油断していました。

ググってみると「JavaScriptでデータを走査する時、namespaceを解決するにはdocument.evaluateを使う」というような感じだったので、それを参考にスクリプトを書いてGoogle Chromeで試したらちゃんと動いたんですけど、InternetExplorerでは動きません。document.evaluateはIEで実装していないらしいです。

という事はIEだけ別のソースを書かなくてはいけないのか?と思ったんですけど、querySelectorだけで両方対応出来ました。

responseTextではなくresponseXMLを使う

XMLHttpRequestで取得したデータをresponseTextで取って、それを次のようにHTMLにしてquerySelectorをかけてもうまくいきません。正確に言うと、これで取れるデータもあるんですが取れないデータがあります。

HTMLの受け皿に、下ではdivを使っていますが、ちゃんとそれ用のobjectがあったような気もします。もし、それをご存知ならそちらを使われた方がよろしいかと。。

var xhr = new XMLHttpRequest();

xhr.onreadystatechange = function(){
    if(xhr.readyState == 4 && xhr.status == 200){
        /* Bad example */
        var div = document.createElement('div');
        div.innerHTML = xhr.responseText;
        var items = div.querySelectorAll('item');
    }
};

xhr.open('GET', rss_url, true);
xhr.send('');

そうではなく、responseXMLにquerySelectorをかければ、namespaceがあってもデータが取得できます。

var items = xhr.responseXML.querySelectorAll('item');

namespaceがついたタグの指定方法

namespaceのついたタグを抽出する時は、querySelectorの引数に、namespaceを除いたタグ名を指定すればいいみたいです。

次のRSSから<dc:date>の中身を抜き出す場合を例にします。

<rdf:RDF xmlns:rdf="" xmlns:dc="" xmlns:admin="" xmlns:content="" xmlns="">
    <channel rdf:about="">
        <title></title>
        <link></link>
        <description></description>
        <dc:language></dc:language>
        <items>
            <rdf:Seq>
                <rdf:li rdf:resource=""/>
                <rdf:li rdf:resource=""/>
            </rdf:Seq>
        </items>
    </channel>
    <item rdf:about="">
        <link></link>
        <title></title>
        <description>
            ...
        </description>
        <dc:subject></dc:subject>
        <dc:creator></dc:creator>
        <dc:date>yyyy-mm-ddT00:00:00+09:00</dc:date>
        <content:encoded>
            <![CDATA[ ... ]]>
        </content:encoded>
    </item>
    <item rdf:about="">
        <link></link>
        <title></title>
        <description>
            ...
        </description>
        <dc:subject></dc:subject>
        <dc:creator></dc:creator>
        <dc:date>yyyy-mm-ddT00:00:00+09:00</dc:date>
        <content:encoded>
            <![CDATA[ ... ]]>
        </content:encoded>
    </item>
</rdf:RDF>

先ほどの

var items = xhr.responseXML.querySelectorAll('item');

これでitemの一覧は配列化されているので、それをループして各itemの中から日付を抽出したい場合はnamespaceのdcとコロンを取り除いたタグ名「date」でエレメントが取得できます。

var items = xhr.responseXML.querySelectorAll('item');

for(var i = 0; i < items.length; i++){
    var date = items[i].querySelector('date');
    alert(date.textContent); // yyyy-mm-ddT00:00:00+09:00
}

属性の取得はgetAttribute

例えば、itemタグのrdf:aboutを取得するには、items[i].about としてもダメでした。この場合はgetAttribute()に取得したい属性名を指定するんですが、こちらの場合はnamespaceも必要みたいです。

// <item rdf:about="http://example.com"></item> からrdf:aboutの値を抽出

var items = xhr.responseXML.querySelectorAll('item');

for(var i = 0; i < items.length; i++){
    alert(items[i].about);                     // undefined
    alert(items[i].getAttribute('about'));     // null
    alert(items[i].getAttribute('rdf:about')); // http://example.com
}

<![CDATA[]]>の中のテキスト抽出にはtextContentを使う

<![CDATA[]]>セクションのテキストを取り出すには、innerHTMLではなくtextContentを使います。

正確に言うと、innerHTMLでも取れなくはないですが、<![CDATA[ と ]]> のタグも含まれたテキストになります。

textContentだと、<![CDATA[ と ]]> のタグがついていません。

参考ページ「Node.textContent – Web API インターフェイス | MDN

CDATAの中は例えHTMLのソースだとしても、あくまでテキストです。そのHTML文の中にあるエレメントを抽出するにはそのソースを元にHTML化します。

例えば、<content:encoded>タグの中にあるブログの各ページの本文から<img />タグの個数を調べるには、次のような方法があります。

var items = xhr.responseXML.querySelectorAll('item');

for(var i = 0; i < items.length; i++){
    var src = items[i].querySelector('encoded').textContent;

    var div = document.createElement('div');
    div.innerHTML = src;

    var img = div.querySelectorAll('img');
    alert(img.length);
}

HTMLの受け皿に、上ではdivを使っていますが、ちゃんとそれ用のobjectがあったような気もします。もし、それをご存知ならそちらを使われた方がよろしいかと。。

XMLHttpRequestで取得したデータを一覧表示するサンプル

以下は取得したいRSSのURLを入力すると、その結果を記事内の最初の画像付きで5個表示させるサンプルです。前のページ「Google Feed APIを使わずXMLHttpRequestでクロスドメインのRSSを取得する方法 まとめ」の方法でクロスドメインに対応しています。

ただしこのソースでは簡略化のためAtomには未対応です。

Atomにも対応させるには下のソースのcallback関数を変えるだけです。このページの一番下に「RSSとAtom両対応版のcallback関数」を置いておきます。

<!-- HTML部分 -->

<p id="sample_form">
<input type="text" name="sample" value="" placeholder="RSSのURLを入力して送信して下さい" />
<input type="button" value="送信" />
</p>

<div id="result"></div>
<!-- JavaScript部分 -->

<script>

// 絵文字を除外する
// imgタグが20ピクセル以下に指定されていたらfalse
// heightもwidthも未指定または20ピクセル以上ならtrue
function image_size_check(img){
    if(img.height && 20 >= img.height){
        return false;
    }

    if(img.width && 20 >= img.width){
        return false;
    }

    return true;
}

// 本文中から画像を抽出
function get_image(content, link){
  // contentの中のimgタグを全部取得
  var div = document.createElement('div');
  div.innerHTML = content;
  var images = div.querySelectorAll('img');

  for(var n = 0; n < images.length; n++){
    // imagesの中で最初に見つかった20ピクセル以上の画像を抽出
    // 20ピクセルは絵文字を除くため
    if(image_size_check(images[n])){
      // ブラウザによってはスラッシュ一つで始まる画像のURLが
      // 開いているページのものになってしまうのでその対策
      var pattern = /^(\S+?:\/)?\/([^\/\s]+).*$/;
      var location_domain = window.location.href.replace(pattern, '$2');
      var origin_domain = link.replace(pattern, '$2');
      var src = images[n].src.replace(location_domain, origin_domain);

      return '<p><img src="' + src + '" alt="画像" /></p>';
    }
  }

  return '<p></p>';
}

// callback本体
function callback(rss){
  var max_count = 5; // 5個まで表示
  var items = rss.querySelectorAll('item');
  var html  = '';

  for(var i = 0; i < items.length && i < max_count; i++){
    var link    = items[i].querySelector('link').textContent;
    var title   = items[i].querySelector('title').textContent;
    var date    = items[i].querySelector('date').textContent;
    var content = items[i].querySelector('encoded').textContent;
    var img     = get_image(content, link);

    date = date.replace(/^(\d{4})\-(\d{2})\-(\d{2})T.+$/,'$1年$2月$3日');

    html += '<li>' + img 
         + '<p><a href="' + link + '">' + title + '</a></p>'
         + '<p>' + date + '</p>'
         + '</li>';
  }

  document.querySelector('#result').innerHTML = '<ul>' + html + '</ul>';
}

function get_rss(url){
    var rss_server = 'ここに自作RSS中継サーバーのURL http://example.com/';

    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('');
    }
}

var form = document.querySelectorAll('#sample_form input');

form[1].onclick = function(){
    get_rss(form[0].value);
}
</script>

下のフォームにRSSかAtomのURLを入力して送信ボタンを押すと最新記事一覧を(画像があるページではページ内の画像付きで)5個表示します。クロスドメイン対応です。

注)
エラーの時はエラーメッセージは表示せず、何もしません。
bloggerのURLはリダイレクトされるので、リダイレクト先のURLを入力しない場合は取得しません。

ちなみにCSSは次の通りです。

CSSの下にRSSとAtom両対応版のcallback関数を置いておきます。callback以外は上と同じです。

<!-- CSS部分 -->

<style>
#result {
 margin:0;
 padding:0;
 border:none;
 width:400px;
 background:transparent;
}

#result ul,
#result li {
 list-style-type:none !important;
 background:transparent;
}

#result li {
 padding-bottom:0.5em;
 margin-bottom:0.5em;
 border-bottom: 2px solid #ddd;
}

#result li:last-child {
 border-bottom:none;
}

#result li:after {
 content: ".";
 display: block;
 height: 0;
 font-size:0;
 clear: both;
 visibility:hidden;
}

#result img {
 width:100%;
 height:auto;
}

#result a {
 text-decoration:none;
 line-height:1.1em;
 background:transparent;
}

#result p {
 margin:0;
 padding:0;
 line-height:1em;
 background:transparent;
}

#result p:nth-child(1) {
 width:80px;
 float:left;
 margin-right:5px;
}

#result p:nth-child(2) {
 display:block;
 overflow:hidden;
}

#result p:nth-child(3) {
 line-height:1.4em;
 text-align:right;
 font-size:90%;
}

#sample_form input:nth-child(1) {
 width:400px;
}
</style>
<!-- RSSとAtom両対応版のcallback関数 -->

<script>
function callback(xml, content_type){
  // HTMLの生成関数(callbackの外に書いてもOK)
  function make_html(link, title, date, img){
    return '<li>' + img 
      + '<p><a href="' + link + '">' + title + '</a></p>'
      + '<p>' + date + '</p>'
      + '</li>';
  }

  var count_max = 5;
  var i = 0;
  var html  = '';

  var items = xml.querySelectorAll('item');

  if(items.length){
    if(items[0].querySelector('date')){
      // RSS
      // Seesaa FC2 livedoor(livedoorはRSSとAtomがある)
      for(var i = 0; i < items.length && i < count_max; i++){
        var link    = items[i].querySelector('link').textContent;
        var title   = items[i].querySelector('title').textContent;
        var date    = items[i].querySelector('date').textContent;
        var content = items[i].querySelector('encoded').textContent;
        var img     = get_image(content, link);

        date = date.replace(/^(\d{4})\-(\d{2})\-(\d{2})T.+$/,'$1年$2月$3日')
        html += make_html(link, title, date, img);
      }
    }else{
      // Atom
      // WordPress blogger
      // ただしbloggerのURLはリダイレクトされるので
      // リダイレクト先のURLを入力しない場合は取得しない
      for(var i = 0; i < items.length && i < count_max; i++){
        var link    = items[i].querySelector('link').textContent;
        var title   = items[i].querySelector('title').textContent;
        var date    = items[i].querySelector('pubDate').textContent;
        var thumb   = items[i].querySelector('thumbnail');
        var content = items[i].querySelector('encoded');
        var img     = '<p></p>';

        date = date.replace(/^\S+?,\s+([0-9]+)\s+(\S+)\s+([0-9]+)\s.+$/,function(all, g1, g2, g3){
          var m = {
            'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04', 'May': '05', 'Jun': '06',
            'Jul': '07', 'Aug': '08', 'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'
          };

          return g3 + '年' + m[g2] + '月' + g1 + '日';
        });

        if(thumb){
          var src = thumb.getAttribute('url');
          img = '<p><img src="' + src + '" alt="画像" /></p>';
        }else if(content){
          img = get_image(content.textContent, link);
        }

        html += make_html(link, title, date, img);
      }
    }
  }else{
    // livedoorのAtom
    items = xml.querySelectorAll('entry');

    for(var i = 0; i < items.length && i < count_max; i++){
      var link    = items[i].querySelector('link').getAttribute('href');
      var title   = items[i].querySelector('title').textContent;
      var date    = items[i].querySelector('issued').textContent;
      var content = items[i].querySelector('content').textContent;
      var img     = get_image(content, link);

      date = date.replace(/^(\d{4})\-(\d{2})\-(\d{2})T.+$/,'$1年$2月$3日')
      html += make_html(link, title, date, img);
    }
  }

  document.querySelector('#result').innerHTML = '<ul>' + html + '</ul>';
}
</script>

Google Feed APIを使わずXMLHttpRequestでクロスドメインのRSSを取得する方法 まとめ

Google Feed APIを使わずにXMLHttpRequestでクロスドメインのRSSを取得してデータを抽出した際に作った自作サーバーの立て方とJavaScriptでの取得方法です。JavaScriptに関して言うとjQueryを使わなくても大した行数にはならないです。

「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');

画像が縦長か横長か調べて自動でクラス名をつけるJavaScript

画像が横長なのか、縦長なのか、正方形なのかで分けて、それぞれクラス名を追加するJavaScriptを書いてみました。よく読んでもらっているページに「絶対はみ出さない画像!自動でサイズ調節するスタイルシート」というのがあるんですが、これに関連したことで「スマホで見ても、すべての画像が横幅いっぱいになって、はみ出さなくなったんですが、縦長の写真だけサイズを変えられないか」という質問を受けました。

うちでよく読んでもらっているページに「絶対はみ出さない画像!自動でサイズ調節するスタイルシート」というのがあるんですが、これに関連したことで質問を受けました。

「スマホで見ても、すべての画像が横幅いっぱいになって、はみ出さなくなったんですが、縦長の写真だけサイズを変えられないか」という内容です。

画像が横長なのか、縦長なのか、正方形なのかで分けて、それぞれクラス名をつければいいんですが、もうすでに大量のページがある場合は全ページを修正しなくてはいけないし、そうでないとしても毎回画像毎にクラス名を割り当てるのは大変です。

そこで、自動でクラス名を追加するJavaScriptを書いてみました。

画像のサイズを調べてクラス名を割り当てるJavaScript

今回、参考になったのは次のページです。これが非常に素晴らしい内容で、作業がとても簡単になりました。ありがとうございます。

[JavaScript] 画像のオリジナル サイズを取得する 最もシンプルな方法 – こじょらぼ

で、実際に作ったソースはこんなカンジです。HTMLのどこに追加しても動きますが、<head> 部分の終わりにある「</head> の直前」につけるのがいいと思います。

<script>
(function(){
	function image_class(){
		var img = new Image();
		var images = document.querySelectorAll('img');

		for(var i=0;i<images.length;i++){
			img.src = images[i].src;

			if(img.width < img.height){
				images[i].className += ' vertically_long';
				images[i].parentNode.className += ' vertically_long_outer';
			}else if(img.width > img.height){
				images[i].className += ' horizontally_long';
				images[i].parentNode.className += ' horizontally_long_outer';
			}else{
				images[i].className += ' square';
				images[i].parentNode.className += ' square_outer';
			}
		}
	}

	if(window.addEventListener){
		window.addEventListener('load', image_class, false);
	}else if(window.attachEvent){
		window.attachEvent('onload', image_class);
	}
})();
</script>

これでページ読み込み完了後
縦長の画像には vertically_long というクラス名が
横長の画像には horizontally_long というクラス名が
正方形には square というクラス名が追加されます。

さらに
vertically_long の親ノードには vertically_long_outer
horizontally_long の親ノードには horizontally_long_outer
square の親ノードには square_outer
というクラス名が追加されます。

このクラス名毎にスマホ専用スタイルシートを書けば、好きなように表示を変えられます。

ただ、お気づきかと思いますが同じ親ノードの中に、縦長と横長の両方の画像があった場合、両方のクラス名が親ノードに追加されるので、その部分は思ったように表示されないと思います。

サンプルページ

このスクリプトを適用していない場合と、適用した場合のサンプルページを用意しました。2つのページの違いはスクリプトをつけているか、いないかだけです。

スマホで見ても絶対にはみ出しませんが、スクリプトを適用したページだけ、縦長と正方形の表示が変わります。

スクリプトを適用していない場合
http://dwm.me/download/vertical_and_horizontal_ratio_js_sample_1.html
スクリプトを適用した場合
http://dwm.me/download/vertical_and_horizontal_ratio_js_sample_2.html

サンプルページのスタイルシートは次のようになっています。

<style type="text/css">

/* これで画像は絶対にはみ出さない */
img {
	max-width: 100%;
	height: auto;
}

/* 縦長の画像を真ん中に配置する */
.vertically_long_outer {
	text-align: center;
}

/* 縦長の画像のサイズを横幅120ピクセルに固定 */
/* 高さは自動で調整 */
.vertically_long {
	width: 120px;
	height: auto;
}

/* 正方形の画像を右寄せで配置 */
.square_outer {
	text-align: right;
}

/* 正方形画像の高さを200ピクセル、幅も200ピクセルに固定 */
.square {
	width: 200px;
	height: 200px;
}

</style>

真ん中合わせにしたいのに、左寄せになってしまう場合は下のように変更してください。

変更前

.vertically_long {
	width: 120px;
	height: auto;
}

変更後

.vertically_long {
	width: 120px;
	height: auto;
	/*2行追加*/
	margin-left: auto;
	margin-right: auto;
}

2015年1月9日追記:今年になってSeesaaブログでの表示で縦の長さが固定されてしまい、表示がおかしくなったというご指摘がありました。その場合は width と height に !important を追加してください。


height: auto !important;

今後更に変更される場合があるので、width と height 以外もすべて !important を追加してもいいかもしれません。

これを、ページの本文にだけ適用させたくて、サイドバーなどそれ以外の部分には影響を与えたくない場合があると思います。

例えばSeesaaブログなら各ページの本文は #content .text の中にあるので、それをスタイルシートに追加してください。(同じSeesaaブログでも構成の違うテンプレートがあるようです。詳しくはコメント欄を参照してください。)

具体的に言うとこんなカンジになります。

<style type="text/css">

#content .text img {
	max-width: 100%;
	height: auto;
}

#content .text .vertically_long_outer {
	text-align: center;
}

〜〜以下省略〜〜

同じページの特定のIDを別窓で開く

同じページの特定箇所へリンクする方法は、以前「ページの途中にリンクする」という記事に書きました。しかし、最近試してわかったのですが、同じページ内へのリンクを別窓で開こうとしてもうまくいきません。

今開いているページへリンクを張り、target属性を指定して開こうとしてもうまくいかない

同じページ内に id=”link” というパートを作り、そこへリンクさせようと、以下の3パターンを試しましたが、全部「別窓で開く」のに失敗しました。

リンク先へは移動するのですが、別窓が開かず、同じウインドウの中で遷移します。

これは、W3Cの仕様なんでしょうか。簡単にググってみましたが、わかりませんでした。ちなみに実行環境はLinux Mint版 Firefox(16.0.2) と Google Chrome(26.0.1410.63) の2つです。

<a href=”#link” target=”_blank“>go link</a>
<a href=”#link” target=”_new“>go link</a>
<a href=”#link” target=”unique_name“>go link</a>

3つ目の target=”unique_name” は、「新しいウインドウに自分で好きな名前を付けて開く」という方法です。今回は「unique_name」という名前をつけていますが、名前は自由に決められます。

リンク先を表示するウィンドウの指定(target)」より引用

<a href=”index.html” target=”morepage”>home</a>

target に自分の好みの自由な名前 を付けて、新しいウインドウを表示させ、そのウィンドウに複数のページのリンク先を切り替わり表示させます。
target の値に「morepage」 と付けて三つのページを同じウィンドウに表示させます。

例:
<a href=”index.html” target=”morepage”>home</a>

<a href=cho3.html” target=”morepage”>タグ教室</a>

<a href=”sitoku.html” target=”morepage”>知っとくコーナー</a>

上の3つのリンク先ページが morepage と名前の付いた 同じ新しいウィンドウの中で切り替わります。最初に選択されたページ(どのページでもいいのですが)には新しいウィンドウで表示されるので、戻るボタンは灰色ですが、次のページからは同じウィンドウに表示されるので戻るボタンは有効になります。

JavaScriptのwindow.openを使えば、同じページも別窓で開ける

当然、JavaScriptが有効でないと使えませんが、window.openで新しい窓を開けます。

window.openの説明は杜甫々さんの「ウィンドウ(Window)」というページが詳しいです。

具体的には下のソースで、新しいウインドウに同じページの id=”link” の場所が表示されます。

<a href=”javascript:void(0);onclick=”window.open(‘#link‘);”>リンクへ</a>

実際に、次のリンクをクリックすると新しい窓で、下の赤い部分が開きます →「リンクへ

window.openに入れるURLは、リンク先のidだけで大丈夫です。
window.open(‘#link‘)

href=”javascript:void(0);” で、リンクを無効にする

aタグはクリックしたらリンク先へ飛ぶものですので、その作用を無効にしないと元のウインドウの画面が、そこへ移動してしまいます。そのために href に、何もしないスクリプトを充てているのがjavascript:void(0);です。

ほとんどの場合、これで問題ありません。しかし、href のリンク先URLを消して、javascript:void(0);を充ててしまったので、JavaScriptが無効だとクリックしてもリンクを表示できません。

それを避けるためにもっとも無難なのは、次の方法です。

  1. HTMLには、通常のリンクを書いておく。
    <a href=”#link” onclick=”window.open(‘#link’);”>リンクへ</a>
  2. それを後から、JavaScriptで書きかえる。

JavaScriptが有効でない場合は、スクリプトが動かないのでリンクが書きかえられません。そうすれば、新しい窓ではなく同じ窓でですが正しくリンク先へ遷移します。

具体的な書きかえ用のJavaScriptは次のようになります。

<a href=”#link” onclick=”window.open(‘#link’);”>リンクへ</a>

<script type=”text/javascript”><!–
document.querySelector(‘a[href=”#link”]’).href = “javascript:void(0);”;
// –>
</script>

このスクリプトは、href=”#link” である aタグを探して、その href を “javascript:void(0);” に書きかえるという動作をします。

“同じページの特定のIDを別窓で開く” の続きを読む

カッコで括るJavaScriptの無名関数 (function(){/*処理*/})();

JavaScriptで、変数名を省略する記法

無名関数について説明する前に、JavaScriptで変数名を省略する書き方を説明します。

例えば、JavaScriptで今年の西暦を調べるには、次のようにします。

var day = new Date;
var year = day.getFullYear();

これは、dayという名前の変数の宣言を省略して、カッコで括っても同じです。つまり、下のようにも書けます。

var year = (new Date).getFullYear();

カッコの中で、new Dateを行ない、その値のメソッドgetFullYearを使って処理をしています。

これは、変数dayを1回しか使わないのであれば、こういう書き方も出来るという例です。

JavaScriptでの関数の宣言方法は2通り

JavaScriptで関数を宣言する方法は、2通りあります。

方法1.  function hoge(){ /*処理*/ }

方法2.  var hoge = function(){ /*処理*/ }

上の2つは、どちらもhogeという名前の関数を宣言しています。

JavaScriptの無名関数

関数宣言の方法2を使って、下のような処理をするとします。

var hoge = function(){ /*処理*/ }
var result = hoge();

これは、今年の西暦を調べたときのように、下のように書けます。

var result = (function(){ /*処理*/ })();

このような、名前を宣言していない関数のことを無名関数と言います。無名関数には、引数も与えられます。

var result = (function(num){ return num * 10; })(2);  /* resultは20になる */

通常、関数は何度も処理を繰り返すために作るので、名前をつけて宣言しますが、1回しか使わない関数の場合は、こういう書き方も出来ます。

1回しか使わない関数とは

1回しか処理を行なわないのであれば、関数にせず、そのまま処理を書いてもいいような気もします。では、HTMLの中で下のように書いたとします。

<script type=”text/javascript”>
var num = 2;
var result = num * 10;
</script>

これでも、resultは20になります。ですが、もし別の場所でも変数numが使われていたとしたらどうなるでしょう。

ブログでは、好きなようにブログパーツを設置できます。上の処理の前に別の人が作った、次のような内容のブログパーツを貼っていたとします。

いらっしゃいませ!あなたは<span id=”elem”></span>人目のお客様です!

<script type=”text/javascript”>
var num = 1000;

window.onload = function(){
  document.getElementById(‘elem’).innerHTML = num;
};
</script>

この後に、先ほどの処理が行なわれると、グローバル変数のnumは、1000ではなく、2に書きかえられます。

window.onloadは、HTMLのすべてを読み終わってから動作しますので、ページ読み込み後に

「いらっしゃいませ!あなたは1000人目のお客様です!」

と表示されるはずのものが

「いらっしゃいませ!あなたは2人目のお客様です!」

と表示されてしまいます。これでは、あんまりではないですか?

こういう悲しい誤作動をなくすために、無名関数を使って、グローバル変数ではなく、ローカル変数にすることも出来ます。

無名関数を使って、関数を返す

無名関数を使って、よくやる手法で、複数回使う関数を、最初に選択しておくという方法があります。例えば下の関数を見てみましょう。

var hoge = function(elem, event, func){
	if(elem.addEventListener){
		elem.addEventListener(event, func, false);
	}else if(elem.attachEvent){
		elem.attachEvent('on'+event, func);
	}
}

この関数をそのまま使うと、呼び出す度にif文の処理をします。しかし、addEventListenerなのか、attachEventなのかは、変化するはずのない結論なので、下の様にすれば、if文の処理は最初の1回だけで済みます。

var hoge = (function(){
	if(window.addEventListener){
		return function(elem, event, func){
			elem.addEventListener(event, func, false);
		}
	}else if(window.attachEvent){
		return function(elem, event, func){
			elem.attachEvent('on'+event, func);
		}
	}
})();