JavaScriptのasync/awaitを再理解してみた

半年ぶりの投稿になりますが、表題のことをやってみました。ちょっと時間の余裕ができたので、改めて勉強しようとしていろんなサイトを見たのですが、「async関数はPromiseを返す」ということが前提だけに、まずはPromiseを勉強するという体裁になっています。一方、「async/awaitは結局のところ、非同期呼び出しを同期呼び出しのように書いてよし」というシンプルに考えていいのかどうか、非常に迷うところでした。ということで、忙しい世の中ですので先に言ってしまうと、そこまでシンプルには残念ながら行かないことがわかります。INTER-Mediator Ver.6の実装で結構苦労したことが、本当にその苦労が必要だったのかということの検証でもあるのですが、これは個人的なところですね。

まず、次のようなプログラムがあるとします。以下は、適当な.jsファイルに入れて、nodeコマンドで実行します。ただし、「npm install xmlhttprequest」あるいはsudo付きで、XMLHttpRequestモジュールのインストールをお忘れなく。ブラウザで適当に動くようにされているのなら不要です。単にWebサイトのデータをAJAXで取り出すプログラムですが、どんな順序で実行されたかわかるように、コンソールへ出力を残しています。

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

async function aFunc() {
  console.log('start aFunc');
  const r = new XMLHttpRequest();
  r.open('GET', 'https://msyk.net')
  r.onreadystatechange = () => {
    if(r.readyState == 4) {
      console.log('receive aFunc');
    }
  }
  console.log('setup aFunc');
  r.send()
  console.log('send aFunc');
}

console.log('before aFunc');
let a = aFunc(); // async関数を普通に呼び出してみた
a.then(console.log('act1')) // thenが呼び出せるということはPromiseが返っている
console.log('after aFunc');

これを実行すると、次のようにコンソールに出てきます。ポイントは「after aFunc」より後に「recieve aFunc」が出るということです。AJAXによるダウンロード処理は非同期で行われているので、関数aFunc内に記述したコードが実行し終わる前にaFuncを呼び出しているステートメントの次に行くのです。これが非同期呼び出しの動作です。JavaScriptは「プログラムとして書いたコード」は、並列では動きませんが、「書いた順序」では実行しないというよくあるプログラムです。thenについてはPromiseを勉強すれば必ず説明されていますので、省略します。

before aFunc
start aFunc
setup aFunc
send aFunc
act1
after aFunc
receive aFunc

さて、ここで、やりたいことは「通信が終わったら次に行く」と言う動作です。要するに、「aFuncを同期的に呼び出したいのでしょ。だったら、awaitを入れればいいじゃん」と思うところですね。やってみましょう。ここで、単にaFunc()の前にawaitをつけると文法エラーになります。awaitはasync関数内部でしか使えません。グローバルエリアはasyncではないので、エラーになるので、新たにbFunc関数を作り、そこで実行します。

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

async function aFunc() {
  console.log('start aFunc');
  const r = new XMLHttpRequest();
  r.open('GET', 'https://msyk.net')
  r.onreadystatechange = () => {
    if(r.readyState == 4) {
      console.log('receive aFunc');
    }
  }
  console.log('setup aFunc');
  r.send()
  console.log('send aFunc');
}

async function bFunc() {
  console.log('before aFunc');
  let a = await aFunc(); // async関数を普通に呼び出してみた
  //a.then(console.log('act1')) // thenはエラーになり呼び出せない
  console.log('after aFunc');
}

bFunc()

おっと、thenの呼び出しでエラーになるので、コメントします。そして、コンソールの出力結果を見ると、次のようになっていました。前と変わりません。after aFuncより後にreceive aFuncがあるということは、aFuncのコールが終わってから実行したコードよりも後に、aFuncの中身が処理されています。つまり、async/awaitを使っても同期呼び出しされていないということです。おやおや、おかしいですね。

before aFunc
start aFunc
setup aFunc
send aFunc
after aFunc
receive aFunc

ここで、thenの呼び出しでエラーを出してみると、This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch().などと書かれています。はい、ここでPromiseの知識が必要です。Promiseでは、thenやcatchメソッドを呼び出すことで、「先に進める」的な動作をするというか、それを期待しているとも言えます。thenメソッドの2つの引数にresolveやrejectメソッド(もちろん、その名前でなくてもいいのですが、たぶん、皆さんは常にそう書いていると思います)が乗ってくるので、そちらの方がお馴染みかもしれません。ともかく、Promiseを書かないで、asyncにしたからと言って、そのままawaitできるのかというと、この例で示すようにできないということになります。

では、どう書けばいいかというと、こういうことです。非同期処理部分をPromiseで包みます。既存の処理をPromise化するのに適した方法として、コンストラクターの引数に、処理を記述する方法です。処理が終わったらresolve()を呼び出します。reject()は省略します。

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

async function aFunc() {
  console.log('start aFunc');
  const p = new Promise((resolve, reject) => {
    const r = new XMLHttpRequest();
    r.open('GET', 'https://msyk.net')
    r.onreadystatechange = () => {
      if(r.readyState == 4) {
        console.log('receive aFunc');
        resolve()
        console.log('resolve aFunc');
      }
    }
    console.log('setup aFunc');
    r.send()
    console.log('send aFunc');
  })
}

async function bFunc() {
  console.log('start bFunc');
  await aFunc()
  console.log('end bFunc');
}

bFunc()

結果はこのようになります。おや?receive aFuncが、end bFuncより後にあります。これじゃあ、全然同期的に呼び出しているんじゃないですね。なんででしょうね?どこが間違っているのでしょう? これは難しいクイズですね。実はブログを執筆しながら、偶然発見してしまったトラブルです。

start bFunc
start aFunc
setup aFunc
send aFunc
end bFunc
receive aFunc
resolve aFunc

