愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

JsDocにやさしいJSの書き方

最近、比較的真面目にJsDocを書くという機会があったので。

(※この記事の前提はJsDoc 3.2.2です)

JSで適当に名前空間的なものを切ってコードを書いていくと、

/**
 * @class
/*
function Klass() {
}

Klass.prototype.foo = function () {
};

my.name.space.Klass = Klass;

みたいな書き方がいいか、

/**
 * @class
/*
my.name.space.Klass = function() {
};

my.name.space.Klass.prototype.foo = function() {
};

みたいな書き方がいいか、という割とどうでも良い選択(意味的には同じなので)を迫られることがありますが、これは結論的には後者の方がよくて、

  • 後者だとJsDocが勝手にKlassがmy.name.spaceに属することを判断してdocumentを作ってくれるが、前者の場合は@memberofとか使わないとその辺を理解してくれない
  • 前者だと余計なシンボルがスコープに定義される (Klass)

という二点において、後者を選んだ方がよいです。

ちなみに僕は「メソッド宣言とかを短く書けるから」という理由で前者を選んでいましたが、JsDocを書く段になって後悔しました。

この辺の話は今後JsDocそのものが賢くなると改善されるかもしれませんし、browserifyなどのモダンなモジュールシステム(?)を使っている場合はあまりこういう話題は登場しないかもしれませんね。

JsDocのannotationを真面目に書くのって割と面倒なので、出来るだけannotationなしでJsDocに構造を理解してもらえるようにコード書いたほうが後々ラクですよ、というお話でした。

Googleは内部的には違うものを使っていると思いますが、JsDocの書き方自体はclosure libraryが参考になる気がしています。

google/closure-library · GitHub

色々なライブラリが群雄割拠している昨今のJS業界ですが、closure libraryにはGoogleの伝統と知恵が詰まってると思うんだ。多分。

初めて真面目にChrome extension作ったのでチラ裏メモ

まだ公開してないけどね。

  • デスクトップ自体のアイドル(ユーザ操作がない)状態やロック状態はchrome.idleで取れる。ユーザ操作がないと判断するまでのtimeoutも自分で決められる。最短15sec。
  • Chrome自体からフォーカスが離れたかどうかはchrome.windows使って判定すればオーケー (chrome.app.windowではない)
  • 参考: Detect browser focus/out-of-focus via Google Chrome Extension - Stack Overflow (厳密に言うとonFocusChangedのコールバックに渡ってくるのはwindowではなくwindowIdなのでこのコードは間違っている)
  • chrome.sendMessageとかmessagePort系とかの機能はchrome.storage.onChangedで代用できないこともないが、storageを使うのは永続化したいデータのみに留めておいた方が無難だろう
  • chrome.browserAction.setPopupで右上ボタンにポップアップを設定するとchrome.browserAction.onClickedが効かなくなるが、そこはpopup内のjsからbackground pageに対してchrome.runtime.connectし、chrome.runtime.onConnectedをシグナルにして処理すれば問題ない
  • もちろんchrome.runtime.onConnectedはclick eventの代替ではないが、どうせpopupとbackground pageの間でメッセージングするんでしょうと。
  • popupのhtmlの中にanchorがあるとChromeが勝手にfocusしてうざい場合はanchor elementにtabindex="-1"を指定する。ってもうちょっとマシな方法ないんかい。(http://stackoverflow.com/questions/16701082/chrome-extension-first-link-is-auto-focused-in-popup)
  • Chrome extensionの中でgapi (いわゆるGoogle系APIのJSライブラリ)をOAuth2の認可通した上で使いたい場合は chrome.identity.getAuthTokenしてのち得られたtokenを gapi.auth.setToken({ access_token: token }); すればよい
  • 読み込み方はこんな感じで (http://stackoverflow.com/questions/18681803/loading-google-api-javascript-client-library-into-chrome-extension)
  • chrome.i18nは普通に使える、それ以上でも以下でもない

総じて、extension開発は、クロスブラウザとか気にせずバリバリ色んな機能を使えるので気持ちがいい。情弱なのでdialog elementとか初めて使った。こういうのがちゃんと使えると、ダイロアグ的なものを出すためだけにUI系のライブラリを入れる必要がなくなるのでうれしい。

あとVue.js割と使いやすかった。小規模なコードだから足りない部分が気にならないだけなのかもだけど。

2014年にもなってWebページをまともに印刷する方法も知らない俺たちは(あるいは、とあるウェッブエンジニアの闘いの記録)

前略、window.printという関数をJavaScriptから呼び出せば、めでたく印刷ダイアログが表示されて、今目の前に表示されているウェッブページを印刷することが出来るわけだ。

が、とりあえずこの機能をそのまま使うとおかしい。何やら色々おかしい。media="print"なスタイルシートで指定したbackground-colorすら反映されない。「ブラウザの印刷オプションで出来る」という噂もあるが、僕の環境ではちゃんと色が出なかった。

これは一体どうなっているんだ?2014年にもなって、俺達はウェッブページを、自分がいま目の前にしているそのままの姿で印刷することすら出来ないのか?HTML5技術のカンファレンスを開いたり、WebSocketでチャットを作ったりしてる場合じゃない!事件はカンファレンスで起きているんじゃない、俺の家のプリンターの前で起きているんだ!

ゲンよ、画像じゃ。画像を使うんじゃ

というわけで僕はアッと驚くワークアラウンドを探すための旅に出たわけだが、何となくまともな印刷機能を提供していそうなGoogleという会社の、Googleカレンダーというサービスについてさらっと調べてみた。

どうやら、Googleカレンダーは「今見ているウェッブページ」をそのまま印刷するのではなく、一旦画像を生成して、その画像を表示したページを印刷することで回避しているようだ。コードをチラ見したところ、"Print"ボタンを押した際にiframeでアレコレしている節もある。

よし、これだ!画像は印刷でそのまま色が載るのだ。画像なら、スタイルシートがどうのこうのと悩むこともない。我が社もこれで行こう。

f:id:zentoo:20140211154855p:plain

画像をどうやって作るか?

で、だ。画像はどうやって作る?Googleはおそらくカレンダーのデータを画像としてレンダリングするような内部APIを持っているのだろう。これは間違いなくクロスブラウザなソリューションだ。が、自分でそんなものを用意するのは面倒くさくて仕方がない。

要は、今見ているページのDOMを、何らかの形でimgのsrcにぶっ込めるURLに変換できればいいわけだ。よし、ちょっと迂遠だがcanvasでいこう。

html2canvasというライブラリがある。試した所、ある程度まともに動きそうだ。これを使おう。

html2canvasを使って、DOM -> canvas -> Data URIという変換を行う

html2canvasを使うと、以下のような感じでいけるのではないか。

  1. html2canvasで、現在のページのDOM(またはその一部)をcanvas elementに変換する
  2. canvas elementのtoDataURL()でData URIを得る
  3. same originかつ不可視なiframeを開き(または予め開いておき)、その中のimage elementのsrcに親frameから先ほどのData URIをぶっ込む
  4. iframe.contentWindow.print()を呼び出す

完璧じゃないか、我が軍は。

実際のコード

なんとなく、Chrome 33.0.1750.70 betaで動くことは確認した。

    function printWithCanvas() {
        html2canvas(document.getElementsByTagName("main")[0], {
            onrendered: function(canvas) {
                var iframe = document.getElementById("print-iframe");
                var iframeWindow = iframe.contentWindow;
                var imageElement = iframeWindow.document.getElementById("canvas-image");

                if ( typeof canvas.toBlob === "function" ) { 
                    var blobURL = URL.createObjectURL(canvas.toBlob());
                    imageElement.src = blobURL;
                }   
                else {
                    var dataURL = canvas.toDataURL();
                    imageElement.src = dataURL;
                }   

                iframeWindow.print();
            }   
        }); 
    };  

iframeの中味は、以下のような感じのHTMLにしておく。

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
    <style type="text/css">
        #canvas-image {
            width: 100%;
            height: 100%;
        }   
    </style>
    <script type="text/javascript">
    </script>
</head>
<body>
    <img id="canvas-image" src="" alt="" />
</body>
</html>

HTMLCanvasElementのtoBlobがある場合はそれを使う。比較的良心的ブラウザーであるところのFirefoxには既に搭載されているようだ。canvasをimageとして扱う際にData URIにわざわざシリアライズしなくてはいけないのはとっても非効率なので、このAPIはとっととcanvasの載っている全てのブラウザーで実装されてほしい。

canvasがまともに動かないレガシーなブラウザでは、uuCanvasを使えばよいだろう。まだ、試していないが...

まとめ

  • window.printでそのままページを印刷するのは厳しい
  • html2canvasはそれなりに動く
  • ブラウザーはすべからくHTMLCanvasElementのtoBlobを実装すべし

今回は駆け足で考えかつ実装しましたが、もっといい方法があれば教えて頂きたいです。

Chrome extensionでclipboardに文字列をコピー (2014/01/26時点)

なんか、やろうと思ってググったらみんな色々なこと書いてて何が正しいのかよくわからん!ムキー!ってなったので同じような人を救うために2014年1月時点での方法をメモ。あまり大した内容ではない。

manifest.json

現時点で必要なのは "clipboardRead" というpermission。以下は例。

{
    "manifest_version": 2,
    "name": "Are",
    "version": "1.0",
    "description": "Are, aredesu",
    "permissions": [
        "clipboardRead"
    ]
}

pasteの仕方

background page (event page) にて、以下のような関数を書いておいてstringを渡せばOK。 ちなみに、textAreaはdisplay:none;とかだとダメです。これには由縁がありそうだが、あまり深追いするメリットなさそうなので「表示はさせた上で要素を飛ばせば大丈夫」くらいの認識で済ませた。

    function saveToClipboard(str) {
        var textArea = document.createElement("textarea");
        textArea.style.cssText = "position:absolute;left:-100%";

        document.body.appendChild(textArea);

        textArea.value = str;
        textArea.select();
        document.execCommand("copy");

        document.body.removeChild(textArea);
    }

じゃあの。

そろそろcreateObjectURLについてひとこと言っておくか

と思ったんだけど、書こうとしていたことは下の記事におおむね書いてあったので俺が一言いう必要はなかった。

createObjectURLとは何か? - NullPointer's Blog

新しめのAPIでURLを期待されるような場面では、createObjectURLで得られるBlob URLが使えるかどうか調べてみることにしている。いわゆるInline Workerも、Blob URLの一つの応用例。

まあしかし、こういう比較的真面目な記事にブクマが全然ついてないあたり(以下略)という感じではある。