開発者ブログ
チュートリアル(基本編) » 09.動画検索アプリの作成

09.動画検索アプリの作成

Last modified by ytoba on 2017/02/09, 15:31

概要

ここでは、基本偏のまとめとしてYoutubeのAPIを使用して動画を検索し一覧表示するアプリケーションを作成します。

  • 2017/2現在 サンプルはYoutube Data API v3に対応しています

画面仕様

view.png

キーワードを入力して、検索ボタンを押下すると関連する動画をリスト表示します。
検索~一覧表示でよくみられる以下のポイントを押さえます。

  1. 非同期で検索する
    • 指定されたキーワードを元に、関連する動画を6件取得する。(非同期処理)
    • 一覧取得中は、処理中を表すインジケータを表示し、ユーザの操作をブロックする。
  2. 結果をテンプレートを使用して表示する
    • 各行のHTMLはテンプレート化されており、サーバから取得したデータを合わせて表示する。
  3. 表示負荷が高い内容は、可視範囲のみ表示する
    • 取得した全ての動画に対して再生用プレイヤーを1度に埋めこむと重いので可視範囲内のものだけ表示する(非同期処理)
  4. 遅延ロード
    • 下までスクロールした場合次の6件を取得する。(非同期処理)

HTML構造

bind.png

画面のHTML構造は以下のようになっています。

  • 赤枠はコントローラをバインドする要素
  • 青枠は検索ボタン押下時にsubmitイベントが発生するform要素
  • 緑枠は検索結果を表示するul要素(テンプレートを使って結果を表示する箇所)

処理の流れ

検索ボタン押下時、スクロールイベント発生時による処理の流れをシーケンス図で表します。
2つで共通する処理は以下の通りです。

  • youtube-controller
    • _searchメソッド
      • 検索ロジック(youtube-logicのsearchメソッド)を実行し、結果を画面へ表示します
    • _addPlayerInView
      • 可視範囲にあるプレイヤー表示div(class="video-frame")を取得し、プレイヤー表示処理(_addPlayer)を呼び出します
      • プレイヤー表示div(class="video-frame")は検索後、テンプレートを使用して画面へ表示されます
    • _addPlayer
      • iframe内に、テンプレートを使用してプレイヤーを表示します
  • youtube-logic
    • searchメソッド
      • 非同期処理でYouTubeからキーワードに関連する動画を検索します

検索ボタン押下時

submit.png

  1. submitイベント
    • 検索ボタンを押下すると画面からsubmitイベントが発生します
    • youtube-controllerのsubmitイベントに定義された処理(submitイベントハンドラ)が実行されます
  2. _search
    • 入力されたキーワードのチェックをして問題ない場合のみ検索処理(_search)を呼び出します
  3. search()
    • youtube-logicの検索処理を呼び出します
  4. h5.ajax
    • 非同期でYouTubeのサーバからキーワードに紐つく動画情報を検索します
  5. 検索結果を返す
    • 結果をqXhrオブジェクト(Promiseオブジェクトの性質を備えているオブジェクト)で返します
  6. 検索結果をリスト表示
    • 検索完了後、youtube-controllerの_searchメソッドで結果をテンプレートを使用してリスト表示します
  7. _addPlayerInView
    • 可視範囲にあるiframeを取得し、プレイヤー表示処理を呼び出します
  8. _addPlayer
    • テンプレートを使用してプレイヤーを表示します
  9. 可視範囲のプレイヤーを表示
    • 可視範囲のプレイヤーが表示されます

遅延ロード(スクロールに応じて次の検索結果を表示)

scroll.png

  1. scrollイベント
    • 画面をスクロールするとスクロールイベントが発生します
    • youtube-controllerはスクロールイベントに定義されたスクロール処理を実行します
  2. _addPlayerInView
    • 可視範囲にあるiframeを取得し、プレイヤー表示処理を呼び出します
  3. _addPlayer
    • テンプレートを使用してプレイヤーを表示します
  4. 可視範囲のプレイヤーを表示
    • 可視範囲のプレイヤーが表示されます
  5. _search
    • 画面下端までスクロールをして現在表示している動画が総件数に達していない場合、検索処理(_search)を呼び出します
  6. search()
    • youtube-logicの検索処理を呼び出します
  7. h5.ajax
    • 非同期でYouTubeのサーバからキーワードに紐つく動画情報を検索します
  8. 検索結果を返す
    • 結果をqXhrオブジェクト(Promiseオブジェクトの性質を備えているオブジェクト)で返します
  9. 検索結果をリスト表示
    • 検索完了後、youtube-controllerの_searchメソッドで結果をテンプレートを使用してリスト表示します

実装手順

ここでは以下のようなStepでアプリの完成をめざします。

Step1 HTMLを用意しよう
Step2 コントローラを書いてみよう
Step3 ロジックを書いてみよう
Step4 コントローラとロジックを連携させよう
Step5 結果を画面へ反映させよう
Step6 遅延ロードを実装してみよう