はい、答えは、aFunc関数の最後に「return p」つまり、生成したPromiseを返すというステートメントが必要になります。async内にPromise生成を記述しないと、自動的に生成されるけど、書いてしまったら、ちゃんと自分で返すようにしないといけないと言うことですね。そうすれば、コンソールには次のように出力されます。無事、非同期通信処理の結果を受け取ってから、await aFunc()の次のステートメントを実行しています。つまり、aFunc()は非同期処理が記述されているけども関数呼び出しは同期的に処理が行われたと言えます。

start bFunc
start aFunc
setup aFunc
send aFunc
receive aFunc
resolve aFunc
end bFunc

ところで、前のプログラムで、bFunc()の部分の次にconsoleを記述したら、どの順序で実行されるでしょう。一番最後? それは違いますね。receive aFuncの前にbFunc()の次に記述したステートメントが実行されます。つまり、bFunc()によって呼び出されるのは非同期関数なのです。当たり前ですね、asyncと頭に書いているから。なので、さらにbFunc()呼び出しの部分を同期的にやりたいとなると、ちょっとしつこいですが、こんな風に書かないといけません。

const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

async function aFunc() {
  console.log('start aFunc');
  const p = new Promise((resolve, reject) => {
    const r = new XMLHttpRequest();
    r.open('GET', 'https://msyk.net')
    r.onreadystatechange = () => {
      if(r.readyState == 4) {
        console.log('receive aFunc');
        resolve()
        console.log('resolve aFunc');
      }
    }
    console.log('setup aFunc');
    r.send()
    console.log('send aFunc');
  })
  return p
}

async function bFunc() {
  console.log('start bFunc');
  await aFunc()
  console.log('end bFunc');
}

async function cFunc() {
  console.log('# before bFunc');
  await bFunc()
  console.log('# after bFunc');
}

cFunc()

コンソールへの出力結果は次のようになり、期待通り、awaitで非同期処理が同期処理として呼び出されています。

# before bFunc
start bFunc
start aFunc
setup aFunc
send aFunc
receive aFunc
resolve aFunc
end bFunc
# after bFunc

最終的に、ここでは、cFunc()という非同期呼び出しがありますが、他の部分のコードは全部、同期的に動いていると言える状態かと思います。ですが、全部の関数にasyncがつけられていて、非同期に動く関数であることを明確に示しています。INTER-Mediatorでの実装では、通信処理は何段階か呼び出した低いレイヤーで行なっています。そこをasync/await対応にしたら、その1つ上をやっぱりasync/await対応にしてということになり、要するに全階層がasync/await対応、つまり、全レイヤーが非同期処理するという状況にまずはなりました。asyncで非同期にして、awaitで同期的に呼び出すと言うことをあちらこちらに適用しないといけなくなりました。なんだか、非同期処理を同期的に記述してプログラムしやすくするという目的があるのに、全体的に非同期処理になってしまうではないかというジレンマに陥りました。結局、全体的に見直しが必要になったのは言うまでもありませんが、AJAXを非同期にするために、なんとか頑張ったのでした。実は、それから数年経過し、その頑張りに意味があったのか、ちょっと検証したくなったので、今日、こんなテストをやっているうちに、この記事を書こうと考えた次第です。まあ、方針としては間違いなかったのかと思います。お付き合いありがとうございます。

FileMaker 19のJavaScriptはES6で書けるのか?

FileMaker 19が発表されました。1番の目玉は、JavaScriptということでチェックしてみました。Webビューアは便利な機能である一方、MacとWindowsでHTMLのレンダリングエンジンが違うことが非常に不便なところであり、結果的に機能制約の多いWindows版で開発して、Macでチェックするということになっていたかと思います。また、JavaScriptに関する追加機能である、JavaScriptからFileMakerのスクリプトを呼び出すことも試してみました。

まず、以下のようなレイアウトを作りました。下半分のテキストを、上半分にあるWebビューアに表示するものです。WebビューアのURLとして「data:text/html,下半分のテキスト」となるような実験環境を作りました。まず、navigator.userAgentの結果をみてわかるように、Internet Explorer Ver.11相当です。Trident 7.0という文字列がそれを示しています。手元で調べた結果だと、FM17がTrident 7.0でした。それ以前のものは残っていないので、わからないのですが、いずれにしても、レンダリングエンジンはFM19では変わっていません。

JavaScriptの関数のなかに、「FileMaker.PerformScript()」という記述があります。このように、JavaScriptからスクリプトを呼び出すことができます。引数は、スクリプト名とスクリプトを引数を指定します。ただし、この関数が使えるようにするには、Webビューアのオプション「JavaScriptによるFileMakerスクリプトの実行を許可」にチェックを入れておく必要があります。最初はオフになっているので、入れ忘れないようにしないといけません。

ここでは次のような、単に1行だけのTestスクリプトを定義しておきました。

Webビューア上に見えているボタンをクリックして、JavaScriptを実行してみます。すると、隣のテキストフィールドをquerySelectorで参照して、そこに入力した文字列をvalueプロパティで取り出し、それをFileMaker.PerformScriptの引数に渡してTestスクリプトを実行し、ダイアログボックスが表示されています。なるほど、JavaScriptとの連携がだいぶんとやりやすくなりました。

なお、もう1つの新機能は「WebビューアでJavaScriptを実行」というスクリプトステップです。このスクリプトステップは1つの関数を呼び出すものです。これにより便利そうなのは、正規表現での文字列処理あたりがまず浮かびます。

ここで、コメントにしている行を有効にしてみます。バッククォートで囲むと、文字列のテンプレート処理が可能になり、${変数}で、変数やあるいは式の値を文字列に埋め込みができます。しかし、残念ながら何も表示されなくなります。つまり、JavaScriptのコンパイルで失敗していて、JavaScriptのプログラムが動かない状態になっています。もちろん、ボタンを押しても何も起こりません。なお、変数定義のlet、constについては、IE11がサポートする数少ないES6機能の1つです。

Mac版のFM19だとこのように、文字のテンプレート処理もきちんと動いています。userAgentで出てくる結果は、OSに入っているSafariと同一でした。ChromeやEdgeは「AppleWebKit/537.36」と出るので、FileMakerはOSに入っているものを利用していると思われます。

