ソースコードで学ぶWebプログラミング

リアルタイム受注反映ツールの作り方

「リアルタイム受注反映」とは、注文状況をサイトへ表示することで、 販売促進へと繋げる機能です。一般的に表示上の反映は javascript で行い、 表示データの取得は PHP で行います。

反映を行う情報は、商品名、発注者名、地域、価格、日時、商品画像、 コメントなどが考えられます。表示データは、API などから取得する構成となります。

頻繁に受注が行われるサイトで特に有効な施策になると思いますが、 入荷情報やコメント情報などの反映にも利用できます。

学べること

・ページ画面への反映方法 ・Ajax通信の仕方 ・JSONデータの扱い方

リアルタイム受注反映に必須の機能とは

プログラムを作成するために、まず要件定義を行います。

それでは「リアルタイム受注反映」に必須の機能をピックアップします。 今回は反映部分を制作対象とし、データ取得処理はテスト用の機能のみ実装します。

テスト用のプログラムで、一部機能を代替するものを「スタブ」、全機能を 利用できるデモ用のものを「モック」と言うようです。 今回のデータ取得プログラムは「スタブ」タイプになります。

1.表示用「JSONデータ」を受け取る 2.表示用「JSONデータ」を変換する 3.変換したデータを「HTML」として出力する

機能としては網羅できているようです。さらに詳細を決めていきます。

・別サーバからAjax通信でデータを受け取る ・取得したデータを画面上に一覧で表示する ・一旦古いものを表示して、新しいものを追加していく

Ajaxとは、javascriptによって非同期でサーバ間通信を行うための仕組みです。 非同期とは、ページ読込と通信処理が同期しない状態を意味します。 例えばページを表示してから、任意のタイミングで通信を行って、データを取得、 反映させる事が可能となります。

一般的にAjax通信やデータのアニメーション反映では、jQueryと呼ばれるライブラリがよく使われています。 今回は学習用のためライブラリは使わず、javascriptのみで実装します。

/* jQueryのAjax通信処理例 */ $.ajax({ url:'api.php', type:'POST', data:{} }).done(function(r){ /* リクエスト成功 */ }).fail(function(r){ /* リクエスト失敗 */ }); /* jQueryのアニメーション処理 */ /* 上から下に徐々に表示 */ $('#target').slideDown(); /* 下から上に徐々に消す */ $('#target').slideUp(); /* 徐々に透明度を下げて表示 */ $('#target').fadeIn(); /* 徐々に透明度を上げて消す */ $('#target').fadeOut();

ここまでの要件をまとめてみます。機能に対して、関連する詳細を当てはめていきます。

1.表示用「JSONデータ」を受け取る ・別サーバからAjax通信でデータを受け取る 2.表示用「JSONデータ」を変換する 3.変換したデータを「HTML」として出力する ・取得したデータを画面上に一覧で表示する ・一旦古いものを表示して、新しいものを追加していく

リアルタイム受注反映の要件をコード化する

それではコーディングに移ります。

1.表示用「JSONデータ」を受け取る

javascriptでのサーバ間通信では、XMLHttpRequest オブジェクトを利用します。 コード内容は、XMLHttpRequest の仕様に合わせた内容となります。 何度か用いるので関数化かクラス化を行います。今回はクラス化します。

/* Ajax通信クラス */ var Ajax = function(ops){ this.ops = ops; } /* 通信に成功した時 */ Ajax.prototype.success = function(req){ switch(req.status){ /* レスポンスOK */ case 200: this.ops.success(req.responseText); break; /* レスポンスOK以外 */ default: this.ops.error(req.responseText); break; } } /* 通信に失敗した時 */ Ajax.prototype.fail = function(req){ this.ops.error(req.responseText); } /* GET送信 */ Ajax.prototype.get = function(){ var u = this.ops.url; /* データがある時、結合 */ if (typeof this.ops.data!=='undefined') { u += '?'+this.ops.data; } var req = new XMLHttpRequest(); /* 成功時、エラー時の関数を登録 */ req.onload = this.success.bind(this,req); req.onerror = this.fail.bind(this,req); req.open('GET',u,true); req.send(null); } /* POST送信 */ Ajax.prototype.post = function(){ var req = new XMLHttpRequest(); /* 成功時、エラー時の関数を登録 */ req.onload = this.success.bind(this,req); req.onerror = this.fail.bind(this,req); req.open('POST',this.ops.url,true); req.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); /* データがある時、送信 */ if (typeof this.ops.data!=='undefined') { req.send(this.ops.data); }else{ req.send(null); } }; /* Ajaxインスタンス作成 */ var AJAX = new Ajax({ url:'', data:'', success: function(r){}, error: function(r){} }); /* GET送信 */ AJAX.get();

