[Nodejs] WEBページの今現在同時閲覧している人数を表示する機能構築(ソース付き)

2017年10月31日

Nodejs テクノロジー プログラミング

最近Twitterのクジラをすっかり見なくなりました。 知らない人の為に説明しておくと、Twitterのクジラとは、サービスサーバーの負荷が高くなってきた時に、サービス表示を止めて切り替えられるページでクジラのイラストが表示する事です。 Twitterのクジラさん | ピクシブ百貨店 サービス開始直後にはあれほど見ていたのに、見なくなると何だか寂しいものです。 だけどユーザーとしては、当時はクジラがでると「イラッ」としたものです・・・ 同時アクセスユーザーの人数が多い事が原因なんですが、SNSが主流となってきたWEB環境では、今現在どのくらいの人数がWBEサイトに集まっているか気になる人もいるし、WEBサイト主にとっては、収益にも影響するので、気になる人も少なくないはずです。 そんな機能を簡単に設置できるソースコードを作ったので、興味のあるエンジニアは、参考までにどうぞ。

技術関連

今時のエンジニアであれば、すぐにNodejs + SocketIOという事を思いつくと思いますが、その構成で構築しました。 ちなみに環境は以下の通りです。
Linux : Ubuntu Nodejs : 7.1.0 (ポート3336)
WEBページにJavascriptタグを貼るだけで実現できるようにしています。 そして、他の機能拡張なども考えて、socketIOのJSタグを直接貼るのではなく、タグマネージャーちっくなものも作っておきました。

サイト構成(ソースコードの設置方法など)

asp.js

- タグマネージャー用ソースコードです。こちらのファイルのタグを貼り付けてください。

asp.run.js

- Nodejsの起動用ファイル

asp.core.js

- タグマネージャーで呼び出されるクライアントサイト用JSファイル

事前準備

NodejsのインストールとSocketIOをnpmでインストールしておきましょう。 とりあえず簡単に済ませたい人は、下記コマンドで行ってください。 # ubuntu , debian系 $ apt-get install nodejs # Centos $ yum install nodejs 最新版を入れたい人は、nvmインストールがオススメです。 # ソース取得 $ git clone git://github.com/creationix/nvm.git ~/.nvm $ source ~/.nvm/nvm.sh # インストールできるバージョン確認 $ nvm ls-remote # ※最新バージョンをインストールする事をオススメ。 $ nvm install v7.10.0 $ apt-get install npm $ yum install npm # インストール後にバージョンを確認 $ node -v v7.10.0 $ npm -v 4.2.0 $ nvm --version 0.33.2

ソースコード

