当ブログ人気記事!
転職体験談
Laravelで学ぶWebアプリ開発
誰でも作れるチャットアプリ
未経験への勧め

【Laravel初心者向け講座】インプレッション数計測機能を実装してみる(フロント編:ポーリング)

プログラミング

インプレッション数を数える機能を作りたい!
Scrollイベント、ポーリングを使いこなしたい!

こういった方に参考になる記事です!


この記事を書いている私(@Shoot58153748)は、
2020年2月現在メガベンチャーの社内スタートアップの部署でエンジニア(1年目)をしており、

プログラミング未経験からメガベンチャーへの転職を成功させた経験・ノウハウ
Webエンジニアになってから学んだこと

をブログにまとめています。


前回、前々回は通報機能(画面API)を実装しましたが、

今回から2記事(フロント側実装、API実装)にわたって、インプレッション数計測機能の実装をしていきます!

フロント編では、
ポーリング
という技術についてピックアップします。

他にもVueでのスクロールイベント実装や
画面の位置情報取得など、
ぜひ参考にしてください!


今回のポイント

  • ポーリング
  • スクロールイベント
  • 画面位置情報取得



今までのLaravelでのWebアプリ開発記事もまとめていますので、
こちらも参考にしてください!

また、ブログでは最低限必要な(更新した)部分のみ掲載するので、
全体のソースコードをみたい場合は以下にあげてあります。
Github: https://github.com/Shuto-san/laravel-vue-docker

スポンサーリンク

インプレッション数とは?

インプレッション数とは、広告が表示された回数のこと。 

Web広告業界では、自社の広告が見られた(表示された)数は、とても重要な指標の1つです。

ツイッターでもインプレッション数を見ることができますよね?


広告じゃなくても、対象のものが何回見られたというデータを管理することは、
ビジネス上とても役に立ちそうです。

そこで今回は、各ツイートが見られた回数(インプレッション数)を数える機能のフロント側を作ります。(次回はサーバー側作ります)

特に押さえてほしいポイントとして、
ポーリングという技術を用いてサーバー側と通信していきます。

ポーリングとは

ポーリング(polling)とは、ざっくり言うと

「WebブラウザとWebサーバが定期的に通信するための技術」

です。

通信の競合を避けたり、更新データにリアルタイム(風)にアクセスしたい場合使用されます。


実は、WebブラウザとWebサーバが定期的に通信するための技術は他にもあります。

通常WebサーバとWebブラウザは、HTTPプロトコルで通信し、
クライアント(ブラウザ)からリクエストしてサーバからレスポンスが返りますよね?

そのため、基本的にはサーバ側からブラウザに新着情報をリアルタイムで通知(プッシュ)できるようにはできていません。
しかしそれでも通知(プッシュ)したい場合、いくつか方法があります。

ポーリングWebsocketCOMETSSE…etc


ポーリングは、クライアントからサーバに定期的に新着を問い合わせるようにします。

ポーリング間隔の分だけ通知が遅延してしまいますが、最も原始的かつ確実なやり方です。


今回、JavaScriptで実装していきます!

画面イメージ

インプレッション計測機能実装例(フロント側)

今回の制作物(実装内容)は以下になります。

  • 画面
  • Js

画面

ファイル名:resources/views/index.blade.php

                <div class="tweet-timeline">
//////////////////////////修正部分//////////////////////////////
                    <div class="tweet-card" v-for="tweet in tweets" :key="tweet.id" :id="tweet.id" :class="{unviewed: !tweet.is_viewed}" v-cloak>
//////////////////////////////////////////////////////////////

                        <div class="tweet-contents">
                            <div class="tweet-contents-tweet">
                                <div>@{{ tweet.tweet }}</div>
                            </div>
                            <div class="tweet-contents-footer">
                                    <i v-if="tweet.is_liked" class="fas fa-heart" @click="pushLike(tweet)"></i>
                                    <i v-else class="far fa-heart" @click="pushLike(tweet)"></i>
                                    <i v-if="tweet.is_reported" class="fas fa-flag" @click="openModal(tweet)"></i>
                                    <i v-else class="far fa-flag" @click="openModal(tweet)"></i>
                            </div>
                        </div>

                        <section v-if="tweet.isDisplayed" class="modal-area">
                          <div class="modal-background"></div>
                          <div class="modal-wrapper">
                            <div class="modal-contents">
                              <h1 v-if="tweet.is_reported">通報を取り消しますか?</h1>
                              <h1 v-else>通報しますか?</h1>
                              <p>※意味不明なツイート、不快なツイート、誹謗中傷を含むツイートは通報できます</p>
                              <button v-if="tweet.is_reported" class="btn" @click="pushReport(tweet)">通報取消</button>
                              <button v-else class="btn" @click="pushReport(tweet)">通報する</button>
                            </div>
                            <div class="modal-close" @click="closeModal(tweet)">
                              ×
                            </div>
                          </div>
                        </section>
                    </div>
                </div>
                <infinite-loading @infinite="fetchTweets"></infinite-loading>

UI自体は全く変わりませんが、

