クライアント側のコードを、パフォーマンスに影響を与えずに結合して圧縮した後でも、可読性に優れ、さらに重要なデバッグ可能性を維持したいと思ったことはありませんか?さて、これもソースマップの魔法です。
ソースマップは、結合または圧縮されたファイルをビルドされていない状態に戻すための方法です。本番環境向けにビルドする際に、JavaScript ファイルの圧縮や結合を行いながら、元のファイルに関する情報を保持するソースマップを生成します。生成された JavaScript で特定の行番号と列番号を照会すると、元の場所を返すソースマップを参照できます。デベロッパー ツール(現在は WebKit ナイトリー ビルド、Google Chrome、Firefox 23 以降)を使用すると、ソースマップを自動的に解析して、圧縮されていない組み合わせのファイルを実行しているように見せることができます。
このデモでは、生成されたソースを含むテキスト領域の任意の場所を右クリックして、[元の場所を取得] を選択すると、生成された行番号と列番号を渡してソースマップを照会し、元のコード内の位置を返します。出力を表示するためにコンソールが開いていることを確認します。
現実
ソースマップの実際の実装をご覧になる前に、Chrome Canary または WebKit のナイトリーでソースマップ機能が有効になっていることを確認してください。そのためには、[Dev Tools] パネルの設定の歯車をクリックして [Enable source maps] をオンにします。
Firefox 23 以降では、組み込みの開発ツールでソースマップがデフォルトで有効になっています。
ソースマップを重視すべき理由
現在、ソース マッピングは、非圧縮/組み合わせ JavaScript と圧縮/組み合わせない JavaScript の間でしか機能しませんが、CoffeeScript などの JavaScript にコンパイルされる言語に関する話題や、SASS や LESS などの CSS プリプロセッサのサポートが追加される可能性もあり、将来は明るい見通しです。
将来的には、ほぼすべての言語を、ブラウザでソースマップによってネイティブにサポートされているかのように、簡単に使用できるようになりました。
- CoffeeScript
- ECMAScript 6 以降
- SASS/LESS など
- JavaScript にコンパイルできるほぼすべての言語
次のスクリーンキャストでは、Firefox コンソールの試験運用版ビルドで CoffeeScript をデバッグしています。
最近、Google Web Toolkit(GWT)にソースマップのサポートが追加されました。GWT チームの Ray Cromwell 氏が、ソースマップのサポート動作を示す優れたスクリーンキャストを作成しました。
また、Google の Traceur ライブラリを使用して、ES6(ECMAScript 6 または Next)を記述し、ES3 互換コードにコンパイルできる別の例もまとめました。Traceur コンパイラはソースマップも生成します。こちらのデモをご覧ください。ソースマップのおかげで、ES6 のトレイトとクラスがブラウザでネイティブにサポートされているかのように使用されている様子をご覧いただけます。
デモのテキスト領域を使用すると、その場でコンパイルされ、ソースマップと同等の ES3 コードを生成する ES6 を記述することもできます。
デモ: ES6 を作成してデバッグし、ソース マッピングの動作を確認する
ソースマップの仕組み
現時点でソースマップの生成をサポートしている JavaScript コンパイラ/ミニファイアは、Closure コンパイラのみです。(使い方は後で説明します)。JavaScript を組み合わせて圧縮すると、ソースマップ ファイルが作成されます。
現在、Closure コンパイラは末尾に特別なコメントを追加していません。このコメントは、ソースマップが使用可能であることをブラウザの開発ツールに示すために必要なものです。
//# sourceMappingURL=/path/to/file.js.map
これによりデベロッパー ツールは、通話を元のソースファイルの場所にマッピングできます。以前のコメント プラグマは //@
でしたが、これに関するいくつかの問題と IE の条件付きコンパイル コメントにより、//#
に変更することが決定されました。現在、Chrome Canary、WebKit Nightly、Firefox 24 以降で新しいコメント プラグマがサポートされています。この構文変更は sourceURL にも影響します。
この奇妙なコメントが気に入らない場合は、代わりにコンパイル済みの JavaScript ファイルに特別なヘッダーを設定することもできます。
X-SourceMap: /path/to/file.js.map
コメントと同様に、これにより、JavaScript ファイルに関連付けられたソースマップを探す場所をソースマップのコンシューマーに伝えられます。このヘッダーは、単一行コメントをサポートしていない言語でソースマップを参照する問題も回避します。
ソースマップ ファイルは、ソースマップを有効にして開発ツールを開いている場合にのみダウンロードされます。また、元のファイルをアップロードして、必要に応じて開発ツールがそれらを参照、表示できるようにする必要があります。
ソースマップを生成するにはどうすればよいですか?
JavaScript ファイルのソースマップを圧縮、連結、生成するには、Closure コンパイラを使用する必要があります。コマンドは次のとおりです。
java -jar compiler.jar \
--js script.js \
--create_source_map ./script-min.js.map \
--source_map_format=V3 \
--js_output_file script-min.js
重要なコマンドフラグは --create_source_map
と --source_map_format
の 2 つです。デフォルトのバージョンは V2 であり、V3 のみを扱うことになるため、これは必須です。
ソースマップの構造
ソースマップについて理解を深めるために、Closure コンパイラによって生成されるソースマップ ファイルの例を取り上げ、「mappings」セクションの仕組みを詳しく解説します。次の例は、V3 仕様の例を少し変更したものです。
{
version : 3,
file: "out.js",
sourceRoot : "",
sources: ["foo.js", "bar.js"],
names: ["src", "maps", "are", "fun"],
mappings: "AAgBC,SAAQ,CAAEA"
}
ご覧のように、ソースマップは、ジューシーな情報を多数含むオブジェクト リテラルです。
- ソースマップのベースとなるバージョン番号
- 生成されたコードのファイル名(圧縮/結合した製品版ファイル)
- sourceRoot を使用すると、ソースの前にフォルダ構造を付加できる。これは、スペース節約の手法でもあります。
- source には、結合されたすべてのファイル名が含まれます。
- name には、コード全体にわたって使用するすべての変数/メソッド名が含まれます。
- 最後に、Mappings プロパティでは Base64 の VLQ 値を使用して、魔法のような動作を行います。実際のスペース節約はここで行われます。
Base64 の VLQ とソースマップを小さくする
もともと、ソースマップの仕様では、すべてのマッピングについて非常に詳細な出力が含まれていたため、ソースマップのサイズは生成されたコードの約 10 倍になっていました。バージョン 2 では約 50% 削減され、バージョン 3 ではさらに 50% 削減されました。したがって、133 KB のファイルの場合、最大 300 KB のソースマップになります。
では、複雑なマッピングを維持しながら、どのようにサイズを削減したのでしょうか。
VLQ(可変長数量)は、値を Base64 値にエンコードする場合に使用します。マッピング プロパティは非常に大きな文字列です。この文字列には、生成されたファイル内の行番号を表すセミコロン(;)が含まれます。各行には、その行内の各セグメントを表すカンマ(,)があります。これらの各セグメントは、可変長フィールドで 1、4、または 5 のいずれかです。一部は長く表示される場合がありますが、これらの要素には連続ビットが含まれます。各セグメントは直前のセグメントに基づいて構築されます。各ビットが直前のセグメントとの相対値になるため、ファイルサイズを縮小できます。
前述のように、各セグメントは、可変長で 1、4 または 5 であることができる。この図は、1 つの継続ビット(g)を持つ 4 の可変長と見なされます。このセグメントを細分化し、ソース地図が元の場所にどのように配置されているかを説明します。
上記の値は純粋に Base64 でデコードされた値であり、真の値を取得するために追加の処理が必要になります。各セグメントは通常、次の 5 つの要素で構成されます。
- 生成された列
- これが表示された元のファイル
- 元の行番号
- 元の列
- 元の名前(ある場合)
すべてのセグメントに名前、メソッド名、引数があるわけではないため、セグメント全体で 4 から 5 の可変長が切り替わります。上のセグメント図の g 値は、継続ビットと呼ばれるもので、これにより、Base64 VLQ デコード段階におけるさらなる最適化が可能になります。継続ビットを使用すると、セグメント値に基づいて、大きな数値を保存しなくても大きな数値を保存できます。これは、MIDI 形式に根ざした非常に優れたスペース節約手法です。
上の図 AAgBC
がさらに処理されると、0、0、32、16、1 が返されます。32 は継続ビットで、次の値 16 を作成できます。Base64 で純粋にデコードされた B は 1 です。したがって、使用される重要な値は 0、0、16、1 です。これにより、生成されたファイルの 1 行目(行はセミコロンで数えられる)が、ファイル 0(ファイルの配列は foo.js)の 16 行目にマッピングされていることがわかります。
セグメントのデコード状況を示すには、Mozilla の Source Map JavaScript ライブラリを参照してください。また、WebKit 開発ツールのソース マッピング コード(こちらも JavaScript で記述されています)
B から値 16 を取得する方法を適切に理解するには、ビット演算子とソース マッピングでの仕様がどのように機能するかについて基本的な理解が必要です。ビット単位の AND(&)演算子を使用して、1 桁の数字(32)と VLQ_CONTINUATION_BIT(バイナリ 100000 または 32)を比較することで、先行する数字 g が連続ビットとしてフラグされます。
32 & 32 = 32
// or
100000
|
|
V
100000
これは、両方が含まれる各ビット位置で 1 を返します。したがって、上の図でわかるように、32 ビットの場所しか共有しないため、Base64 でデコードされた 33 & 32
の値は 32 を返します。その後、先行する各連続ビットについて、ビットシフト値を 5 ずつ増やします。上記の例では 5 だけシフトしただけなので、1(B)を 5 だけ左にシフトします。
1 <<../ 5 // 32
// Shift the bit by 5 spots
______
| |
V V
100001 = 100000 = 32
その値はその後、数値(32)を 1 点右にシフトして VLQ の符号付き値から変換されます。
32 >> 1 // 16
//or
100000
|
|
V
010000 = 16
こうして 1 が 16 になります。このプロセスは複雑すぎるように思えるかもしれませんが、数字が大きくなるにつれ、より理にかなっています。
XSSI に関する潜在的な問題
この仕様では、ソースマップの使用により発生する可能性のある、クロスサイト スクリプトのインクルージョンに関する問題について説明しています。この問題を軽減するには、ソースマップの 1 行目の前に「)]}
」を付加し、JavaScript を意図的に無効にして構文エラーがスローされるようにすることをおすすめします。WebKit デベロッパー ツールはすでにこれを処理できます。
if (response.slice(0, 3) === ")]}") {
response = response.substring(response.indexOf('\n'));
}
上記のように、最初の 3 文字がスライスされ、仕様の構文エラーと一致するかどうかが確認されます。一致している場合は、最初の新しい行エンティティ(\n)までのすべての文字が削除されます。
sourceURL
と displayName
の実例: 評価関数と匿名関数
ソースマップの仕様には含まれていませんが、次の 2 つの規則を使用すると、eval 関数や匿名関数を使用する際に開発がはるかに容易になります。
1 つ目のヘルパーは //# sourceMappingURL
プロパティとよく似ており、実際にはソースマップ V3 の仕様で言及されています。コードに以下の特別なコメントを含めると、評価対象になります。eval に名前を付けると、デベロッパー ツールでより論理的な名前として表示されるようになります。CoffeeScript コンパイラを使用する簡単なデモをご覧ください。
デモ: eval()
のコードが sourceURL を介してスクリプトとして表示されるをご覧ください。
//# sourceURL=sqrt.coffee
もう 1 つのヘルパーでは、匿名関数の現在のコンテキストで利用可能な displayName
プロパティを使用して、匿名関数に名前を付けることができます。次のデモで、displayName
プロパティの動作を確認します。
btns[0].addEventListener("click", function(e) {
var fn = function() {
console.log("You clicked button number: 1");
};
fn.displayName = "Anonymous function of button 1";
return fn();
}, false);
デベロッパー ツール内でコードをプロファイリングすると、(anonymous)
のようなプロパティではなく、displayName
プロパティが表示されます。しかし、displayName は完全に廃止されたため、Chrome には導入されません。しかし、すべての希望を失うことはなく、debugName というより優れた提案が提案されています。
この記事の執筆時点で、eval の名前機能は Firefox ブラウザと WebKit ブラウザでのみ利用可能です。displayName
プロパティは WebKit ナイトリーにのみ存在します。
力を合わせましょう
現在、CoffeeScript へのソースマップのサポートについて、非常に長い議論が続いています。問題を確認して、CoffeeScript コンパイラにソースマップの生成を追加するためのサポートを追加してください。CoffeeScript とその熱心なファンにとって、これは大きな利点になるでしょう。
UglifyJS にはソースマップの問題もありますので、ぜひ確認してみてください。
Coffeescript コンパイラを含め、多くのtoolsがソースマップを生成します。今のところ、これは要点だと思っています。
ソース マップを生成できるツールが多いほど、Google の改善につながります。お気に入りのオープンソース プロジェクトに、ソース マップのサポートを依頼するか追加してください。
完璧ではない
現在のところ、ソースマップが対応していないことの一つは watch 式です。問題は、現在の実行コンテキスト内で引数や変数名を検査しても、実際には存在しないため何も返されないことです。この場合、検査する引数/変数の実名とコンパイル済み JavaScript 内の実際の引数/変数名を照合するために、なんらかのリバース マッピングが必要になります。
もちろん、これは解決可能な問題であり、ソースマップへの注目を高めることで、いくつかの優れた機能と安定性の向上を目にすることができます。
問題
最近、jQuery 1.9 は公式 CDN から提供される場合のソースマップのサポートを追加しました。また、jQuery が読み込まれる前に IE の条件付きコンパイル コメント(//@cc_on)が使用されると、独特なバグが指摘されました。その後、sourceMappingURL を複数行コメントでラップすることでこれを緩和するための commit が行われています。学習すべきレッスンでは、条件付きコメントを使用しないでください。
この問題は、構文を //#
に変更することで対処されました。
ツールとリソース
ご検討いただきたいリソースとツールをいくつかご紹介します。
- Nick Fitzgerald さんは、ソースマップをサポートする UglifyJS をフォークしています。
- Paul Irish はソースマップを紹介する便利なデモ
- リリースされたタイミングに関する WebKit の変更セットをご確認ください。
- チェンジセットにはレイアウト テストも含まれており、これがこの記事全体のきっかけとなりました。
- Mozilla には、組み込みのコンソールでソースマップのステータスを確認する必要があるバグがあります。
- Conrad Irwin は、すべての Ruby ユーザー向けに非常に便利なソースマップの gem を作成しました
- 評価の命名と displayName プロパティの詳細
- ソースマップの作成については、Closure Compilers ソースを確認できます
- いくつかのスクリーンショットがあり、GWT ソースマップのサポートについて説明しています。
ソースマップは、デベロッパー ツールセットに含まれる非常に強力なユーティリティです。ウェブアプリをシンプルに保ちつつ、簡単にデバッグできることは、非常に便利です。また、初心者にとっても非常にパワフルな学習ツールで、経験豊富な開発者が圧縮された判読不能なコードに煩わされることなく、アプリをどのように構成し、作成しているのかを確認できます。
迷っている時間はありません。すべてのプロジェクトのソースマップの生成を今すぐ開始しましょう。