2018年11月19日

CPU負荷を抑えて重い処理を軽くするJavaScriptライブラリ「chillout.js」

「chillout.js」とは?


chillout.js は「処理時間を短くする」という物理的な高速化とは違い、CPU負荷を抑えてリソースに余裕を持たせ、重い処理でも軽く感じさせることでユーザーにとって体感的・心理的な高速化につなげる JavaScriptライブラリです。


重い処理から開放されるために


JavaScriptでfor文など繰り返し(ループ)処理をしたとき、重い処理により一瞬でもページが固まってしまうことがあります。
そんなときブラウザ画面は読み込み中のままローディングのくるくるが止まらなかったり、ゲーム中にマウスが効かなくてカクカクだったり、
Webページだけじゃなくnode.jsなど画面のいらない処理でも高CPU負荷が続くとマシンごと重くなって大変です。

特にスペックの低いPCやタブレットの場合、 CPU使用率100%の状態では加熱してきて冷却ファンが高回転になり、そのまま使い続けると冷却が追いつかずに熱暴走してしまう可能性もあるので、うっかり重いページを開いてファンが「ウイーン!」って言い出すとヒヤヒヤします。

最近のPCは性能がよくなってるのでそんな事態になりにくいかもしれませんが、自分のPCも真夏の気温で動かなくなって修理にだしたのはまだ新しい記憶です😭 (直りました)。

「CPUファンが回りっぱなしかと思ったらフリーズして電源が落ちた」なんてことになると作業途中だったらやり直しになるし、HDDやSSDなどの内部にもダメージが残るかもしれないのでなるべく処理を軽くしたいところです。

JavaScriptでCPU負荷を抑えるには?


重い処理のほとんどはループ処理によって発生します。ループの中でさらにループ、その中でさらにループ…。
ループ間の処理が重くなってくるとマシンのCPUは休む暇なく動き、結果として内部が熱くなるため冷却ファンをたくさん回すことになります。
単純に考えた場合、そうならないようループの途中で一定時間処理を休止させればいいんですが、それができません。
JavaScriptには一定の時間休む sleep のような機能がないからです。

そこで、sleepするにはどうするか?というと「非同期」でループ処理します。

CPUを休ませるために非同期でループ処理する


JavaScriptのループは for文や while文、また Array に対して forEachなどがありそれぞれ同期で処理されますが、上述したように sleep して休ませることができないため、これらのループ処理を非同期で実現 します。

同期処理を非同期化するには、
  • setTimeoutを使う
  • process.nextTickを使う(node.js)
  • DOMイベントを使う
  • MessageChannelを使う

などの方法があり、一連の処理を Promise と組み合わせると非同期ループが実現できます。

(setInterval や requestAnimationFrame を使うこともできますが、これらは精度を保とうとより正確にループしようとするため今回の用途には不向きです。)

今までも CPU負荷を抑えるためのライブラリをいくつか作ってきたのですが、当時は Promise という便利なものがJavaScriptになかったので自前で Deferred と呼ばれる Promise のようなものを定義していました。そのためにライブラリのサイズが大きくなるしAPIがガラパゴス化してたんですが、ようやくシンプルな実装にできたと思います。

処理時間を短くするんじゃなく、体感速度を向上させる心理的な高速化



処理の高速化というと、とにかく1ミリ秒でも処理時間を短くすることが手法とされますが、Webページやアプリ、ゲームといった、人が画面を見たり操作する場合 心理的に「速い」と感じればユーザーのストレスが減り結果として高速化につながります。


Twitterでいい例があったので紹介。
これはエクセルマクロの話ですが 処理時間を短くするんじゃなく待ち時間を退屈させないようにして体感速度を向上させて、心理的な高速化しています。


CPU負荷を抑えてループ処理を軽くするJavaScriptライブラリ「chillout.js」



冒頭が長くなっちゃいましたが、ライブラリの紹介です。

chillout.js は、ループ処理中に適度な休憩(sleep)を入れてあげ、カクカクする重さを感じさせないライブラリ です。
また、処理が重くなるとでる「警告: 応答のないスクリプト」というブラウザ警告なしでJavaScriptを実行できます。

ループ処理が重いときにはCPUが休まるくらいの休止時間、処理が速いときには休止時間なしか、わずかな休止時間をいれ本来のループを邪魔しないようにします。

ベンチマーク


for文と chillout.repeat を比較します。

function heavyProcess() {
  var v;
  for (var i = 0; i < 5000; i++) {
    for (var j = 0; j < 5000; j++) {
      v = i * j;
    }
  }
  return v;
}

例として上のような重い処理(テスト用に5000*5000回繰り返す処理)に対して、

for文

JavaScriptのfor文。

var time = Date.now();
for (var i = 0; i < 1000; i++) {
  heavyProcess();
}
var processingTime = Date.now() - time;
console.log(processingTime);

CPUグラフ:
  • 処理時間: 107510ms.
  • CPU平均使用率(Nodeプロセス): 97.13%

CPUグラフは上のようになり、CPU均使用率は 100% までいってないけど 97.13% になりました。

chillout.repeat

これを「chillout.js」のメソッド chillout.repeat で実行します。

var time = Date.now();
chillout.repeat(1000, function(i) {
  heavyProcess();
}).then(function() {
  var processingTime = Date.now() - time;
  console.log(processingTime);
});

CPUグラフ:
  • 処理時間: 138432ms.
  • CPU平均使用率(Nodeプロセス): 73.88%

ベンチマーク結果


  ForStatement (for文) chillout.repeat
処理時間 107,510ms. 138,432ms.
CPU平均使用率(Nodeプロセス) 97.13% 73.88%


グラフでは少しわかりにくいかもしれませんが、CPU平均使用率は for文 97.13% に対し、 chillout.repeat は 73.88% となり、for文よりもCPU使用率が抑えられてます。

そのかわりループ中にCPUを休ませてるため処理時間は 107,510ms から 138,432ms となり少しかかっています。
もっとCPU使用率を抑えるには単純に休止時間を増やせばいいんですが、そうすると処理時間が長くなるため適度なバランスにしています。

画面上ではわかりませんが、for文を実行してるときはCPUファンが「ウイーン!」と激しく回っていたのに対し、chillout.repeat のときはファンが静かめでした(環境によってそこまで変わらないかもですが)。

chillout.js は、処理速度を少し遅くするかわりに低いCPU使用率で安定してJavaScriptを実行できる特徴があります。

特にブラウザやゲームなど、人が画面を見て操作するJavaScriptのパフォーマンスにおいて最も重要なことの一つは、 数値的な速度だけでなく、安定したレスポンスによってユーザーにストレスなく動かすことと考えています。

(ベンチマークスペック: Windows8.1 タブレット / Intel(R) Atom(TM) CPU Z3740 1.33GHz)

API



for文やwhile文、Array.forEachに対応するメソッド4つがあります。そのうち2つ紹介。

chillout.repeat

for文のような動きをします。

// 5回繰り返す
chillout.repeat(5, function(i) {
  console.log(i);
}).then(function() {
  console.log('done');
});
// 0
// 1
// 2
// 3
// 4
// 'done'

chillout.forEach

Array.forEachと同じように動く。