やっている事

・var Ajax = function(){} でクラスを定義 ・this.ops でクラスのプロパティを定義 ・Ajax.prototype でクラスのメソッドを定義 ・var req = new XMLHttpRequest() でインスタンスを生成 ・req.open() で送信の準備 ・req.send() で送信の実行 ・Ajax.prototype.success でサーバレスポンスの処理用関数を呼び出し ・Ajax.prototype.fail で通信失敗時の処理用関数を呼び出し ・.bind() で関数の引数などを設定

処理内容としては、サーバにデータを送信し、レスポンスがあった時に登録した関数を実行しています。 この「レスポンスがあった時」の部分が非同期処理となります。

非同期で行う具体的な処理は、Ajaxインスタンス作成時に定義しています。 success:function(){}、error:function(){} の部分が該当します。現在は空処理です。

new Ajax({ success:function(r){}, error:function(r){} });

続いてテスト用のデータ取得プログラムをPHPで作成します。 今回は「商品受注のデータ」と「コメント受付のデータ」の2種類を、表示用のレスポンスとして返す プログラムにします。

この部分を実運用上の処理に変えることで、実際に反映処理が行われます。 実務での処理内容としては、主に同サーバに設置してDBからデータを取得したり、 APIへ接続してデータを取得する処理になるかと思います。

テスト用の処理分岐は、単純にPOST送信かGET送信で分けて、 Ajaxクラスの送信テストも兼ねるようにしています。

<?php /* タイムゾーン */ date_default_timezone_set('Asia/Tokyo'); $data = array(); if ($_SERVER['REQUEST_METHOD']=='POST') { /* 連想配列タイプのデータ */ for($i=0;$i<20;$i++){ $price = mt_rand(1000,10000); $data[] = array( 'title'=>'商品名'.($i+1), 'date'=>date('Y年m月d日 H時',time()-900*$i), 'price'=>$price, ); } } else { /* 配列タイプのデータ */ for($i=0;$i<20;$i++){ $data[] = ' '.($i+1).'件目'.str_repeat('・',mt_rand(5,30)); } } /* クロスドメイン対策 */ header('Access-Control-Allow-Origin: *'); /* JSONデータを出力 */ header('Content-Type: application/json; charset=UTF-8'); echo json_encode($data);

やっている事

・日時関数 date() を利用しているためタイムゾーンを設定 ・$_SERVER['REQUEST_METHOD'] で送信メソッドGETやPOSTを判定 ・for() で繰り返し処理、このコードでは20回処理 ・mt_rand() でランダムな数値を生成 ・$data[] = で配列に追加代入 ・time()-900*$i で処理毎に日時を900秒ずらすように ・str_repeat() と mt_rand() でランダムな文字列の繰り返し文を生成 ・Access-Control-Allow-Origin: * でクロスドメインによるアクセスを許可 ・Content-Type: application/json; charset=UTF-8 でJSONデータの出力指定 ・json_encode() で連想配列をJSONデータ化

for() 内の繰り返し数 20 を変更することで、レスポンスとして返すデータ数を変更できます。 PHPでJSONデータを扱う方法は色々ありますが、連想配列で変数を作成し、json_encode() で 変換する方法が簡単です。 逆にJSONデータを連想配列化するには、json_decode() を使います。

2.表示用「JSONデータ」を変換する

次はレスポンスとして受け取った「JSONデータ」をHTML画面に表示するために、 変換処理を行います。

