統合して圧縮した後でも、パフォーマンスに影響を及ぼすことなく、クライアントサイド コードを読み取りやすく、さらに重要なデバッグ可能にしたいと思ったことはありませんか?ソースマップを活用できるようになりました。
ソースマップは、結合または圧縮されたファイルを未ビルドの状態にマッピングする方法です。本番環境用にビルドし、JavaScript ファイルを圧縮して結合すると、元のファイルに関する情報を保持するソースマップが生成されます。生成された JavaScript で特定の行番号と列番号をクエリする場合、元の位置を返すソースマップをルックアップできます。デベロッパー ツール(現時点では WebKit ナイトリー ビルド、Google Chrome、Firefox 23 以降)を使用すると、ソースマップが自動的に解析され、圧縮されていない、または結合されていないファイルを実行しているかのように表示することができます。
デモでは、生成されたソースを含むテキスト領域の任意の場所を右クリックします。[元の場所を取得] を選択します生成された行番号と列番号を渡してソースマップをクエリし、元のコードの位置を返します。出力を確認できるように、コンソールが開いていることを確認してください。
実世界
ソースマップの実際の実装を表示する前に、Chrome Canary または WebKit ナイトリーのいずれかでソースマップ機能が有効になっていることを確認してください。そのためには、デベロッパー ツール パネルの設定の歯車アイコンをクリックし、[ソースマップを有効にする] チェックボックスをオンにします。選択します。
Firefox 23 以降では、組み込みの開発ツールでソースマップがデフォルトで有効になっています。
ソースマップの重要性
現時点では、ソース マッピングは非圧縮/結合 JavaScript と圧縮/非結合 JavaScript の間でのみ機能しますが、CoffeeScript などの JavaScript にコンパイルされる言語や、SASS や LESS などの CSS プリプロセッサのサポートを追加する可能性について、明るい未来を見据えています。
将来的には、ほとんどすべての言語が、ソースマップを使用してブラウザでネイティブにサポートされているかのように簡単に使用できるようになります。
- CoffeeScript
- ECMAScript 6 以降
- SASS/LESS など
- JavaScript にコンパイルできるほぼすべての言語
Firefox コンソールの試験運用版ビルドでデバッグされる CoffeeScript のスクリーンキャストをご覧ください。
最近、Google Web Toolkit(GWT)にソースマップのサポートが追加されました。 GWT チームの Ray Cromwell が、実際のソースマップのサポートを示す素晴らしいスクリーンキャストを作成しました。
また、ES6(ECMAScript 6 または Next)を作成して ES3 互換コードにコンパイルできる、Google の Traceur ライブラリを使用する例もあります。Traceur コンパイラはソースマップも生成します。こちらのデモでは、ソースマップのおかげで、ブラウザでネイティブにサポートされているかのように使用されている ES6 トレイトとクラスを確認できます。
デモのテキスト領域では、その場でコンパイルされる ES6 を記述し、ソースマップと同等の ES3 コードを生成することもできます。
デモ: 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 コンパイラによって生成されるソースマップ ファイルの小さな例を取り上げ、「マッピング」がセクションが機能します。次の例は、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 値にエンコードするときに使用されます。mappings プロパティは非常に大きな文字列です。この文字列には、生成されたファイル内の行番号を表すセミコロン(;)があります。各行には、その行内の各セグメントを表すカンマ(,)があります。これらの各セグメントは、可変長フィールドでは 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 の Source Map JavaScript ライブラリを参照します。JavaScript で記述された WebKit 開発ツールのソース マッピング コードを見ることもできます。
B から値 16 を取得する方法を適切に理解するには、ビット演算子の基本と、ソース マッピングの仕様の仕組みを理解する必要があります。先行する数字 g は、ビット演算 AND(&)演算子を使用して数字(32)と VLQ_CONTINUATION_BIT(バイナリ 100000 または 32)を比較して、継続ビットとしてフラグを立てます。
32 & 32 = 32
// or
100000
|
|
V
100000
両方が出現するビット位置ごとに 1 が返されます。したがって、上の図に示すように、Base64 でデコードされた 33 & 32
の値は 32 を返します。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 仕様で言及されています。次の特別なコメントを eval 対象のコードに含めることで、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 はより良い結果を生み出します。お気に入りのオープンソース プロジェクトに、ソースマップのサポートを依頼するか、追加してください。
完全ではない
現時点では、ソースマップで対応していないのは watch 式です。問題は、現在の実行コンテキスト内で引数または変数名を検査しようとしても、実際には存在しないため、何も返されないことです。これには、コンパイルした JavaScript の実際の引数/変数名と比較して、検査する引数/変数の実名を検索するには、なんらかの逆マッピングが必要です。
もちろんこれは解決可能な問題であり、ソースマップへの注目が高まるにつれて、優れた機能と安定性の向上が見られるようになります。
問題
最近、jQuery 1.9 で、公式 CDN から提供されるソースマップのサポートが追加されました。また、jQuery が読み込まれる前に IE の条件付きコンパイル コメント(//@cc_on)が使用されるという特殊なバグも示されていました。その後、sourceMappingURL を複数行のコメントでラップすることで、この問題を軽減するための commit が行われています。学ぶ教訓では、条件付きコメントは使用しません。
この問題は、構文を //#
に変更することで対処されています。
ツールとリソース
以下のリソースやツールもあわせてご覧ください。
- Nick Fitzgerald がソースマップをサポートしている UglifyJS のフォークを作成
- Paul Irish がソースマップを紹介する便利なデモ
- この削除時期の WebKit 変更セットをご確認ください
- 変更セットには、この記事全体の基礎となるレイアウト テストも含まれていました。
- Mozilla にはバグがあるので、組み込みコンソールでソースマップのステータスを確認する必要があります。
- すべての Ruby ユーザーのために、Conrad Irwin が作成した非常に便利なソースマップの gem を作成しました。
- 評価の命名と displayName プロパティに関する追加情報
- ソースマップの作成については、Closure Compilers のソースをご覧ください。
- GWT ソースマップのサポートに関するスクリーンショットと説明
ソースマップは、デベロッパーのツールセットに含まれる非常に強力なユーティリティです。ウェブアプリを無駄のないものにしつつ、デバッグも簡単にできると非常に便利です。また、初心者向けの非常に強力な学習ツールで、圧縮された判読不能なコードを知らなくても、経験豊富なデベロッパーがアプリをどのように構造化し、記述しているかを理解できます。
迷っている時間はありません。すべてのプロジェクトのソースマップの生成を今すぐ始めましょう。