// 配列を回す
var values = ['a', 'b', 'c'];
chillout.forEach(values, function(value) {
  console.log(value);
}).then(function() {
  console.log('done');
});

// 'a'
// 'b'
// 'c'
// 'done'

非同期処理で Promise が返されるため then で繋ぎます。
(他のAPIの詳細は chillout.jsのGitHub を参考ください。)

比較表


既存のJavaScriptループを chillout.js のAPIに置き換えると、大抵の場合 CPU使用率を抑えて実行できます。

変換例:

JavaScript chillout.jsの場合
[1, 2, 3].forEach(function(v, i) {}) chillout.forEach([1, 2, 3], function(v, i) {})
for (i = 0; i < 5; i++) {} chillout.repeat(5, function(i) {})
for (i = 10; i < 20; i += 2) {} chillout.repeat({ start: 10, step: 2, done: 20 }, function(i) {})
while (true) {} chillout.till(function() {})
while (cond()) {} chillout.till(function() {
  if (!cond()) return chillout.StopIteration;
})
for (value of [1, 2, 3]) {} chillout.forOf([1, 2, 3], function(value) {})


GitHub / chillout.js



GitHub / chillout.js

※このブログの内容は常に更新してるわけじゃないので古くなってる可能性があります。特にAPIの最新の情報は GitHub / chillout.jsを参考ください。


2015年10月30日

ECMAScriptを正規表現で解く

最近、こつこつと Chiffon というECMAScriptのパーサを書き進めています。

この Chiffon の主な解析方法は正規表現です。
というのも、前に書いたJavaScriptコードをトークンに分解する正規表現が発端で、これを改良し、es6 にも対応させようとしています。

ECMA-262 の仕様を調べながらパーサを書いて、今まであまり知らなかったことがでてきたり、
いろんな構文パターンをテストしていくうちに、正規表現のミスや修正が必要なものがでてきました。

数値リテラル

数値リテラルにマッチする正規表現は
/0(?:[xX][0-9a-fA-F]+|[0-7]+)|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[1-9]\d*/g
でいいかと思っていましたが、
ECMA-262 11.8.3 Numeric Literals をあらためて見ると表現が足りないことに気付きます。

DecimalLiteral はドットから始まることができるので、.1 などにマッチする必要があります。
また、0x の他に BinaryIntegerLiteral (0bXXX)、OctalIntegerLiteral (0oXXX) があります。

/0(?:[xX][0-9a-fA-F]+|[oO][0-7]+|[bB][01]+)|(?:\d+(?:[.]\d*)?|[.]\d+)(?:[eE][+-]?\d+)?|[1-9]\d*|0[0-7]+/g
現在はこの正規表現になっています。reffidleデモ

文字列リテラル

/'(?:\\.|[^'\\])*'/g
文字列リテラルにマッチする正規表現はこのパターンが有名です。
バックスラッシュでエスケープされたシングルクォート \' に対して正しくマッチします。

ただし、この正規表現は不完全で、改行にもマッチしてしまいます。
改行にはマッチしないが、バックスラッシュでエスケープされた改行にはマッチしたい。

/'(?:\\[\s\S]|[^'\r\n\\])*'/g
この正規表現だと改行にマッチしなくなり、なおかつ
'aaa\
bbb\
ccc'
のような、改行をバックスラッシュでエスケープした場合はマッチするようになります。

ただ、この正規表現にはまだ罠があります。
ECMA-262 11.3 Line Terminators では <CR><LF> の2文字で改行扱いされます。
'aaa\
bbb\
ccc'
この改行コードが <CR><LF> だった場合、マッチに失敗してしまうのです。
\<CR>\<LF> と言った具合に、<CR><LF> の各文字の前にバックスラッシュがあればマッチすることになります。

<CR><LF> にも正しくマッチさせるには、単純に \\\r\n を追加して、
/'(?:\\\r\n|\\[\s\S]|[^'\r\n\\])*'/g
この正規表現で改行コードに関しても正しくマッチします。refiddleデモ

※ECMA-262 11.3 Line Terminators は他にもありますが、この記事では簡略化しています。

正規表現リテラル

正規表現リテラルは以前こう書いていました。
/\/(?!\*)(?:\\.|[^\/\r\n\\])+\/(?:[gimy]{0,4}|\b)/g

これは大抵の場合、正しくマッチしますが、
ECMA-262 11.8.5 Regular Expression Literals では文字グループ [...] 内に限りスラッシュ記号を直接書くことができます。
/[/]/
このような正規表現リテラルに対しマッチに失敗してしまいます。

文字グループ内はバックスラッシュのエスケープなしでスラッシュ / を書けることを考慮し、
RegularExpressionFlags も g, i, m, u, y に直します。
/\/(?![*\/])(?:\\[\s\S]|\[(?:\\[\s\S]|[^\]\r\n\\])*\]|[^\/\r\n\\])+\/(?:[gimuy]+\b|)/g
この正規表現で、確実に正規表現リテラルにマッチさせることができます。 refiddleデモ

ただし、ECMAScript には / や /= といった演算子があり、 正規表現リテラルじゃないものにまでマッチする可能性があります。
var a = 1, b = 2, c = [1, 2], g = 2, i = 3;
a /= a / b / c[2/g] /i

このようなコードに対して誤ったマッチをしてしまいます。

もし JavaScript に否定後読み (?<!...)、もしくは肯定後読み (?<=...) があったらいけそうな気がしますが、 おそらく解決できないでしょう。
if (1) /a/
や
if (((a||b)&&c)||d) /a/.test('a') ? ... : ...
などのように if, while, for, with などが前にある場合、括弧を超えて判断する必要があるからです。

ECMAScript の正規表現リテラルにマッチする正規表現は、少なくとも JavaScript の正規表現では100% 確実には無理なので、
Chiffon では正しいマッチかどうかバックトラックして判断しています。

テンプレートリテラル

