mozilla

Area Tweet アプリの作成

設計目標やコーディング標準をすべて見てきたところで、実際にアプリのコーディングに取り掛かりましょう。

HTML

あらゆる Web アプリケーションテンプレートと同様に、HTML5 DOCTYPE 宣言を行い、<head><body> 要素を含んだ HTML ページが必要です。

<!doctype html>
<html>
<head>
  <title></title>
 
  <meta charset="utf-8" />
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
 
  <meta name="viewport" content="width=device-width, initial-scale=1">
 
  <!-- スタイルシートはここに -->
 
</head>
<body>
 
  <!-- HTML アプリ構造はここに -->
 
 
  <!-- JavaScript はここに -->
 
</body>
</html>

この例では jQuery Mobile を JavaScript フレームワークとして使います。そのため、jQuery Mobile に必要な CSS と JavaScript リソースを読み込みます。

<!-- スタイルシート -->
<link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.css" />
	
<!-- スクリプト -->
<script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
<script src="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.js"></script>

アプリケーションの基本テンプレートと jQuery Mobile のリソースが揃ったので、ページにコンテンツをウィジェットを追加しましょう。アプリケーションフレームワークに jQuery Mobile を選択したため、アプリの HTML 構造はその規定のウィジェット構造に従います。1 番目のペインでは検索ボックスと履歴一覧を提供します。

<!-- ホームペイン -->
<div data-role="page" id="home">
 
  <div data-role="header">
    <h1>Area Tweet</h1>
  </div>
 
  <div data-role="content">
    
    <div class="ui-body ui-body-b">
      <h2>場所の検索</h2>
      <form id="locationForm">
        <input type="search" name="location" id="locationInput" placeholder="Your Location" />
        <button type="submit" data-role="button" data-theme="b">検索</button>
      </form>
    </div>
    
    <div id="prevLocationsContainer" class="opaque">
      <h2>
        以前の場所
        <a href="#" id="clearHistoryButton" data-role="button" data-icon="delete" data-iconpos="notext"
                data-theme="b" data-inline="true" style="top: 5px;">履歴の消去</a>
      </h2>
      <ul id="prevLocationsList" data-role="listview" data-inset="true" data-filter="true"></ul>
    </div>
    
  </div>
</div>

上のコードについてはいくつか触れておくべき点があります。

  • HTML5 の構造はセマンティクスに固執しており、最大限のアクセシビリティを確保します。
  • あらゆる Web ベースのアプリと同じく、スタイル付けしたい、あるいは JavaScript コードからアクセスしたい要素に CSS のクラスと ID を割り当てています。

jQuery Mobile の個別の機能、ノード属性、構造について質問がある場合は、jQuery Mobile のドキュメント を参照してください。

2 番目のペインはツイート一覧を表示するのに使われます。

<!-- ツイートペイン -->
<div data-role="page" id="tweets">
 
  <div data-role="header">
    <a href="#home" id="tweetsBackButton">戻る</a>
    
    <h1><span id="tweetsHeadTerm"></span> ツイート</h1>
  </div>
 
  <ul id="tweetsList" data-role="listview" data-inset="true"></ul>
 
</div>

CSS

この Web アプリの CSS には、要素をフェードさせるとともに、jQuery Mobile のスタイルや一般的な要素のスタイルを上書きする CSS アニメーションを追加します。

/* アニメーション */
@-moz-keyframes fadeIn {
  0%    { opacity: 0; }
  100%  { opacity: 1; }
}
@-webkit-keyframes fadeIn {
  0%    { opacity: 0; }
  100%  { opacity: 1; }
}

.fadeIn {
  -moz-animation-name: fadeIn;
  -moz-animation-duration: 2s;
 
  -webkit-animation-name: fadeIn;
  -webkit-animation-duration: 2s;
 
  opacity: 1 !important;
}

/* 独自 CSS クラス */
.clear        { clear: both; }
.hidden       { display: none; }
.opaque       { opacity: 0; }


/* 基本スタイル */
#prevLocationsContainer { margin-top: 40px; }

/* jQuery スタイルのカスタマイズ */
#tweetsList li.ui-li-divider,
#tweetsList li.ui-li-static     { font-weight: normal !important; }


/* ツイート一覧 */
#tweetsList li {
 
}

#tweetsList li .tweetImage {
  float: left;
  margin: 10px 10px 0 10px;
}

#tweetsList li .tweetContent {
  float: left;
  /* margin: 10px 0 0 0; */
}

#tweetsList li .tweetContent strong {
  display: block;
  padding-bottom: 5px;
}

