[JavaScript]変数の存在確認

・同じ比較方法の場合、if文よりも三項演算子の方がやや速い
・もっとも高速な方法は論理和を使う方法
・もっとも時間がかかる方法は型比較をif文で行う方法
・null比較、型比較で三項演算子を利用すると真偽によって速度が(割と)変わる
( 変数の存在を確認する方法と速度比較(その2) – blog.katsuma.tv)

五年前の記事なのだけれど、関数に渡ってきた引数を存在チェックするとき、どう書くのが早いだろう?という実験レポート。
このレポートによると、

var ret;
if(arg) {
ret = arg;
} else {
ret = 'bar';
}

という比較的普通のコードと比べて

var ret = (arg!=null)? arg : 'bar';

三項演算子を使うと倍近く早いらしい。

でも、変数の初期化だったら僕はこう書くことが多い。

if(!arg) { arg = 'bar';}

ということで、五年経った現状と、僕のコードどうなんだろう、というのを見たいと思ったので試してみた。

Google Chrome 26.0.1410.65

[checkStrTernary]   1786
[checkStrIf]    1786
[checkArgsLogicalAdd]    49
[checkArgsTernary]   45
[checkArgsIf]   51
[checkArgsIf2]  48
[checkNullTernary]  74
[checkNullIf]   59
[checkTypeTernary]  61
[checkTypeIf]   48

待てw

やるんじゃないかとは思っていましたが、実質的に何もしていないことを見ぬいて最適化された結果、Date()の分解能テストと化しています。たぶんループが丸ごと削除されていますね。
もちろん、checkStrTernaryとcheckStrIf以外の順位は実施のたびに入れ替わりますので、個別のコードの有利不利を判定することはできません。

FireFox 17.0.1

[checkStrTernary]   14555
[checkStrIf]    14263
[checkArgsLogicalAdd]    10572
[checkArgsTernary]   10674
[checkArgsIf]   11215
[checkArgsIf2]  11258
[checkNullTernary]  10659
[checkNullIf]   10750
[checkTypeTernary]  11102
[checkTypeIf]   11714

うむ。さすが高機能JavaScript開発環境(僕命名)。
これを見ていると、やっぱりcheckStrTernaryとcheckStrIfが遅いものの、あとはそれほど大きな差じゃない程度になっています。checkArgsIfとcheckArgsIf2などは実施のたびに早いほうが入れ替わるので、誤差の範囲内です。

InternetExplorer 9.0.8112.16421

ログ: [checkStrTernary]   11868
ログ: [checkStrIf]    11766
ログ: [checkArgsLogicalAdd]    8618
ログ: [checkArgsTernary]   8525
ログ: [checkArgsIf]   10521
ログ: [checkArgsIf2]  10767
ログ: [checkNullTernary]  10777
ログ: [checkNullIf]   10655
ログ: [checkTypeTernary]  11507
ログ: [checkTypeIf]   10584

おおむねFireFoxと同じ傾向が出ています。よほど変な書き方をしない限り大差ないということですね。

所感

近年のブラウザにおいては、最適化が進んだ結果、以前ほど三項演算子有利ではなくなってきました。
それでも1割程度の差がつくので、シビアな場面ではトライする価値があるかもしれないですが、普通にコード書くときは、読みやすいほうを使うのでいいんじゃないかなーと思いました。

コード:

[iOS] タブを残さずURLスキームを開く

TitaniumのユーザーBBSで、ChatWork のiPhoneアプリの挙動が話題になっています。

ChatWork の謎 | Don’t Fall – Titanium Mobileユーザー会サポートBBS

ChatWorkアプリでは、facebook認証でアプリを使うことが出来るのですが、これまでよく使われていたWebView上での認証ではなくて、Ti.Platform.openURL() を使ってブラウザを立ち上げてfacebook認証を行っています。