tweet-carddivタグにクラスのバインディングを追加しました。

:class="{unviewed: !tweet.is_viewed}"


サーバーからこのツイートがすでに表示されたかどうかの情報を受け取り、
まだ表示されていないツイートに関しては、
unviewedというクラスを追加

JSでunviewedがついたツイートのみリクエスト内容に含めるという処理を書きます。

JS

ファイル名:resources/js/tweet.js

new Vue({
    el: '#tweet',
    data: {
...

        userAction: {
            like: {
                list: [],
                debouncedList: []
            },
            report: {
            },
+            impression: {
+                tweetIdList: [], // インプレッション計測されたツイートのIDリスト
+                postedTweetIdList: [], // ポストされたツイートのIDリスト
+                updateTweetImpressionCountTimer: null // ポーリング対象の関数を格納
+            }
        },

...

    },

+    created: function() {
         // ポーリングの実装部分。3秒に一回サーバーにリクエストを送る関数を実行する。(実際にリクエストを送らない場合もある)
+        this.updateTweetImpressionCountTimer = setInterval(this.pushImpressionCount, 3000)
         // スクロールイベントを定義。スクロールされたらインプレッション数を計測する
+        window.addEventListener('scroll', this.countImpression);
+    },

    methods: {
      // インプレッション数を計測する処理部分
+        countImpression() {
+            let windowTop = pageYOffset;
+            let windowHeight = window.outerHeight;
+            let impressionTweetList = document.querySelectorAll(".unviewed");
+            for (let i = 0; i < impressionTweetList.length ; i++) {
+                let impressionTweetId = impressionTweetList[i].id;
+                let elemPosition = impressionTweetList[i].offsetTop + impressionTweetList[i].offsetHeight *3/4;
+                if (!(this.userAction.impression.tweetIdList.indexOf(impressionTweetId) >= 0)){
+                    if (elemPosition > windowTop && elemPosition < windowTop + windowHeight) {
+                        this.userAction.impression.tweetIdList.push(impressionTweetId);
+                    }
+                }
+            }
+        },

         // インプレッション数が計測されたツイートIDのリストを作成し、サーバー側に通知する
+        pushImpressionCount() {
+            let postTweetIdList = this.userAction.impression.tweetIdList.filter(id =>
+                !this.userAction.impression.postedTweetIdList.includes(id)
+            );
+            postTweetIdList.forEach(value => {
+                this.userAction.impression.postedTweetIdList.push(value);
+            });
+            if (postTweetIdList.length !== 0) {
+                this.postImpressionCount(postTweetIdList);
+            }
+        },

         // サーバー側にポストする関数
+        postImpressionCount(tweetIdList) {
+            console.log('impression count request!');
+            console.log(JSON.stringify(tweetIdList));
+            axios.post(window.location.origin + `/tweet/impression`, {
+                impressionTweetIdList: JSON.stringify(tweetIdList)
+            })
+            .then(response => {
+            })
+            .catch(error => {
+            });
+        },

    },

...

});

+の行が今回追記した部分です。

今回の肝は、createdで定義する「スクロールイベント」と「setInterval」

実はこのsetIntervalがポーリングを実現する実装です。

3000ms毎にpushImpressionという関数を実行します。


変数は以下の通り

tweetIdList:インプレッション計測されたツイートのIDリスト

postedTweetIdList:ポストされたツイートのIDリスト

updateTweetImpressionCountTimer:ポーリング対象の関数を格納

unviewedというクラスを持つタグのみ(ツイートカード)をquerySelectorAllで取得し、

JSで画面や要素の位置情報をします。

画面トップと画面下部の位置情報と、
各ツイートカードの要素の一番上と要素高さを取得

各ツイートカードの要素が画面トップと画面下部に収まっている場合、
(正確に言うと、要素の3/4が表示されていたら)

tweetIdListにツイートIDを追加していきます。



メソッドは以下の通り

countImpression:画面がスクロールされたら実行される。画面に表示されたツイートを配列に追加する(画面に収まっているツイートのIDリストを作る)。

pushImpression:3秒に1回実行される関数。画面に表示されたツイートIDをサーバーに送る。すでにサーバー側に送られたツイートIDリスト(postedTweetIdList)に含まれるツイートIDは除く。

postImpression:サーバーに通報情報をポストする



npm run devでJSをビルド

まとめ:ポーリングはJsのsetIntervalで

以上、インプレッション機能のフロント側の実装でした!

今回のポイント

  • ポーリング
  • スクロールイベント
  • 画面位置情報取得

Webの開発現場では「インプレッション」「ポーリング」という単語が飛び交うことがあります。

開発者の会話についていけるかは、
その技術を知っているかどうかなので

これを機にポーリングについてぜひ学んでいきましょう。

画面の位置情報を取れる、ということも知っているだけで
実装の幅が広がりますよね!


次回はインプレッション機能のAPIを実装します!




それではまた!

プログラミング
スポンサーリンク
シェアする
SHOOTをフォローする
末っ子WebエンジニアSHOOTのブログ

コメント

タイトルとURLをコピーしました