17/02/07雑記

ちょっと本格的にAngularを再開。

色々作りこんでいく中でいくつか悩みとアイディアが出たのでメモ。

RestAPIにするか否か

顧客管理のようなシステムを作っている。

顧客への「発送」業務があり、これは顧客によりあったりなかったりする。

データモデルがつまり 顧客 1 ― * 発送 という形になっている。

基本的にresource型のRestfulAPIを守ろうとしたが、 どうしてもブラウザ上で「顧客」を登録してから「発送」を登録することになる。

ただでさえ遠いWebサーバーに2往復もするのに加えて、 業務手続でできれば分離したいコードが、準コントローラーなコード中にむき出しになってしまう。 しかもこれは、1画面に1つ占有するコードみたいなもので、再利用が難しい。(思い切ってService化するのもアリだが)

Restじゃなくて、普通にWebAPI化しようとも悩んだ。(あくまで業務手続という体を守りつつ、実質1画面専用のAPI) がしかし、どうしてもパラメータが複雑になってしまう。 変なJSONにしてPHPとPOSTパラメータの周りをうろちょろするぐらいなら、 JSON親和性の高いうちに、クライアントでシンプルなAPIパラメータに変形してしまったほうが良いだろう。

・・・と思い、結局RestfulAPIで、クライアントにもりもりコードを持っていくスタイルを取った。 WebAPIのリクエストベースなテストも書きやすくなるしね。

しかし、Angularのテスト可用性が本当に死んでいる。 もともとリッチUIの予定だったので、打検はもともとだったが。。

Validation

HTMLのValidatorAPI。ありゃだめだった、エラーメッセージが1つしか出ない。

おとなしくAngular$validationsなりフレームワークやライブラリに頼って、 ラベルか自分でポップアップするかしたほうがよさそう。

【JavaScript】ValidationAPIの標準エラーメッセージをフックして上書きする関数作った

GitHub

github.com

これで、Scriptトリガーの日本語英語切り替えが楽になる。

長さなどを文字列埋め込みできるようにメッセージをformatできるようにすればよかったけど、 Formatメソッド作らなきゃいけなくなるから削る。

【HTML】JavaScriptで検証結果のポップアップを呼び出す

ハマった。

デフォルトのValidationAPIはsubmitされないとポップアップしない?

<form id="f">
  <input type="text" name="firstname" id="firstname" required> 
  <button type="submit">submit</button>
</form>

これをJavaScriptから

let elForm = document.getElementById('f');
elForm.submit();

したら、検証を無視してページ遷移が発生。

で、それっぽいAPIがあるんだけど、どれを見ても検証結果の true / falseを知ることができるだけで、検証結果のメッセージがポップアップしない。

if (elForm.checkValidatity()) {
   document.getElementById('error').innerText = ('エラーがあります。');
}

俺はデフォルトのあの動作が欲しいんだ。

とりあえずの解決法

function popupInvalided(elForm) {

    const fnSubmitHandler = (event) {
        event.preventDefault();
    }

    let btn = document.createElement('button');
    btn.setAttribute('type', 'submit');
    btn.style.display = 'none';

    elForm.addEventHandler(fnSubmitHandler);
    elForm.appendChild(btn);
    
    setTimeout(() => {
        btn.click();
        setTimeout(() => {
            elForm.removeEventListener('submit', fnSubmitHandler);
            btn.remove();
        });
    }, 0);
}

探したらポップアップするAPIあると思うんだけど。 これだけのためにW3C文書読む気になれない。

おまけ:setCustomValitiyの挙動

setCustomValidityで自由にエラー扱いにできるしエラーメッセージも動的に決められるが、 JavaScriptで空文字を再設定しない限り、ずっとその項目はエラーになり続ける。

// 書きかけたバグ
if (!input.checkValidity()) {
    input.setCustomValidity('オリジナルのメッセージ');
}

回避策

  • HTMLにtitleつけて回避する。
<input type="text" required title="名前は必須です。">
  • setCustomValidityを使うところは、checkValidity()の前だけにする。

【Postgresql】トランザクション中、CURRENT_TIMESTAMPなどの値は固定

ハマった。

