JavaScriptを圧縮・整形するコマンド作り
最近、ブックマークレットを書く時には、Closure Compilerをよく使う。Closure Compilerは、Googleが提供しているJavaScriptコードの圧縮・整形サービスの一つである。
圧縮といってもzip圧縮などとは違う。正確には、コンピュータが実行しやすいように最適化しているのだ。
- コメントや空白文字を削除したり、
- 使っていない関数を削除したり、
- 変数名や関数名を短縮したり、
- 最適化レベルによっては、関数の中身のコードを展開することもある。
以上の作業を機械的に行って、最適化されたコードを返してくれる。人間にとってはめちゃくちゃ読み難いコードだけど、コンピュータにとっては無駄のない、実行しやすいコードとなるのだ。
逆に、そのようなめちゃくちゃ読み難いワンライナーにインデントや改行を追加して、少しでも人間が解釈しやすいコードに整形することもできる。(短縮されてしまった変数名や関数名、展開されてしまった関数コードなどは元に戻らないが)他人の書いたブックマークレットを読む時に、とても重宝している。
ブックマークレットを作り始めると、いつも上記のWebサービスのページを開いて、コピー&ペーストを繰り返していた...。コピー&ペーストは偉大な発明である。しかし、頻繁に繰り返していると、いいかげん面倒になってきた。どうにかしたい。
コード比較
- 元のコード
javascript: (function(d,f,s){ s=d.createElement('script'); s.src='//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js'; s.onload=function(){f()};/* <---onload属性を追加 */ d.body.appendChild(s); f=function(){ /* 内部コード */ alert(typeof CryptoJS); alert(CryptoJS.AES.encrypt('hello','1234')); }; })(document)
javascript:(function(d,f,s){%20%20s=d.createElement('script');%20%20s.src='//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js';%20%20s.onload=function(){f()};/*%20<---onload属性を追加%20*/%20%20d.body.appendChild(s);%20%20f=function(){%20%20%20%20/*%20内部コード%20*/%20%20%20%20alert(typeof%20CryptoJS);%20%20%20%20alert(CryptoJS.AES.encrypt('hello','1234'));%20%20};})(document)
- Closure Compilerの最適化(WHITESPACE_ONLY)
javascript:(function(d,f,s){s=d.createElement("script");s.src="//crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js";s.onload=function(){f()};d.body.appendChild(s);f=function(){alert(typeof%20CryptoJS);alert(CryptoJS.AES.encrypt("hello","1234"))}})(document);
Closure CompilerのAPIを使う
Closure CompilerはWebページのGUIサービスだけでなく、Web APIも提供してくれている。
- その使い方は、けっこうシンプル。
- 以下コードはPythonなのだけど、
#!/usr/bin/python2.4 import httplib, urllib, sys # Define the parameters for the POST request and encode them in # a URL-safe format. params = urllib.urlencode([ ('js_code', sys.argv[1]), ('compilation_level', 'WHITESPACE_ONLY'), ('output_format', 'text'), ('output_info', 'compiled_code'), ]) # Always use the following value for the Content-type header. headers = { "Content-type": "application/x-www-form-urlencoded" } conn = httplib.HTTPConnection('closure-compiler.appspot.com') conn.request('POST', '/compile', params, headers) response = conn.getresponse() data = response.read() print data conn.close()Communicating with the Closure Compiler Service API | Closure Compiler | Google Developers
- Ruby育ちなので書き直してみた。
#!/usr/bin/ruby require 'net/http' require 'uri' # Define the parameters for the POST request and encode them in # a URL-safe format. params = {js_code:ARGV[0], compilation_level:'WHITESPACE_ONLY', output_format:'text', output_info:'compiled_code'} # Always use the following value for the Content-type header. url = URI('http://closure-compiler.appspot.com/compile') response = Net::HTTP.post_form(url, params) data = response.body puts data
- さっそくテストしてみると、うまく動いている感じ!
$ ruby compile.rb 'alert("hello");// This comment should be stripped'
alert("hello");
コマンドにする
- RubyからClosure Compilerを利用する仕組みを理解できたので、
- あとはコマンドらしく、オプション指定できるようにすればいいのだ!
- コマンドの作り方について、以前調べたことがある。
- オプション解析の機能を追加して、以下のようなコードを書いてみた。
#!/usr/bin/ruby require 'optparse' require 'net/http' require 'uri' compilation_level_names = Hash.new{|h,k| k} compilation_level_names.merge!({'1'=>'WHITESPACE_ONLY', '2'=>'SIMPLE_OPTIMIZATIONS', '3'=>'ADVANCED_OPTIMIZATIONS'}) option_hash = {} OptionParser.new do |opt| opt.on('-l','--compilation_level=STR|NUM', 'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)', '1 | 2 | 3 (Default:1 )') {|v| option_hash[:compilation_level] = compilation_level_names[v]} opt.on('--output_format=STR', 'text | xml | json (Default:text)') {|v| option_hash[:output_format] = v } opt.on('--pretty_print', 'Add new line and indent for readable code.') {|v| option_hash[:formatting] = 'pretty_print'} opt.on('Example:', ' cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print', ' cat FILE_PATH | js-compile.rb -l1 --pretty_print', ' cat FILE_PATH | js-compile.rb --pretty_print', ' js-compile.rb --pretty_print "`cat FILE_PATH`"', 'The above commands output same compiled codes.') opt.parse!(ARGV) end #p option_hash url = URI('http://closure-compiler.appspot.com/compile') input = URI.decode(ARGV[0] || STDIN.gets(nil)) params = {js_code:input, compilation_level:'WHITESPACE_ONLY', output_format:'text', output_info:'compiled_code'} res = Net::HTTP.post_form(url, params.merge(option_hash)) puts res.body STDERR.puts "", "Before: #{input.length}", "After : #{res.body.length}", "Rate : #{res.body.length.to_f / input.length.to_f * 100}"
- 以上のコードをjs-compile.rbとして保存した。
- 実行権限を追加しておいた。
$ chmod a+x js-compile.rb
optparseの使い方の新発見(自分の中で)
- 1文字オプションとロングオプションは同時に設定できる。
- ロングオプション側で引数設定しておけば、1文字オプション側の引数設定は不要になる。
- 同様に、1文字オプション側で引数設定しておけば、ロングオプション側の引数設定は不要になる。
- 4番目以降の文字列は、オプション説明項目の2行目、3行目となる。
opt.on('-l', '--long=NUM', 'オプションの説明1行目', 'オプションの説明2行目', ...){処理}
- オプション文字を書かなければ、単なるhelp解説になる。
- opt.on内のすべてのテキスト先頭が-で始まらなければ、
- それはhelpの時インデントなしで表示される解説となる。
opt.on('Example:', ' cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print', ' cat FILE_PATH | js-compile.rb -l1 --pretty_print', ' cat FILE_PATH | js-compile.rb --pretty_print', ' js-compile.rb --pretty_print "`cat FILE_PATH`"', 'The above commands output same compiled codes.')
- 実際にヘルプ表示してみると、こんな感じの出力になる。
$ js-compile.rb -h Usage: js-compile [options] -l, --compilation_level=STR|NUM WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY) 1 | 2 | 3 (Default:1 ) --output_format=STR text | xml | json (Default:text) --pretty_print Add new line and indent for readable code. Example: cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print cat FILE_PATH | js-compile.rb -l1 --pretty_print cat FILE_PATH | js-compile.rb --pretty_print js-compile.rb --pretty_print "`cat FILE_PATH`" The above commands output same compiled codes.
以上は、断片的な発見である。
- 本来はマニュアルを精読すれば、ちゃんと書いてあるはず。
- そう思って読み直してみると、さらに便利な使い方がいろいろ書いてある。
- 後日、optparserの使い方をもっと詳しく調べ直す予定だ。
JavaScriptコードを引数だけでなく、標準入力からも受け取れるようにした
input = URI.decode(ARGV[0] || STDIN.gets(nil))
- opt.parse!(ARGV)を実行すると、ARGVの配列からオプションがすべて取り除かれる。
- ARGV[0]には、オプションを取り除いた後の第1引数が代入されている。
- 標準入力からテキストデータ全体は、STDIN.gets(nil))で取得できる。
- URIデコードしておくことで、ブックマークレット内のURIエンコードされたJavaScriptでも正常に処理できるようにした。
コードの圧縮率などの付加情報を標準エラーに出力するようにした。
- STDERR.putsによって、標準エラーへの出力となる。
STDERR.puts "", "Before: #{input.length}", "After : #{res.body.length}", "Rate : #{res.body.length.to_f / input.length.to_f * 100}"
- こうしておくことで、標準出力にはJavaScriptコードのみが出力されるので、パイプやリダイレクトでコードのみを取り出せるのだ。
使い方
- 不要な空白文字のみ取り除く最適化をする。
$ cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY
- ロングオプション--compilation_levelは、1文字オプション-lと同じ。
- また、3つのcompilation_levelは、1から3の番号に対応している。
1 | 2 | 3 |
---|---|---|
WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS |
- よって、以下のコードでも、不要な空白文字のみ取り除く最適化となる。
$ cat FILE_PATH | js-compile.rb -l1
- そもそもWHITESPACE_ONLYはデフォルト設定なので、オプションなしでも不要な空白文字のみ取り除く最適化となる。
$ cat FILE_PATH | js-compile.rb
- よく使うのは、SIMPLE_OPTIMIZATIONSまで。
$ cat FILE_PATH | js-compile.rb -l2
pbcopy・pbpasteとの組み合わせ
- コピーしたJavaScriptコードを最適化して、クリップボードに保存する。
$ pbpaste | js-compile.rb | pbcopy
- コピーしたJavascriptコードを整形表示する。
$ pbpaste | js-compile.rb --pretty_print
改行すべてを削除して1行にしたい時
$ cat FILE_PATH | js-compile.rb | perl -pe 's/\n//g'