各Stepでの作業手順は以下の通りです。

  1. お題を確認する
  2. TODOを確認する
  3. コードを記述する
  4. 動作確認を行い期待通りに動くことを確認する
  5. 完成版と自分が書いたコードを見比べる

必要最低限のコードを記述していますので、前回までの基礎を思い出しながらコードを書いてみましょう。
わからなくなったら、参考リンクをたどったり完成版のコードを参照してください。

完成版のコードと見比べるときは、自分のコードと違う箇所はないか?違う箇所の意味は?など考えながら比べましょう。

Step1 HTMLを用意しよう

まず、土台となるHTMLを用意して表示して見ましょう

お題

YouTube検索アプリHTMLを記述し画面を表示しましょう

TODO

  1. 下記の参考HTMLをstep9.htmlというファイル名で保存してください
  2. 必要なライブラリを読み込んでください
    1. jQuery、hifive(h5.css, ejs-h5mod.js, h5.dev.js)が必要です
    2. 以降で作成するyoutube-controller.jsやyoutube-logic.jsも適時読み込んでください

参考:HTML

<!doctype html>
<html>
   <head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="UTF-8">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="-1">
<meta name="viewport" content="width=device-width">

       <!-- 必要なライブラリを適時読み込んでください -->

       <title>hifive YouTube検索サンプル</title>
   </head>
   <body>
       <!-- header -->
       <div class="navbar navbar-inverse">
           <div class="container">
               <p class="navbar-brand" >hifive YouTube検索サンプル</p>
           </div>
       </div>
       <!-- container -->
       <div id="container" class="container theme-showcase">
           <!-- 検索窓 -->
           <div class="page-title jumbotron">
               <p>キーワードを入力し検索ボタンを押下すると、関連する動画を表示します。</p>
               <form id="search" class="search-form corner-all search">
                   <div class="search search-form-content">
                       <input type="text" id="keyword" class="corner-all form-control search-keyword" placeholder="キーワード"/>
                   </div>
                   <div class="search-form-content">
                       <input type="submit" class="corner-all btn btn-info" value="検索"/>
                   </div>
               </form>
           </div>
           <!-- 検索結果 -->
           <div class="page-header">
               <h3 class="result-message">検索結果: <span id="totalCount">0</span></h3>
               <ul id="result" class="result-lists list-group"></ul>
           </div>
       </div>
   </body>
</html>

動作確認

YouTube検索アプリ画面がエラー無く表示されることを確認しましょう

Step2 コントローラを書いてみよう

次に、hifiveの基礎であるコントローラを記述して画面からのイベントを受け取って処理をしましょう

お題

検索ボタンをクリックしてテキストボックスの入力値をイベントハンドラで取得しましょう

TODO

  1. コントローラを記述しましょう
  2. formのsubmitイベントハンドラを書いてみましょう

処理の流れ

step1.png

  1. 検索ボタンを押下するとsubmitイベントが起きます
  2. youtubeControllerのsubmitイベントハンドラが実行されます
  3. submitイベントハンドラで入力値をalert表示します

コントローラの作成

  • 参考:コントローラ
  • youtube-controller.jsというファイル名で作成後、HTMLで読み込んでください。
  • __nameプロパティは、youtube.sample.YoutubeController にしてください。

submitイベントハンドラの作成