そういうわけで、FM19でJavaScriptがどの程度ジャンプするのか気になったところですが、Windows版がIE11相当なエンジンであるので、残念ながらES6(正しくはES2015というべき?)でのプログラミングができる状況ではありません。Babel使うくらいなら、ES5というか、IE11互換で記述した方が良さそうですね。

ちなみに、現在、JavaScriptで作られた様々なライブラリは「ES6のみ」という限定がされているものはまだ少数派ですが、Reactなんかは最初からES6で作られていますし(ReactアプリをIE11で動かす方法はある)、INTER-Mediatorの最新版はIE非対応としています。一方、メジャーなライブラリはIE11対応しているので、IE11エンジンだからといって、世間のライブラリが使えないということはほとんどないと思います。いずれにしても、Claris社がすっきりと「ES6対応です」と言わないのは何故だろうかと思っていたのですが、そういうことだったわけです。

Chart.jsのチャートのサイズ増加が止まらん!

Chart.js v2.5使ってます。久しぶりに開いたページの中にChart.jsによるレーダーチャートがあるのですが、描画後、チャートがビヨーン、ビヨーンとサイズがデカくなるのでした。以前はそんなことはなかったはず、なんでやねん! 変なプログラムを書いてしまったか?と思ったけど、そうではない。デバッガを見ていると、canvasのwidthとheight属性が、すごいスピードで+1ずつインクリメントしているのです。durationの値は0にしている。そこに100とか数字を入れても同じだったので、これの問題ではないということですね。かなりあれこれ探したら、なんと、公式ドキュメントに書いてあるじゃないですか。「canvasのコンテナはposition: relativeにしなさい」ということで、なんとか、ビヨーンはなくなりました。しかし、もう、画面に何も表示されないくらい、チャートがデカくなっていくのは悲しい画面でしかないですね。ということで、Chart.jsのcanvasは、divなどで囲ってください。

JavaScriptの通信がPendingとなった理由は通信の問題ではなかった

JavaScriptとPHPで構築しているINTER-Mediatorで、分かりにくいバグに遭遇してしまったので、記録の意味も含めてここに書いておく。そもそも発端は、Ver.5.6-RC2を入れて動かしていたアプリケーションで、そろそろVer.5.7もリリース近いので、入れ替えるかと思って入れ替えたのが直接の原因なのだが、その瞬間にバグが顕在化したのならまだしも、ある程度時間が経過してから顕在化したので、根本的な原因を突き止めるのに非常に労力が消費されてしまった。

不具合が発生したのは、勤務先の業務システムで、スタッフ向けのページはかなり重たい動作のものある。全てのページで問題が出ないで、一部の「重たいと思われる」ページでのみで不具合が顕在化した。ここで、まず判断を狂わせることになる。システムは、サクラVPSで運用している。ここ数ヶ月、徐々にレスポンスが悪くなっている感じもあったので、プロバイダやネットワークをまずは疑ってしまった。不具合による結果は、ページを表示しようにもページ自体がフリーズしてしまうのである。ログインパネルはちゃんと出るが、正しいユーザ名とパスワードを入れると、フリーズする。これに気づいたのは2018年2月の上旬だ。

当然ながら、まずやることはデバッガでの確認だ。すると、以下のように、Statusが(Pending)となている。サーバーのログを追ったところ、Apache2はステータス200でデータを全部出し切っている。クライアント側で通信に入ったものの、一切のデータ受信ができない結果になっている。色々観察した結果、ページごとにPendingになるタイミングやこのページでの行番号は違うのもの、同一ページでは常に同じタイミングで発生した。ちなみに、Chromeだとこれが発生すると高い確率でそのタブを閉じることができず、他のタブは利用できる状態であるにも関わらず、Chromeの強制終了が必要になる。

今まで動いていたという気持ちがあると、当然ながら、アプリケーション側に問題はないとまずは考えてしまう。これも、後から考えれば間違いなのだが、エンジニアの皆さんはとりあえず人のせいにしてしまう気持ちはよく分かると思う。折しも、色々探すと、サクラのサポート情報で、「さくらのVPSで回線速度が遅くアクセスに時間がかかります。」というのがあった。ページのフリーズと、遅くなるのは違うのではあるが、こういう時には楽観的に物事を見てしまう。なんだ、原因分かっているんだと思いつつ、「さくらのVPSにおいて、不特定のVPS収容ホストとクライアント環境(プロバイダ等)の組み合わせにより、 さくらのVPSからのダウンロード方向の通信に遅延が発生する場合があります。」という記述をみて、ネットワークの問題があるということに意識がかなり振られてしまった。

当然ながら、ここにある対処をしたのだが、結果は同じだった。むしろ、どのページも遅くなったので、仮にうまくいっても恒常的な対処にはならない。しかしながら、「ネットワークに問題がある」というところで判断がスタックしてしまった。もし、ネットワークに問題があるとしたら、プロバイダを変えるか、職場のネットワーク内にサーバーを立てるしかない。GMOクラウドのVPSは無料で試せるので、ともかく移動して動かして見た。結果は同じだ。同じページで、同じようにフリーズする。GMOではネットワークの問題は解決しないとしたら、一度、自宅のサーバーにセットアップして、自宅内でアクセスするとどうなるかをチェックして見た。なんと、同じようにフリーズする。また、別の軽いページを改良している時にも、フリーズが発生した。つまり、「ネットワーク」「重いページ」はどうも原因ではないという結論になった。

しかし、なんでPendingになるのか?色々検索すると、Ajaxがデッドロックしたみたいな書き込みがあった。しかし、それへの回答でも、JavaScriptはユーザープログラムは絶対に並列的に動作しないで、処理を1つずつしかしないのだから、他の言語等で発生する問題が同じように発生する可能性は低いと書かれている。もっともだ。しかも、INTER-Mediatorのこのバージョンは、Ajaxを非同期では利用せず、同期通信しているため、フレームワーク内で複数の通信が発生することはない。なお、次のバージョンでは完全に非同期通信を行うようになる。