/* 端末対応のためのメディアクエリ */
@media screen and (max-device-width: 480px) {
  #prevLocationsContainer { margin-top: 10px; }
}

JavaScript

JavaScript は Web アプリに追加する最後の主要なコンポーネントです。以下のコードがアプリのすべての機能を網羅します。

$(document).ready(function() {
 
  // ページ読み込みから開始
  if(window.location.hash) {
    window.location = "index.html";
  }
 
  // 機能テストと設定
  var hasLocalStorage = "localStorage" in window,
    maxHistory = 10;
 
  // 使用する要素をリストアップ
  var $locationForm = $("#locationForm"),
    $locationInput = $("#locationInput"),
    
    $prevLocationsContainer = $("#prevLocationsContainer"),
    
    $tweetsHeadTerm = $("#tweetsHeadTerm"),
    $tweetsList = $("#tweetsList");
    
  // 複数のリクエストを防ぐため jqXHR の最後のリクエストを保持
  var lastRequest;
 
  // アプリケーションオブジェクトの作成
  app = {
    
    // アプリの初期化
    init: function() {
      var self = this;
      
      // 検索ボックスにフォーカス
      focusOnLocationBox();
      
      // フォーム送信イベントを追加
      $locationForm.on("submit", onFormSubmit);
      
      // 項目が存在する場合は履歴を表示
      this.history.init();
      
      // ツイートペインで戻るボタンがクリックされたときはフォームとフォーカスをリセット
      $("#tweetsBackButton").on("click", function(e) {
        $locationInput.val("");
        setTimeout(focusOnLocationBox, 1000);
      });
      
      // 位置情報の取得
      geolocate();
      
      // ツイートペイン上でスワイプされたときはホームペインへ戻る
      $("#tweets").on("swiperight", function() {
        window.location.hash = "";
      });
      
      // ボタンがクリックされたときは履歴を消去
      $("#clearHistoryButton").on("click", function(e) {
        e.preventDefault();
        localStorage.removeItem("history");
        self.history.hideList();
      })
    },
    
    // 履歴モジュール
    history: {
      $listNode: $("#prevLocationsList"),
      $blockNode: $("#homePrev"),
      init: function() {
        var history = this.getItemsFromHistory(),
          self = this;
        
        // リストに項目を追加
        if(history.length) {
          history.forEach(function(item) {
            self.addItemToList(item);
          });
          self.showList();
        }
        
        // イベント委任を使ってクリックされたリスト項目を検索
        this.$listNode.delegate("a", "click", function(e) {
          $locationInput.val(e.target.textContent);
          onFormSubmit();
        });
      },
      getItemsFromHistory: function() {
        var history = "";
        
        if(hasLocalStorage) {
          history = localStorage.getItem("history");
        }
        
        return history ? JSON.parse(history) : [];
      },
      addItemToList: function(text, addToTop) {
        var $li = $("<li><a href='#'>" + text + "</a></li>"),
          listNode = this.$listNode[0];
          
        if(addToTop && listNode.childNodes.length) {
          $li.insertBefore(listNode.childNodes[0]);
        }
        else {
          $li.appendTo(this.$listNode);
        }
        
        this.$listNode.listview("refresh");
      },
      addItemToHistory: function(text, addListItem) {
        var currentItems = this.getItemsFromHistory(),
          newHistory = [text],
          self = this,
          found = false;
        
        // 履歴を巡回してこれが存在するかどうか確認
        $.each(currentItems, function(index, item) {
          if(item.toLowerCase() != text.toLowerCase()) {
            newHistory.push(item);
          }
          else {
            // 「リピーター」にヒットしたら、リストから削除する
            found = true;
            self.moveItemToTop(text);
          }
        });
        
        // 新しい項目をリストの一番上に追加
        if(!found && addListItem) {
          this.addItemToList(text, true);
        }
        
        // 履歴を 10 項目に制限
        if(newHistory.length > maxHistory) {
          newHistory.length = maxHistory;
        }
        
        // 新しい履歴をセット
        if(hasLocalStorage) {
          // Mobile Safari のプライベートブラウジングに関する問題を防ぐため try/catch ブロックに包む
          // http://frederictorres.blogspot.com/2011/11/quotaexceedederr-with-safari-mobile.html
          try {
            localStorage.setItem("history", JSON.stringify(newHistory));
          }
          catch(e){}
        }
        
        // リストを表示
        this.showList();
      },
      showList: function() {
        $prevLocationsContainer.addClass("fadeIn");
        this.$listNode.listview("refresh");
      },
      hideList: function() {
        $prevLocationsContainer.removeClass("fadeIn");
      },
      moveItemToTop: function(text) {
        var self = this,
          $listNode = this.$listNode;
        
        $listNode.children().each(function() {
          if($.trim(this.textContent.toLowerCase()) == text.toLowerCase()) {
            $listNode[0].removeChild(this);
            self.addItemToList(text, true);
          }
        });
        
        $listNode.listview("refresh");
      }
    }
    
  };
 
  // 検索の送信
  function onFormSubmit(e) {
    if(e) e.preventDefault();
    
    // 値をトリミング
    var value = $.trim($locationInput.val());
    
    // ツイートペインへ移動
    if(value) {
      
      // 検索を履歴へ追加
      app.history.addItemToHistory(value, true);
      
      // ペインのヘッダを更新
      $tweetsHeadTerm.html(value);
      
      // 他にリクエストが存在する場合はキャンセル
      if(lastRequest && lastRequest.readyState != 4) {
        lastRequest.abort();
      }
      
      // Twitter へ JSONP リクエストを送信
      lastRequest = $.ajax("http://search.twitter.com/search.json", {
        cache: false,
        crossDomain: true,
        data: {
          q: value
        },
        dataType: "jsonp",
        jsonpCallback: "twitterCallback",
        timeout: 3000
      });
    }
    else {
      // 検索ボックスへフォーカス
      focusOnLocationBox();
    }
    
    return false;
  }
 
  // Twitter からの検索結果受け取り
  window.twitterCallback = function(json) {
    
    var template = "<li><img src='{profile_image_url}' class='tweetImage' /><div class='tweetContent'>"
                   +"<strong>{from_user}</strong>{text}</div><div class='clear'></div></li>",
      tweetHTMLs = [];
      
    // 基本エラー処理
    if(json.error) { // Twitter のエラー
      showDialog("Twitter エラー", "Twitter がツイートデータを提供できません。");
      return;
    }
    else if(!json.results.length) { // 結果なし
      showDialog("Twitter エラー", "あなたがいる地域周辺のツイートは見つかりませんでした。");
      return;
    }
    
    // ツイートのフォーマット
    $.each(json.results, function(index, item) {
      item.text = item.text.
            replace(/(https?:\/\/\S+)/gi,'<a href="$1" target="_blank">$1</a>').
            replace(/(^|\s)@(\w+)/g,'$1<a href="http://twitter.com/$2" target="_blank">@$2</a>').
            replace(/(^|\s)#(\w+)/g,'$1<a href="http://search.twitter.com/search?q=%23$2" target="_blank">#$2</a>')
      tweetHTMLs.push(substitute(template, item));
    });
    
    // ツイートデータをフォームに追加
    $tweetsList.html(tweetHTMLs.join(""));
    
    // リストビューを適切なフォーマットで更新
    try {
      $tweetsList.listview("refresh");
    }
    catch(e) {}
    
    // ツイートビューへ移動
    window.location.hash = "tweets";
  };
 
  // テンプレート代入
  function substitute(str, obj) {
    return str.replace((/\\?{([^{}]+)}/g), function(match, name){
      if (match.charAt(0) == '\\') return match.slice(1);
      return (obj[name] != null) ? obj[name] : "";
    });
  }
 
  // ユーザの位置情報取得
  function geolocate() {
    if("geolocation" in navigator) {
      // ユーザの位置情報取得を試みる
      navigator.geolocation.getCurrentPosition(function(position) {
        // 住所の位置を設定
        if(position.address && position.address.city) {
          $locationInput.val(position.address.city);
        }
      });
    }
  }
 
  // 入力ボックスにフォーカス
  function focusOnLocationBox() {
    $locationInput[0].focus();
  }
 
  // モーダル関数
  function showDialog(title, message) {
    $("#errorDialog h2.error-title").html(title);
    $("#errorDialog p.error-message").html(message);
    $.mobile.changePage("#errorDialog");
  }
 
  // アプリを初期化
  app.init();
 
});

JavaScript の各部について詳しくは触れませんが、指摘すべき点はいくつかあります。

  • このスクリプトでは localStoragegeolocation が使えるかどうか機能テストを行っています。ブラウザにそれらの機能が実装されていなかった場合は使いません。
  • geolocation API はユーザの現在地判別に、localStorage API はユーザの検索履歴保持に使われています。
  • JavaScript はテストを簡単にするためモジュラー形式で書かれています。
  • ツイートペインではスワイプイベントを監視しています。ペイン上でスワイプされると、最初のペインへ戻ります。
  • Twitter が全データの取得元です。このアプリでは独自のサーバサイド処理は行っていません。

ドキュメントのタグと貢献者

タグ: 
Contributors to this page: ethertank, kohei.yoshino
最終更新者: ethertank,