formのsubmitイベントハンドラを書いてみましょう( 参考:イベントハンドラの基本構文

  • イベントハンドラ内で入力値をalert表示してみましょう。ただし、入力値が未入力または長さ0の場合は何もしないようにしてください。
  • デフォルトのイベント動作をキャンセルしましょう。(参考:デフォルトのイベント動作をキャンセルする
    • formのsubmitイベントが行われるとフォームの送信(画面遷移)が行われますが今回は画面遷移の必要がないのでデフォルトのイベント動作をキャンセルしましょう

以下のようなコードになります。

(function($) {

   /**
     * コントローラ
     *
     * @name YoutubeController
     * @namespace
     */

   var youtubeController = {

       /**
         * コントローラ名
         *
         * @memberOf youtube.sample.YoutubeController
         */

        __name: 'youtube.sample.YoutubeController',
      
       /**
         * 検索キーワード
         *
         * @memberOf youtube.sample.YoutubeController
         */

        _keyword: '',

       /**
         * 「検索」ボタン押下時に実行するハンドラ
         * <p>
         * 検索結果・件数の表示部分をクリアし、youtubeからユーザが入力したキーワードが含まれる動画情報を取得します。 取得した情報からDOM要素生成し、検索結果として一覧表示します。
         *
         * @param {Object} context コンテキスト
         * @param {jQuery} $el イベントターゲット要素(form)
         * @memberOf youtube.sample.YoutubeController
         */

       // formのsubmitイベントのイベントハンドラを定義
       '#search submit': function(context, $el) {

           // submitイベントのデフォルト動作をキャンセルする
           context.event.preventDefault();

           // 画面に入力されたキーワード取得
           var $keywordInput = this.$find('#keyword');
           this._keyword = $keywordInput.val();

           // キーワードチェック(未入力または長さ0の場合は何もしない)
           if (!this._keyword || $.trim(this._keyword).length === 0) {
               return;
            }
           //キーワード表示
           alert(this._keyword);
        }
    };
    h5.core.expose(youtubeController);
})(jQuery);
$(function(){
    h5.core.controller('#container', youtube.sample.YoutubeController);
})    

動作確認

キーワードを入力し、検索ボタンを押下すると入力値がalert表示されることを確認しましょう。

Step3 ロジックを書いてみよう

次は、非同期処理を記述して実行結果を確認できるようにしましょう

お題

非同期処理でYouTubeから動画を検索してみよう

TODO

  1. ロジックを作成しよう
  2. ロジックに非同期処理を書いてみよう
    1. YouTube Data API を確認しよう
    2. ロジックに非同期処理を行うメソッド(search)を追加しましょう
  3. ロジックを実行して結果を確認しよう

処理の流れ

step2.png

  1. ロジック内のsearchメソッドでキーワードに関連する動画を検索します(非同期処理)
  2. 検索結果が返ります

ロジック作成

  • 参考:ロジックの定義
  • youtube-logic.jsというファイルを作成後、HTMLで読み込んでください
  • ロジックの__nameプロパティは、youtube.sample.YoutubeLogic にしてください

YouTube Data API の確認

  • YouTube Data API v3を使用するためにはアプリケーションの承認認証情報が必要です
  • プロジェクトを選択してAPIキーの取得を行いましょう
  • ライブラリでYouTube Data API v3を選択しましょう

GoogleAPI1.png

  • YouTube Data API v3を有効にしましょう

GoogleAPI2.png

  • 認証情報を作成を選択してAPIキーを取得します

GoogleAPI3.png

  • YouTubeで動画を検索するには以下URLにリクエストを送信します
  • 必要なリクエストパラメータは以下の通りです
    • q : キーワード
    • type : 検索クエリの対象を指定
    • part : 取得プロパティの指定(カンマ区切りで複数選択可)
    • key : APIキーの指定(各自取得したAPIキーを指定)
    • pageToken : 次に検索するページの指定
    • maxResults : 何件取得するか(結果の最大件数)
    • videoEmbeddable : 埋め込み可能な動画の指定
  • レスポンスの中身は以下の通りです
    • items:結果リスト(取得した動画情報が入ったリスト)
      • 以下はitemsの1要素の内容です。「.」つなぎでアクセスできます
        • id.videoId:動画ID
        • snippet.description : 動画説明文
        • snippet.title : タイトル
    • pageInfo.totalResults:動画の総件数
    • pageInfo.resultsPerPage : レスポンスに含まれる動画の数
  • 詳細は以下を参照してください

非同期処理を行うメソッド(search)を追加

  • h5.ajaxメソッドはjQuery.ajaxメソッドをラップしたものです。戻り値はjqXhrオブジェクトです。
    • jqXhrオブジェクトはPromiseオブジェクトの性質を備えているので、そのままコントローラに戻り値として渡すことで非同期処理の制御が可能です。
  • h5.ajaxに設定するパラメータは以下の通りです
    • url : リクエストを送信するURL(今回はYouTubeのURL)
    • data : 指定したURLのリクエストパラメータ(今回はYouTubeのリクエストパラメータ)をオブジェクトで指定する
    • dataType : リクエストのフォーマット(今回は’jsonp’を指定する)
    • cache : 通信結果をキャッシュするかどうか(true:キャッシュする false:キャッシュしない)今回はキャッシュする

以下のようなコードになります。

(function($) {
   /**
     * Youtubeビデオ検索URL
     */

   var URL = 'https://www.googleapis.com/youtube/v3/search';

   /**
     * YoutubeLogic
     *
     * @class
     * @name youtube.sample.YoutubeLogic
     */

   var youtubeLogic = {

       /**
         * ロジック名
         *
         * @memberOf youtube.sample.YoutubeLogic
         */

        __name: 'youtube.sample.YoutubeLogic',

       /**
         * 指定された条件でyoutubeのフィードにリクエストを送る
         *
         * @memberOf youtube.sample.YoutubeController
         * @param {String} keyword キーワード
         * @param {String} nextPageToken 次の検索ページ
         * @param {Number} maxResults 何件取得するか
         * @returns {jqXHR} jqXHRオブジェクト
         */

        search: function(keyword, nextPageToken, maxResults) {
           // 非同期通信でyoutubeへアクセスする
           var promise = h5.ajax({
                dataType: 'jsonp',
                data: {
                   'q': keyword,
                   'type': 'video',
                   'part': 'snippet',
                   'key': 'API_KEY', // 取得したAPIキーに変更する
                   'pageToken': nextPageToken,
                   'maxResults': maxResults,
                   'videoEmbeddable': 'true'
                },
                cache: true,
                url : URL
            });
           // Promiseオブジェクト(jqXHRオブジェクト)を返す
           return promise;
        }
    };
   //ロジックを単体で実行できるように公開する
   h5.core.expose(youtubeLogic);
})(jQuery);  

YouTube Data APIの組み合わせ

上記のコードでも良いのですが、後のステップで「視聴数」と「高く評価したユーザー数」の情報を使いたいです。
しかし、現在使用しているsearchだけでは一度に取ってくることが出来ません。

  • 現在使っているsearchはキーワードでの検索が出来ますが、partで指定できるのがidとsnippetのみとなっています
    • 「視聴数」と「高く評価したユーザー数」の情報がある「statistics」プロパティが選択できません

そこでYouTube Data APIを続けて呼びましょう。

  • httpリクエストにはsearch,videosなど複数の種類があります。(/youtube/v3/リクエスト名)
  • 今回使いたい「視聴数」と「高く評価したユーザー数」は、この「videos」を使うことで取得できます。以下URLにリクエストを送信します
  • 必要なリクエストパラメータは以下の通りです
    • id : 検索するvideoのidを指定(カンマ区切りで複数可)
    • type : 検索クエリの対象を指定
    • part : 取得プロパティの指定
    • key : APIキーの指定(各自取得したAPIキーを指定)
    • videoEmbeddable : 埋め込み可能な動画の指定
  • レスポンスの中身
    • items:結果リスト(取得した動画情報が入ったリスト)
      • 以下はitemsの1要素の内容です。「.」つなぎでアクセスできます
        • id:動画ID(searchの時は複数でしたが、idのみ(このidはsearch時のvideoId)になるので注意してください)
        • snippet.description : 動画説明文
        • snippet.title : タイトル
        • statistics.viewCount : 視聴数
        • statistics.likeCount : ボタンを押して動画を高く評価したユーザーの数
    search: function (keyword, nextPageToken, maxResults) {
       var dfd = this.deferred();
       // 非同期通信でyoutubeへアクセスする(search)
       h5.ajax({
        dataType: 'jsonp',
            data: {
               'q': keyword,
               'type': 'video',
               'part': 'snippet',
               'key': API_KEY,
               'pageToken': nextPageToken,
               'maxResults': maxResults,
               'videoEmbeddable': 'true'
            },
            cache: true,
            url: SEARCH_URL
        }).done(function(searchData){
           // videosで検索するvideoId一覧を配列にまとめる
           var videoIdList = new Array();
            $.each(searchData.items,function(index, item){
                videoIdList.push(item.id.videoId);
            });
           // 非同期通信でyoutubeへアクセスする(videos)
           h5.ajax({
                dataType: 'jsonp',
                data: {
                   'id': videoIdList.join(','),
                   'type': 'video',
                   'part': 'snippet, statistics',
                   'key': API_KEY,
                   'videoEmbeddable': 'true'
                },
                cache: true,
                url: VIDEOS_URL
            }).done(function(videoData){
               // pageTokenやmaxResultsを使うのでsearchDataのitemsプロパティを置き換える
               searchData.items = videoData.items;
                dfd.resolve(searchData);
            });
        });
       // Promiseオブジェクト(jqXHRオブジェクト)を返す
       return dfd.promise();
    }

動作確認

ロジックを実行して結果を確認しましょう。
ロジックを公開(expose)しておくと、ロジック単体で実行することができます。

  1. ChromeブラウザでF12を押下して開発者ツールを開きConsoleを開きましょう
  2. Consoleで、以下コマンドを入力しロジックを実行しましょう
    youtube.sample.YoutubeLogic.search('hifive', '', 6).done(function(data) { console.log(data.pageInfo.totalResults)});
    • YoutubeLogicのsearchメソッドへ引数を渡し総件数を表示するコマンドです
    • 引数にはキーワード('hifive')、開始page('')、何件取得するか(6)を指定しています
    • 検索処理完了後に総件数(data.pageInfo.totalResults)をコンソールへ表示します
      logic_console.png
  1. 以下のような結果がコンソールに表示されることを確認しましょう
    logic_exit.png
  1. Networkタグでは検索結果のJSONを確認することができます
    logic_exit_network.png

Step4 コントローラとロジックを連携させよう

次は、コントローラとロジックを連結させてみましょう。

お題

検索ボタンを押下してロジックを実行してみよう

TODO

  1. youtubeController内に、検索処理(_search)を作成しよう
  2. 検索処理からロジックの非同期処理(search)を実行し、総件数をalert表示しよう
  3. 検索処理の実行中は画面全体にインジケータを表示しよう

処理の流れ

step3.png

  1. _searchを呼び出す
    • youtubeController内に、_searchメソッド(検索処理)を作成します
    • submitイベントハンドラで、入力値が未入力でない場合に_searchメソッドを呼び出します(引数として入力値を渡します)
  2. logicのsearchを呼び出す
    • _searchメソッドから、youtubeLogicのsearchメソッドを呼び出します。(引数として入力値、検索開始page、検索件数を渡します)
    • コントローラからロジックを呼び出す場合は、コントローラでロジックの宣言が必要になります。忘れずに宣言しましょう。
    • 参照:ロジックの定義
  3. Promiseオブジェクトを受け取る
  4. 総件数をalert表示
  5. 検索中はインジケータを表示する
    • 検索中は画面全体にインジケータを表示します
    • インジケータのオプションプロパティのpromiseにPromiseオブジェクトを指定すると通信が終了すると同時にインジケータを画面から除去します
    • 参考:indicator API
    • 参考:インジケータサンプル

ロジックの宣言

コントローラからロジックを参照する場合はロジックの宣言が必要です。
以下のように、コントローラのプロパティとして追加しましょう。

   /**
     * YoutubeLogicを宣言
     *
     * @memberOf youtube.sample.YoutubeController
     */

    _youtubeLogic: youtube.sample.YoutubeLogic,

検索処理(_search)

以下コードの「NUMBER_TO_LOAD_AT_ONCE」は一度に読み込む件数の定数です。(6が設定されています)

       /**
         * 「検索」ボタン押下時に実行するハンドラ
         * <p>
         * 検索結果・件数の表示部分をクリアし、youtubeからユーザが入力したキーワードが含まれる動画情報を取得します。 取得した情報からDOM要素生成し、検索結果として一覧表示します。
         *
         * @param {Object} context コンテキスト
         * @param {jQuery} $el イベントターゲット要素(form)
         * @memberOf youtube.sample.YoutubeController
         */

       // formのsubmitイベントのイベントハンドラを定義
       '#search submit': function(context, $el) {

           // submitイベントのデフォルト動作をキャンセルする
           context.event.preventDefault();

           // 画面に入力されたキーワード取得
           var $keywordInput = this.$find('#keyword');
           this._keyword = $keywordInput.val();

           // キーワードチェック(未入力または長さ0の場合は何もしない)
           if (!this._keyword || $.trim(this._keyword).length === 0) {
               return;
            }
           // _searchメソッドを呼んで、videoを検索する
           var promise = this._search(this._keyword, '', document.body, '検索中...');
        },

       /**
         * キーワードに関連する動画を検索します
         * <p>
         * 検索ロジックを実行し、結果を画面へ表示します。
         *
         * @memberOf youtube.sample.YoutubeController
         * @param {String} keyword キーワード
         * @param {String} nextPageTokne 次の検索ページ
         * @param {String | Object} indicatorTarget
         *            インジケータを表示する対象(セレクタまたはjQueryオブジェクトまたはDOMオブジェクトを指定)
         * @param {String} indicatorMessage インジケータに表示するメッセージ
         * @returns {Promise} promiseオブジェクト
         */

        _search: function(keyword, nextPageToken, indicatorTarget, indicatorMessage) {
           
           //検索ロジックが返すpromiseオブジェクトを変数に保持する
           var promise = this._youtubeLogic.search(keyword, nextPageToken, NUMBER_TO_LOAD_AT_ONCE);

           //検索ロジック完了
           promise.done(function(data) {
               //続きの検索ページを保持する
               this._nextPageToken = data.nextPageToken;
               //総件数を表示
               alert(data.pageInfo.totalResults);
            });
           
           //検索中はインジケータ表示(検索ロジックが返すpromiseオブジェクトを設定する)
           this.indicator({
                target: indicatorTarget,
                message: indicatorMessage,
                promises: promise
            }).show();

           // promiseオブジェクトを返す
           return promise;
        },

動作確認

  • 以下の通り動作することを確認しましょう
    • キーワード入力→検索ボタン押下→画面全体にインジケータ表示→インジケータが非表示になる→総件数がalert表示される

Step5 結果を画面へ表示させよう

次は、検索結果をテンプレート使用して画面へ表示してみましょう

お題

検索ボタンをクリックして検索した結果を画面へ表示しよう

TODO

  1. 検索結果リストを表示しよう
    1. 検索結果リストを表示するテンプレートを作成しよう
    2. 検索完了後、テンプレートを使用して結果を画面へ表示しよう
  2. 総件数を表示しよう(テンプレート未使用)

処理の流れ

step4.png

  1. 検索結果をリスト表示
    • テンプレートを使用するには、テンプレートのロードが必要です。忘れずに読み込みましょう(参考:テンプレートのロード
    • 検索完了後、promise.doneに登録する関数内でテンプレートを使用して結果を画面へ反映させましょう(参考:テンプレートに対する操作
    • promise.doneに登録する関数内でコントローラを指すthisを使う場合は、工夫が必要です。関数をthis.ownで囲む必要があります(参考:own API
  2. 総件数を表示する
    • _searchの結果をpromiseオブジェクトで受け取り、総件数をspanタグ(id="totalCount")のテキストとして表示します。

検索結果テンプレートの作成

以下のような検索結果を表示するテンプレートを作成しましょう。
template_image.png
なお、このテンプレートを画面に反映した時点ではプレイヤーは表示されません。

  • list.ejsというファイル名で保存しましょう
  • テンプレートは<script type="text/ejs"></script>で囲んだ中に記述します
  • テンプレートidは‘list’です
    • <script>タグのidで指定した「items」が、this.view.get()で呼び出すときに必要なテンプレートIDとなります
  • JSPと同じように、テンプレートの[% %]または[%= %]の位置にjavascriptコードを記述することができます
  • 参考:テンプレートの基本構文

このテンプレートでは、結果リスト(items)をfor文でループし、1要素(1動画情報)を1つのliタグ内に表示しています。

<% /*---------- 検索結果リスト ----------*/ %>
<script type="text/ejs" id="list">
[% for (var i = 0, len = items.length; i < len; i++) {
    //動画情報を取得
    var snippet = items[i].snippet;
    //動画タイトル取得
    var title = snippet.title;
    //動画ID取得
    var id = items[i].id;
   //動画のURL
   var url = 'https://www.youtube.com/watch?v=' + id;
    //視聴数のundefinedチェック(undefinedの場合は0を設定する)
    var viewCount;
    if( undefined === items[i].statistics || undefined === items[i].statistics.viewCount ){
        viewCount = 0;
    }else{
        viewCount = items[i].statistics.viewCount;
    }

    //高く評価したユーザーの数のundefinedチェック(undefinedの場合は0を設定する)
    var likeCount;
    if( undefined === items[i].statistics || undefined === items[i].statistics.likeCount){
        likeCount= 0;
    }else{
        likeCount= items[i].statistics.likeCount;
    }

    //動画の説明文取得
    var description = $.trim(snippet.description);

    //プレイヤーのURL作成
    var player = playerUrl + '/' +  id;
 %]
<li class="list-group-item">
    <div>
        <!-- プレイヤー表示領域 -->
        <div class="video-container">
            <div class="video-frame">
                <!-- プレイヤーのurlを表示する -->
                <input type="hidden" class="videoSrc" value="[%= player %]" />
            </div>
        </div>
        <!-- 動画情報表示 -->
        <div class="summary">
            <div class="titleContainer">
                <a class="title" href="[%= url %]" title="[%= title %]">[%= title %]</a>
            </div>
            <div class="count">
                <span class="viewCount">[視聴数]</span> [%= viewCount %]
                <span class="likeCount">[高く評価したユーザー数]</span> [%= likeCount %]
            </div>
            <div class="description">
                [%= description %]
            </div>
        </div>
        <div style="clear:both"></div>
    </div>
</li>
[% } %]
</script>

コントローラでテンプレートを使用するには、テンプレートの読み込みが必要です。
以下コードは検索結果テンプレート(list.ejs)の読み込みです。コントローラのプロパティに追加しましょう。

   /**
     * 使用するテンプレート
     *
     * @memberOf youtube.sample.YoutubeController
     */

    __templates: './list.ejs',

テンプレートを使用して結果を画面へ表示

_searchメソッドの検索完了後に結果表示処理を追加しましょう

  • 検索結果を表示するDOMは<ul id=“result“>です
  • テンプレートidは‘list’です
  • テンプレート内で使用する変数は以下をオブジェクトで指定します
    • items : 結果リスト(今回は、data.itemsを指定)
    • playerUrl:YouTubeプレイヤーのurl(今回は‘http:www.youtube.com/embed’を指定)

以下コードの「YOUTUBE_PLAYER_URL」はYouTubeプレイヤーのURLの定数です。('http://www.youtube.com/embed'が設定されています)

   //検索ロジック完了
   promise.done(this.own(function(data) {
       //続きの検索ページ
       this._nextPageToken = data.nextPageToken;
       
       //検索結果をリスト表示
       this.view.append('#result', 'list', {
            items: data.items,
            playerUrl: YOUTUBE_PLAYER_URL
        });
       
    }));

総件数の表示

submitイベントハンドラで、検索処理の結果(_searchの結果)をpromiseオブジェクトで受け取り、
総件数をspanタグ(id="totalCount")のテキストとして表示します。
data.pageInfo.totalResultsから、見つかった動画数(総件数)を取得します
総件数(_totalCount)はプロパティで保持しておきましょう。

       // _searchメソッドを呼んで、videoを検索する
       var promise = this._search(this._keyword, '', document.body, SEARCH_INDICATROR_MESSAGE);

        promise.done(this.own(function(data) {
           // サーバの返り値から総件数取得、表示
           this._totalCount = data.pageInfo.totalResults;
           this.$find('#totalCount').text(this._totalCount);
        }));
    },

プレイヤーの追加

次に、検索結果リストにプレイヤーを追加してみましょう

TODO

  1. プレイヤーを表示しよう
    1. プレイヤーを表示するテンプレートを作成しよう
    2. 検索結果リスト表示後、テンプレートを使用してプレイヤーを表示しよう
    3. プレイヤー表示中は、プレイヤー表示領域にインジケータを表示しよう

処理の流れ

step4-2.png

プレイヤー表示の処理の流れは以下の通りです。

  1. プレイヤーURL表示
    • 検索結果リストが画面へ表示されると、非表示のinputタグ(class="videoSrc")にプレイヤーのurlが表示されます
  2. _addPlayerInView
    • 可視範囲にあるプレイヤー表示領域を取得します(参考:h5.ui.isInView
  3. _addPlayer
    • 可視範囲にある場合のみ、テンプレートを使用してプレイヤーを表示します
    • プレイヤーの表示にはプレイヤーurlが必要です。
    • テンプレートidは‘player’です
    • テンプレートで使用する変数は以下をオブジェクトで指定します
      • src : プレイヤーurl
  4. インジケータ表示
    • プレイヤー表示中はプレイヤー表示領域divタグ(class="video-container")にインジケータを表示します

プレイヤーテンプレートの作成

プレイヤーのテンプレートはlist.ejsの末尾に追加しましょう。(参考:テンプレートの複数記述)

<% /*---------- ビデオプレイヤー ----------*/ %>
<script type="text/ejs" id="player">
<iframe class="video" src="[%= src %]" frameborder="0" allowfullscreen></iframe>
</script>

プレイヤーの表示

プレイヤー表示のコードは以下です。
_searchメソッドの検索完了後(promise.doneに登録する関数内)に_addPlayerInViewを呼び出しましょう。

以下コードの「VIDEO_LOADED_CLASS」はプレイヤー読み込み済みクラスの定数です。('videoLoaded'が設定されています)

       /**
         * 可視範囲にあるvideoframeへプレイヤーを埋め込みます
         *
         * @memberOf youtube.sample.YoutubeController
         */

        _addPlayerInView: function() {
           //プレイヤー読み込み済みクラスが設定されていないdivを取得する
           var $players = this.$find('div.video-frame:not(div.' + VIDEO_LOADED_CLASS + ')');
           if (!$players.length) {
               return;
            }

           //可視範囲内のプレイヤーを埋め込む
           $players.each(this.own(function(i, player) {
               if (h5.ui.isInView(player)) {
                   this._addPlayer(player);
                }
            }));
        },

       /**
         * プレイヤー表示用コンテナにプレイヤーを埋め込みます
         *
         * @memberOf youtube.sample.YoutubeController
         * @param {Object} player プレイヤー読み込み用DOM
         */

        _addPlayer: function(player) {

           var $target = $(player);

           //videwのURLが設定されていない場合処理しない
           var $videoSrc = $target.find('input.videoSrc');
           if (!$videoSrc.length) {
               return;
            }

           //プレイヤーを読み込む
           this.view.update(player, 'player', {
                src: $videoSrc.val()
            });

           //プレイヤー読み込み済みのクラスを追加する
           $target.addClass(VIDEO_LOADED_CLASS);

           //videoをload中にインジケータを表示する
           var $videoContener = $target.parent();
           var indicator = this.indicator({
                target: $videoContener
            }).show();

           //videoがload完了後、インジケータを非表示にする
           var $videoIFrame = $target.children('iframe');
            $videoIFrame.load(function() {
                indicator.hide();
            });
        },

動作確認

以下の通り動作するか確認しましょう
キーワード入力→ 検索ボタン押下→画面全体にインジケータ表示→インジケータが非表示になる→総件数が画面に表示される→結果リストが画面に表示される

Step6 遅延ロードを実装してみよう

最後に、スクロールイベントに処理を定義してみましょう。
ここでは、遅延ロードと呼ばれる手法を実装します。

  • 遅延ロード
    • 必要なとき(今回であれば可視範囲が移動した場合、つまりスクロール時)に必要な情報をロードすることで負荷を分散しブラウザが固まることを防ぐ手法

残っている処理は以下2つです。2つとも今までに作成したメソッドを呼ぶことで実現できます。

  1. 残りのプレイヤーを表示する
    1. Step5までの処理で可視範囲のプレイヤーは表示されました。下にスクロールすると今まで可視範囲外だった領域にはプレイヤーが表示されていないので表示する必要があります。
  2. 残りの動画情報を検索して表示する
    1. 表示件数が総件数より少ない場合は、残りの動画を再度検索して表示しましょう
    2. 検索中は検索結果リストの最後にインジケータを表示しましょう。(インジケータを表示する領域はテンプレートで追加しましょう)

お題

遅延ロードを実装してみよう

TODO

  1. スクロールイベントハンドラを追加しよう
  2. スクロールバーの最下部からの位置 > 50の場合、 残りのプレイヤーを表示しよう
  3. スクロールバーの最下部からの位置 <= 50の場合、残りの動画情報を検索して表示しよう
    1. 検索中は検索結果リストの最後尾にインジケータを表示しよう(テンプレート使用)

スクロールイベントハンドラの追加

まずは、スクロールイベントに処理を定義してみましょう。
スクロールイベントが発生する要素はdocumentです。documentのイベントハンドラの記述方法は特殊ですので以下を参考にしてください。

参考:特別なオブジェクト(window, document等)へのイベントハンドラの設定

   /**
     * documentでscrollイベント時に実行するハンドラ
     * <p>
     * リストの末尾に新しい検索結果を追加します。
     *
     * @param {Object} context コンテキスト
     * @param {jQuery} $el イベントターゲット要素(イベントを発生させた要素)
     * @memberOf youtube.sample.YoutubeController
     */

   // documentのscrollイベントのイベントハンドラを定義
   '{document} [scroll]': function(context, $el) {
       //処理
   }

スクロール位置の計算

スクロールの位置によって実行する処理を分けましょう。

  1. スクロールバーの最下部からの位置 > 50の場合、 残りのプレイヤーを表示
  2. スクロールバーの最下部からの位置 <= 50の場合、残りの動画情報を検索して表示

スクロールイベントハンドラの第二引数「$el」はイベントを発生させた要素、つまりスクロールバーです。
スクロールの位置計算に利用しましょう。

   //スクロールバーの最下部からの位置を求める
   var scrollTop = $el.scrollTop();
   var scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
   var clientHeight = window.innerHeight;
   var remain = scrollHeight - (scrollTop + clientHeight);

残りのプレイヤーを表示する

スクロールイベントハンドラ内に残りのプレイヤー表示処理を追加しましょう。
プレイヤー表示処理は、スクロールイベント毎に実行されると負荷が高いのでsetTimeout関数で少し間を空けて実行しましょう。

以下コードの「SCROLL_DELAY」はスクロール停止と判断するまでの間隔の定数です。(単位はms。500が設定されています。)
「_timer」はタイマー用変数です。コントローラのプロパティとして保持します。

       // タイマー(this._timer)をクリアする
       if (this._timer) {
            clearTimeout(this._timer);
        }
       //可視範囲内のプレイヤーを埋める(scroll毎に処理が走ると負荷が高いので、タイマー関数で少し間を空けて実行する)
       this._timer = setTimeout(this._addPlayerInView(), SCROLL_DELAY);

残りの動画情報を検索し表示する

スクロールイベントハンドラ内に動画情報を検索し表示する処理を追加しましょう。

繰り返し検索を行うので、今まで表示した件数(_searchCount)をコントローラのプロパティで保持しておきましょう。

以下scrollイベントのイベントハンドラ内、コードの「NUMBER_TO_LOAD_AT_ONCE」は一度に読み込む件数の定数です。(6が設定されています)

       //次に読み込む件数を加算する
       this._searchCount += NUMBER_TO_LOAD_AT_ONCE;

       // 総件数より大きくなった場合は、検索を行わない
       if (this._totalCount <= this._searchCount) {
           return;
        }

       // videoを検索する
       var promise = this._search(this._keyword, this._nextPageToken, '#indicatorSpace', '');

このままではvideoの検索に時間がかかった場合、スクロールの判定がされて同じ動画を再取得してしまうことがあります。検索中(_isSearching)であるかチェックするためのプロパティを作成し、以下のようにします。また、_search関数内の最初と最後に状態を変更する処理を追加します。

       //読み込み中か判定
       if(this._isSearching){
           return;
        }
       // videoを検索する
       var promise = this._search(this._keyword, this._nextPageToken, '#indicatorSpace', '');

検索中に表示するインジケータについて

scroll_indicator.png
残りの動画を検索中は、検索結果リスト(id="result")の最後にインジケータを表示しましょう。
インジケータを表示する領域(liタグ)はテンプレートで追加しましょう。今回はテンプレートへ引数を渡す必要はありません。

テンプレートは以下の通りです。list.ejsの末尾に追加しましょう。

<% /*---------- インジケータ ----------*/ %>
<script type="text/ejs" id="indicator_space">
<li class="list-group-item" id="indicatorSpace">
<div class="listContainer"></div>
</li>
</script>

インジケータは常に検索結果リストの末尾に表示するので、検索前に結果リストに追加(this.view.append)し、
検索が完了後にインジケータ表示領域をDOMから削除しましょう。


       //インジケータ表示用領域を取得
       var $indicatorSpace = this.$find('#indicatorSpace');
       if ($indicatorSpace.length === 0) {
           // インジケータ表示用領域がなければ、遅延ロードインジケータ表示領域を追加
           this.view.append('#result', 'indicator_space');
            $indicatorSpace = this.$find('#indicatorSpace');
        }

       // videoを検索する
       var promise = this._search(this._keyword, this._nextPageToken, '#indicatorSpace', '');

       //検索完了
       promise.done(this.own(function() {
           // インジケータ表示用領域削除
           $indicatorSpace.remove();
        }));

動作確認

以下の通り動作するか確認しましょう(検索結果表示後から記述しています)
下方向にスクロールする→今まで可視範囲外だったプレイヤーが表示される→一番下までスクロールする→結果リストの末尾にインジケータが表示される→検索結果が末尾に追加される

完成版

サンプルでは見た目をよくするためにbootstrapと、自前のCSS(youtube.css)を読み込んでいます。

YouTube検索サンプル

次のステップ⇒チュートリアル10.スマートフォン対応(jQueryMobileとの連携)


Copyright (C) 2012-2017 NS Solutions Corporation, All Rights Reserved.