本文首發於個人博客( 點此查看),歡迎關注。
source map 是開發時調試代碼的利器之一。現代的構建工具如 webpack 早已對 source map 有了完備的支持,對照文檔就能很容易在打包時順手生成而後在現代瀏覽器如 Chrome/Firefox 中使用。關於相關配置的介紹使用已經有不少文章,這裏就再也不贅述。本文想探究的是 source map 在編譯器中的實現原理。javascript
首先對於 source map 還不是特別清楚其原理及使用方式的同窗能夠先看一下阮一峯老師對其的介紹。一句話總結就是 source map 是一種存儲了源代碼和編譯後代碼映射關係的信息文件。當你的編譯後代碼出現問題時,根據 source map 就能精準定位到源代碼對應的位置。不然,直接在天書通常的編譯後(加上可能壓縮後)代碼中進行調試,難度不小。html
source map 揭示了源代碼和處理後代碼之間的映射關係,而從源碼處處理後代碼的過程天然離不開編譯。一個典型的編譯過程以下:java
AST,即抽象語法樹,是源代碼語法結構的一種抽象表示。其以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構(來自維基百科解釋)。感興趣的同窗可訪問 https://astexplorer.net/ 查看代碼經 parser 處理爲 AST 的過程。在 AST 中,每一個節點都會保存本身的來源信息,以下所示:node
interface Node { type: string; loc: SourceLocation | null; } interface SourceLocation { source: string | null; start: Position; end: Position; } interface Position { line: uint32 >= 1; column: uint32 >= 0; }
每一個 Node 的 loc 屬性(視 parser 不一樣可能解析爲其餘名稱,如 traceur 將其解析爲 location)包含其 start 和 end 的行列位置信息。在 generate 環節,start 位置信息就是生成 source map 的關鍵。而一般 generator 不會本身去作映射關係的 VLQ 編碼(source map 的位置信息存儲方式),而是交由專業的庫來處理,好比 Mozilla 出品的 source-map。webpack
source-map 庫封裝了底層的映射關係計算的邏輯,在生成 source map 時向開發者提供了兩種類型的 API,一種是低級 API,其單純地經過向結果中插入源代碼和編譯後代碼的行列對應關係來生成 source map,官方示例以下:git
var map = new SourceMapGenerator({ file: "source-mapped.js" }); map.addMapping({ generated: { line: 10, column: 35 }, source: "foo.js", original: { line: 33, column: 2 }, name: "christopher" }); console.log(map.toString()); // '{"version":3,"file":"source-mapped.js","sources":["foo.js"],"names":["christopher"],"mappings":";;;;;;;;;mCAgCEA"}'
另外一種高級 API 則直接侵入了編譯過程。在 generate 步驟,source-map 提供了 SourceNode
用於在保留原有節點信息的同時添加該節點對應源代碼的行列信息。最後再借助 SourceNode
提供的 toStringWithSourceMap
方法同時輸出代碼和 source map。官方示例以下:github
function compile(ast) { switch (ast.type) { case 'BinaryExpression': return new SourceNode( ast.location.line, ast.location.column, ast.location.source, [compile(ast.left), " + ", compile(ast.right)] ); case 'Literal': return new SourceNode( ast.location.line, ast.location.column, ast.location.source, String(ast.value) ); // ... default: throw new Error("Bad AST"); } } var ast = parse("40 + 2", "add.js"); console.log(compile(ast).toStringWithSourceMap({ file: 'add.js' })); // { code: '40 + 2', // map: [object SourceMapGenerator] }
顯然,高級 API 對於 source-map 的依賴和耦合性比較高。不過筆者在探究各個 generator 對於 source map 的支持時發現兩種 API 均有使用。好比 @babel/generator 使用了低級 API,而 escodegen 則使用了高級 API。web
生成 source map 的原理並不複雜,使用 source-map 的低級 API 時, generator 的生成代碼是一個遍歷 AST node 而後根據其類型將對應的語句逐個拼裝的過程,這其中會維護生成代碼的行列信息,而在 node 中則保存有源代碼的位置信息,如此即可調用 source-map 的低級 API 去生成 source-map。而使用高級 API 的原理則更簡單,generator 處理好各個 node 對應生成的代碼語句,拿到 node 中的源位置信息,而後調用 new SourceNode()
和toStringWithSourceMap
交給 source-map 去處理和生成代碼和 srouce map 便可。npm
最後,回到 source-map 庫的實現上來。在其代碼庫的 lib/source-node.js
中咱們能夠看到,SourceNode 實例的 toStringWithSourceMap
方法本質上作的工做也無非就是將生成好的代碼片斷拼接起來並同時調用低級 API 來生成 source map。至於 VLQ 編碼的方式,源碼裏也有,讀者有興趣可結合原理自行查看。編程