通過webpack打包壓縮以後的javascript, css文件和原有構建以前的文件相差比較大,對於開發者而言比較難在客戶端上調試定位問題。爲了解決這類問題,webpack引入了source maps。javascript
source maps是一個存儲了打包前全部代碼的行,列信息的mapping文件,其核心的mapping屬性以;,以及VLQ編碼的字母記錄着轉換前的代碼的位置。css
本文重點不在sourcemap的算法實現。而是重點介紹的是基於github source-map之上,在webpack中的工程實現。
SourceNode是該庫生成mapping文件的核心。因此先介紹SourceNode。java
它提供了一種抽象方法將含有行,列關係的多個代碼片以插入或者鏈接的形式組合成含有這些代碼的行,列信息的對象。node
舉個例子:webpack
一個A.js 文件內容以下:git
var a = 1; console.log(a);
調用SourceNode方法github
const SourceNode = require("source-map").SourceNode; var node = new SourceNode(null, null, null, [ new SourceNode(1, 0, 'webpack:///./example/a.js', "var a = 1;\n"), '\n', new SourceNode(3, 0, 'webpack:///./example/a.js', 'console.log(a);"') ]); node.setSourceContent('index.js', 'var a = 1;\n\nconsole.log(a);') const t = node.toStringWithSourceMap({file: "index.js"}) const map = t.map.toJSON(); map.sourceRoot = '/'; console.log(JSON.stringify(map))
生成的mapping數據結構以下:web
{ "version":3, "sources":["webpack:///./example/a.js"], "names":[], "mappings":"AAAA;;AAEA", "file":"index.js", "sourcesContent":["var a = 1;\n\nconsole.log(a);"], "sourceRoot":"/" }
在chrome下的調試模式效果以下:算法
瞭解了SourceNode以後,往下接能夠講在webpack內的實現了。chrome
首先得抽象一個模塊originalSource,它負責記錄源代碼的行,列信息,並經過這些信息結合源代碼生成出mapping對象。
將源碼以n形式分割組合成數組,在經過數組索引來肯定行信息。
以上圖A.js爲例,表示3行信息的代碼以下:
new SourceNode(1, 0, 'webpack:///./example/a.js', "var a = 1;\n") //第1行 \n //第2行 new SourceNode(3, 0, 'webpack:///./example/a.js', 'console.log(a);"')//第3行
將行信息的代碼塊以(;,{,})進行分割組成數組後在經過每次分割的字符串長度來肯定列信息。
源碼
var a = 1;var b=2;{a=b}
生成soucemap代碼
new SourceNode(1, 0, 'webpack:///./example/a.js', "var a = 1;") //第0列 new SourceNode(1, 10, 'webpack:///./example/a.js', "var b=2;{") //第10列 new SourceNode(1, 19, 'webpack:///./example/a.js', "a=b}") //第19列
光標下標能夠在第10列上的效果以下:
故基於行,列信息生產mapping對象的核心代碼以下:
node(options) { const value = this._value; const name = this._name; const lines = value.split('\n'); const len = lines.length; const columns = options.columns; const node = new SourceNode(null, null, null, lines.map((line, idx) => { let i = 0; const content = idx !== len - 1 ? line + '\n' : line; if (/^\s*$/.test(content)) return content; if (columns === false) { return new SourceNode(idx + 1, 0, name, content); } return new SourceNode(null, null, null, _splitCode(content).map(item => { const result= new SourceNode(idx + 1, i, name, item); i = i + item.length; return result })) })); node.setSourceContent(name, value); return node; }
上圖A.js的在originalSource下的數據結構以下
SourceNode { children: [ SourceNode { children: [ { "children": [ "var a = 1;\n" ], "sourceContents": {}, "line": 1, "column": 0, "source": "/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js", "name": null, "$$$isSourceNode$$$": true } ], sourceContents: {}, line: null, column: null, source: null, name: null, '$$$isSourceNode$$$': true }, '\n', SourceNode { children: [ { "children": [ "console.log(a);\n" ], "sourceContents": {}, "line": 3, "column": 0, "source": "/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js", "name": null, "$$$isSourceNode$$$": true } ], sourceContents: {}, line: null, column: null, source: null, name: null, '$$$isSourceNode$$$': true } ], sourceContents: { '/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js': 'var a = 1;\n\nconsole.log(a);\n' }, line: null, column: null, source: null, name: null, '$$$isSourceNode$$$': true }
webpack其餘的source實例如cachedSource,prefixSource,concatSource,replaceSource除去一些本生特性以外,底層都是調用originalSource實現。
另外webpack在不展現columns的狀況下,優先使用source-map-list,這塊實現是參考mozilla的github source-map實現,並結合自身狀況,去優化生成sourcemap的性能。如大規模的字符串split使用遞歸indexOf和substr去替代。
proto.sourceAndMap = function(options) { options = options || {}; if(options.columns === false) { //console.log(this.listMap(options).debugInfo()); return this.listMap(options).toStringWithSourceMap({ file: "x" }); } const temp=this.node(options) var res = this.node(options).toStringWithSourceMap({ file: "x" }); return { source: res.code, map: res.map.toJSON() }; };
對這塊有興趣的能夠到webpack-sources去深刻探索
webpack經過SourceMapDevToolPlugin,EvalSourceMapDevToolPlugin,EvalDevToolModulePlugin這三個插件以及devtool對外輸出sourcemap。devtool本質也是indexOf截取不一樣的關鍵字而實例化不一樣的plugin類
if(options.devtool && (options.devtool.indexOf("sourcemap") >= 0 || options.devtool.indexOf("source-map") >= 0)) { ... comment = legacy && modern ? "\n/*\n//@ source" + "MappingURL=[url]\n//# source" + "MappingURL=[url]\n*/" : legacy ? "\n/*\n//@ source" + "MappingURL=[url]\n*/" : modern ? "\n//# source" + "MappingURL=[url]" : null; let Plugin = evalWrapped ? EvalSourceMapDevToolPlugin : SourceMapDevToolPlugin; compiler.apply(new Plugin({ filename: inline ? null : options.output.sourceMapFilename, moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate, fallbackModuleFilenameTemplate: options.output.devtoolFallbackModuleFilenameTemplate, append: hidden ? false : comment, module: moduleMaps ? true : cheap ? false : true, columns: cheap ? false : true, lineToLine: options.output.devtoolLineToLine, noSources: noSources, })); } else if(options.devtool && options.devtool.indexOf("eval") >= 0) { ... compiler.apply(new EvalDevToolModulePlugin(comment, options.output.devtoolModuleFilenameTemplate)); }
對於這幾個插件,實現思路以下
整個webpack的打包流程完畢compilation的after-optimize-chunk-asset階段的source做爲原始源碼。拼湊bundle文件底部map信息,修改map文件文件名。以devtool爲source-map爲例生成文件以下
bundle文件
(function(module, exports) { var a = 1; console.log(a); }) ]); //# sourceMappingURL=main.output.js.map
map文件
{ "version":3, "sources":["goc:///webpack/bootstrap 0ee5c00ca3b31f99a2e0?","goc:///./src/index.js?"], "names":[], "mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;AC7DA;;AAEA", "file":"main.output.js", "sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/dist/\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 0ee5c00ca3b31f99a2e0","var a = 1;\n\nconsole.log(a);\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = 0\n// module chunks = 0"], "sourceRoot":"" }
每一個module構建完成,moduleTemplate的module階段的source做爲原始源碼,以module id作爲文件名,將獲得的sourcemap以base64形式存儲在sourceMappingURL內,並緊跟在module源碼後面。bundle文件以下
eval("var a = 1;\n\nconsole.log(a);" + "\n//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImdvYzovLy8uL3NyYy9pbmRleC5qcz8iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7O0FBRUEiLCJmaWxlIjoiMC5qcyIsInNvdXJjZXNDb250ZW50IjpbInZhciBhID0gMTtcblxuY29uc29sZS5sb2coYSk7XG5cblxuXG4vLy8vLy8vLy8vLy8vLy8vLy9cbi8vIFdFQlBBQ0sgRk9PVEVSXG4vLyAuL3NyYy9pbmRleC5qc1xuLy8gbW9kdWxlIGlkID0gMFxuLy8gbW9kdWxlIGNodW5rcyA9IDAiXSwic291cmNlUm9vdCI6IiJ9" + "\n//# sourceURL=webpack-internal:///0\n");
不包含sourcemap信息,每一個module構建完成,moduleTemplate的module。用eval包裹,並拼湊底部信息,sourceURL信息。
bundle文件以下:
eval("var a = 1;\n\nconsole.log(a);" + "\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = 0\n// module chunks = 0\n" + "\n//# sourceURL=goc:///./src/index.js?" );
本人的簡易版webpack實現simple-webpack
(完)