前述のChromeのデバッガを見ると、CSSの通信がPendingになっている唯一、並列的に通信が発生するとしたら、CSSのサーバーからの取得と、INTER-Mediatorのデータベースアクセスの通信が並列にはなりうる。ちなみに、CSS自体もサーバーで生成しているので、URLは.phpになっている。そこで、フレームワークをいじってCSSアクセスのlinkタグを突っ込まないようにして見た。それでも同じところでフリーズする。やはりこれも原因ではない。

ここで、根本的な原因が全くわからない状態になった。ネットワークでも通信でもないということで、フレームワークに問題があることはほぼ明白になった。そして、2017年9月のVer.5.6.1に戻して見たところ、問題なく動いた。これで、INTER-Mediator側に問題があることが確実となった。INTER-MediatorはGitHubのレポジトリにコードが残っている。そこで、どのコミットでバグが発生したかを突き止めることにした。大雑把に2分割をしながら、2018年1月のあるコミットであるところまで突き止めた。コードを見ても、明らかに通信ではない。通信に問題があるが、原因は通信でないということはさらに明白になった。

しかし、見当がつかない状態と、ここまで絞りきれても原因の特定まではできない。このコミットでファイルは4つ変更されているが、1つはメタデータで処理には関係ないので、そのうちの3つのどれかである。そこで、ファイルを1つずつ、前のコミットに戻すことで、INTER-Mediator-Calc.jsに問題があることが分かった。diffの結果はあれこれあるが、setUndefinedToAllValuesにメソッドしか変更していないので、問題はここにある。何れにしても、計算式の処理部分に問題があることが分かったのである。

ここで腹を括って、地道にステップ動作をさせて見たところ、ついに問題の根本が分かった。325〜335行のdoループが、ある条件の場合だけ、無限ループしている。つまり、ここのループ処理にバグがあったのだが、ポップアップメニューのタグ要素に計算式の結果を適用する場合だけ、無限ループし、その他のタグ要素の場合には無限ループにならず、間違えたコードだが正しい結果が得られているという状況になっていたのである。直したところ、えらく時間がかかるようになったので、処理を効率化して高速化する必要もあったものの、ともかく修正ができて、Pendingは出なくなった。原因追及から修正完了まで、1日かかってしまった。

結論は明らかだ。ページがフリーズしたら無限ループを疑えということになる。だが、なんで、こんな処理が入っているのかという点もあって、モデルをさらに改良してここで問題になったコードは取り除く予定ではあるものの、「動いている」つもりになっていたので、後回しになってしまっているのである。テストでクリアできないかということもあると思うが、ページ展開していないとテストのしようがない部分であり、単体テストではカバーできていなかった。現在、Seleniumベースでの統合テストを組み込んでいるところであり、そこで、計算式のテストページに、ポップアップメニューに計算式を仕込んだものも作っておいた。やはり、見つけにくいバグは、テストから漏れている箇所に存在するということだ。

なんとも当たり前なことの確認に、ひどく時間がかかってしまったのではあるが、自戒の意味も含めてブログに記録する。

[IM]JavaScriptのメソッド内関数だけをテストする

INTER-Mediatorだけの話ではなく、JavaScript一般的な話です。メソッドの中に、そのメソッドの中だけで使う関数を書くということが可能なので、INTER-Mediatorではそういう実装をあちらこちらでやっています。処理の多いメソッドでは関数に分離することで見通しが良くなりますが、加えて、メソッド内の関数レベルでのテストをするということも考えました。

以下のプログラムで、変数anObjectはあるオブジェクトを参照しています。オブジェクトはmethod1というメソッドがあり、そのメソッドの中で、内部の関数であるfunc1を呼び出しています。このプログラムにより、コンソールには、”method1″ “func1″と2行の出力が行われます。

var anObject = {
  prop1: null,
  method1: function() {
    console.log("method1");
    func1();
    // method1のその他の処理
    function func1() {
      console.log("func1");
    }
  }
}

anObject.method1();

ここで、func1だけをテスト対象にしたく、かつmethod1の実処理を行いたくないと考えたとします。その場合、次のようにプログラムを変更しました。このプログラムは、こちらのページの記載を参考にしました。

var bObject = {
  prop1: null,
  method1: function(isTesting) {
    console.log("method1: isTesting="+isTesting);
    this.func1 = function() {
      console.log("func1");
    };
    if(isTesting) {
      return;
    }
    this.func1();
    // method1のその他の処理
  }
}

bObject.method1();
(new bObject.method1(true)).func1();

ポイントは、内部関数func1をプロパティにしていることです。そして、テストか通常利用かを示す引数isTestingを追加します。この引数は省略すると、undefinedになりますので、テスト時はtrueを追加するとして、省略時は何も指定しないことにします。テストの時には、プロパティへ内部関数の設定だけを行い、メソッドはそのまま終了します。

最後からの2行目は、普通にmethod2を利用する場合を想定しており、コンソールには、”method1: isTesting=undefined” “func1″と出力されます。この記述は、最初のプログラムの最後の行にある「anObject.method1();」と全く同じで、method1はつまりは通常利用では何も変更せずにそのまま利用できます。

一方、一番最後の行の実行により、コンソールには”method1: isTesting=true” “func1″と表示されます。newがあることでmethod1をコンストラクタとして実行するので、メソッド自体を返し、その結果、func1関数がメソッドのように実行できます。コンソールに”func1″と表示するのは、method1内部のthis.func1()を実行している部分ではありません。isTesting編集がtrueなので、その前にmethod1は終了します。つまり、「(new bObject.method1(true)).func1();」という記述で、func1だけを実行できるので、この関数だけの単体テストができるということです。

ちなみに、この方法は、method1に引数があっても利用できます。

[IM]日付時刻関数の実装

INTER-Mediatorに日付時刻関数を実装しようとしています。というか、少し実装しました。とりあえずの目標はマニュアルに書いた関数のサポートです。クライアントサイドで動かすので、JavaScriptの仕組みとうまく連動させないといけません。しかし、そのままのスペックはちょっとどうかと思い、このような仕組みを考えました。