処理内容を再度、確認してみると、古いデータを表示し、新しいデータを追加表示する必要があります。 データを分ける方法は色々ありますが、HTML要素を対象とした場合、対象が複数の時はclassを利用し、 一つの時はIDを利用して特定できるようにします。

今回は新しいデータはcssのクラス名「live-new」、古いデータは「live-old」を設定し、区別できるようにします。

/* リアルタイム受注反映クラス */ var LiveDisplay = function(p,c){ var n = p.querySelectorAll('.live-new'); /* 新着のみ非表示 */ for(var i=0,len=n.length;i<len;i++){ n[i].style.display = 'none'; } /* 表示領域の高さを固定 */ p.style.height = p.getBoundingClientRect().height+'px'; this.p = p; this.child = c; }

やっている事

・var LiveDisplay = function(p,c) でインスタンス生成時にp,cを受け取るように ・p は、表示領域のHTML要素で parent を想定 ・c は、表示領域内の1レコードを表示する領域で children を想定 ・querySelectorAll() で領域内のクラスを取得 ・.style.display = 'none' で非表示化 ・.style.height で表示領域の高さを設定 ・.getBoundingClientRect().height で表示領域の高さを取得

押さえるべき要点として、新着の非表示化を行った後に、表示領域の高さを取得して 設定している点があります。非表示化の後で行うことで、表示データ数のみの高さで固定されます。 表示領域の高さを固定するのは、後で表示を行う際に、徐々に上から表示するためです。

続いて、その徐々に上から表示するアニメーション処理を実装します。

/* スライド表示 */ LiveDisplay.prototype.slideDown = function(c){ /* 属性に保存した高さを取得 */ var h = c.getAttribute('live-h'), ht = c.getAttribute('live-ht'); /* 初回処理時 */ if (!h) { /* 要素を表示する */ c.style.display = 'block'; c.style.overflow = 'hidden'; h = 0; ht = c.getBoundingClientRect().height; c.setAttribute('live-ht', ht); } /* 高さを1pxずつ増やす */ c.style.height = (++h)+'px'; c.setAttribute('live-h', h); /* 増加中の高さが最大値に達した時 */ if (ht <= h) { /* 属性を除去 */ c.removeAttribute('live-h'); c.removeAttribute('live-ht'); return false; } /* 時間差でもう一度処理、変動速度 20ミリ秒 */ setTimeout(this.slideDown.bind(this,c),20); }

やっている事

・function(c) で表示予定の要素を引数に ・.getAttribute() で属性を値(変数)の読込に利用 ・.style.display = 'block' で非表示要素を表示 ・.style.overflow = 'hidden' で表示されていない領域を隠す ・.setAttribute() で属性を値(変数)の保存に利用 ・style.height = (++h)+'px' で要素の高さを増やし、上から徐々に表示されるように ・.removeAttribute() で変数利用した属性を削除 ・return false; で処理から抜ける、以降の処理を行わない ・setTimeout() で時間差でスライド処理を再度呼び出し

アニメーション処理は、少しずつ対象を動かす処理となりますが、HTML画面上では、 cssにより座標や高さ、横幅などの数値を増減することで表現できます。 また時間差を生み出すにはsetTimeout()の他、setInterval()が使えます。

次は表示データの位置を移動する処理です。新着データは、非表示の状態で HTML要素として存在しているので、これを表示領域の先頭に移動させます。 また末尾の要素を削除します。

/* 表示要素を移動 */ LiveDisplay.prototype.moveUp = function(p){ var p = this.p, c = this.child, last = c.length-1, o = p.querySelectorAll('.live-old'); /* すべて表示し終わった時 */ if (c[last].style.display!=='none') return false; /* 新着を先頭に */ p.insertBefore(c[last], c[0]); /* 古い表示を除去 */ p.removeChild(o[ o.length-1 ]); this.slideDown( c[0] ); /* 時間差でもう一度処理、移動速度 5000ミリ秒 */ setTimeout(this.moveUp.bind(this),5000); };

やっている事

