クライアント側のコードを結合して圧縮した後も、パフォーマンスに影響を与えることなく、読みやすく、さらにはデバッグ可能な状態にしておきたいと思ったことはありませんか?ソースマップを使えば、その問題を解決できます。
ソースマップは、結合または圧縮されたファイルをビルド前状態にマッピングする方法です。本番環境用にビルドするときに、複数の JavaScript ファイルを最小化して組み合わせると同時に、元のファイルの情報を保持するソースマップを生成します。生成された JavaScript の特定の行番号と列番号をクエリすると、ソースマップ内の検索で元の場所が返されます。デベロッパー ツール(現在は WebKit ナイトリー ビルド、Google Chrome、Firefox 23 以降)では、ソースマップを自動的に解析し、未圧縮で未結合のファイルを実行しているように見せることができます。
このデモでは、生成されたソースを含むテキスト エリアの任意の場所を右クリックできます。[元の場所を取得] を選択すると、生成された行番号と列番号を渡してソースマップをクエリし、元のコード内の位置を返します。出力を確認できるように、コンソールが開いていることを確認します。
現実世界
ソースマップの実際の実装を以下で確認する前に、Chrome Canary または WebKit ナイトリーのいずれかでソースマップ機能を有効にしてください。DevTools パネルの設定の歯車アイコンをクリックし、[ソースマップを有効にする] オプションをオンにします。
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 のトレイトとクラスのデモをご覧ください。
デモの textarea では、ES6 を記述してその場でコンパイルし、ソースマップと同等の ES3 コードを生成することもできます。
デモ: ES6 を記述してデバッグし、ソースマッピングの動作を確認する
ソースマップの仕組み
現在、ソースマップの生成をサポートしている JavaScript コンパイラ/圧縮ツールは Closure Compiler のみです。(使用方法については後で説明します)。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 ファイルに関連付けるソースマップを検索する場所をソースマップ コンシューマに指示します。このヘッダーは、単一行コメントをサポートしない言語でソースマップを参照する場合の問題も回避できます。
ソースマップ ファイルは、ソースマップを有効にしてデベロッパー ツールを開いている場合にのみダウンロードされます。また、必要に応じてデベロッパー ツールが参照して表示できるように、元のファイルをアップロードする必要があります。
ソースマップを生成するにはどうすればよいですか?
Closure コンパイラを使用して、JavaScript ファイルの最小化、結合、ソースマップの生成を行う必要があります。コマンドは次のようになります。
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
です。これは、デフォルトのバージョンが 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 を使用すると、ソースの前にフォルダ構造を追加できます。これはスペースを節約する手法でもあります。
- sources には、結合されたすべてのファイル名が含まれます。
- names には、コード全体に表示されるすべての変数名とメソッド名が含まれます。
- 最後に、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 は、ファイル 0(ファイル 0 の配列は foo.js)の行 16 列 1 にマッピングされます。
セグメントがどのようにデコードされるかを示するために、Mozilla の ソースマップ JavaScript ライブラリを参照します。また、JavaScript で記述された WebKit デベロッパー ツールのソースマッピング コードも確認できます。
B から値 16 を取得する仕組みを正しく理解するには、ビット演算子と、ソースマッピングの仕様の仕組みを基本から理解する必要があります。先頭の桁 g は、ビット単位の AND 演算子(&)を使用して、桁(32)と VLQ_CONTINUATION_BIT(バイナリ 100000 または 32)を比較することで、連続ビットとしてフラグが立てられます。
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 に関する潜在的な問題
仕様では、ソースマップの使用によって発生する可能性のあるクロスサイト スクリプト挿入の問題について説明しています。この問題を軽減するには、ソースマップの最初の行に「)]}
」を追加して JavaScript を意図的に無効にし、構文エラーをスローすることをおすすめします。WebKit 開発ツールではすでにこの処理に対応しています。
if (response.slice(0, 3) === ")]}") {
response = response.substring(response.indexOf('\n'));
}
上記のように、最初の 3 文字がスライスされ、仕様の構文エラーと一致するかどうかが確認されます。一致する場合は、最初の改行エンティティ(\n)までのすべての文字が削除されます。
sourceURL
と displayName
の使用例: 評価関数と匿名関数
ソースマップの仕様には含まれていませんが、次の 2 つの規則を使用すると、eval と匿名関数を扱う際に開発がはるかに容易になります。
最初のヘルパーは //# sourceMappingURL
プロパティによく似ており、実際にはソースマップ V3 の仕様に記載されています。評価されるコードに次のような特殊なコメントを挿入することによって、よりわかりやすい名前として DevTools に表示されるように、eval に名前を付けることができます。CoffeeScript コンパイラを使用した簡単なデモを確認します。
デモ: sourceURL を介してスクリプトとして表示される eval()
コードを確認する
//# sourceURL=sqrt.coffee
他のヘルパーを使用すると、匿名関数の現在のコンテキストで使用可能な 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 コンパイラなど、多くのツールで生成できます。現時点では、この点は重要ではないと考えています。
ソースマップを生成できるツールが増えれば、Google にとってもメリットが大きくなります。お気に入りのオープンソース プロジェクトにソースマップ サポートをリクエストするか、追加してください。
完璧ではありません
ソースマップでは現在、ウォッチ式式に対応していません。問題は、現在の実行コンテキスト内で引数または変数名を検査しようとすると、実際には存在しないため何も返されないことです。これを行うには、コンパイル済み JavaScript の実際の引数/変数名と比較して、検査する引数/変数の実際の名前を検索する何らかの逆マッピングが必要です。
もちろん、これは解決可能な問題です。ソースマップにより注意を払うことで、優れた機能と安定性の向上を実現できます。
問題
先日、jQuery 1.9 で、公式 CDN から提供されるソースマップのサポートが追加されました。また、jQuery の読み込み前に IE 条件コンパイル コメント(//@cc_on)を使用すると、特有のバグが発生することも指摘されています。その後、sourceMappingURL を複数行コメントでラップすることで、この問題を軽減するcommit が行われました。条件付きコメントを使用しないことを学びました。
この問題は、構文を //#
に変更することで解決されました。
ツールとリソース
以下に、確認しておくべきその他のリソースとツールを示します。
- Nick Fitzgerald は、ソースマップ サポート付きの UglifyJS のフォークを作成しています。
- Paul Irish がソースマップを示す便利なデモを公開しています。
- この機能が削除されたときの WebKit の変更セットを確認する
- この変更セットには、この記事のきっかけとなったレイアウト テストも含まれています。
- Mozilla にはバグがあります。組み込みコンソールでソースマップのステータスを確認してください。
- Conrad Irwin が、すべての Ruby ユーザー向けに非常に便利なソースマップ ジェムを作成しました
- eval の命名と displayName プロパティに関するその他の参考資料
- ソースマップの作成については、Closure Compiler のソースをご覧ください。
- GWT ソースマップのサポートに関するスクリーンショットや言及がいくつかあります。
ソースマップは、デベロッパーのツールセットの中で非常に強力なユーティリティです。ウェブアプリをスリムなまま簡単にデバッグできるようにすることは非常に便利です。また、読みづらい圧縮コードを調べることなく、経験豊富なデベロッパーがアプリをどのように構造化して記述しているかを学ぶことができる、非常に強力な学習ツールでもあります。
迷っている時間はありません。すべてのプロジェクトのソースマップの生成を今すぐ開始しましょう。