webpack源码分析之五:sourcemap
前言
经过webpack打包压缩之后的javascript, css文件和原有构建之前的文件相差比较大,对于开发者而言比较难在客户端上调试定位问题。为了解决这类问题,webpack引入了source maps。
source maps是一个存储了打包前所有代码的行,列信息的mapping文件,其核心的mapping属性以;,以及VLQ编码的字母记录着转换前的代码的位置。
本文重点不在sourcemap的算法实现。而是重点介绍的是基于github source-map之上,在webpack中的工程实现。
SourceNode是该库生成mapping文件的核心。所以先介绍SourceNode。
功能分析
SourceNode
它提供了一种抽象方法将含有行,列关系的多个代码片以插入或者连接的形式组合成含有这些代码的行,列信息的对象。
举个例子:
一个A.js 文件内容如下:
var a = 1; console.log(a);
调用SourceNode方法
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数据结构如下:
{ "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内的实现了。
功能实现
首先得抽象一个模块originalSource,它负责记录源代码的行,列信息,并通过这些信息结合源代码生成出mapping对象。
originalSource实现
行信息
将源码以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去深入探索
plugins实现
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)); }
对于这几个插件,实现思路如下
- 含有sourcemap信息的插件需要取source以及对应sourcemap(EvalDevToolModulePlugin不含sourcemap信息除外),eval模式每个module源码用eval包裹源码。
- 文件名生成。如需要将真实路径/Users/zhujian/Documents/workspace/webpack/webpack-demos-master/src/index.js转变为goc:///./src/index.js
- 文件底部信息WEBPACK FOOTER生成。展示信息有原始文件名,moduleId,chunkId。
SourceMapDevToolPlugin
整个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":"" }
EvalSourceMapDevToolPlugin
每个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");
EvalDevtoolModulePlugin
不包含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
(完)