・querySelectorAll() で領域内のクラスを取得、このコードでは古い記事を取得 ・c[last].style.display!=='none' で表示要素がすべて表示済となった時を判定 ・.insertBefore() で要素を移動 ・.removeChild() で要素を削除 ・this.slideDown() で移動した要素をスライド表示 ・setTimeout() で時間差で移動処理を再度呼び出し

3.変換したデータを「HTML」として出力する

最後にHTMLタグとして出力する処理を実装します。実際のコーディングでは前後するかも 知れませんが、いままでのコード仕様に合わせてコード化していきます。

初めにエスケープ処理を作成しておきます。取得データ内でHTMLタグを利用させたい場合は不要です。

/* HTMLエスケープ関数 */ function hEsc(s){ return s.replace(/[&<>"']/g,function(m){ return { '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[m]; }); }

やっている事

・.replace() で文字列の置換処理 ・//g でマッチした文字をすべて置換 ・[] でマッチ文字範囲を指定、一文字をマッチ判定 ・function(m){} でマッチした文字への置換処理を定義 ・return {}[m]; でマッチした文字に対応する置換文字を返す

データの新旧判定としては、今回は単純に半分に分けることにします。 移動処理を行わせるために、新しいデータ群を古いデータ群の後ろに追加します。

まず「コメント受付のデータ」用の実装を行います。処理内容は配列内の値を そのまま表示するだけです。

/* サーバレスポンスのJSONデータを変数化 */ var j = JSON.parse(r), /* 受け取ったデータ数の半分のインデックス値を算出 */ l = parseInt(j.length/2), h = ''; /* 半数以降を古いデータに */ var a = j.slice(l); for(var i in a){ h += '<div class="live-old">'+hEsc(a[i])+'</div>'; } /* 半数以前を新しいデータに */ a = j.slice(0,l); for(var i in a){ h += '<div class="live-new">'+hEsc(a[i])+'</div>'; } /* 作成したHTMLタグを表示領域に代入 */ p.innerHTML = h; var c = p.getElementsByTagName(child); /* リアルタイム受注反映クラスを生成 */ var LD = new LiveDisplay(p,c); LD.moveUp();

やっている事

・JSON.parse() でJSONデータをjavascriptの連想配列に変換 ・parseInt() で整数化 ・.slice() で配列の一部を取得 ・.innerHTML = で要素内のHTML要素として代入 ・LD.moveUp() で移動処理を開始

続いて「商品受注のデータ」を作成します。ほぼ同じ処理ですが、受け取ったJSONデータの配列内の値が連想配列になっています。

/* サーバレスポンスのJSONデータを変数化 */ var j = JSON.parse(r), /* 受け取ったデータ数の半分のインデックス値を算出 */ l = parseInt(j.length/2), h = ''; /* 半数以降を古いデータに */ var a = j.slice(l); for(var i in a){ h += '<table class="live-old"><tr><td>'+hEsc(a[i].date)+'</td><td>'+hEsc(a[i].title); h += '</td><td>'+hEsc(a[i].price.toLocaleString())+'円</td></tr></table>'; } /* 半数以前を新しいデータに */ a = j.slice(0,l); for(var i in a){ h += '<table class="live-new"><tr><td>'+hEsc(a[i].date)+'</td><td>'+hEsc(a[i].title); h += '</td><td>'+hEsc(a[i].price.toLocaleString())+'円</td></tr></table>'; } /* 作成したHTMLタグを表示領域に代入 */ p.innerHTML = h; var c = p.getElementsByTagName(child); /* リアルタイム受注反映クラスを生成 */ var LD = new LiveDisplay(p,c); LD.moveUp();

やっている事

・連想配列.キー で値を取得 ・.toLocaleString() で通貨表示をカンマ区切りに

以上で完成となります。

プログラム完成後のカスタマイズ

受注状況の表示サンプル

コメント受付の表示サンプル

ソースファイルはこちらより入手(購入)可能です。

今回は学習用として作成しているため、必要最小限の機能となっていますが、 下記のようなカスタマイズで、高機能化を目指せます。

カスタマイズ例

・データ取得プログラムで、DB接続を行う ・データ取得プログラムで、API接続を行う ・反映プログラムで、定期的にサーバ問い合わせを行う