msful チューニングを行うの巻
msful と言う、最初nodejsで簡単なマイクロサービスをつくろうかと簡易的なWebAPIサーバ的なものを作ってみて面白いので色々と機能をあれやこれやとしていくと、何だかよさそうになり、npmに登録してみると結構ダウンロードされていたりして、それにつられて色々と機能追加を続ける日々を送っているのが、この存在ですw
msful - npm
気がつけばversion0.0.1 から始まって現在は0.0.37 とモリモリ機能追加を続けていく今日この頃。
と言うか暇なときに気ままに、機能追加できてすぐに動かせるってのがよいよねNodeJS。
だけど欠点は、abベンチとかで実行すると遅いってのがあるわけでして。
こんな感じ。
$ ab -c 50 -n 10000 http://127.0.0.1:3333/api/test
Server Software: msful(0.0.36)
Server Hostname: 127.0.0.1
Server Port: 3333Document Path: /api/test
Document Length: 227 bytesConcurrency Level: 50
Time taken for tests: 11.391651 seconds
Complete requests: 10000
Failed requests: 0
Write errors: 0
Total transferred: 5490000 bytes
HTML transferred: 2270000 bytes
Requests per second: 877.84 [#/sec] (mean)
Time per request: 56.958 [ms] (mean)
Time per request: 1.139 [ms] (mean, across all concurrent requests)
Transfer rate: 470.61 [Kbytes/sec] received
まあ、実装内容次第ではあると思うのだけど、やっていることは単純でこんな感じのこと
// entity examples require("./lib/entities") var user1 = { id: "12", name: "maachang", age:40, mail:"maachang@maachang.com" }; var user2 = { id: "21", name: "saito", age:27, mail:"saito@maachang.com" }; var users = { limit: 2, list: [user1, user2] } return entity.make("users", users);
./lib/entities.js
// entities. entity.expose("user" ,"id" ,"number" ,"num | rename 'number'" ,"name" ,"string" ,"req" ,"details" ,"{" ,"" ,"age" ,"number" ,"num" ,"mail" ,"string" ,"email" ,"details" ,"}" ,"" ,"code" ,"string" ,"default 'unknown'" ); require("./lib/entityParent");
./lib/entityParent.js
// entityParent entity.expose("users" ,"offset" ,"number" ,"default 0" ,"limit" ,"number" ,"default 0" ,"list" ,"$user" ,"" );
とテスト的なAPI実装をそのままApacheABでぶん回しただけのものなんだけど、まあ遅いと言うかもう少し速度が出て欲しいな~って思った。
と言うわけでチューニング。
実行処理自身をキャッシュ化すればOKってことで、ごにょごによとキャッシュ化。
以下実装部分ね。
APIをキャッシュ化 v0.0.37 · maachang/msful@03860bd · GitHub
で、もう一度ベンチを図る。
$ ab -c 50 -n 10000 http://127.0.0.1:3333/api/test
Server Software: msful(0.0.37)
Server Hostname: 127.0.0.1
Server Port: 3333Document Path: /api/test
Document Length: 227 bytesConcurrency Level: 50
Time taken for tests: 4.893279 seconds
Complete requests: 10000
Failed requests: 0
Write errors: 0
Total transferred: 5490000 bytes
HTML transferred: 2270000 bytes
Requests per second: 2043.62 [#/sec] (mean)
Time per request: 24.466 [ms] (mean)
Time per request: 0.489 [ms] (mean, across all concurrent requests)
Transfer rate: 1095.58 [Kbytes/sec] received
おおー倍以上の性能がw
msful (micro service RESTful web api server) がある程度使えそうだ
node js 凄いよね。
何が凄いかって、Webサーバ作るのにソースコードの量が半端なく少ないこと。
Clusterなどがあり、並列で動いてくれる。
ライブラリが豊富。
JSONの実装と展開が簡単(Jsだから当たり前)なので、コンフィグファイル的なものでも簡単記載できる。
まあ、そんな感じで色々と便利さを痛感して、とりあえず目標はWebAPIサーバを作ろうかと思って、実装すると2~3時間程度で実装ができたわけで、いやはやすげーって思った次第であるのは、前回記載した内容のとおり。
これを気分転換を行う場合や、時間のあるときに少しずつ、機能追加を行ってきたが、気がつけばバージョンは0.0.16で16回機能追加をおこなっていたわけで、いやはや、空いた時間で実装するのも1つの楽しみとなってきた今日この頃。
これまで大体何を追加したかと言えば、
1.console機能
2.config機能
3.WebAPIスクリプト実装をevalでなく、vmモジュールで実行できるように変更.
4.デフォルトモジュールの呼び出し
5.静的コンテンツがgz圧縮されている場合、その内容を優先的に送付する.
7.help表示機能
8.新規プロジェクトを作成するコマンド実装
まあ、大きめなのはconsoleとデフォルトモジュールの呼び出しでしょうか。
これらの実装で、最低限ではあるがとりあえず、マイクロサービスは作れそうか?って感じのものになったと思います。
ぜひ皆様もつかっていただき、ご意見などいただければと思います。
www.npmjs.com
https://github.com/maachang/msful
nodejs すごいよ2
せっかくなので、先ほどのWebApiサーバを npm インストールできるようにしてみました。
www.npmjs.com
GitHub - maachang/msful: msful is Web API server for micro service
NodeJSすごいよw
nodeJSはサーバ側のプログラムもJSでかけるので、ブラウザ側の実装と同じものが使えて便利だ。それとGoogleChromeのV8EngineであのGoogle製の高速JavaScriptEngineで、C10K問題を解決するNoneBlockingI/Oなど等、話題性を掻っ攫って登場したのは、2009年の約9年前。
だけど現在はGitBookや、ReactCLIなどの基盤で使われることが多くあるが、普通にWebサーバで利用されることはあまりない状況である。
何故なのか!!ってのは簡単で、NonBlockingだから1プロセス1スレッドでしか動かないから、結局Enterpriseでは使えないってことらしい。あとはバージョン毎の変化が激しいからとか。
私がこれまでNodeJSをまともに使おうと全く考えなかったこと、それは「1プロセス1スレッド」動作だから、それじゃあ重い処理たとえば「DBアクセス」中における待ち時間の時間において「他の通信I/O部分」がすべて停止してしまうと言う問題があるわけで、それじゃあ使えないねって思っていたわkです。
ただ、これも解決方法があって、そんな重い処理のときだけ、別途別スレッドで動かせばよいと言う話があるわけですが、それを一々実装時において気にしながら作るってのも、まあつらいよねって思うわけで「使わない自由」を行使しようと思うわけです。
・・・・・・・・・・・・・・・・・・・・・・・
そんな拒否反応からNodeJSと言うものの存在を軽く見ていた私なのですが、甘かったと言うか「激アマ」だよねって思ったのが「Cluster」の存在なわけです。
一々Nginx立てて、HTTPサーバも面倒だと思い、簡単なJS実装があればたいていの環境でNodeJSを入れているから、簡単なHTTPサーバを簡易的に立てれるwwwってことで、作ったやつに、じゃあ簡易的にWebAPI的にJSON返却ができるもの的があれば、色々と簡単なものを作る場合に便利だよねって思い、2時間ぐらいでJSを実装、300行足らずで普通に動くHTTPサーバ+WebAPIサーバを作ったわけですが、せっかくだからとWebAPI側の呼び出しは「マルチスレッド」で動かそうかとしらべてみたのですが、そこでみつかったのが「Cluster」と言うもの。
そして、それが超簡単で、たったこれだけ。
// クラスタ起動. var cluster = require('cluster'); var MAX_SERVER = require('os').cpus().length; if (cluster.isMaster) { for (var i = 0; i < MAX_SERVER; ++i) { cluster.fork(); } cluster.on('exit', function (worker, code, signal) { cluster.fork(); }); // サーバ起動. } else { // 普通にHTTPサーバの起動を行う系の処理を記載 }
これだけでたとえば4コアCPUならば、4つのプロセスでNodeJSが起動して、しかも「Bindしたポートを共有してくれる」わけで、いやはや「凄い」と思った次第。
これで、NodeJSの欠点である1プロセス1スレッドこれの問題はなくなりそうです。
ちまみに、作ったNodeJSのプログラムを以下に公開します。
使い方は簡単。
ファイル名を[msful.js]として作成するとして、
$ node ./msful[port]
※[port]はオプション、省略すると3333がバインドポートとなる.
とすれば、起動できます。
使い方は単純でWebAPIにアクセスする場合は
のようにパス[/api/]でアクセスすることで、WebAPIのJSファイルを実行してくれます。
index.jsの実装は普通に最終的にJSON型で返却したいものを返却すればOKです。
return {hoge: "moge"}
とすればレスポンスで {hoge: "moge"} と返却されます.
あとはファイル名を省略すると[index.js]が呼び出し対象となります.
通常のコンテンツファイルについては、WebAPIパス以外のパス名を利用することで、これは普通のNginxなどと同じようにI/Oできます。
これもファイル名を省略すると[index.html]が呼び出し対象となります。
次にフォルダ構成としては、
・msful.js
|
・[html]
| |
| L-- index.html など
|
・[api]
|
L-- index.js など
とmsful.js を実行するカレントパス以下のフォルダが対象で、[html]フォルダにはコンテンツ系のファイルを、[api]フォルダにはWebAPI実装のJSファイルを配置することで、WebAPIの返却実行が行われます。
以下ソースコード
// micro json server. version 0.0.1. (function (_g) { var fs = require('fs'); var http = require('http'); var PORT = 3333; var SERVER_NAME = "msful"; var HTML_DIR = "./html"; var API_DIR = "."; // ポート番号が指定されている場合は、そのポートを割り当てる. if (process.argv.length > 2) { var port = null; try { port = parseInt(process.argv[2]); if (port > 0 && port < 65535) { PORT = port; } } catch (e) { } } // クラスタ起動. var cluster = require('cluster'); var MAX_SERVER = require('os').cpus().length; if (cluster.isMaster) { for (var i = 0; i < MAX_SERVER; ++i) { cluster.fork(); } cluster.on('exit', function (worker, code, signal) { cluster.fork(); }); console.info("## listen: " + PORT); // サーバ起動. } else { // httpサーバ生成. var createHttp = function (call) { return http.createServer(function (req, res) { var data = ""; req.on("data", function (chunk) { data += chunk; }); req.on("end", function () { res = call(req, res, data); }); }) } // パラメータ取得. var getPms = function (request, response, data) { var ret = {}; var m = request.method.toLowerCase(); if (m == "get") { ret = _get(request.url); } else if (m == "POST") { ret = _post(request, data); } return ret; } // GETパラメータを処理. var _get = function (url) { var p = url.indexOf("?"); if (p == -1) { return {}; } return _analysisParams(url.substring(p + 1)); } // POSTパラメータ処理. var _post = function (request, data) { var c = request.headers["content-type"]; if (c.indexOf("/json") != -1) { return JSON.parse(data); } return _analysisParams(data); } // パラメータ解析. var _analysisParams = function (n) { var list = n.split("&"); var len = list.length; var ret = {}; for (var i = 0; i < len; i++) { n = list[i].split("="); if (n.length == 1) { ret[n[0]] = ''; } else { ret[n[0]] = decodeURIComponent(n[1]); } } return ret; } // HTTP実行. var exec = function (req, res, data) { var url = getUrl(req); // WebApi返却. if (url.indexOf("/api/") == 0) { readApi(req, res, data, url); // ファイル返却. } else { readFile(req, res, url); } } // api読み込み. var readApi = function (req, res, data, url) { if (url.lastIndexOf("/") == url.length - 1) { url += "index.js"; } var name = API_DIR + url; fs.readFile(name, function (err, src) { var status = 200; var headers = { 'Server': SERVER_NAME }; var body = ""; var bodyLength = 0; try { if (err) throw err; src = "return (function(_g) {\n" + "var request = args.req\n" + "var response = args.res\n" + "var params = getPms(request, response, args.data)\n" + "args = null\n" + "getPms = null\n" + src + "\n})(global)"; var eres = new Function('args', 'getPms', src)( { req: req, res: res, data: data }, getPms) src = null; body = JSON.stringify(eres); eres = null; bodyLength = utf8Length(body); headers['Content-Type'] = 'application/json; charset=utf-8;'; headers['Connection'] = 'close'; } catch (e) { // 例外が発生した場合は、エラー返却. // ただし、ファイルが存在しない場合は、404返却. if (e.code && e.code == 'ENOENT') { status = 404; } else { status = 500; console.error(e, e); } headers['Content-Type'] = 'application/json; charset=utf-8;'; headers['Connection'] = 'close'; body = "{\"error\": " + status + "}"; bodyLength = utf8Length(body); } headers['Date'] = toRfc822(new Date()); headers['Content-Length'] = bodyLength; // 返却処理. res.writeHead(status, headers); res.end(body); }); } // ファイル読み込み. var readFile = function (req, res, url) { if (url.lastIndexOf("/") == url.length - 1) { url += "index.html"; } var name = HTML_DIR + url; fs.stat(name, function (err, stat) { var status = 200; var headers = { 'Server': SERVER_NAME }; var body = ""; var bodyLength = 0; try { if (err) throw err; headers['Content-Type'] = mimeType(url); headers['Last-Modified'] = toRfc822(new Date(stat.mtime)); // リクエストヘッダを見て、If-Modified-Sinceと、ファイル日付を比較. if (req.headers["if-modified-since"] != undefined) { if (isCache(stat.mtime, req.headers["if-modified-since"])) { status = 304; } } if (status == 200) { // ファイル情報は非同期で読む. headers['Content-Length'] = stat.size; headers['Date'] = toRfc822(new Date()); // 返却処理. res.writeHead(status, headers); var readableStream = fs.createReadStream(name); readableStream.on('data', function (data) { res.write(data); }); readableStream.on('end', function () { res.end(); }); return; } } catch (e) { // 例外が発生した場合は、エラー返却. // ただし、ファイルが存在しない場合は、404返却. if (e.code && e.code == 'ENOENT') { status = 404; } else { status = 500; console.error(e, e); } headers['Content-Type'] = "text/html; charset=utf-8"; headers['Connection'] = 'close'; body = "error: " + status; bodyLength = utf8Length(body); } headers['Date'] = toRfc822(new Date()); headers['Content-Length'] = bodyLength; // 返却処理. res.writeHead(status, headers); res.end(body); }); } // キャッシュ条件かチェック. var isCache = function (a, b) { return parseInt(new Date(a).getTime() / 1000) == parseInt(new Date(b).getTime() / 1000); } // 正しいURLを取得. var getUrl = function (req) { var u = req.url; var p = u.indexOf("?"); if (p == -1) { return u; } return u.substring(0, p); } // ファイル拡張子からMimeTypeを返却. var mimeType = function (name) { var p = name.lastIndexOf("."); if (p == -1) { return "text/plain"; } var n = name.substring(p + 1) switch (n) { case "htm": case "html": return "text/html; charset=utf-8;"; case "xhtml": case "xht": return "application/xhtml+xml; charset=utf-8;"; case "js": return "text/javascript; charset=utf-8;"; case "css": return "text/css; charset=utf-8;"; case "rtf": return "text/rtf"; case "tsv": return "text/tab-separated-values"; case "gif": return "image/gif"; case "jpg": case "jpeg": return "image/jpeg"; case "png": return "image/png"; case "svg": return "image/svg+xml"; case "rss": case "xml": case "xsl": return "application/xml"; case "pdf": return "application/pdf"; case "doc": return "application/msword"; case "xls": return "application/vnd.ms-excel"; case "ppt": return "application/vnd.ms-powerpoint"; case "docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document docx"; case "xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx"; case "pptx": return "application/vnd.openxmlformats-officedocument.presentationml.presentation pptx"; case "dtd": return "application/xml-dtd"; case "sh": return "application/x-sh"; case "tar": return "application/x-tar"; case "zip": return "application/zip"; case "jar": return "application/java-archive"; case "swf": return "application/x-shockwave-flash"; case "mpga": case "mp2": case "mp3": return "audio/mpeg"; case "wma": return "audio/x-ms-wma"; case "wav": return "audio/x-wav"; case "3gp": return "video/3gpp"; case "3g2": return "video/3gpp2"; case "mpeg": case "mpg": case "mpe": return "video/mpeg"; case "qt": case "mov": return "video/quicktime"; case "mxu": case "m4u": return "video/vnd.mpegurl"; case "asf": case "asx": return "video/x-ms-asf"; case "avi": return "video/x-msvideo"; case "wmv": return "video/x-ms-wmv"; case "flv": return "video/x-flv"; case "ogg": return "application/ogg"; case "mpg4": return "video/mp4"; } return "application/octet-stream"; } // 日付情報をRFC-822に変換. var toRfc822 = function (n) { if (typeof (n) == "number") { n = new Date(parseInt(n)); } return n.toUTCString(); } // UTF8文字列のバイナリ長を取得. var utf8Length = function (n) { var c; var ret = 0; var len = n.length; for (var i = 0; i < len; i++) { if ((c = n.charCodeAt(i)) < 128) { ret++; } else if ((c > 127) && (c < 2048)) { ret += 2; } else { ret += 3; } } return ret; } // サーバー生成. var server = createHttp(exec); // 指定ポートで待つ. server.listen(PORT); } })(global)
ローカルストレージでは容量が足らんと、怒られました!!
誰に怒られたの=>答え:ブラウザにw
要するに、ローカルストレージの許容範囲を超えたと言うことでした><
今回作っている内容は、できるだけ、起動時にデータを吸い上げて、それを使って描画するので、基本的には、画像などのコンテンツ系のHTTPアクセス(キャッシュ確認のアクセス)すらさせないようにと思い、実装していたわけです。
ローカルストレージ、便利なんですが、容量が数メガ、そしてこれらは「ブラウザによって、容量が違う」らしく、我がiphone5sだと、mobilesafariでどうも2メガバイト程度と少ないわけでありまして、いやーまいっちんぐ!!って困っちゃったわけです。
容量を減らそうとしようにも、まあそれを行うのは、もう少し先の話であって、まずは別の口、そういえば、HTML5になって、IndexedDBとかあったような?って思って調べたら、ありましたし、あとWebSQLと言うindexedDBの前身的なものもあって、現在のブラウザでは、大体のところどちらも利用可能であるそうです。
次にこれらを「ローカルストレージ」風に利用できるライブラリがあるか、調べてみたら、見つかりました。
localForage
github.com
たしかに、基本的な使い方は「殆どローカルストレージ」に近いのですが、欠点として「async」getItemなどの取得がcallbackメソッドでしか取得できないと言う点が1点と、普通に[put,get,remove,clear]程度がやりたいだけなのに、やたら「でかい」と言うこと。
あと、中でどんな動きしているのか、分かりづらいということ、と言うことで、もう少し分かりやすくて、自分が使う分だけ使いたいと思ったので、実装してみました。
ただし「localForage」と同じく[dbStorage.get(key,function(value) { ... })]のように、callbackメソッドは同様に必要になりますが、コード的に最低限した書いてないので、すっきりしていますし、読みやすいと思います(少なくともlocalForageよりは).
ちなみに、PCのFireFox(57.0)とGoogleChrome(62.0.3202.94)とopera(49.0)とIE(11.0.48)とで動かしてみたけど、普通に動きました(すべてIndexedDBですが)。
一応WebDBでもサポート環境(GoogleChrome)ではためしたので、まあ大丈夫でしょうし、利用優先は(IndexedDB) > (WebDB) > (Memory)の順になっていますので、昨今の大抵のブラウザでは[IndexedDB]1択でしょうがw
使いたい人が居れば、利用してみてください。
// DBストレージ. // ブラウザ内データベースを利用した // ローカルストレージの代わりをする処理. // (function(_global) { "use strict"; // undefined定義. var _u = undefined ; var _c = function(call,value) { if(typeof(call) == "function" ) call(value); } // indexedDBが利用可能. try { _global.indexedDB = _global.indexedDB || _global.mozIndexedDB || _global.webkitIndexedDB || _global.OIndexedDB || _global.msIndexedDB; } catch(e) { } var idxdbs = null; if(_global.indexedDB != _u) { idxdbs = function() { var _name = 'dbLocalStorage' var _version = 100; // 1.00. var _table = "DbStorageByLocalTable"; var _keyColumn = "pkey"; var _valueColumn = "value"; var _db = null; var o = {}; // 初期処理. var _init = function(c) { var object = indexedDB.open(_name,_version); object.onupgradeneeded = function(ev) { console.log("### [indexedDB] upgradeneeded"); try { var db = object.result; db.transaction.onerror = function(err) { console.log("error indexedDB(init[onupgradeneeded])",err); } if (db.objectStoreNames.contains(_table)) { db.deleteObjectStore(_table); } db.createObjectStore(_table, {keyPath: ""+_keyColumn, autoIncrement: false}); } catch(e) { console.log("error init[onupgradeneeded]:" + e); } } object.onsuccess = function(ev) { console.log("### [indexedDB] success"); try { _db = (ev["target"] != _u && ev["target"] != null) ? ev["target"].result : ev.result; } catch(e) { _db = null; console.log("error init[onsuccess]:" + e); } if(c != _u) c(); } } // テーブル内クリア. var _clear = function(tx,call) { try { var store = tx.objectStore(_table); store.clear(); tx.oncomplete = function () { _c(call, true); } tx.onabort = tx.onerror = function () { _c(call, false); } } catch(e) { console.log("error clear:" + e); _c(call, false); } } // 行削除. var _delete = function(tx,key,call) { try { var store = tx.objectStore(_table); store.delete(""+key); tx.oncomplete = function () { _c(call, true); } tx.onabort = tx.onerror = function () { _c(call, false); } } catch(e) { console.log("erro delete:" + e); _c(call, false); } } // 行追加. var _insert = function(tx,key,value,call) { try { var store = tx.objectStore(_table); var v = {}; v[_keyColumn] = "" + key; v[_valueColumn] = "" + value; store.put(v); tx.oncomplete = function () { _c(call, true); } tx.onabort = tx.onerror = function () { _c(call, false); } } catch(e) { console.log("error add:" + e); _c(call, false); } } // 1行取得. var _get = function(tx,key,call) { try { var store = tx.objectStore(_table); var req = store.get(""+key); req.onsuccess = function() { var v = req.result; if(v == _u || v == null) { v = ""; } else if(v.value == _u || v.value == null) { v.value = ""; } _c(call,v.value); } req.onerror = function() { _c(call,null); } } catch(e) { console.log("error get:" + e); _c(call, null); } } // readWrite transaction. var _tran = function() { try { return _db.transaction(_table,"readwrite"); } catch(e) { console.log("error getTransaction(rw):" + e); } } // readOnly transaction. var _rTran = function() { try { return _db.transaction(_table,"readonly"); } catch(e) { console.log("error getTransaction(r):" + e); } } // 初期処理. var _initFlag = false; var init = function(c) { var n = _initFlag; _initFlag = true; if(!n) { _init(c); } else { if(c != _u) c(); } } // 全データクリア. o.clear = function(call) { init(function() { _clear(_tran(),call); }); } // データセット. o.add = o.put = function(key,value,call) { init(function() { _insert(_tran(),key,value,call); }); } // データ削除. o.remove = function(key,call) { init(function() { _delete(_tran(),key,call); }); } // データ取得. o.get = function(key,call) { init(function() { _get(_rTran(),key,call); }); } // 区分. o.type = function() { init(); return "indexedDB"; } return o; } } // WebSQLが利用可能な場合. var wsqls = null; if(_global["openDatabase"] != _u) { wsqls = function() { var _name = 'dbLocalStorage' var _version = '1.0' var _description = 'dbLocalStorage' var _size = 5 * 1048576; var _table = "DbStorageByLocalTable"; var _keyColumn = "id"; var _valueColumn = "value"; // データベース取得. var _db = openDatabase(_name, _version, _description, _size); var o = {}; // 基本テーブル作成. var _create = function(tx,call) { tx.executeSql("create table if not exists "+_table+ " ("+_keyColumn+" TEXT NOT NULL PRIMARY KEY UNIQUE" + " ,"+_valueColumn+" TEXT)", [], function() { _c(call, true); }, function() { _c(call, false); } ); } // テーブル破棄. var _drop = function(tx,call) { tx.executeSql("drop table "+_table, [], function() { _c(call, true); }, function() { _c(call, false); } ); } // 行削除. var _delete = function(tx,key,call) { tx.executeSql("DELETE FROM "+_table+" WHERE "+_keyColumn+"=?", [ "" + key ], function() { _c(call, true); }, function() { _c(call, false); } ); } // 行追加. var _insert = function(tx,key,value,call) { tx.executeSql("INSERT INTO "+_table+" VALUES ( ?, ? )", [""+ key, "" + value ], function() { _c(call, true); }, function() { _c(call, false); } ); } // 初期処理. var _initFlag = false; var init = function(c) { var n = _initFlag; _initFlag = true; if(!n) { // 基本テーブル作成. _db.transaction( function(tx) { _create(tx); if(c != _u) c(); } ); } else { if(c != _u) c(); } } // 全データクリア. o.clear = function(call) { init(function() { _db.transaction( function(tx) { _drop(tx,function() { _create(tx,call); }); }); }); } // データセット. o.add = o.put = function(key,value,call) { init(function() { _db.transaction( function(tx) { _delete(tx,key,function() { _insert(tx,key,value,call); }); }); }); } // データ削除. o.remove = function(key,call) { init(function() { _db.transaction( function(tx) { _delete(tx,key,call); }); }); } // データ取得. o.get = function(key,call) { init(function() { _db.transaction( function(tx) { tx.executeSql( "SELECT " + _valueColumn + " FROM " + _table + " WHERE " + _keyColumn + "=? limit 1", [ key ], function(tx, rs) { var len = rs.rows.length; if(len != 1) { _c(call, null); } else { var row = rs.rows.item(0); _c(call, row[_valueColumn]); } } ); }); }); } // 区分. o.type = function() { init(); return "webDB"; } return o; } } var dbStorage = null; // indexedDBが利用可能な場合は、それで処理対象とする. if(idxdbs != null) { dbStorage = idxdbs(); // WebSQLが利用可能な場合は、それで処理対象とする. } else if(wsqls != null) { dbStorage = wsqls(); // 対応Webデータベースが存在しない場合は、メモリで代わりを行う. } else { dbStorage = (function() { var o = {}; var map = {} o.clear = function(call) { map = {}; _c(call, true); } o.add = o.put = function(key,value,call) { map[""+key] = value; _c(call, true); } o.remove = function(key,call) { delete map[""+key]; _c(call, true); } o.get = function(key,call) { var ret = map[""+key]; _c(call, ret); } o.type = function() { return "memory"; } return o; })(); } _global.dbStorage = dbStorage; })(window); } dbStorage.init(); _global.dbStorage = dbStorage; })(window);
ちなみに、localStorageの容量が分かる便利なサイトがあり、家にあるやつで色々調べてみたら、以下の通りでした。
Web Storage Support Test
・iphone5s ios11 mobile safari
localStorage 2.5M
sessionStorage unlimited
・android4.1 mobile firefox57.0
localStorage 5M
sessionStorage 5M
・windows7 firefox57.0 or chrome62.0.3202.94
localStorage 5M
sessionStorage 5M
以外と少ないらしいので、画像関連の情報をlocalstorageに入れようかとか、考える場合は結構容量的に少ないので、このライブラリを使えば、少しはキャッシュできるかと思います。
iosとandroidでは、傾きによる縦横解像度変更のイベント取得方法が違う。
HTML5Canvasで画面全体を描画している内容、これを「傾き」によって「縦横の解像度」の変更に対する「イベント」をトリガーにすることで、この現状の状態を保持し、Canvas自体を初期化しなおして、再描画前の状態で再描画させることで、あたかも「縦」「横」それぞれで、表示領域を変更させると言うことをしたいと思った。
ちなみにHTML5Canvasは面倒なことに、一度生成した解像度(width,height)これを変更することはできない。
なので、画面いっぱいに広げたHTML5Canvasに対して、スマホの「傾き」で画面が「縦・横」に切り替わったとき、普通のHTML描画の画面のように、解像度が切り替わらず、そのため、今回、この「傾き」での「縦・横」切り替えをトリガーとして、Canvasをその解像度にあわせて、再作成する必要となった次第。
と、これが、前置きの話。
・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
まず、調べると便利な「イベント」を見つける。
「orientationchange」と言うイベント。
このイベントは、「傾き」によって「縦横の解像度」の変更がなされたときに呼び出されるというもの。
また「window.orientation」で実際の傾きの角度を取得することで、上記イベントとの挙動とあわせて取得すると、「縦向き」か「横向き」かを判別することができると言う「優れもの」だそうだ。
・・・・・・・・・・・・・・・・・
しかし、そんなに「世の中」は甘くない><
と言うのも、上記イベントのコンボは「ios」では「理想どおり」に「動く」けど「Android」では、理想どおりに動かない。
何故か、色々とログ入れて調べると、どうも「orientationchange」イベントは発火するけど、このときに「window.orientation」を取ると、どうも90,0,90,180と取れる値の割り当てが「Android」毎に「ばらばら」のようで、そして、致命的なのが「orientationchange」イベント内で取得した[innerWidth]や[innerHeight]は、その時の「前回の値」つまり、前回が「縦」で今回が「横」の場合、その時取得できる値は「縦」で取得されるべき値であると言うこと。
色々と調べたことの内容を記載したが、まあわかりづらくなってしまったが、要するに普通に「orientationchange」は「ios」では使えるが「Android」では使えないと言うことだ。
で、調べると、解決方法がありました。
qiita.com
で、これを元に実際に実装した内容は以下の通りです。
// タブレット端末の傾き状態を保持. // vertical : 縦向きの状態. // landscape : 横向きの状態. // none : 未検知の状態. var tabletWidthHeightMode = "none" ; // タブレット傾き検知メソッド. var functionInclination = [] ; // androidの場合. if(isAndroid()) { var defaultOrientation; // window.orientationが0または180の時に縦長であればtrue window.addEventListener("orientationchange",function() { // スクロール位置をゼロに指定. // これを行わないと、iphoneなどでは画面がずれる場合がある. pageTop() ; },false); // 初期化処理 window.addEventListener("load", function() { if('orientation' in window) { var o1 = (window.innerWidth < window.innerHeight); var o2 = (window.orientation % 180 == 0); defaultOrientation = (o1 && o2) || !(o1 || o2); checkOrientation(); } },false); // 画面回転時に向きをチェック window.addEventListener("resize", function(){ checkOrientation(); },false); var checkOrientation = function() { if('orientation' in window) { // defaultOrientationがtrueの場合、window.orientationが0か180の時は縦長 // defaultOrientationがfalseの場合、window.orientationが-90か90の時は縦長 var o = (window.orientation % 180 == 0); if((o && defaultOrientation) || !(o || defaultOrientation)) { tabletWidthHeightMode = "vertical"; } else { tabletWidthHeightMode = "landscape"; } // イベントに対する追加メソッド呼び出しが要求される場合. if( functionInclination.length > 0 ) { var list = functionInclination; var len = list.length; var m = tabletWidthHeightMode; for(var i = 0; i < len; i ++) { list[i]( m ) ; } } } } } // iosの場合. else if(isIOS()) { // 現在の方向を取得. var _getWidthOrHeight = function() { if(isTablet()) { if( window.orientation == 0 || window.orientation == 180 ) { return true ; } } else { if(window.innerHeight >= window.innerWidth) { return true; } } return false ; } // 傾きのイベントを取得. window.addEventListener("orientationchange",function( e ) { // スクロール位置をゼロに指定. // これを行わないと、iphoneなどでは画面がずれる場合がある. pageTop() ; // 傾き情報を算出. if( _getWidthOrHeight() ) { tabletWidthHeightMode = "vertical" ; } else { tabletWidthHeightMode = "landscape" ; } // イベントに対する追加メソッド呼び出しが要求される場合. if( functionInclination.length > 0 ) { var list = functionInclination; var len = list.length; var m = tabletWidthHeightMode; for(var i = 0; i < len; i ++) { list[i]( m ) ; } } },false); } // 傾き検知用メソッドの追加. // func : 傾き検知用のメソッドを設定します. // このメソッド呼び出しに対して、第一引数に[width]と設定された場合は横向きです. // このメソッド呼び出しに対して、第一引数に[height]と設定された場合は縦向きです. // ※また、この内容はタブレット端末で、縦横検知がサポートされているブラウザ専用です. // 戻り値 : trueの場合、正しく登録されました. var addInclinationFunction = function( func ) { if( typeof( func ) == "function" ) { removeInclinationFunction(func); functionInclination[functionInclination.length] = func ; return true ; } return false ; } // 傾き検知用メソッドの削除 // func : 傾き検知用のメソッドを設定します. // このメソッド呼び出しに対して、第一引数に[width]と設定された場合は横向きです. // このメソッド呼び出しに対して、第一引数に[height]と設定された場合は縦向きです. // ※また、この内容はタブレット端末で、縦横検知がサポートされているブラウザ専用です. // 戻り値 : trueの場合、正しく登録されました. var removeInclinationFunction = function( func ) { if( typeof( func ) == "function" ) { var len = functionInclination.length; for(var i = 0; i < len; i ++) { if(functionInclination[i] == func) { functionInclination.splice( i,1 ) ; return true; } } } return false ; } // 傾き検知用メソッドの全クリア. var clearAllInclinationFunction = function() { functionInclination = []; }
※上記での[isAndroid]や[isIOS]や[isTablet()]は、各自で用意してみてください^^
あと、この実装を使っていて、別件で困ったこととして、Androidの場合、傾きではなく、ブラウザサイズの変更をトリガーとしているが、このトリガー条件だと「テキスト入力」などで利用する「ソフトウェアキーボード」これが下から「ぬぬー」って感じで出てくるけど、このタイミングでも、傾き検知の「トリガー」が走ってしまう><
そんな「些細な」問題もあるので「要注意」ですw
Webフォント、HTML5Canvasで使うときの注意^^
Webフォントを使うと、ブラウザやOSが違っても同じ文字表現が出来るので便利だ。
だけど、結構な大きさのファイルをダウンロードさせる必要があるため(10MByte以上)中々導入となると、ちょっとねえ・・って二の足を踏んでしまう。
なので、これまであまりWebフォントを使わずに、なるべく内部のフォントを使っていたわけだが、今回ios,android,pcでそれぞれ表現させると、内部のフォントが結構ちがっていたこと、Webフォントを使うと、結構かっこよく見えたため、今回はWebフォントを採用しようと思った次第。
・・・・・・・・・・・・・・・・・・・・・・・
だけど、問題が勃発。
と言うのも、Canvasを使っている部分が主であり、このWebフォントをCanvasで利用したいと思っていたのだけど、画面を表示させたしょっぱなだけ、何故かWebフォントが使われないと言う、問題が発生していた。
と言うのも、PCでは普通に問題なく動くことから、発見が遅れたと言うか、実際にiosの実機でMobileSafariから起動させると、Canvasに対して、最初の表示文字がWebフォントを使われず、次に表示されるときからWebフォントが対応されると言う状況が発生したわけで、最初?何だろうか?画面がおかしいって感じで思った次第。
・・・・・・
結果論から言うと、CanvasでWebフォントを読もうとすると、非同期で実行されるため、だからCanvasの最初の表示では、文字に対してWebフォントは利用できない状況となり、内部のフォントが利用されていたと言うことである。
たぶん別途最初にCanvas上に対象のフォントを読み込み処理すれば、問題なくWebフォントが反映されると思うが、もっと簡単に対応できるものはないのか?って思い探したが、まあそんなものはなく、結果的に以下URLのサイトを参考に作った次第。
var ERROR_COUNT = 100; var CHECK_TIME = 50; // 指定されたWebフォントをCanvas向けにロード. var loadWebFont = function(name,bold,successCall,errorCall) { //webフォントのロード状況を確認する var c1 = document.createElement("canvas"); var c2 = c1.cloneNode(false); var ctx1 = c1.getContext("2d"); var ctx2 = c2.getContext("2d"); var boldText = (bold?"bold ":""); ctx1.font = " " + boldText + "15px "+name+", serif"; ctx2.font = " " + boldText + "15px serif"; var text = "load web font test."; //テキスト幅を比較する //webフォントが利用可能となると,フォント幅が一致する. var isLoaded = function(){ var tm1 = ctx1.measureText(text); var tm2 = ctx2.measureText(text); return tm1.width != tm2.width; } var exitLoad var cnt = 0; var tryDraw = function(){ if(!isLoaded() && cnt < ERROR_COUNT){ cnt++; setTimeout(tryDraw, CHECK_TIME); } else if(cnt >= ERROR_COUNT) { if(typeof(errorCall) == "function") { errorCall(); } } else { if(typeof(successCall) == "function") { successCall(); } } } tryDraw(); } // Webフォント読み込み成功処理. var viewSuccessWebFont = function(name,bold) { var canvas = document.createElement("canvas"); var ctx = canvas.getContext("2d"); ctx.fillStyle = "white"; var boldText = (bold?"bold ":""); ctx.font = " " + boldText + "15px "+name+", serif"; ctx.fillText(" ... success", 0, 0); document.body.appendChild(canvas); } // こんな感じで利用する。 loadWebFont("\'Noto Sans JP\'",true,function() { // WebFont読み込み成功. viewSuccessWebFont("\'Noto Sans JP\'",true); });