日付や時刻は原則として整数あるいは小数点数を取るにしても、連続した数値になります。この点は問題ないのですが、JavaScriptなら1ミリ秒が「1」、Excelだと1日が「1」というように、そのルールを知らないといけませんし、処理系によっては1秒が1の場合もあります。覚えておけばいいといえばいいのですが、決まっているから書かないという状況になると、つどつど調べないといけません。これは不便です。

この1単位の問題に加えて、データベースでは、DATE、TIME、DATETIMEという3つの型を併用します。もちろん、1ms=1にまとめるメリットはあるかと思いますが、あえて、INTER-Mediatorの中では、2つのスタンダードを作ることにしました。

  • 1日=1、つまり、DATE型フィールドに対応(date関数で生成=日付データ)
  • 1秒=1、細かい点はさておいて、TIME、DATETIMEに対応(datetime関数で生成=日時データ)

とします。日付の計算は日単位、日付時刻の計算は秒単位とするわけです。つまり、

  • date(‘2014-10-8’) – date(‘2014-10-6’) → 2
  • datetime(‘2014-10-8 09:00:00’) – datetime(‘2014-10-6 21:00:00’) → 129,600(36時間)

とすることで、データベースの型と、日付時刻関数の結果の対応付けを考えました。

ちなみに、JavaScriptではなぜgetMonth関数だけが0〜11になって実際の月の数値を得るには+1しないといけないのかとか、見方を変えればgetDate関数が0スタートになっているべきなのではなど、疑問は多々わきます。日付計算のしやすさなどの理由はあるかもしれませんが、ここがけっこうJavaScriptのはまりどころだったりします。

そこで、日付時刻の要素取得(つまり月や日を数値として取り出す)関数と、特定の要素に対する計算を行う関数を用意すれば、結果的には「見える通りの数値」として扱えるのではないかと考えました。たとえば、月を得る関数がmonth()だとして、11月なら11という数字が得られ、加えて3ヶ月後などの日付を求めるaddmonth(d, x)があれば用途は足りると考えたわけです。しかし、日付と日時というダブルスタンダードを持ち込むデメリットがここで発生します。整数化された数値を見て、どちらの型なのかがわかりません。結果的に、

  • 日付データの月を返す:monthd(x)
  • 日時データの月を返す:monthdt(x)
  • 日付データに指定した月数を加えた値を返す:addmonthd(x)
  • 日時データに指定した月数を加えた値を返す:addmonthdt(x)

という手法にならざるを得ないと考えました。関数が増えるだけ面倒もあるかもしれませんが、要素取得や計算の関数の末尾が「d」なのか「dt」なのかという点を注意すればいいので、さほどややこしいルールとも考えられません。

しかし、このままだと、month関数やaddmonth関数の存在が気になります。「なし」でもいいのですが、名前的にはもったいないです。考えたのは、なんらかの設定で、日付計算するか、日時計算するのかということを、定義ファイル上のオプション指定で決められるようにするという手法です。たとえば、デフォルとは日付としても、何かの指定をすれば日時になるという具合です。キーワードなどはまだ考えていません。つまりこういう関数があるということです。

  • 月を返す(単位は設定依存):month(x)
  • 指定した月数を加えた値を返す(単位は設定依存):addmonth(x)

 

関数の数は増えますが、把握できる内容ではないかと思います。

タイムゾーンもまともに考えれば頭が痛いですが、日時の解釈の問題と考え、そしてデータベース内ではタイムゾーンは一定にする、あるいはしたいぞと思うことが普通なので、日付あるいは日時データの書式化の問題かとも考えていますが、ここはまだじっくり考えていません。

以上のような方針で実装を考えていますが、どうでしょうか?ちなみに、今現在レポジトリにあるものは、date関数とdatetime関数は実装されています。

JavaScriptの日付のパースはあまりに微妙

JavaScriptの日付や時刻の処理ははまりどころが多いのでも有名ですが、こんなはまりポイントを発見したので、まとめておきます。JavaScriptのDateクラスを使えば日付時刻を記録できるオブジェクトです。getTime()メソッドで1970年1月1日からのミリ秒経過時間(基準からの経過時間)を得て計算するのが一般的なパターンです。文字列で与えられた日付や時刻があったとき、Dateクラスのスタティックメソッドのparseにより、基準からの経過時間が得られます。MDNのドキュメントには、RFC2822 or ISO 8601に従った文字列ならOKとなっています。前者は「Oct 5, 2014」、後者は「2014-10-05」といった文字列です。

では、「2014-10-5」でも一瞬いいのじゃないかなと思って調べてみました。実は、この形式はISO 8601のルール上では間違いです。月と日は常に2桁である必要があります。まず、Chromeです。最初の2行を見れば、一見すると、2014-10-05でも、2014-10-5でもいいように見えます。しかし、Date.parse()の数値を見ると、右側の実行結果の2行目になりますが、ちょっとだけ違う数値になっています。3月3日について、月日の1ないしは2桁の全ての組み合わせてやってみたのが、引き続く4行です。最初の2行に戻ると、9時間の差があります。

shot8846

つまり、Chromeでは、ISO 8601に従った「2014-10-05」は、日本時間の10月5日のAM 9:00であり、そうでないものは標準時での10/5 0:00になるとうことですね。つまり、日本時間にすると9時間前なので、ちょっとだけ小さな数字になります。

Firefoxを見てみましょう。なんと、「2014-10-5」などのISO 8601のルールをはずれたものは、エラーとなります。正しいといえば正しい。

shot8847

Chromeで次のような式を計算したら、-1になったんですよね。0になることを期待しますが、そうではなかった。それで気づきました。それに、Firefoxでは結果がでないということもありますね。

Date.parse("2014-10-5") / (60*60*24*1000) - Date.parse("2014-10-05") /(60*60*24*1000)

ほんとにはまりどころ満載です。

[IM]コンテキストの共有化とPusherの利用