https://www.postgresql.jp/document/7.2/user/functions-datetime.html#FUNCTIONS-DATETIME-CURRENT

CURRENT_TIMESTAMP と、それに関連する関数はすべて現在のトランザクションが開始された時間を返すことを認識してください。とても重要なことです。この値はトランザクションが実行されている間に増加しません。とは言っても、timeofday() は実際の現在時間を返します。

代替として、clock_timestamp()timeofday()を使う。

【Java】JsoupのWhitelistは使わないほうがいい

前任者が残したコードが土壇場になって牙を剝いた。

Jsoupとは

Jsoupは、あいまいなHTMLでも確実に構文木にパースしてくれる高性能ライブラリ。

例えば<b>とだけ書いても、<html><head></head><body><b></b></body></html>まで補完してくれる。 あまつさえ、jQueryではないが、getElementByTagなどといったJavaScriptでおなじみのAPIみたいなものが使える。

Whitelistの罠

JsoupにはWhitelistクラスというフィルタールールみたいなものがあるが、結構罠を踏んだ。

  1. 単純なルールしか無理。「○○属性は必須だけど××は任意、その他全部禁止」とかできない。
  2. 致命的なのが、<html><body><head>は無条件でホワイトリストを通過すること。(任意の付きでも通過。XSSじゃん!)

2に関しては何か抜け道があると信じたいが、JsoupクラスのAPI見る限り期待できなさそう。 そもそも、厳格なルール付けには適していなさそう。

セキュアな実装をするならば、また他のHTMLパーサを探すか上記3タグについてメタいことをしたWhitelistを実装するしかない結論に至った所で帰った。

2つのインタフェースと1つの共通インタフェースをめぐる失敗録

HTMLタグのホワイトリスト実装にはまって半日を無駄にしてしまった。ありがちな、汎用性を求めすぎた結果だった。

実装したかったこと

  • テキストボックスに入力された文字列中の使用HTMLタグホワイトリストチェック
  • パースは別途ライブラリがする
  • 要求は以下
    • 「特定の属性必須」
    • 「特定の属性はあってもなくても良い」
    • 「特定の属性値は指定の正規表現にマッチすること」
    • 「指定した属性以外は許容しないこと」
    • 上記4条件を組み合わせたタグを定義すること
    • 同じタグに対して、複数のホワイトリストが定義できること。(例:<span style="color:red;"><span style="text-decoration:underline;">のみ許可)

最初にしようとしてつぶれたこと

言語はJavaJavaといえばインタフェース。

デザインパターンでいうDecoratorやComposerのようなことをしようとして、以下のインタフェースを定義してみた。

public interface TagMatcher {
   /**
    * 渡されたHTMLタグがマッチングルールに適合するかを判定します。
    * @param el HTMLタグ
    * @return 成否
    */
   public boolean isMatch(Element el);
}

これ自体は問題なかった。呼び出し元からバインドする事項は高々これ1つだった。

まず、「属性-属性値を1つ検証するルール」「無条件でtrueとするルール」など単発のルールを実装して、 それらをDecoratorする形で「配下に持つ複数のルールのand(or)をとるルール」「配下のルールを否定するルール」を作った。

ぶちあたったのはもう一つのインタフェース。

このホワイトリストは元のXMLから与えらえる引数によって引数が定義される仕様だった。 例として、先ほどの2つのspanタグの定義は、以下のように与えられる。

span[style="color:red"],span[style="text-decoration:underline;"]

なるほど単純な繰り返しで、パース自体は比較的簡単に可能だ。

しかしつなげてみると

ホワイトリスト定義
  -> (ホワイトリストを分解して汎用データ構造に仕立てるロジック)
  -> 汎用データ構造
  -> (汎用データ構造をもとに、やりたい検証内容を仕立てるロジック)
  -> 検証結果

というロジックになってしまった。言い換えるなら以下だ。

インタフェースA
  -> (インタフェースA → 汎用データ構造の変換処理)
  -> 汎用データ構造
  -> (汎用データ構造 → インタフェースBの変換処理)
  -> インタフェースB

公約数

例えて言うと、ここでいう「汎用データ構造」は公約数を因数にする数のようなものだと考えている。

