これでできる! クロスブラウザJavaScript入門

第13回 簡単なアプリケーションの作成

この記事を読むのに必要な時間:およそ 8 分

TwitterのAPIでは<や&などがエスケープされた状態(&lt;,&amp;)になっています。それを信用するならinnerHTMLに入れれば意図通りの表示ができますが,何かしらの理由でエスケープ漏れがないとも限らないので,今回はテキストノードとして扱います。そこで,&lt;や&amp;などを<,&に戻す処理が必要となります。

個々の発言の作成#1

    /* エスケープされた文字を戻す */
    var node = document.createTextNode(usr.text.
        replace(/&(lt|gt|quot|amp);/g,function(_$,_1){
          return {lt:'<', gt:'>', quot:'"', amp:'&'}[_1];
        })
    );
    /* テキストノードの挿入 */
    entry.appendChild(node);
    linkfy(entry, '@(\\w+)', '[^\\w@]|$',
        'http://twitter.com/');
    linkfy(entry, '#(\\w+)', '[^\\w#]|$',
        'http://search.twitter.com/search?q=%23');
    linkfy(entry, '(https?://.*)',
        '[  \\)\\]\'\"\n]|$', '');
    expandUrl(entry);
    highlight(entry, keyword);

さて,テキストノードとして挿入した発言内のURLや@ユーザー名などをリンクにしてみましょう。といっても,この処理は第8回のHTML中の文字列を置換する方法で解説済みですね。

テキストのリンク化

function linkfy(element, start, end, prefix){
  for (var i =0,l = element.childNodes.length;i < l;i++){
    var node = element.childNodes[i];
    if(node.nodeType !== 3){
      continue;
    }
    if (node.nodeValue.search(start) >= 0) {
      var text = node.nodeValue, index;
      var parent = node.parentNode;
      while (text && (index=text.search(start)) >= 0 ){
        // テキストを分割し、後ろ側のノードを取得
        var _txt = node.splitText(index);
        // キーワードの終わりで再度分割
        var _end = _txt.nodeValue.search(end);
        var __txt = _txt.splitText(_end);
        var a = document.createElement('a');
        a.href = prefix + _txt.nodeValue.match(start)[1];
        a.target = '_blank';
        a.appendChild(_txt);
        if (!__txt.nodeValue || !__txt.parentNode){
          parent.appendChild(a);
        } else {
          parent.insertBefore(a, __txt);
        }
        // ループ用に初期化
        text = __txt.nodeValue;
        node = __txt;
      }
    }
  }
}

最後は短縮URLの展開処理です。こちらも第11回のJSONPの活用例で短縮URLを展開するAPIを紹介していますが,今回はあえて前回少しだけ触れたクロスオリジン通信を利用してみます。

短縮URLの展開(クロスオリジン対応)

function expandUrl(element){
  var links = element.getElementsByTagName('a');
  for (var i =0,l = links.length;i < l;i++){
    var a = links[i];
    // 長いURLやtwitter.comの内部リンクは対象外
    if (a.href.length < 30 && a.host !== 'twitter.com'){
      getCrossSiteXhrOrJsonP(a);
    }
  }
}
function getCrossSiteXhrOrJsonP(a){
  // 現在のURLとAPIのURLが同一オリジンかチェック
  var same_origin = location.hostname === 'ss-o.net' &&
        (location.port==='' || location.port==='80') &&
        location.protocol === 'http:';
  var xhr;
  var onload =  function(){
    var data = JSON.parse(xhr.responseText);
    if (data.url && data.url !== a.href){
      a.textContent = data.url;
      a.href = data.url;
    }
  };
  if (same_origin) {
    xhr = new XMLHttpRequest();
  } else if(window.XDomainRequest){
    xhr = new XDomainRequest();
  } else if(window.XMLHttpRequest) {
    xhr = new XMLHttpRequest();
    if (!('withCredentials' in xhr)){
      xhr = {
        open:function(method, url){
          var s = document.createElement('script');
          xhr.__script = s;
          var callback = ('JSONP_' + new Date().getTime() +
              Math.random()).replace(/\W/,'');
          s.src = url + '&callback=' + callback;
          window[callback] = function(data){
            xhr.responseText = JSON.stringify(data);
            onload();
            document.body.removeChild(s);
            delete window[callback];
          };
        },
        send:function(){
          document.body.appendChild(xhr.__script);
        }
      };
    }
  }
  xhr.open('GET', 'http://ss-o.net/api/reurl.json?url=' +
      encodeURIComponent(a.href), true);
  if (!('onload' in xhr)){
    xhr.onreadystatechange = function(){
      if(xhr.readyState === 4 && xhr.status === 200){
        onload();
      }
    };
  } else {
    xhr.onload = onload;
  };
  xhr.send(null);
}

まず,URLがある程度長い場合は短縮URLではないだろうと判断して対象外としています。今回は30文字としましたが,この値はマジックナンバーであり,改善の余地があります。/の数,ドメイン部分の長さ,?を含むかどうかなど,条件も工夫してみてもよいかもしれません。

続いて,現在のURLとAPIのURLが同一オリジンかチェックしています。同一オリジンであれば通常のXMLHttpRequestを使用します。

オリジンが異なる場合はXMLHttpRequest level 2かXDomainRequestを使用しますが,IE 6~7とOperaはそれらに対応していません。そこで,クロスオリジン通信ができない場合はJSONPを使用します。

XMLHttpRequestのインターフェースを持ったJSONPオブジェクト

      xhr = {
        open:function(method, url){
          var s = document.createElement('script');
          xhr.__script = s;
          var callback = ('JSONP_' + new Date().getTime() +
              Math.random()).replace(/\W/,'');
          s.src = url + '&callback=' + callback;
          window[callback] = function(data){
            xhr.responseText = JSON.stringify(data);
            onload();
            document.body.removeChild(s);
            delete window[callback];
          };
        },
        send:function(){
          document.body.appendChild(xhr.__script);
        }
      };

このようにXMLHttpRequestのインタフェースを実装したオブジェクトで,内部ではJSONPを行うという力技な実装にしてみました。わざわざこういったことをせずとも,最初からJSONPを使えばよいケースなのであまり実用的ではありませんが,クロスオリジン通信のサンプルとして見て頂ければと思います。

まとめ

今回はここまでの復習として12回で取り上げた内容を使って簡単なアプリケーションを作成してみました。もし忘れているところなどがあったら是非復習してみてください。次回からはJavaScriptの基礎に再び戻って,prototypeとthisについて見ていきたいと思います。

著者プロフィール

太田昌吾(おおたしょうご,ハンドルネーム:os0x)

1983年生まれ。JavaScriptをメインに,HTML/CSSにFlashなどのクライアントサイドを得意とするウェブエンジニア。2009年12月より、Google Chrome ExtensionsのAPI Expertとして活動を開始。

URLhttp://d.hatena.ne.jp/os0x/