テンプレートリテラルの正規表現は、
/`(?:\\[\s\S]|[^`\\])*`/g
このパターンは、バッククォートからバッククォートまで ` ... ` バックスラッシュのエスケープも考慮してマッチします。 refiddleデモ

テンプレートリテラルは、
ECMA-262 11.8.6 Template Literal Lexical Components にあるように、改行 (LineTerminatorSequence) が含まれます。

しばらくはこの正規表現で問題ないと思っていたのですが、
テンプレートリテラルの Expression 部分 ${ ... } には、
その中にさらにテンプレートリテラルが書けるので、これを表現するのは正規表現の再帰が必要になり、無理ではないかと思っています。
そこで Chiffon では、有限ではあるものの、ある程度のネストを考慮して、
/`(?:\\[\s\S]|\$\{(?:\\[\s\S]|[^{}\\]|\{(?:[^{}]*(?:\{[^{}]*\})?)*\})*\}|[^`\\])*`/g
このような正規表現になっています。refiddleデモ

このパターンは、${ から } までを別でマッチさせて、 ある程度の深さまでなら正しく扱えるようになっています。
ただし再帰パターンは JavaScript では書けないため、
` ${
      `${
           `${
                1 + 1
             }`
       }`
   }
`
ここまでの深さまでマッチしますが、
` ${
      `${
           `${
                `${
                     1 + 1
                 }`
             }`
       }`
   }
`
これ以上になるとマッチに失敗します。

ただ、通常ここまで深くなるテンプレートリテラルを書かないと思うので、そこまで問題視していません。

<!--, --> でコメントになる

ECMA-262 B.1.3 HTML-like Comments をみると、 <!-- や --> がコメント扱いになることがわかります。 詳しい仕様まではあまり一般的ではないように思いますが、
var a = 1, b = a<!--a;
console.log(b); // 1
b の値は false ではなく 1 になります。

<!-- 以降がコメント扱いされているため、
var a = 1, b = a<!--a;
b = a で終わってしまっています。

また、 --> に関しては、 <!-- から --> の間がコメントになるのではなく、 それ以降の行末までがコメントになります。
var a = 1;
--> a = 2;
console.log(a); // 1
上のコードは --> a = 2 がコメントになり、1 が console.log されます。

--> は、大雑把に言うと行頭にない場合コメントになりません。
var a = 1, b = a-->a;
console.log(b); // true
上のような場合はコメント扱いされません。

Identifier に UnicodeEscapeSequence が使える

ECMA-262 11.6 Names and Keywords によると、 IdentifierName に UnicodeEscapeSequence が使えます。

今までせいぜい数えられるくらいしか出会ってないのと、使う機会もないものですが、
\u0074\u0079\u0070\u0065\u006F\u0066 \u307B\u3052
これをコンソールで実行すると 'undefined' と結果が返ると思います。
typeof ほげ
やってることは上のコードと同じです。 Identifier を \uXXXX もしくは \u{XXXX} で表すことができます。
部分的に表すこともできるので、typeof ほ\u3052 でも同じです。
var \u307B\u3052 = 1;
console.log(ほげ); // 1
非ASCII文字が含まれるソースコードを、ASCIIのみにしたい時などに使えます。

RegExp.prototype.exec から string.match(regex) で高速化

Chiffon はもともと、RegExp#exec を使って、
var m;
while ((m = re.exec(source)) != null) {
  // ...
}
といったやり方で字句解析してたのですが、
これがなかなか遅いので、どうにかならないかとあれこれ試して、
var m = source.match(re);
正規表現リテラルに g フラグをつけて、String#match を使ったら、約250% 速くなりました。
いくらビルトインメソッドとはいえ exec はループで回すとなると扱いが難しいです。



なんとなく書き始めた Chiffon ですが、
ECMA-262 の仕様を知るいいきっかけになってくれています。
まだ tokenizer (字句解析) までの実装で厳密なパーサではないですが、日々進めていこうと思ってます。

Chiffon レポジトリ




2015年10月11日

ECMAScriptのParser/Tokenizer(字句解析)をJavaScriptで書きました

ECMAScript のコードを字句解析するコンパクトなパーサーライブラリ、chiffon.js を書きました。
chiffon.min.js は、3KB ほどのサイズです(v1.2.0 現在)。

特徴

  • ライブラリのファイルサイズが軽い
  • ECMAScript6 の仕様 を元にしてる
  • 解析結果が esprima のtokenize結果と同じになる
  • esprima ほど厳密ではない(構文エラーあっても解析を続けるなど)
  • 速度は esprima と同じくらいかちょっと速い
  • 正規表現で解析してる

Chiffon はとにかくファイルサイズが軽いのが特徴です。
正規表現で一発解析してて、その正規表現が少し長くなっちゃってるため、これ以上なかなかファイルサイズが削れないです。
構文エラーなどあってもエラーを吐かずに解析を続けます。
esprima ほど厳密ではないものの、ほとんどは esprima と同じ結果になります。
Parser と言ってますが、いまのところ Tokenize(字句解析) してるだけなので、厳密には Parser とは言わないのかもしれません。

使い方

ブラウザの場合:

<script src="chiffon.js"></script>
または、
<script src="chiffon.min.js"></script>
Chiffon というオブジェクトがグローバルに定義されます。

Node.js の場合:

npm install chiffon
var Chiffon = require('chiffon');

bower:

bower install chiffon

tokenize

ECMAScriptのコード文字列をトークン化します。
var tokens = Chiffon.tokenize('var a = 1');
console.log(tokens);
/*
[ { type: 'Keyword',    value: 'var' },
  { type: 'Identifier', value: 'a' },
  { type: 'Punctuator', value: '=' },
  { type: 'Numeric',    value: '1' } ]
*/

返される結果のトークン名リスト:

  • Comment
  • LineTerminator
  • Template
  • String
  • Punctuator
  • RegularExpression
  • Numeric
  • UnicodeEscapeSequence
  • Identifier
  • Null
  • Boolean
  • Keyword

※多少変化する可能性があります。最新の情報はこちらを参考ください
※JavaScript AST は現在サポートされてません。

Options

  • comment : trueにするとCommentトークンを残します (default=false)
  • lineTerminator : trueにするとLineTerminatorトークンを残します (default=false)

untokenize

tokenize() で返されたトークンを文字列に戻します。
var tokens = Chiffon.tokenize('var a = 1');
var code = Chiffon.untokenize(tokens);
console.log(code); // 'var a=1'

minify

JavaScriptのコードを minify します。
var min = Chiffon.minify('var a = 1 + 1; // comment');
console.log(min); // var a=1+1;

minify は以下のような感じで単純に実装されてます。
function minify(code) {
  return untokenize(tokenize(code));
}

Demo

レポジトリ




2015年4月3日

SyncFiddleリニューアルしました

半年前に作った SyncFiddle を新しくしました。
(過去記事: JavaScriptが実行できるリアルタイムオンラインエディタ作ってみました)

SyncFiddle は、複数人とリアルタイムでコードが共有できるオンラインエディタです。
HTML, CSS, JavaScript が実行でき、他の人のコードやカーソル位置、選択範囲が同期されます。

URL は http://syncfiddle.net/ になりました。

SyncFiddleは現在ベータテスト中で無料で使えます(無料機能は予告なしに変わる可能性があります)。


今回ほとんど作りなおしましたが、既存の機能は基本的にそのままです。

機能とか

  • アクセスすると http://syncfiddle.net/xxxxxxxx のような新しい共有空間に飛ぶ
  • 飛んだ先は URLを共有しない限り基本的にプライベート
  • URLを共有して複数人で同時編集できる (Shareボタンから共有可能)
  • オンライン中のユーザー数とユーザー一覧(名前と色)がわかる
  • 自分の名前が編集可能
  • コードとカーソル位置、選択範囲がわかる
  • JavaScriptコンソールがある (コンソールは共有されない)
  • その時のHTMLコードを .html で保存できる (Exportボタン)
  • ブラウザで新しいウィンドウ開いて試してみるとわかりやすいかも

先日、リアルタイム・バックエンドサービスの Firebase を知って
これは SyncFiddle ぴったり!と思って作り直したくなってコツコツ進めていました。

技術的なこととか

もともと Backbone で作られていたので、そのまま引き継いで Backbone を使っています。
環境は webpack babel es6 で書いています。

バックエンド Firebase、エディタに CodeMirror の htmlmixed モードを使って、
Firepad という firebase が提供しているエディタを拡張して実装しています。

Firebase の Hacker Plan (FREE Plan) なので、最大接続数や容量がきついかもしれません。
Firebase にログインするとプランをアップグレードしろと真っ赤な表示で言われていますが、
一番安いプランでも個人で払えそうな額ではないので安くしてほしいです (Candle で月額 $49)。

SyncFiddle は JSFiddle などのサービス方向性からアイディアをもらっていますが、
音声通話などしながらリアルタイムに共有するにはきびしいのと、
そういったサービスが見つけられなかった (JavaScript の実行ができない) ので、
JavaScript が実行できる共有サービスとして作っています。

スクリプトの実行を自動的にやってしまうと どうかなと思い、
今のところユーザーの手で Run ボタンを押さない限り実行されないようになっています。

実行には iframe の sandbox 属性を使用しています。
origin を分けているので親 window は操作できないと思いますが、 もし問題あったら教えてください。

また、JSFiddle 等のサービスとはリアルタイム性として方向性が違い、
コードのログがいつまでも残ったところで意味はないのと、 容量スペースがあまりないので、
いまのところ ユーザー数 0 のまま一定時間経つと消えるようになっています。
(ベータテスト期間の状況で変わるかもしれません)
どちらかというとデザイン、レイアウトのほうがんばっていますが、
CSSはそこまで使いこなせてないです。作りながら調べつつ勉強になりました。
※ロゴ画像は poioq さんに作っていただきました、ありがとうございます。

おわりに

ちょっとしたツール的なものを作ることはよくありますが、
なかなか発展しなかったり止まってしまったりで、そういった継続力がほしいと思う日々です。
SyncFiddle はとても気に入っていて使える機会はあるので使いやすくしていきたいところ。
使ってみて不便な点とかアイディアとかありましたら教えてくれるとうれしいです。


Link: SyncFiddle

2015年2月17日

webpack babel-loaderを使ってES6でWebを書く

2015-11-21 babel presetsに合わせて更新
そろそろ ES6 で書きたいので環境メモを残しておきます。

はじめに

このエントリでは Node.js を使用してフロントエンドで ECMAScript 6th を書くことを目的にしています。
その際に babel を使います。
babel は grunt, gulp, browserify など多くのプラグインが提供されていますが、ここでは grunt を使っていきます。

以前は RequireJS でモジュール書いて r.js (1ファイルにまとめるやつ) でコンパイル(minify) てやってたのですが、
今回 webpack と babel-loader を使った ES6 環境で進めようと思います。

インストール

Node.js はすでにインストール済み前提とします。
まず適当なところにディレクトリを作って移動して npm init で package.json を作成します。
$ mkdir my_project
$ cd my_project
$ npm init
# いろいろ聞かれるけどとりあえずエンター連打

$ ls
package.json # できてること確認

ファイル構成

ファイル構成は以下のような感じです。
$ tree
.
├── package.json
├── Gruntfile.js        # これから作るgruntタスク
└── public              # 公開ディレクトリ
        ├── index.html  # これから作るhtml
        ├── js          # jsソースを置くディレクトリ
        └── assets      # コンパイル済みのjsなどを置くディレクトリ
まだ package.json しか作ってないので、public ディレクトリと、 その中に js, assets を作ります。

今回使うタスクランナーの grunt をインストールします (-g をつけてグローバルで)。
$ npm install -g grunt-cli

続いて grunt-webpack や grunt-contrib-uglify (圧縮),
grunt-contrib-watch (ファイルの更新を監視) などをインストールします。

本来なら webpack と grunt-webpack だけで済むっぽいですが、
非圧縮ファイルを確認したいときがあるので webpack で1ファイルにまとめてから uglify で圧縮します。
あえて非圧縮ファイルを残します。
$ npm install --save-dev grunt grunt-contrib-uglify grunt-contrib-watch
--save-dev オプションを指定して package.json の devDependencies に追加します。

それから webpack, babel-loaderをインストールします(1回でまとめてインストールしても同じです)。
$ npm install --save-dev grunt-webpack webpack babel-loader babel-preset-es2015

Gruntfile.js

プロジェクトのルート (package.json と同じ階層)に Gruntfile.js を作成します。
// Gruntfile.js
var webpack = require('webpack');

module.exports = function(grunt) {
  var pkg = grunt.file.readJSON('package.json');

  grunt.initConfig({
    // webpackの設定
    webpack: {
      build: {
        progress: true,
        entry: {
          // メインjsファイル (ここでは app.js とする)
          app: './public/js/app.js'
        },
        output: {
          // 出力ファイル (ここでは assets/js に出力)
          path: './public/assets/js',
          filename: 'bundle.js'
        },
        module: {
          loaders: [{
            // .jsに対してbabelを指定してES6で書けるようにする
            test: /\.js$/,
            exclude: /node_modules/,
            loader: 'babel',
            query: {
              presets: ['es2015']
            }
          }]
        },
        resolve: {
          // 読み込む際に拡張子を省略できるようにする
          extensions: ['', '.js']
        }
      }
    },
    // JS圧縮(minify)/最適化
    uglify: {
      options: {
        // Source Mapをつける
        sourceMap: true,
        sourceMapName: './public/assets/js/bundle.map'
      },
      build: {
        files: {
          // 出力ファイル: 元ファイル
          './public/assets/js/bundle.min.js': './public/assets/js/bundle.js'
        }
      }
    },
    // ファイルの変更/更新を監視
    watch: {
      js: {
        files: [
          // js/ 以下を再帰的に監視する
          './public/js/**/*.js',
        ],
        tasks: ['webpack', 'uglify']
      }
    }
  });

  grunt.loadNpmTasks('grunt-webpack');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-watch');

  // デフォルトタスクの登録
  grunt.registerTask('default', ['webpack', 'uglify']);
};

上のコードで Gruntfile.js に書き込んで保存します。

ES6でコードを書く

せっかくなので es6 modules を使って書いてみます。

public/js に app.js を作成します。
// app.js
import * as math from './lib/math';

// Template Strings
var msg = `2π = ${math.sum(math.pi, math.pi)}`;
alert(msg);

続いて public/js/lib (lib ディレクトリを作成) に math.js を作成します。
// lib/math.js
export function sum(x, y) {
  return x + y;
}
export var pi = 3.141593;

public に index.html を作成します。
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>webpack es6 babel-loader example</title>
<script src="assets/js/bundle.min.js"></script>
</head>
<body>
  <h1>webpack es6 babel-loader example</h1>
</body>
</html>

Build

各ファイルを作成したら grunt を実行してみます。
単に grunt と実行すると、default のタスクが実行されます。
$ grunt
...
Running "webpack:build" (webpack) task
...
Running "uglify:build" (uglify) task
...
>> 1 file created.

Done, without errors.
みたいに表示されれば成功です。

結果

index.html をブラウザで開くと、2π = 6.283186 とalertで表示されます。

出力

public/assets に以下の3つのファイルが作られています。
  • bundle.js : 非圧縮のファイル
  • bundle.map : Source Mapファイル
  • bundle.min.js : 圧縮済みのファイル

html 内では圧縮されたファイル assets/js/bundle.min.js を読み込んでいますが、
非圧縮の bundle.js を見ると変換されたコードも確認できます。

ファイルの変更を監視

毎回 grunt と入力するのも手間になるので、以降はファイルの変更を監視するようにします。
$ grunt watch
を実行すると、ファイルが更新される度に自動でタスクを実行してくれます。
Ctrl+C で中止できます。

babel-runtimeを含める

Promise などを使う場合、babel の runtime (polyfill) を含めるように指定します。
runtime を使う場合は babel-runtime のインストールが必要です。
$ npm install babel-runtime --save

Gruntfile.js の webpack loader を修正します。
// Gruntfile.js
  ...

    // webpackの設定
    webpack: {
      build: {
        ...
        module: {
          loaders: [{
            // .jsに対してbabel-loaderを指定してES6で書けるようにする
            test: /\.js$/,
            exclude: /node_modules/,
            // ランタイムを含めるよう指定する
            loader: 'babel-loader?experimental&optional=selfContained'
          }]
        },
        ...
      }
    },
  ...

runtime を含めると、それなりにサイズがあるので個別でライブラリ等を使うのもいいかもしれません。

Demo

今回作成した動作デモ:

Source

今回作成したソース:

おわりに

フロントエンド周りは移り変わりが激しいですが、安定したものを使っていきたいところです。
RequireJS 使った r.js の最適化は魅力的なのですが、 あらかじめひとまとめにする前提ってなると webpack が便利に感じます。
最近は CoffeeScript 書くことが多くて Coffee も楽しいなぁと思いつつ、 やっぱり ES6 で書けると嬉しいです (まだまだ勉強中…)。


2014年11月29日

encoding.js 1.0.12 リリースしました

JavaScript 文字コード変換ライブラリ encoding.js 1.0.12 リリース しました

アップデート内容

  • UTF-8 <=> JIS 変換テーブルのバグ修正 (文字化けが直りました)
  • JIS X 0208 を対象にした変換テストが通るようになりました
  • ファイルサイズが約60% 減りました
  • 実行速度が向上しました
  • convert で引数にオブジェクトが渡せるようになりました

UTF-8 <=> JIS 変換テーブルのバグ

UTF-8 から JIS, JIS から UTF-8 への変換テーブルが変なことになってました。
変換テーブルは MS 基本コードポイント (Unicode) を参照していましたが、 UTF-8 に変換する際に無効なコードポイントになってしまい文字化けが発生していました。
MS 基本コードポイントと IBM 基本コードポイントの差は以下のようになっています。

コードポイント:
文字名 IBM CP (Unicode) MS CP (Unicode) IBM CP (UTF-8) MS CP (UTF-8)
― (全角ダッシュ) U+2014 U+2015 E28094 E28095
~ (波形ダッシュ) U+301C U+FF5E E3809C EFBD9E
∥ (二重縦線) U+2016 U+2225 E28096 E288A5
- (負符号) U+2212 U+FF0D E28892 EFBC8D
¢ U+00A2 U+FFE0 C2A2 EFBFA0
£ U+00A3 U+FFE1 C2A3 EFBFA1
¬ U+00AC U+FFE2 C2AC EFBFA2

encoding.js の場合、MS コードポイントのまま変換しようとしていまい、対応する文字がなく '?' などになってしまっていました。
これは元々直したつもりでいたのですが、 encoding.js 1.0.10 Line 2973 あたりを見てみると以下のようになっていて
0xEFBC8F:0x213F,0xEFBCBC:0x2140,0xE3809C:0x2141,0xEFBD9E:0x2141,0xE288A5:0x2142,
そもそも 0x2141 が消しきれてなくて重複していたり、対応する JIS テーブルに文字がなくて変換すると 1文字分消えてしまったりと、 バグが多かったのですが
今回、IBM 基本コードポイントの差分によって変換してあげることで、ぶじ文字化けが解消されました。

JIS X 0208 を対象にした変換テストが通るようになりました

この変換テーブルの修正により、JIS X 0208-1997 (JIS規格 第一・第二水準漢字) を対象にした変換テスト (UTF-16, UTF-8, Shift_JIS, EUC-JP, ISO-2022-JP(JIS)) が通るようになりました。 また、Unicode サロゲートペア の処理も修正しています。

ファイルサイズが約60% 減りました

encoding.js では、変換テーブルだけでかなりのサイズを占めています。
前バージョンまでは、JIS→UTF-8 テーブルと UTF-8→JIS テーブルの 2つを保持していたのですが、
今回、UTF-8→JIS のみを静的に持ち、JIS→UTF-8 変換が必要になった際、動的にもう一つのテーブルを生成するようにしました。
生成されるのは呼ばれた最初の 1回のみです。
その際の速度が気になり、ベンチとってみましたが 約 2~5ms 程度で問題ないと思われます。
ファイルサイズは非minifyで 261KB (min: 219KB) だったのが 152KB (min: 116KB) になり、約60% 減らすことができました。

実行速度が向上しました

前までは、変換テーブルに文字があるかないかを、if (TABLE[c] === void 0) { ... } みたいな感じで判断していましたが、
これは非常に遅く、また in を使うより hasOwnProperty が速く、実行環境にもよりますが
var hasOwn = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty);
if (hasOwn(obj, key)) { ... }
のように bind して使うよりも hasOwnProperty.call(obj, key) のほうが若干速かったため、後者を採用しています。

変換速度は、1MB の日本語テキスト (UTF-8) に対して SJIS に文字コード変換した場合、平均 約 50ms. (EUC-JP, JIS 等も同程度)、
UNICODE (UTF-16) から UTF-8 とその逆は約 5ms ほどで、前よりだいぶ速くなりました。

convert で引数にオブジェクトが渡せるようになりました

convert の第二引数に Object を渡せるようになりました。
// 以前の指定の仕方 (SJISに変換)
var sjisArray = Encoding.convert(unicodeArray, 'SJIS', 'UNICODE');

// Object で指定
var sjisArray = Encoding.convert(unicodeArray, {
  to: 'SJIS',
  from: 'UNICODE'
});
前のように 第二引数を to, 第三引数を from として文字列で渡しても動きます。


文字化けという大きなバグを修正しているので、アップデートをおすすめします。

Download


Node.js では、encoding-japanese というモジュール名になっています
npm install encoding-japanese
var encoding = require('encoding-japanese');

上のようにして利用できます。

encoding.convert() は Buffer に対しても使えます(配列で返ります)

Demo

Repository




2014年10月16日

LZアルゴリズムでBase62に圧縮するJavaScriptライブラリ書きました

LZSS(LZ77) 圧縮アルゴリズムで文字列を圧縮/解凍します。
圧縮された結果は Base62 (0-9A-Za-z) からなるバイナリセーフな文字列になります。
サイズ制限のある localStorage や sessionStorage, cookie など、 例えば localStorage は 5MB 使えたとしても UTF-16 の場合、実質的に 2.5MB 分になってしまします。
圧縮結果は内容にもよりますが おおよそ 60~80% 程度のサイズになります。
例外として、繰り返しのない文字列は1~2割程度サイズが増えることがあります。

追記

2014-10-24 現在、高速化にすべく見なおして圧縮率も上がりました。
繰り返しのない文字列 (例えば U+0000 - U+FFFF 1文字ずつなど) も大幅なサイズ増加なく圧縮できます

var data = 'hello hello hello';
console.log(data.length); // 17

var compressed = lzbase62.compress(data);
console.log(compressed); // 'tYVccfrgxGL'
console.log(compressed.length); // 11
console.log(compressed.length < data.length); // true

var decompressed = lzbase62.decompress(compressed);
console.log(decompressed); // 'hello hello hello'
console.log(decompressed === data); // true

Download

※バージョンが違うとフォーマットも違う可能性があります。できるだけ最新バージョンをご利用ください

Demo

Repository




2014年9月25日

JavaScriptが実行できるリアルタイムオンラインエディタ作ってみました

複数人で同期して編集できるWebエディタを探していたのですが、 見つからず・・・

jsfiddle や jsdo.it は同時編集できないため、ちょっと探してたのとは違って、
Scratchpad はリアルタイムで編集できてよいのですが、JavaScriptが実行できない。
CodePen は同期するのに PRO アカウントが必要らして、諦めて、仕方ないので作ってみました。

追記:
SyncFiddle はリニューアルし、使用しているフレームワークの構成が変わりました。
URL は http://syncfiddle.net/ になりました。

仕様とか

  • アクセスするとランダムにid振ったページ空間に飛ぶ
  • そのURLを共有すると複数人で同時編集できる
  • 相手のことがわかるのはidだけ
  • コードとカーソル位置がわかる
  • JavaScript コンソールがある
  • その時のHTMLコードを .html で保存できる
  • ページの破棄という機能はないですが、コードを空にしてユーザ数 0 になるとそのうち消える
  • プライベートタブ(シークレットタブ)とか使ってみるとわかりやすいかも

レイアウト

  • PHP フレームワーク PunyApp
  • JavaScript Backbone + r.js
  • エディタは CodeMirror の HTML5 mixed モード

こないだ公開したフレームワーク PunyApp のテストも兼ねて作ってみたのですが、 なにしろサーバが古くて大変。最初は WebSocketにしようと思ったけどフレームワークのテストにならないので pollingしてます。今更ですがサーバーから立て直したほうがよかったです。一応公開してますが、サーバが不安定になるかもしれないです。
Backbone.js と r.js (require.js)、前から触ってみたくて、今回はじめて使ってみました。
自由度が高くて、View周りとかちょっと道を外れると ぐしゃってしまうので注意が必要ですが、 自分にとっては使い心地がよかった。まだまだ勉強中です。



2014年9月6日

軽量PHPフレームワークPunyAppというの作りました


最近、ちまちまとPHP を書くことがあって、 ちょっとしたWebアプリ等作るときに
CakePHP などのある程度規模のあるフレームワークを使うまでもないかなって思うことが多々あり、
そういった用途に 小規模向けなフレームワーク (マイクロフレームワーク) PunyApp というのを作りました。

以前から自分だけで使ってて少しずつ更新して温めていたもので、 ある程度形になったので公開しました。
軽量で中小規模向けなもので、 おおまかな設計は CakePHP を参考にしてて、使い方も似てます。

PunyApp は MVC モデルで他のPHP拡張等は必要ありません。

ダウンロード

要件

  • PHP 5.2.0 +
  • mod_rewrite が有効 (Apache Server)

ライセンス

  • MIT

機能/特徴

  • MySQL, PostgreSQL, SQLite, Posql に対応
  • Controller, Model, Viewアクション
  • フォームのValidation
  • Viewテンプレート変数はデフォルトでHTMLエスケープされる
  • Session (データベース)
  • Cookie (デフォルトで暗号化)
  • データベースエラー時などのEvent
  • サンプルのログインフォームが入ってる


追記

以下の情報はバージョンアップに伴い古くなっているので https://github.com/polygonplanet/PunyApp を参照ください

レイアウト

  • ファイル構成
/application                → アプリケーションディレクトリ
    /controllers            → コントローラ
    /models                 → モデル
    /views                  → ビュー
    /libraries              → 共通ライブラリやヘルパー等
    /storage                → SQLiteなどのファイルベースデータベースやログを保管
        /logs               → /storage配下を書き込み可にしておく
        /databases          → 同様
    /settings               → 設定
        app-settings.php    → アプリケーション設定ファイル
        app-scheme.php      → データベーススキーマ
    /public                 → 公開ディレクトリ
        /css
        /js
        index.php
/lib                        → PunyApp内部ライブラリ
/vendors                    → 外部ライブラリ等を入れるディレクトリ
index.php

Controllers

コントローラのアクションは、GET や POST などのリクエストメソッドで切り分けができます。

class SampleController extends PunyApp_Controller {

  public $models = array('sample');

  /**
   * GET /login
   */
  public function getLogin($params) {
    $this->view->render('sample/login');
  }

  /**
   * POST /login
   */
  public function postLogin($params) {
    $has = $this->sample->hasUser($params['id'], $params['pass']);
    if ($has) {
      $this->session->userid = $params['id'];
      $this->redirect('home');
    }

    // ...

    $this->view->render('sample/login');
  }

  /**
   * Any /login (あらゆるリクエストメソッドに対応)
   */
  public function anyLogin($params) {
    // ...
  }

  /**
   * Before /login (前処理)
   */
  public function beforeLogin($params) {
    if (!empty($this->session->userId)) {
      $this->redirect('home');
    }
  }

  /**
   * After /login (後処理)
   */
  public function afterLogin($params) {
    // ...
  }

  /**
   * GET /home
   */
  public function getHome($params) {
    // ...
  }
}
before や after で前処理などが設定できます。
any は、すべてのリクエストメソッドに対応します。
メソッド名を any だけ (function any() {}) にすると、404 にならずに any が実行されます。
引数 $params はリクエストパラメータが渡されます。

コントローラのメソッド名の命名規則は、

リクエストメソッド名 + アクション名

になります。
GET に対応する hoge だったら、getHoge になります。

URL は、
http://www.example.com/コントローラ名/アクション名

となります。
アクション名は、リクエストメソッド名を外した名前です。
SampleController で getHoge の場合は、
http://www.example.com/sample/hoge
という感じになります。 ファイル名は sample.php です

AnyController (any.php) という名前にすると、すべてのリクエストに対応します。

Models

モデルは、PDO を使っているのもあり 基本的にプリペアドステートメントを使って実行します (プレースホルダ)。

class SampleModel extends PunyApp_Model {

  public function addUser($userid, $email, $pass) {
    // Insert
    $sample = $this->newInstance();
    $sample->userid = $userid;
    $sample->email = $email;
    $sample->pass = sha1($pass);
    return $sample->save();
  }


  public function deleteUser($userid) {
    return $this->delete(
      array('userid' => '?'),
      array($userid)
    );
  }


  public function getUser($userid) {
    return $this->findOne(
      array(
        'fields' => array('id', 'userid', 'email'),
        'where' => array('userid' => '?')
      ),
      array($userid)
    );
  }


  public function hasUser($userid, $pass) {
    return $this->has(
      array(
        'where' => array(
          'userid' => ':userid',
          'pass' => ':pass'
        )
      ),
      array(
        ':userid' => $userid,
        ':pass' => sha1($pass)
      )
    );
  }
}

  • array find ( array $query = array(), array $params = array())

find() の引数 $query は、 'distinct', 'fields', 'from', 'as', 'joins', 'where', 'group', 'having', 'order', 'limit', 'offset' が使えます。
  public function getUserByName($name) {
    return $this->find(
      array(
        'distinct' => false,
        'fields' => array(
          'U.id AS id', 'U.name AS name',
          'U.category AS cat', 'P.url AS url'
        ),
        'as' => 'U',
        'joins' => array(
          'type' => 'LEFT',
          'table' => 'profile',
          'as' => 'P',
          'on' => array('U.id' => 'P.id')
        ),
        'where' => array(
          'name' => ':name'
        ),
        'group' => 'cat',
        'order' => 'id DESC',
        'limit' => 10,
        'offset' => 5
      ),
      array(
        ':name' => $name
      )
    );
  }
ある程度のクエリは扱えます。

モデル内では $this->getDatabase() が PDO として使えるので、

  public function getName($id) {
    $sql = 'SELECT name FROM foo WHERE id = ?';
    $stmt = $this->getDatabase()->prepare($sql);
    $stmt->execute(array($id));
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
  }
上のように直接クエリを書いてもいいですが、
直接クエリを書かずにモデルの find() や delete() 等を使うメリットとしては、
'created' と 'modified' というフィールドが自動てセットされることが大きいです。 テーブル定義にあらかじめ created もしくは modified を定義しておくと、 insert (save), update のタイミングでそれぞれ現在時刻を設定します。
integer や int(11) で定義すると 現在の time() が設定されます。
datetime は Y-m-d H:i:s になります。
varchar(255) で定義すると現在時刻をミリ秒で設定します。
  • created : 作成された日時
  • modified : 更新された日時
これらはアプリケーション側で created, modified をパラメータに扱う場合は無視されます。

コントローラでのモデル定義は $models に使うモデル名(テーブル名)を記述します。

class SampleController extends PunyApp_Controller {

  public $models = array('sample');

  public function getLogin($id) {
    $user = $this->sample->getUser( ... );
  }
}

Views


$this->view->text = 'Hello!';
$this->view->render('index');
ビューでは PHP本来が持ってるテンプレートを使います。

<html>
  <body>
    <h1>Sample</h1>
    <?php echo $text; ?>
  </body>
</html>

テンプレート変数はデフォルトでHTMLエスケープされます。

  $this->view->text = '<script>alert(1)</script>';


  <p><?php echo $text ?></p> // &lt;script&gt;alert(1)&lt;/script&gt;


Validation

リクエストパラメータのバリデーションはコントローラ内で行います。

class SampleController extends PunyApp_Controller {

  public $validationRules = array(
    'id' => array(
      'required' => true,
      'rule' => array('regex', '/^[a-z0-9]{1,10}$/i'),
      'message' => 'Only letters and integers, max 10 characters'
    ),
    'email' => array(
      'required' => true,
      'rule' => array('email'),
      'message' => 'Invalid email address'
    ),
    'pass' => array(
      'required' => true,
      'rule' => array(
        array('minLength', 4),
        array('maxLength', 20)
      ),
      'message' => 'Min 4 characters, max 20 characters'
    )
  );

  // ...
}
バリデーションのルールは
email, url, ip, between, minLength, maxLength, regex
等が定義されています。または自分で定義します。

その他、コールバックが使えます。

  $this->validationRules['nickname'] = array(
    'required' => true,
    'rule' => array('callback', function ($value) {
      return preg_match('/^[ぁ-ん]/u', $value) && preg_match('/[!]$/u', $value);
    }),
    'message' => 'ひらがなではじまって「!」で終わらないとダメです'
  );
$this->validate() で実行します。 任意のルールを引数に渡すこともできます。
引数を省略すると $validationRules に対して実行します。

class SampleController extends PunyApp_Controller {

  // ...

  public function postLogin($params) {
    if ($this->validate()) {
      // ...
    }
  }
}
以下のViewメソッドからメッセージを取得できます。

追記

以下の情報はバージョンアップに伴い互換性があるかわかりません
詳細は https://github.com/polygonplanet/PunyApp を参照ください

  • 名前を指定して取得 : string getValidationError( string $name );
  • 最後のメッセージを取得 : string getLastValidationError( );
  • 全部取得 : array getValidationErrors( );
  • HTML<li>で全部取得 : string getValidationErrorMessages( array $attributes = array('style' => 'color:red') );
コントローラ内では $this->view->getValidationError('hoge') のように取得できます。
ビュー内では以下のようにして表示できます。

  <div class="error">
    <?php echo $this->getValidationError('param_name') ?>
  </div>

インストールと設定

1. 展開したファイルをサーバーの任意のディレクトリに置きます

2. application/settings/app-settings.php を開いて設定します
$settings = array(
  /**
   * System settings
   */
  'system' => array(

    /**
     * Debug mode
     *
     * true = show errors
     * false = hide errors
     */
    'debug' => true, // デバッグモードだとエラーが起きたとき表示される

    /**
     * Timezone
     *
     * e.g., 'America/Chicago', 'Asia/Tokyo' etc.
     */
    'timezone' => '', // 日本の場合はここを 'Asia/Tokyo' に設定しておく

    ...
  ),

  /**
   * Database settings
   */
  'database' => array(

    /**
     * Database engine
     *
     * Available engines: "mysql", "sqlite" and "posql".
     */
    'engine' => '', // 使用するデータベース

    ...
  ),

  /**
   * Session settings
   */
  'session' => array(

    /**
     * Session engine
     *
     * Available engines: "php", "file" and "database".
     */
    'engine' => '', // セッションエンジンを設定

    ...
  )
);
3. データベーススキーマを作成します、または application/settings/app-schema.php に記述します

4. application/storage 配下のファイル、ディレクトリのパーミッションを「書き込み可」に設定します

5. 1 で展開したディレクトリにブラウザでアクセスしてみて、PunyApp ~と表示されたら成功です

6. 実際に作ってみましょう

サンプル

/sample/ にサンプルのログインフォームが入っています。
/sample/ にアクセスして確認できます。




なにかあれば以下のレポジトリのIssues, または @polygon_planet へお願いします。

レポジトリ




2014年4月20日

スマートウォッチPebbleを買ってみて

はじめに

Pebble というスマートウォッチ。

なんとなくウェアラブルな端末について調べてたら、
Pebble というのを知って、調べてるうちにどうやら JavaScript でアプリ開発できるとのこと。 すぐ購入。
理由としてはそれだけじゃないのですが、スマートフォンは家に忘れるから。とか、Pebble のオープンな環境がよかったからとか、 そんなこんなで届いて1週間ほど経ちました。
何も考えずに黒を買ってしまったのですが、赤かオレンジがよかったなぁ・・・と。バンドも変えられて、常に身につけて見えるものだから、 購入を考えてる方は好みの色がいいと思います。

さっそく作ってみた時計画面(Watchface)

Pebble

Pebble Appstore

Pebble は、スマートフォン側の「Pebble」というアプリを通して Bluetooth でやり取りして動いています。
「Pebble」アプリ内から Pebble Appstore に行けます。
Pebble Appstore から Pebbleアプリや時計画面(Watchface) などをインストールできます。
アプリの管理や設定も「Pebble」アプリ内でできます。
画面は E-Ink (厳密には違う?) で白と黒のドットで表されるアナログ感あふれるもの。 それゆえにバッテリーの持ちがすごくいい。

※この記事ではスマートフォン=Androidで書いています

Pebble JavaScript Framework


Pebble のアプリは、基本的に C言語で書いたものを出力しますが、JavaScriptで書くこともできます。
Pebble のプロジェクト内に C と JavaScript の両方のコードを含めることができ、 イメージ的には C で書いたものは Pebble 時計側で動き、JavaScript で書いたコードはスマートフォン側で動きます。
C と JavaScript 間でメッセージのやり取りができ、JavaScript で処理した結果を Bluetooth を通して C (時計側) で受け取れる感じです。
XMLHttpRequestなどの非同期処理、JavaScriptでやりやすいことは余裕のあるスマートフォン側に処理をさせて、 時計側がデータを受け取って表示。時計側の処理を最小限に抑えやすくなります。

Simply.js

これまでは、最低でも時計画面に出力するのに多少は C で書く必要がありました。
そこで、完全に JavaScript コードだけで完結できる Simply.js というアプリがあります。
Simply.js をインストールすると、URL を入力する設定欄がでてきて、ここに JavaScript (.js) のURLを入れると、 Pebble アプリと同様に動かすことができます。


自分のサーバや Dropbox などに .js ファイルを置いて実行できます。

参考: 以下の記事ではサーバのロードアベレージなどを表示させていました
ひとりぶろぐ » 2.0で面白くなったスマートウォッチPebbleを、JavaScriptゴリゴリでネット監視装置として使う

CloudPebble

Pebble の開発環境 CloudPebble は、クラウド上で C のソースがコンパイルできます。
コードは JavaScriptエディタ (CodeMirror)で書けて補完ウィンドウなどもでてきます。
スマートフォンのIPを入れておくと「build」ボタンを押した時に、
PC(ブラウザ)→スマートフォン→Pebble 経由で
書いたコードがアプリになってPebble時計にインストールされ結果が分かる仕組みです。
もはやブラウザだけで開発できる環境。

そして、CloudPebble では、任意の github プロジェクトのURLを入れて「import」ボタンを押すと、 Pebble アプリとして適切か判断した後、自分のプロジェクト内に取り込まれます。
自分のプロジェクトを github に export (push) するのも同様にできます。

新規作成時は C と Simply.js が選べます

実際使ってみて

メインで使ってる時計アプリは、Bluetooth切断時に長めのバイブ機能があるため、
スマートフォンを忘れることがなくなりました。
(ジェネレータで作ったものも同じ機能がつきます)

あとは、メールなどの通知が来た時、
  1. スマートフォンを出す
  2. 画面をタッチ
  3. 内容を確認
とやっていたのが、
  1. Pebbleを見る
だけになったり、
音楽の制御がリモコン感覚で楽になったり…

Android の場合、Tasker というアプリと Pebble の連携で、 ほとんどのことは自動化もしくは ON/OFF するだけで済むようになると思います。
マナーON/OFF、メディア音量調節、消音、マップ起動時にGPS有効、場所を指定してナビ起動、Pebbleから画面ロック/解除等…

変化といえば、あまりスマートフォン画面を見ることがなくなった。

今のところPebbleは日本語表示に難ありですが、 Pebble 日本語 (ひらがな, カタカナ) カスタムファームウェア | pebble 日本語表示 のおかげで助かってます。



何か間違い等ありましたらコメントか http://twitter.com/polygon_planet までお願いします

2014年1月29日

WSH/JScriptをECMAScript標準にするWSHModuleを作りました


WSH/JScript を ECMAScript-262 5th に可能な限り近づける WSHModule というのを作りました。

WSHModule はWSH/JScriptで構築されたJavaScriptのVMです。
可能な限りECMAScript5標準としてJavaScriptを実行します。 スクリプトは、モジュール空間 (Node.js 互換 *1) で実行されます。
WSHModuleは、ECMA-262標準に近づけるためのwscript.exeのアップグレード版として利用することも可能(かもしれません)。
用途によっては、Node.jsを使用したほうが手っ取り早いです。が、JScriptは、Windowsにバンドルされています。 WSHModuleはwscript.exeで実行されるように、UI向けです。 Windowsで拡張子 .js のバッチスクリプトを実行するときに、WSHModule.exe を通すことで、快適な環境になると思います。

*1: Node.js自体がCommonJSの仕様のうち「Module 1.0」と「Unit Testing 1.0」には一応準拠してるだけなので、CommonJS準拠とは言えません

機能

  • ECMAScript5標準の構文をサポート
  • モジュール空間
  • UTF-8で書ける
  • alert, confirm, prompt, setTimeout, clearTimeout, setInterval, clearIntervalが使える
  • (JScript Bug) catchなしのtry-finallyが書ける
  • (JScript Bug) String.prototype.substr, Array.prototype.indexOf, Array.prototype.splice などを ECMA-262標準に修正
  • WSHModule.exe ダブルクリックでシェル画面が開く
  • いくつかのWScriptで扱えない機能をモジュールとして追加(clip, console, fs, util 等)

ダウンロード

要件

以下の環境で動作します
  • Windows (32bit/64bit)
WSHModuleは、環境変数(PATHなど)を登録しないので、必要あれば任意に追加してください。

実行方法

WSHModule.exe "C:/path/to/myscript.js"
もしくは、 "C:/path/to/myscript.js" を WSHModule.exe にドラッグドロップ

簡単な方法:
  1. WSHModule.exe のショートカットをデスクトップ等に作成
  2. 任意のスクリプト(.js)をショットカットにドラッグドロップして実行

テスト

以下の手順で簡単なテストスクリプトが実行できます。
  1. cd WSHModule
  2. "bin/WSHModule.exe" "scripts/test.js"

ToDo

いろいろと大げさなこと言ってますが以下の項目が未実装です
  • Getter と Setter がない
  • "use strict" が使えない

サンプルコード

Hello World:
alert(' hello   world  !'.split(/\s+/).filter(function(s) {
  return s;
}).map(function(s) {
  return s.charAt(0).toUpperCase() + s.slice(1);
}).join(' ')); // 'Hello World !'

クリップボードを扱う例:
// src/clip.js参照
var clip = require('clip');
var curClip = clip.getAsText();
alert('現在のクリップボード「' + curClip + '」');
clip.setAsText('test');
alert('クリップボードに「test」を設定しました');

ファイルを扱う例:
// fs は Node.js/fs とだいたい同じ仕様になってます(扱えるのはSyncのみ)
// http://nodejs.org/api/fs.html
// http://nodejs.jp/nodejs.org_ja/docs/v0.10/api/fs.html

var fs = require('fs');
console.log(fs.readFileSync(__filename), 'このファイルの中身');


なにかあれば以下のレポジトリのIssues, または @polygon_planet へお願いします。

レポジトリ