インタフェース間で受け渡す情報量や表現力を増やすには、公約数を大きくせねばらなない。 乗除を減らすにはインタフェースA-Bの公約数を増やせばいい話だが、 この約数は「2」とか「10」とかのcommonな分かりやすい数が好ましい。 (例えば「19」とか「113」とか、メタったルールを約数にすると嫌なコードになる)

実装したいことと今までに積み上げたことを整理してみる。

  • インタフェースA(ホワイトリスト定義)で定義する内容は結構多い
  • 共通データ構造(isMatch)にバインドする内容は結構少ない
  • インタフェースBを通して呼び出し元がしたい内容は結構多い

言い換えると

  • インタフェースAの数はでかい
  • 公約数が小さい
  • インタフェースBの数はでかい

これを守った上で取る施策は以下になる。

  1. 汎用データ構造をインタフェースB寄りにする(インタフェースBの約数を増やす。インタフェースA → 汎用データ構造の変換処理のボリュームを積む)
  2. 汎用データ構造 → インタフェースBの変換処理のボリュームを積む(約数を単純にする。)

どうあがいてもロジックに影響が出る。また、互いの機能が連携しあうため、修正が入るならばどちらか膨らんだほうのロジックに手を出さなければならない。

思い返せば、典型的なインビーダンスミスマッチだった。各両端のインタフェースの公約数、なにより内部の汎用データ構造が1つのインタフェースであると見抜けなかった。

顛末

結局、インタフェースや汎用性の美しさを捨てて、古典的な業務モデルクラスを設計した。 気を付けたのは約数の種類、具体的には業務的ルールに関する構造と言語特性・データ構造に関する構造をなるべく共通化することだった。 公約数を大きくした。

共通データ構造を新しいインタフェースで利用したり、互いのインタフェースの因数が非常に多い場合、小さな公約数にも利があったと思う。 が、局所実装は結局仕様変更によって捨てられるコードであり、まだ300行に満たない共通実装はインタフェース変更を受けるし多少の汚れがあっても十分読めるはず。。願わくば今のきれいなままで死んでほしいけれどもきっと叶わないだろう。

モダンなブラウザクライアントJavaScript開発マネージャjspm

遅れながらjspmに触った。

JavaScriptのパッケージマネージャ、ブラウザ未サポートの新鋭機能をES5にしてくれるようなトランスレータ、おまけにバンドルも勝手にしてくれるすごいやつだ。 System.jsとの併用をオススメしているが、ぶっちゃけるといらないぐらい便利だ。

ES6以降のJavaScriptは最初が面倒くさい

今現在、新しめのJavaScriptの環境で開発しようとすると、開発環境づくりが面倒くさい。

理由として、ちょっと昔話。

ES6以降、JavaScriptは複数ソースコード間の連携についてやっと取り込み始めたが、ES5既存そのまま開発者が使いだすと、色々問題がでてくる。

ひとつが、新機能文法の実装が、各ブラウザの足並みがそろわないこと。(新鋭のものでも、結構遅い。古いIEなんかは実装すらしない)

ふたつめに、ファイルを分割しまくると、下手すると数百個のファイルをブラウザがダウンロードすること。(しかもjsごとに依存チェーンを解決しながらなので、ほぼシーケンシャルにファイルをダウンロードすることになる。。)

これらの解決に、色々ツールを使っている(ES6→ES5へのTranslate、1つのjsファイルにするbundleなど)。 相互の設定が非常に面倒くさい。それぞれコマンドラインツールなので、だいたいtask runnerを導入してビルド自動化するが、プロジェクトごとにファイル構成が違ったり、あとスクリプトファイルのメンテも面倒くさい。

jspmが解決してくれること

jspmは、上記でやることを一つのコマンドラインツールにパッケージングしたツールだ。 つまり、ツールの個別インストール+タスクランナーに書かなければいけなかったことを、全部中に隠ぺいしてくれる。

jspm init としたら初期設定とどのTranslator使う?とか設定できたり、コマンド一発でbundleできたりする。 ライブラリの導入も、jspm installとNodeライクにできてしまう。

ES2015+の過渡期、JavaScriptの敷居を下げるのに大いに役立ってほしい。

サンプル

github.com