INTER-Mediatorでは、「コンテキスト」は、データベースに対するデータの出入り口的なイメージのものであり、検索条件などでの意味づけされたデータソースを意味します。その「共有化」とは、同一エンティティが複数のページ上のオブジェクトに展開されているとき、1つのエンティティを変更すると、その結果が他のオブジェクトにも反映される仕組みと定義します。Ver.4.4までに、単一ページ内のコンテキストの共有化が実現しています。つまり、あるページ上に、同一フィールドとバインドした要素があるとすると、一方を変更すると、もう一方は自動的に更新します。この動作を実現するためのプログラミングは必要なく、バインドの設定(ターゲット指定の付与)だけで可能です。

Ver.4.5に向けて、コンテキストの共有化をマルチクライアントで実現する仕組みを開発しており、概ね動くところまできました。つまり、同一のページを複数のクライアントで参照しているとき、誰かがデータを変更すると、その結果は他のユーザのページにも反映されるという動作が典型的です。従って、1つのフィールドを単一の要素にバインドしている場合でも、マルチユーザつまり複数のブラウザで同一のエンティティをバインドしているという点で「共有化」されていると言えるわけです。

コンテキストの共有化を実現するために、ページファイル上でのターゲット指定や、定義ファイルでのコンテキスト定義以外に何をしなければならないかをこの文書にまとめておきます。単一ページ内のコンテキストの共有化は特別な仕掛けは不要です。しかしながら、マルチクライアントでのコンテキストの共有化では、WebRTCを利用したPusherというサービスを利用することにしました。試用程度なら無償ですが、実運用には有償となってしまうものの、開発の効率化のために利用することにしました。

Pusherアカウントの取得とアプリケーション登録

Pusherのサイトでアカウントを取得します。Pusherでは「App」という単位で管理ができるので、たとえばINTER-Mediatorで作る1つのソリューションを、1つのPusherのAppとして登録するという方法もありますし、複数のソリューションで共有してもいいかもしれません。いずれにしても、アカウントを作成し、New Appというボタンなどで新たに1つのAppを作成します。ページ上に表示されるapp_id、key、secretの3つの情報がこの後に必要となります。

Pusherのサーバプログラムのインストール

PusherのサーバモジュールはPHP版を利用します。こちらのレポジトリをダウンロードし、そこから得られるlibディレクトリにあるPusher.phpという1つのファイルだけをサーバにインストールします。他は使用しません。ファイルはPHPの設定ファイル(php.iniが代表的)で、include_pathの設定で参照できるディレクトリにあればかまいません。もっとも安直な方法は、INTER-Mediatorフォルダに入れて、サーバにコピーしておくことです。もし、設定が以下のようなものであれば、例えば/usr/lib/phpディレクトリにPusher.phpをコピーしておけば良いでしょう。

include_path = ".:/usr/lib/php/pear:/usr/lib/php"

ページファイルへの追加

Pusherのクライアントソフトウエアを、ページファイルで組み込む必要があります。たとえば、以下のように、ヘッダ部で定義ファイル(include_MySQL.php)の読み込みの前に読み込みます。この方法だと、Pusherのサイトから直接取り出すので、ファイルを自分のサーバにコピーする必要はありません。ソースはこの通りコピペで大丈夫ですが、Pusherのバージョンが変わった時などはそれに合わせてください。

<html>
<head>
    :
    <script src="http://js.pusher.com/2.2/pusher.min.js" type="text/javascript"></script>
    <script type="text/javascript" src="include_MySQL.php"></script>
</head>

定義ファイルあるいはparams.phpへの追加

Pusherで定義したAppに関する指定は、定義ファイルのオプション部あるいはparams.phpで指定をします。原則的にはどちらか一方で定義をしてください。両方指定すると、定義ファイルの方が優先されます。定義ファイルでは、pusherをキーにした配列を定義し、さらにPusherのAppで示された3つの値を配列の各要素の値とします。以下は、定義ファイルでの定義例です。

IM_Entry(
    array( 
              /* コンテキストの定義 */ 
       ),
    array(
        :
        'pusher' => array(
            'app_id' => '1234',
            'key' => '9876543210',
            'secret' => '9876543210',
        ),
    ),
    array('db-class' => 'PDO'),
    false
);

params.phpファイルに記述するときには、以下のように、$pusherParameters変数に同様な配列として定義をします。

$pusherParameters = array(
 'app_id' => '1234',
 'key' => '9876543210',
 'secret' => '9876543210',
);

上記のいずれかがあると、マルチクライアントのコンテキストの共有化がオンになります。定義ファイルあるいはparams.phpの指定の有無だけで、共有化の利用/不使用が決まります。指定がないと一切何も行いません。指定があるのに、Pusherのサーバあるいはクライアントソフトウエアが利用できない状態になると、なんらかのエラーが発生します。

現状での制約

レコードの追加においては、そのコンテキストの検索条件を加味して、検索条件に合わないレコードの追加は行いません。しかしながら、別のクライアントで作成したレコードが当初はコンテキストに合わないものの、フィールドの値を変更してコンテキストの検索条件に合うようになっても、現状ではそのレコードが見えるようにはなりません。

さらに、コンテキストのソート条件は現状では加味されておらず、一連の表示リストのサイトに常に追加されます。

[IM]JavaScriptコンポーネントのプラグイン

以前から、HTMLエディタのtinyMCE、コードエディタのCodeMirrorや独自作成したファイルアップロードコンポーネントを使えるようにしていたのですが、なんとか「プラグイン」的に使える状態になったので、一度ドキュメントを作成します。JavaScriptで作ったコンポーネントに、データベースにあるフィールドの値を表示し、修正するとそれが書き戻される仕組みを提供します。ただし、コンポーネントごとに、初期化の方法は違うので、その部分を吸収するプラグインを作らないといけません。

JavaScriptコンポーネントの使い方

まず、JavaScriptのコンポーネントを使いたい場合には、次のように、data-im-widget属性にキーワードを書きます。プラグインができていればこれだけです。

<div data-im="testtable@text1" data-im-widget="tinymce"></div>