WebViewを使わない理由は、「OAuthの認証にWebViewを使うのはやめよう – Shogo’s Blog」に書かれているとおりだと思います。具体的な実装としてChatWorkアプリはとても参考になりますね。

さて、ブラウザ上でfacebook認証が成功すると、ChatWorkサーバはURLスキームを使ってChatWorkアプリケーションを起動させます。
ここがマニアックに興味深いところです。

  • ユーザーはリンクをクリックしていないのに、なぜURLスキームが起動したのか?
  • ChatWorkからブラウザに戻ったとき、URLスキームを起動したWEBページが残っていないのは何故?他のアプリだと残っちゃうよね?

結論から言うと、こういうふうに書けば同じ挙動が再現できました。

(本当は”chatwork:///”でChatWorkアプリが起動するのですけど、テストのために意味不明な起動を繰り返すとご迷惑になりそうだったので、メッセージアプリを起動しています)

ページを読み込むと同時にURLスキームを起動させる

 URLスキームは普通、aタグのhrefに書いて、ユーザーさんにクリックしてもらって使います。例えば、

<a href="sms:///">sms</a>

と書かれたHTMLのリンクをユーザーさんがクリックすると、メッセージアプリが立ち上がります。でも、facebook認証が済んだら、ユーザーさんの手を煩わせることなくアプリケーションに戻ってほしいですよね。

 location.hrefで強制移動とか、aタグを書いてjQuery(hoge).click();とか、いろいろ試してみたのですけど、そういうのは駄目なようです。
ページ内に動的にiFrameを生成してURLスキームを持たせることで上手く起動させることが出来ました。

URLスキームを起動したあと自分を閉じる

 URLスキームでアプリを起動したのはいいものの、そのHTMLがブラウザ上に残ったままになっていると、あとでブラウザを開き直した時に変なページが見えてしまったり、場合によっては勝手にアプリが立ち上がってイラっとさせられることがあります。結構有名なアプリでもこの現象起こしますよね。

 JavaScriptで自分を表示しているタブを閉じる場合、理屈上はwindow.close()で出来るのですけど、safariのwindow.closeはjavascriptで開いたページしかclose出来ないので、このようなごまかしテクニックを使うことで自分をcloseすることが出来ます。

window.opener = window;
var win = window.open(location.href,”_self”);
win.close();

URLスキームとwindow.close、どっちかが他方を妨害しそうな気がしますけど、実際には両方上手く動くみたいです。

P.S 1:自分でJavaScript書いて同様の挙動を再現させることに成功しただけなので、ChatWorkさんのアプリと厳密には一致しないかも知れません。
P.S 2:[iOS]って書いてますが、URLスキームをintentに変えればAndroidでもそのまま動作しました。
P.S 3:ChatWorkの中の方、出来るだけ迷惑かけないように調査したつもりですけど、不審なログとして調査対象に上がっていたりしたらごめんなさい。

[GoogleMapsAPI] APIと組み合わせる時のイベントはidleを使う

gmap.jpg

四隅の緯度経度を渡したら範囲内のスポット情報が得られる、というAPIは最近そこら中にあって、

これとGoogleMapsAPIを組み合わせて地図上に何らかのスポットを表示する、というのもそこら中で行われています。

こういうサービスを実現するためには、ユーザーが地図の表示位置を変更した時、新しい位置のスポット情報を取得して表示するためにAPI呼び出しをする必要があります。
この実装例をググると、bounds_changed(領域が変化した時)やdragned(ドラッグが終了した時)イベントのタイミングでAPIを呼び出すようにしている実装が多いのですけど、idleイベントを使うとすごい綺麗に実装することが出来ます。
以下、bounds_changedやdragnedを使った場合に発生する問題と、idleイベントだと何がうれしいのかをまとめました。

bounds_changedを使った時の問題

bounds_changedイベントは、「This event is fired when the viewport bounds have changed.」と定義されていて、地図の表示領域が変更になった場合に発生するイベントです。
このタイミングでAPIを呼び出すようにすれば、ユーザーが地図をドラッグした時も、ズームが変更された時もイベントが発生するので、わりといい感じに動くように思えます。

bounds_changedイベントの問題は、発生頻度が多すぎることです。bounds_changedは領域が変わるたびに呼ばれるので、ユーザーが地図をドラッグすると、一秒に数回の勢いでbounds_changedイベントが発生し続けます。
つまり、単純に実装すると、秒間数回のペースでAPIを呼び出し続けることになります。サーバ負荷の観点から絶対止めてほしいですし、ブラウザ側だって、そんな頻度で結果が帰ってきたら処理しきれなくてブラウザが固まってしまうことが予想されます。

カウンタをもうけて10回に一回だけAPIを呼び出すとか、タイマーを使って何秒に一回だけAPIを呼び出すとか、やってやれないことはないのですが、ずいぶん複雑なコードになってしまいます。

dragendを使った場合の問題

dragendイベントでAPIを呼び出すという実装も考えられます。ドラッグ終了時に一回だけ発生するので、その点はいいのですけど、今度はイベントの発生が少なすぎて弊害が発生します。

ズーム変更の時スポットが更新されない

さすがにこれは誰でも気づくので、dragendとzoom_changedでAPIを呼び出す実装にする人が多いと思います。

ドラッグが終わるまでAPIが呼び出せない

dragnedイベントはマウスを放すまで発生しないので、地図をドラッグしながら「この辺かなー?」って考えてもスポットが更新されません。ダメじゃないですけど、ユーザーフレンドリーじゃないですよね。

キーボードで地図を移動させた時APIが呼び出せない

そんな人はあまりいないと思うけど、GoogleMapsはキーボードでも操作ができるようになっています。この場合dragendイベントは発生しません。
また、最近のGoogleMapsは駅などをクリックするとその部分が中央に移動して詳細がポップアップするようになっていますが、この移動の場合もdragendイベントは発生しません。

自分でsetCenterイベントを呼んだ時も発生しない

これが最大の問題で、例えば検索フィールドを作って入力された住所の位置に移動するような実装をする場合、ジオコーディングして得られた緯度経度にmap.setCenterすることになると思うのですけど、この場合もdragendイベントは発生しません。
setCenterを呼び出す時は自前でAPI呼び出しをするコードを追加して・・・(後日APIが変更になった時、片方だけ変更しわすれてバグを埋め込むパターン)

dragendを使った場合の問題

こんな具合にして、起こりうる各種問題に場当たりの対応を繰り返すと、複数のイベントを組み合わせてフラグ管理をすることになったり、コード中のあちこちからAPIを呼び出すことになったり、どうしても複雑なコードになってしまいます。
実際僕もそうやって書いていたのですけど、ある時、idleイベントで全部カバーできることに気づきました。

idleイベントは「This event is fired when the map becomes idle after panning or zooming.」と定義されています。

実際ログを入れて試してみるとわかりますが、

  • Mapをnewすると、地図の表示が終わった時点で一回発生します。つまりここで、最初の一回目のAPI呼び出しを行うことが出来ます
  • 地図をドラッグしおわった時に一回発生。ドラッグ中も、ちょっと手を止めるとそこで発生。idleにならないと発生しないので、API呼び出しが多くなりすぎる心配がありません。
  • ズーム変更の時も発生
  • キーボードで地図を動かした時も発生
  • setCenterやsetZoomを呼び出した時も移動直後に発生

と、理想的な挙動を示します。つまり、HTML表示直後の一回もマップ移動時のAPI呼び出しも、全部まとめて

google.maps.event.addListener(self.gmap, 'idle', function() {
//ここでAPI呼び出し
});

としておくだけで意図した動作をさせることが出来ます。

JavaScriptで安易にAPIキーを使っちゃいけない

1267752_24260920.jpg

 具体的には、twitter公式短縮URLサービス、bitlyさんのAPIなんだけど。
このAPIを使うためには、bitlyにユーザー登録して、APIキーというのを取得します。で、そのユーザー名とAPIキーがあれば、

http://api.bit.ly/shorten?version=2.0.1&login=[ユーザー名]&apiKey=[API Key]&longUrl=[目的のURL]

というようなURLにアクセスすることで短縮URLを生成することが出来ます。

問題なのは、このAPIキーをJavaScriptでも使うことが想定されていることで。

JavaScriptで使うということは、ブラウザでソースを見たらユーザー名もAPIキーも丸わかりなのに、堂々と使っている方が結構おられます。

悪用シナリオ1

 被害者Aさんが、JavaScriptでbitlyAPIを使ったWEBページを開設していたとします。
スパム業者がやってきて、Aさんのページからユーザー名とAPIキーを拾います。
スパム業者は、このユーザー名とAPIキーを使ってスパムページへの短縮URLを生成して、スパムメールに貼り付けて送信しました。
数日すると、このURLはスパムメールであるという通報がbitlyに届くので、該当の短縮URLは凍結されます。
ついでに、スパム生成に使われたAさんのアカウントも凍結される可能性が高いですよね。悪くすると、Aさんはスパムメールの送信者と疑われてしまうかも知れません。

悪用シナリオ2

悪意のある第三者Bが、Aさんのページからユーザー名とAPIキーを拾います。
Bは、Aさんのユーザー名とAPIキーを使ってアダルトサイトの短縮URLを生成しました。
Bさんはこういう風に公表します。「あれ?Aさんこんなページも見てるんだぁ!やーい、エッチエッチ!」
Aさんにとって身に覚えのない話ではありますが、自分の生成履歴にある以上、なかなか説明の難しい立場に追い込まれてしまいました。

対策

 本来ならば、bitlyがAPIキーの仕様を変えて、登録されたドメイン以外からアクセスがあったらはじいてしまうようになっているべきだと思います。
googleMapsとかはそうなっています。

でも、公式フォーラムに出ている指摘を見ると、「そんなのいいじゃない、気になるならJavaScript以外から使えば?」と思う人もいらっしゃるみたいです。

上記のようなシナリオは困るよね、と思う方は、bit.lyのAPIをJavaScriptから直接使うのではなくて、PHPやRubyなどのサーバ側で動作する言語から使うことをオススメします。

[PHP]bitlyのAPIKeyを遮蔽する方法まとめ-RinGoonPOP!!

ブラウザサイズを変えてテストをする

今ちょっと変わったサイトを開発中です。
開発中に「1280×1024のブラウザで見ると様子がおかしい」というレポートをいただいて。

そういえばあまりそういうテストをしていなかったな、と思って、ブラウザのサイズを自在に変える仕組みというのを作ってみました。

ウィンドウのサイズを選んでください。

  • ボタンを押すとブラウザのサイズが変わります。
  • IE7だと、複数タブを開いた状態では上記動作が利かないみたいです。最初にこのページを開いてブラウザサイズを変えてから所望のサイトに移動するのがいいみたい。
  • ウィンドウを指定したサイズに変更するを参考にしました

IE,FireFox,safari,chrome,Opera対応文字列検索ブックマークレットの作り方

 ブックマークレットというのがあって、
ブラウザのブックマーク(お気に入り)に登録しておくと、色々便利な機能を使うことが出来ます。
特に多いのが、選択した文字列をWikipedia/英和辞典/和英辞典などで検索するというもの。ところが、インターネットエクスプローラー用です、FireFox用です、と分けてあったり、そもそもFireFoxでしか動かない物が多かったりするのですよね。

Google英語検索ブックマークレットは、インターネットエクスプローラーと、FireFoxと、ChromeとSafariで動作確認してあります。あ。Operaも。

ということで、インターネットエクスプローラーと、FireFoxと、ChromeとSafari(Operaも)で動く、選択した文字列に対して~するブックマークレットを作る方法まとめです。

IEで選択文字列を取得

「document.selection.createRange().text」。IE以外では全く動かない。

FireFoxで選択した文字列を取得

window.getSelection。document.getSelectionだと、Chromeやsafariで動かない。

Chrome/Safariで選択した文字列を取得

window.getSelectionでうごく。ただし、to_stringが微妙らしく、そのまま文字列として扱うことが出来ないので、「window.getSelection()+””」としておくと文字列として取得することが出来る。これは、FireFoxやOperaでも動作する。

IEとそれ以外の分岐

if(navigator.appName==”Microsoft Internet Explorer”) がまっとうだけど、長いのでdocument.allが定義されているかどうかで代用。

ここまでをまとめると

「var q=(document.all)?document.selection.createRange().text:(window.getSelection()+”);」

文字列を選択していない時

「if(!q){void(q=prompt(‘keyword:’,”))}; 」としておくと、文字列を選択していない時は入力を促すことが出来て親切。

おまけ

URLを移動させる場合、window.location.hrefにURLを入れる人が多いけれど、これもChromeで動かない。location.hrefなら共通で動く。

動作確認環境は以下の通り。

  • InternetExplorer7.0.5730.13
  • FireFox3.08
  • GoogleChrome1.0.154.53
  • Safari3.2.2
  • Opera/9.60

ブックマークレットを作る時は、参考にしていただけると嬉しいです。

webteko第五回

webteko 第五回に行ってきました。会を重ねて、なんだか安定感が出てきましたね。

今回は、「JavaScript入門 ~なぜCSSにclassとidがあるのか~」というお話をさせていただきました。

HTMLを書く人向けに、JavaScriptを書く人の事情を知っておいていただくと、ちょっと幸せになれるよ、というようなお話です。

プレゼン中で紹介した二つのサンプルはこちらです。

メールアドレスのミスを指摘してくれる入力フォーム

#この記事は、[javascript]Re:入力ミスを減らすメールアドレス入力フォームの作り方 – もぎゃろぐで紹介したものをまとめ直したものです。

指摘例

お問い合わせフォームとかイベントの申し込みなどで、ユーザーさんにメールアドレスを入力していただく場面はたくさんあると思うのですが、ユーザーさんは結構な頻度で入力ミスをします。
で、その間違いをその場で指摘できたら、入力ミスが減るんじゃないかということで、ありがちな間違いを指摘してくれるライブラリというのを作りました。

試してみる


メールアドレス:

mogya@example.co.jpではないですか?mogya@example.cojpと入力されています。


“mogya@example.cojp”(ドットが一個抜けた)とか、”mogya@example.co”(.comのmが抜けた)とか、”mogya@gmai.com”(gmail?)などを入力してみてください。入力欄を抜けた時点で指摘が表示されます。

ダウンロード

checkEmail.js

使い方

 普通のメールアドレス入力欄はこんなタグで出来ているかと思います。

メールアドレス:
<input type="text" name="email"/>

これをこういうふうに変えてください。

メールアドレス
<input type="text" name="email" id="email_input"/>
<p class="warn" id="mail_error_msg"></p>

つまり、

  • メールアドレス入力欄のinputタグのidを”email_input”にする
  • 警告メッセージを表示できるように、”mail_error_msg”というidのpタグを用意する

あとは、タグの中で、

<script type="text/javascript" charset="UTF-8" src="checkmailaddress.js}"></script>

という具合にライブラリを読み込んであげれば、ライブラリが入力フォームにイベントを設定して、入力されたメールアドレスのチェックを行います。

ライセンス

このライブラリは、パブリックドメインに寄付してしまいます。使いたい人は自由に使ってくださいませ。著作権表示もリンクも不要です。