最近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のイベント情報の調査中なので、分かったらまたブログでお知らせしたいと思います。
また、不明点などあればご質問ください。
0 件のコメント:
コメントを投稿