ここで、tinyMCEを使うにはプラグインが必要ですが、これについては、すでにINTER-Mediatorの中にあります。Samples/Sample_webpage/tinymce_im.jsがそれなので、たとえば、ページファイルのヘッダ部に、次のような記述を行ってtinyMCE自身の読み込みと、プラグインの読み込みを行っておきます。もちろん、パスは適切なものを指定してください。

<script type="text/javascript" src="tinymce/js/tinymce/tinymce.min.js"></script>
<script type="text/javascript" src="tinymce_im.js"></script>

Ver.4.4現在、tinyMCE、CodeMirror、それから独自に作ったファイルアップロードコンポーネント(Samples/Sample_webpage/fileupload_MySQL.htmlがサンプル)が利用可能です。

JavaScriptコンポーネントプラグインの作り方

プラグイン(前記のtinymce_im.jsに相当)は、JavaScriptで記述します。もちろん、tinymce_im.jsもサンプルとして参照する必要があるでしょう。

プラグインのファイルでは、IMParts_Catalog変数のオブジェクトに、プラグインのオブジェクトを追加します。このときのキーが、data-im-widget属性に指定するキーワードとなります。以下はその基本構造です。

IMParts_Catalog["tinymce"] = {
    instanciate: function (parentNode) { },
    ids: [],
    finish: function (update) { }
}

右辺のオブジェクトは、instanciateとfinishという2つのメソッドを持つ事が重要です。INTER-Mediatorは、ページ合成時に、im-data-widget属性があるノードを見つけると、そのノードを引数にとって、instanciateメソッドを呼び出します。

一方、ページ合成の最終段階、つまり、DOMオブジェクトが確定してページ上に存在する状態になった後に、finishメソッドが呼び出されます。結果的に、im-data-widget属性が設定された要素×レコード数の回数instanciateメソッドが呼び出され、最後に1回finishが呼び出されます。

プラグインの作業として必要なこと

この2つのメソッドが行うことは、コンポーネントに対するゲッタおよびセッタメソッドをそれぞれ展開したコンポーネントに対して設定することです。また、対応コンポーネントのid属性値を得るメソッドも実装します。たとえば、instanciateメソッドに記述するとしたら、次のようになります。instanciateメソッドを呼び出されたときの引数parentNodeあるいはコンポーネントのルートの要素に対して、以下の決められた名称のメソッドを実装します。メソッドの中身はtinyMCEの場合の例です。

parentNode._im_getComponentId = function () { // data-im-widgetのある要素に設定
    var theId = newId;
    return theId;
};
parentNode._im_setValue = function (str) { // data-im-widgetのある要素に設定
    var targetNode = newNode;
    targetNode.innerHTML = str;
};
targetNode._im_getValue = (function () { // コンポーネントのルートの要素に設定
    var thisId = targetId;
    return function () {
        return tinymce.EditorManager.get(thisId).getContent();
    }
})();

instanciateメソッドでの作業

通常、JavaScriptのコンポーネントは、特定の要素にidやclass値を適当に与えて、その要素の中に必要なオブジェクトを詰め込むといった動作をします。つまり、起点となる要素を用意しておき、そこに必要なオブジェクトを追加します。tinyMCEだと、手軽な作り方はTEXTAREAタグ要素を用意することですが、ページファイル上に記述した要素がどんな種類のタグ要素でもいいように、data-im-widget属性がある要素の子要素にTEXTAREAタグ要素を作り、その要素をtinyMCEで初期化するようにしています。

そのTEXTAREA要素のid属性は、適当に付けます。targetNodeで参照されるリピーター内の要素は、すでにid属性が設定されているので、そのid値に適当な文字を追加すれば、一意なid属性になります。_im_getComponentIdメソッドは、ここでのTEXTAREA要素に付けたidを返します。

なお、instanticateメソッド中は、まだ、リピーターはエンクロージャーに挿入されておらず、documentからたどれない状態になっています。その場合、初期化をしてもうまく動作しないと思われます。従って、instanciateメソッドでは、元になるTEXTAREA要素を作り、id番号を振り、そのid番号をidsプロパティの配列に追加して、必須のメソッドを定義するところまでしかできないのが一般的かと思います。idsプロパティはなくてもいいのですが、この後のfinalizeメソッドでそれぞれの要素を初期化するために、初期化すべき要素を後から特定できるようにするために、instanciateで作成した要素のidを残します。

instanciateメソッドの段階で実際に利用されるのは、_im_getComponentIdメソッドと_im_setValueメソッドなので、_im_getValueメソッドは実際には設定する必要はありません。_im_setValueメソッドについては、JavaScriptコンポーネントが初期化前であることを考慮して、機能する前の状態での値設定が可能なプログラムを記述する必要があります。

finalizeメソッドでの作業

この段階では、全ての要素がdocument配下にいるので、JavaScriptコンポーネントの初期化を実際に行います。なお、初期化した結果、ゲッタやセッタの動作を変えたい場合は、設定をしなおします。JavaScriptのコンポーネント内で修正した結果をデータベースに書き戻すには、_im_getValueメソッドを、初期化が終わった後の状態でのゲッタとして動作するように設定をする必要があります。単にデータベースに書き戻すだけでいいのなら、ゲッタの設定のみでかまいません。以下は、tinyMCEの例で、idsプロパティにコンポーネントのid属性値が配列として残されています。1つ1つの要素に対して、_im_getValueメソッドを追加しています。

 for (var i = 0; i < this.ids.length; i++) {
     var targetNode = document.getElementById(this.ids[i]);
     var targetId = this.ids[i];
     if (targetNode) {
         targetNode._im_getValue = (function () {
             var thisId = targetId;
             return function () {
                 return tinymce.EditorManager.get(thisId).getContent();
             }
         })();
     }
 }

tinyMCEのプラグインは、ページ上にあるすべてTEXTAREAをHTMLエディタにしてしまう動作で初期化するので、tinyMCE.initメソッドを呼び出すだけです。コンポーネントによっては、個別にオブジェクトをidsプロパティから取得したid値で参照して、それぞれに何らかのメソッドを適用しないといけないかもしれません。