/** Tag-system == */ (function(){ var $$ = function(){ if(document.readyState === "complete"){ $$.prototype.start(); } else{ $$LIB.prototype.setEvent(window,"load",$$.prototype.start); } }; $$.prototype.data = { serviceName : "syncpv", port:3336, module:"/asp.core.js" }; $$.prototype.start = function(){ var script = $$.prototype.searchScriptTag(); if(!script){return;} var urlinfo = $$LIB.prototype.urlinfo(script.src); var s = document.createElement("script"); s.type = "text/javascript"; // s.src = urlinfo.dir + "/asp/js/socket.io.js"; s.src = "//"+ urlinfo.domain + ":"+ $$.prototype.data.port + $$.prototype.data.module; document.body.appendChild(s); }; $$.prototype.searchScriptTag = function(){ var scripts = document.getElementsByTagName("script"); var elm = null; for(var i=0; i<scripts.length; i++){ // console.log(scripts[i].getAttribute("class")); if(scripts[i].getAttribute("class") === $$.prototype.data.serviceName){ elm = scripts[i]; break; } } return elm; }; /* Library */ /** Ajax $$AJAX({ url:"", // "http://***" method:"POST", // POST or GET async:true, // true or false data:{}, // Object query:{}, // Object querys:[] // Array }); */ var $$AJAX = function(options){ if(!options){return} var ajax = new $$AJAX; var httpoj = $$AJAX.prototype.createHttpRequest(); if(!httpoj){return;} // open メソッド; var option = ajax.setOption(options); // 実行 httpoj.open( option.method , option.url , option.async ); // type httpoj.setRequestHeader('Content-Type', option.type); // onload-check httpoj.onreadystatechange = function(){ //readyState値は4で受信完了; if (this.readyState==4){ //コールバック option.onSuccess(this.responseText); } }; //query整形 var data = ajax.setQuery(option); //send メソッド if(data.length){ httpoj.send(data.join("&")); } else{ httpoj.send(); } }; $$AJAX.prototype.dataOption = { url:"", query:{}, // same-key Nothing querys:[], // same-key OK data:{}, // ETC-data event受渡用 async:"true", // [trye:非同期 false:同期] method:"POST", // [POST / GET] type:"application/x-www-form-urlencoded", // [text/javascript]... onSuccess:function(res){}, onError:function(res){} }; $$AJAX.prototype.option = {}; $$AJAX.prototype.createHttpRequest = function(){ //Win ie用 if(window.ActiveXObject){ //MSXML2以降用; try{return new ActiveXObject("Msxml2.XMLHTTP")} catch(e){ //旧MSXML用; try{return new ActiveXObject("Microsoft.XMLHTTP")} catch(e2){return null} } } //Win ie以外のXMLHttpRequestオブジェクト実装ブラウザ用; else if(window.XMLHttpRequest){return new XMLHttpRequest()} else{return null} }; $$AJAX.prototype.setOption = function(options){ var option = {}; for(var i in this.dataOption){ if(typeof options[i] != "undefined"){ option[i] = options[i]; } else{ option[i] = this.dataOption[i]; } } return option; }; $$AJAX.prototype.setQuery = function(option){ var data = []; if(typeof option.query != "undefined"){ return $$AJAX.prototype.getDataQuery(option); } if(typeof option.querys != "undefined"){ return $$AJAX.prototype.getDataQuerys(option); } }; $$AJAX.prototype.getDataQuery = function(option){ for(var i in option.query){ data.push(i+"="+encodeURIComponent(option.query[i])); } }; $$AJAX.prototype.getDataQuerys = function(option){ for(var i=0;i<option.querys.length;i++){ if(typeof option.querys[i] == "Array"){ data.push(option.querys[i][0]+"="+encodeURIComponent(option.querys[i][1])); } else{ var sp = option.querys[i].split("="); data.push(sp[0]+"="+encodeURIComponent(sp[1])); } } }; /********** //style値を取得 概要:対象項目のCSS値を取得 param:element 対象項目 **********/ var $$CSS = function(){}; $$CSS.prototype.getStyle=function(e,s){ if(!s){return} //対象項目チェック; if(typeof(e)=='undefined' || e==null || !e){ e = $b; } //属性チェック; var d=''; if(typeof(e.currentStyle)!='undefined'){ d = e.currentStyle[$$LIB.prototype.camelize(s)]; if(d=='medium'){ d = "0"; } } else if(typeof(document.defaultView)!='undefined'){ d = document.defaultView.getComputedStyle(e,'').getPropertyValue(s); } return d; }; //スタイルシートの値を読み出す $$CSS.prototype.getCSS = function(css , selector , styleName){ if(!css || !selector){return} if(styleName){ for(var j=0;j<css.cssRules.length;j++){ if(css.cssRules[j].selectorText==selector){ return css.cssRules[j].style[styleName]; } } } else{ for(var j=0; j<css.cssRules.length; j++){ if(css.cssRules[j].selectorText==selector){ return css.cssRules[j].cssText; } } } }; //特定のselector情報にcss設定を追加 $$CSS.prototype.setCSS = function(css , selTxt , styleName , value){ if(!css || !selTxt || !styleName){return} //selectorTextの指定がある場合 for(var j=0;j<css.cssRules.length;j++){ if(css.cssRules[j].selectorText==selTxt){ css.cssRules[j].style[styleName] = value; return true; } } //対象セレクタが無い場合 css.addRule(selTxt , styleName+":"+value); }; //特定のselectorからcss設定を削除 $$CSS.prototype.delCSS = function(css , selTxt , styleName){ if(!css || !selTxt){return} if(!css.cssRules){return} //selectorTextの指定がある場合 for(var j=css.cssRules.length-1;j>=0;j--){ if(css.cssRules[j].selectorText && css.cssRules[j].selectorText.match(selTxt)){ } } }; //rgb(**,**,**) -> #** $$CSS.prototype.rgb2bit16 = function(col){ if(col.match(/rgb(.*?)\((.*)\)/)){ var rgb = RegExp.$2.split(","); var val="#"; for(var i=0;i<3;i++){ var val2 = parseInt(rgb[i],10).toString(16); if(val2.length==1){ val+="0"+val2; } else{ val+= val2; } } col = val; } return col; }; var $$LIB = function(){}; $$LIB.prototype.urlinfo = function(uri){ if(!uri){uri = location.href;} var data={}; //URLとクエリ分離分解; var query=[]; if(uri.indexOf("?")!=-1){query = uri.split("?")} else if(uri.indexOf(";")!=-1){query = uri.split(";")} else{ query[0] = uri; query[1] = ''; } //基本情報取得; var sp = query[0].split("/"); var data={ url:query[0], dir:$$LIB.prototype.pathinfo(uri).dirname, domain:sp[2], protocol:sp[0].replace(":",""), query:(query[1])?(function(q){ var data=[]; var sp = q.split("&"); for(var i=0;i<sp .length;i++){ var kv = sp[i].split("="); if(!kv[0]){continue} data[kv[0]]=kv[1]; } return data; })(query[1]):[], }; return data; }; $$LIB.prototype.pathinfo = function(p){ var basename="", dirname=[], filename=[], ext=""; var p2 = p.split("?"); var urls = p2[0].split("/"); for(var i=0; i<urls.length-1; i++){ dirname.push(urls[i]); } basename = urls[urls.length-1]; var basenames = basename.split("."); for(var i=0;i<basenames.length-1;i++){ filename.push(basenames[i]); } ext = basenames[basenames.length-1]; return { "hostname":urls[2], "basename":basename, "dirname":dirname.join("/"), "filename":filename.join("."), "extension":ext, "query":(p2[1])?p2[1]:"", "path":p2[0] }; }; //ハイフン区切りを大文字に変換する。 $$LIB.prototype.camelize = function(v){ if(typeof(v)!='string'){return} return v.replace(/-([a-z])/g , function(m){return m.charAt(1).toUpperCase();}); }; // URL切り替え処理 [key , value , flg(before,*after)] $$LIB.prototype.setUrl = function(key,val,flg){ var urlinfo = $$LIB.prototype.urlinfo(); var query = []; if(flg==="before"){ query.push(key + "=" + val); } for(var i in urlinfo.query){ if(i !== key){ query.push(i + "=" + urlinfo.query[i]); } } if(flg!=="before"){ query.push(key + "=" + val); } history.pushState(null,null,urlinfo.url+"?"+query.join("&")); }; $$LIB.prototype.number_format = function(num){ num = num.toString(); var tmpStr = ""; while (num != (tmpStr = num.replace(/^([+-]?\d+)(\d\d\d)/,"$1,$2"))){num = tmpStr;} return num; }; $$LIB.prototype.setEvent = function(target, mode, func){ //other Browser if (typeof target.addEventListener !== "undefined"){ target.addEventListener(mode, func, false); } else if(typeof target.attachEvent !== "undefined"){ target.attachEvent('on' + mode, function(){func.call(target , window.event)}); } else{ console.log(target); console.log("[warning] "+target); } }; new $$; })(); var $$ = { data:{} }; /** user-count $$.users[%url%][%user-id%] */ $$.userCnt = {}; $$.userUrl = {}; var fs = require("fs"); // サーバーアクセスモジュールを拡張子で判別処理 var server = require("http").createServer(function(req, res) { // ソースコードを繁栄 if(req.url.match(/\/.+?\.js/) && fs.existsSync("." + req.url)){ res.writeHead(200, {"Content-Type":"text/javascript"}); var output = fs.readFileSync("." + req.url, "utf-8"); res.end(output); } else if(req.url.match(/\/.+?\.html/) && fs.existsSync("." + req.url)){ res.writeHead(200, {"Content-Type":"text/html"}); var output = fs.readFileSync("." + req.url, "utf-8"); res.end(output); } else if(req.url.match(/\/.+?\.css/) && fs.existsSync("." + req.url)){ res.writeHead(200, {"Content-Type":"text/css"}); var output = fs.readFileSync("." + req.url, "utf-8"); res.end(output); } else{ // res.writeHead(200, {"Content-Type":"text/html"}); // var output = "<h1>web socket</h1>"; // res.end(output); // res.writeHead(200, {"Content-Type":"text/javascript"}); // var output = ";(function(){console.log('--')})();"; // res.end(output); } }); server.listen(3336,function(){ console.log("Run..."); }); // var lib = require("./asp.lib.js"); // lib.func(server); var io = require("socket.io").listen(server); io.sockets.on("connection", function (socket) { // connect-start socket.on("connected", function (data) { // console.log("[connect] "+ socket.id + " / [url] "+data.url); if(typeof $$.userCnt[socket.id] !== "undefined"){return;} $$.userCnt[socket.id] = {url:data.url,time:(+new Date())}; if(typeof $$.userUrl[data.url] === "undefined"){ $$.userUrl[data.url] = [socket.id]; } else if($$.userUrl[data.url].indexOf(socket.id) === -1){ $$.userUrl[data.url].push(socket.id); } if(typeof $$.userUrl[data.url] === "undefined"){ $$.userUrl[data.url] = []; } io.sockets.emit("setUserCount", $$.userUrl[data.url].length); }); socket.on("click", function (data) { console.log("[click] "+socket.id); // console.log("[click] " + data.id +" : "+ data.url); // browserに反映 io.sockets.emit("click-res", data); }); // finish socket.on("disconnect", function (val) { console.log("[disconnect] "+ socket.id); if(typeof $$.userCnt[socket.id] === "undefined"){return;} var url = $$.userCnt[socket.id].url; var time = $$.userCnt[socket.id].time; delete $$.userCnt[socket.id]; console.log("Leave : "+ socket.id +" (" + ((+new Date() - time)/1000) +" s)"); if(typeof $$.userUrl[url] === "undefined"){ $$.userUrl[url] = []; } else if($$.userUrl[url].indexOf(socket.id) !== -1){ // console.log($$.userUrl[url].indexOf(socket.id)); $$.userUrl[url].splice($$.userUrl[url].indexOf(socket.id),1); } io.sockets.emit("setUserCount", $$.userUrl[url].length); }); }); ;(function(){ var $$={}; // make-id $$.id = (+new Date()); $$.url = location.href; $$.socketServer = "http://%your-site%:3336"; /********** Initial **********/ $$.__construct = function(){ if(document.readyState=== "complete"){ if(typeof window.$$MBSYNC !== "undefined"){ console.log("Allready start-up !!"); return; } $$.setStart(); } // onload else{ $$.setEvent(window , "load" , $$.setStart); } }; /********** Proccess **********/ $$.setStart = function(){ $$.viewCountElement(); var script = document.createElement("script"); script.type = "text/javascript"; script.src = $$.socketServer + "/socket.io/socket.io.js"; script.onload = function(){ $$.io = io.connect($$.socketServer); //sent $$.io.emit("connected", {id: $$.id , url:location.href}); $$.io.on("click-res", function (data) { console.log("[res] " + data.id +" : "+ data.url); }); $$.io.on("setUserCount", function (data) { // console.log("[users] " + data +"人"); var elm = document.getElementById("syncpv_"+$$.id); if(elm !== null){ elm.innerHTML = "閲覧 "+data+" 人"; } }); }; document.head.appendChild(script); $$.setEvent(window , "click" , $$.setClick); // unload window.onbeforeunload = function(e) { var dialogText = 'Dialog text here'; e.returnValue = dialogText; return dialogText; }; }; $$.setClick = function(e){ // console.log("[click] "+e.target.tagName); $$.io.emit("click", {id: $$.id , url:location.href}); }; $$.viewCountElement = function(){ var div = document.createElement("div"); div.id = "syncpv_"+$$.id; div.className = "syncpv-view"; div.style.setProperty("position","fixed",""); div.style.setProperty("display","inline-block",""); div.style.setProperty("z-index","1000000",""); div.style.setProperty("background-color","black",""); div.style.setProperty("opacity","0.8",""); div.style.setProperty("color","white",""); div.style.setProperty("font-size","12px",""); div.style.setProperty("margin","0",""); div.style.setProperty("padding","0 8px",""); div.style.setProperty("border-radius","10px",""); div.style.setProperty("min-width","30px",""); div.style.setProperty("height","30px",""); div.style.setProperty("left","8px",""); div.style.setProperty("top","50px",""); div.style.setProperty("line-height","30px",""); div.style.setProperty("text-align","center",""); var html = ""; div.innerHTML = html; document.body.appendChild(div); // $$.io.emit("getUserCount", {}); }; /********** Library **********/ /** イベント処理(マルチブラウザ対応) Event-Set param @ t : Target-element param @ m : mode ["onload"->"load" , "onclick"->"click"] param @ f : function **/ $$.setEvent = function(t, m, f){ //other Browser if (t.addEventListener){t.addEventListener(m, f, false)} //IE else{ if(m=='load'){ var body = d.body; if(typeof(body)!='undefined'){body = w;} if((typeof(onload)!='undefined' && typeof(body.onload)!='undefined' && onload == body.onload) || typeof(eval(onload))=='object'){ t.attachEvent('on' + m, function() { f.call(t , w.event); }); } else{f.call(t, w.event)} } else{t.attachEvent('on' + m, function() { f.call(t , w.event); })} } }; $$.__construct(); window.$$MBSYNC = $$; return $$; })();

注意事項

asp.core.jsの6行目にある「%your-site%」は、ソースコードの設置するドメインを入れてください。

サンプル

とりあえず、私の会社のWEBページに設置しているので、下記リンクをクリックして見てみてください。 左上の黒いマークに閲覧人数が表示されます。 PCでもスマホでも閲覧できます。 株式会社MYNTページ (*2022.11.29 : 現在この機能はwebページから外しています。スミマセン) ただし、現時点での課題は、スマホなどのスリープモードから復旧した時に数値がおかしくなる現象があります。 これは、reconnectが正常に機能として入れ込まれていないことが原因なのですが、SocketIOのイベント情報の調査中なので、分かったらまたブログでお知らせしたいと思います。 また、不明点などあればご質問ください。

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。