結果的に、それぞれのJavaScriptコンポーネントごとにうまく初期化をしないといけませんが、場合によってはフレームワークの更新も必要になるかもしれません。

自分でJavaScriptコンポーネントを作る場合

自分で1からコンポーネントを作る場合は、以下のプラグインの骨格を作り、後は自由につくっていいでしょう。このクラスにメソッドを集めてもいいですし、他のファイルから参照してもかまいません。

IMParts_Catalog["myjscomponent"] = {
    instanciate: function (parentNode) { },
    ids: [],
    finish: function (update) { }
}

[IM]JavaScript、プロパティ、セッタ、オブザーバブル?

JavaScript一般のお話ですが、詳細はINTER-Mediatorがらみとなります。

まず、プロパティは、オブジェクトに対して自由に用意できるのですが、Object.definePropertyというメソッドを使えば、セッタ、ゲッタの実装ができます。IEのみ、このメソッドが限定された状況でしか使えないこともありますが、やはり複雑な事をするときにはセッタやゲッタを利用するのはきわめて自然な発送です。

ここで、オブザーバブルです。最近のJavaScriptフロントエンド系フレームワークではもはや実装されていて当たり前の機能です。MVCという点よりも、むしろオブザーバブルな方が重要な仕組みであるとも言えます。これは、フロントエンドなのでユーザインタフェースを構築することが大きな目的であることから来ます。ユーザインタフェース、つまりMVCのViewとModelの連動のために、イベントに対応するプログラムを書くという手法ではなく、イベントが自動的に伝搬されて行く仕組みが欲しい訳で、それをベースにすると、同一のエンティティにバインドした2つの要素が連動するとか、それをさらにはクライアント間で連動させるといったベースになるのです。ここを自動化できれば、アプリケーションの複雑さはそこそこ緩和でき、より手軽にアプリケーションの作成ができるようになると言えばいいでしょうか。

INTER-Mediatorでは、コンテキストに対する検索条件を、INTERMediator.additinalConditionというプロパティに記述することで、条件を追加できます。コンテキスト、つまりはテーブルへのアクセスにおいて、プロパティの値を条件として記述します。しかしながら、条件の記述においてはさまざまなパラメータが必要になるので、結果として、次の様な記述を行うのを基本にしています。

INTERMediator.additinalCondition["コンテキスト名"]
    = [{field: "フィールド名", operator: "演算子", value: "値"}];

連想配列や配列として解釈すると、きわめて階層が深いですが、必要な情報なので記録が必要です。右辺のオブジェクトの配列を指定できるので、単一のコンテキストで複数の条件を指定したり、あるいはもちろんのこと、コンテキストごとに追加条件を指定できます。

ここで、この追加条件を変更したときに、クッキーに記録させることを考え、なるほど、それならば、このプロパティのセッタを作ればいいのではないかと考えました。もちろん、大した実装でもないので、セッタやゲッタは簡単にできたのですが、セッタが呼ばれない事が時々あり、考え込んでしまいました。考えてみれば当たり前のことなのですが、既存のサンプルなどの動きが正しくなくなると、正しい答えに行くのに回り道をしてしまいます。

まず、以下のようにすればセッタが動きます。

INTERMediator.additinalCondition 
    = {"context": [{field: "f1", operator: "=", value: "123"]};

しかし、以下のようにするとセッタは動きません。ようするに、サンプルの多くの書き方では、セッタは動かないのです。

INTERMediator.additinalCondition["context"] 
    = [{field: "f1", operator: "=", value: "123"];

ここをえらく勘違いしていました。なぜ、後者はセッタが動かないのか? これは、additinalConditionプロパティそのものへのに対する代入をしていないからです。左辺の記述により、additinalConditionプロパティにアクセスするのでゲッタは動きます。そこで、オブジェクトがあれば取り出されて、そのオブジェクトに対して、右辺を代入しているので、additinalConditionプロパティ自体への代入はまったくしていないのです。そこから参照されているオブジェクトに対しての変更処理をしているので、additinalConditionプロパティのセッタは稼働しません。

結果的に、ここでの最下層にあるfieldプロパティなどを書き換えてもadditinalConditionプロパティのセッタが動くようにならないといけないわけでして、それを突き詰めれば、オブザーバブルな配列、オブザーバブルなオブジェクトが必要になってくるわけです。残念ながら、INTER-Mediatorではそこの実装はしていません。KnockoutやEmber.jsではそういう仕組みが用意されているのです。モデルに対するプログラミングインタフェースを持つとしたら、確かにオブザーバブルな配列は必要かもしれません。また、これらの代表的フレームワークに関わらず、オブザーバブルな配列の実装はかなりたくさん公開されています。

INTER-Mediatorに汎用的なオブザーバブルな配列やオブジェクトを組み込むのも、ある意味はいいのですが、その前に本来の目的に立ち返られないといけません。INTER-Mediatorはモデルをプログラマが定義しないと使えないのではなく、宣言的な記述で定義したコンテキストを仮想的に「モデル」として見る以上のとは通常はしなくてもかまいません。オブザーバブルな配列があれば、開発している私はいいのですが、利用者の直接的なメリットではないと考えます。

一方、プロパティの値を自動的にクッキーに入れれば、一見すると便利なのですが、アプリケーションで必要なのはむしろ、細かな記録と消去のような気がします。自動的に覚えさせるのは、確かにデモウケは良さそうですが、いくつかのサンプルに実装してみて感じることは「忘れてくれ!」と思う場面も多いことです。このあたり、どういう実装にするか非常に悩みます。今現在、セッタから呼び出されるようにしていますが、もしかすると、クッキーへの記録は明示的に開発者に記述してもらうのがいいのかもしれません。もし、現状のままだと、additinalConditionプロパティへの値の設定の書き方がきわめて限定される点も考慮すべきところです。この点はしばらく考えるつもりですが、ご意見があれば、FacebookのINTER-Mediatorのグループへどうぞ。