- 发布于
sourceMap的原理
- Authors
- Name
- 田中原
什么是sourceMap?
sourceMap的主要作用是为了方便调试,因为现在的前端代码都是模块化、组件化的,在上线前会对js和css文件进行合并、压缩混淆,如果对这样的线上代码进行调试,肯定痛苦万分,sourceMap的作用就是能让浏览器的调试面板将生成后的代码映射到源码文件中,开发者可以在源码文件中debug。简单的说,sourceMap就是一个文件,里面储存着位置信息。它跟构建后的文件同在一个目录下。
sourceMap的配置项
配置项其实只是五个关键字eval、source-map、cheap、module和inline
关键字 | 含义 |
---|---|
source-map | 产生.map文件 |
eval | 使用eval包裹模块代码 |
inline | 将.map作为DataURI嵌入,不单独生成.map文件 |
cheap | 不包含列信息,也不包含loader的sourcemap |
module | 包含loader的sourcemap(比如jsx to js),否则无法定位源文件 |
source-map
src/index.js
let a = 1
let b = 2
let c = 3
dist/bundle.js
;({
'./src/index.js': function (module, exports) {
var a = 1
var b = 2
var c = 3
},
})
//# sourceMappingURL=bundle.js.map
dist/bundle.js.map
{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./src/index.js"],"names":["a","b","c"],"mappings":";QAAA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;;QAEA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;;;QAGA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA,0CAA0C,gCAAgC;QAC1E;QACA;;QAEA;QACA;QACA;QACA,wDAAwD,kBAAkB;QAC1E;QACA,iDAAiD,cAAc;QAC/D;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,yCAAyC,iCAAiC;QAC1E,gHAAgH,mBAAmB,EAAE;QACrI;QACA;;QAEA;QACA;QACA;QACA,2BAA2B,0BAA0B,EAAE;QACvD,iCAAiC,eAAe;QAChD;QACA;QACA;;QAEA;QACA,sDAAsD,+DAA+D;;QAErH;QACA;;;QAGA;QACA;;;;;;;;;;;;AClFA,IAAIA,CAAC,GAAG,CAAR;AACA,IAAIC,CAAC,GAAG,CAAR;AACA,IAAIC,CAAC,GAAG,CAAR,C","file":"bundle.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, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\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 = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./src/index.js\");\n","let a = 1;\nlet b = 2;\nlet c = 3;"],"sourceRoot":""}
总结:会生成对应的.map文件
eval-source-map
src/index.js
let a = 1
let b = 2
let c = 3
dist/bundle.js
"./src/index.js":
(function(module, exports) {
eval("var a = 1;\nvar b = 2;\nvar c = 3;//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXguanM/YjYzNSJdLCJuYW1lcyI6WyJhIiwiYiIsImMiXSwibWFwcGluZ3MiOiJBQUFBLElBQUlBLENBQUMsR0FBRyxDQUFSO0FBQ0EsSUFBSUMsQ0FBQyxHQUFHLENBQVI7QUFDQSxJQUFJQyxDQUFDLEdBQUcsQ0FBUiIsImZpbGUiOiIuL3NyYy9pbmRleC5qcy5qcyIsInNvdXJjZXNDb250ZW50IjpbImxldCBhID0gMTtcbmxldCBiID0gMjtcbmxldCBjID0gMzsiXSwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///./src/index.js\n");
})
});
总结:不会单独生成.map文件,而是把源代码用eval包裹起来。sourceURL=[module]来源是一个模块,sourceMappingURL把映射文件变成一个base64字符串放到代码内部了。官方比较推荐开发场景下使用eval的构建模式,因为它能cache sourceMap,从而rebuild的速度会比较快。
inline-source-map
src/index.js
let a = 1
let b = 2
let c = 3
dist/bundle.js
"./src/index.js":
(function(module, exports) {
var a = 1;
var b = 2;
var c = 3;
})
});
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vLy4vc3JjL2luZGV4LmpzIl0sIm5hbWVzIjpbImEiLCJiIiwiYyJdLCJtYXBwaW5ncyI6IjtRQUFBO1FBQ0E7O1FBRUE7UUFDQTs7UUFFQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTs7UUFFQTtRQUNBOztRQUVBO1FBQ0E7O1FBRUE7UUFDQTtRQUNBOzs7UUFHQTtRQUNBOztRQUVBO1FBQ0E7O1FBRUE7UUFDQTtRQUNBO1FBQ0EsMENBQTBDLGdDQUFnQztRQUMxRTtRQUNBOztRQUVBO1FBQ0E7UUFDQTtRQUNBLHdEQUF3RCxrQkFBa0I7UUFDMUU7UUFDQSxpREFBaUQsY0FBYztRQUMvRDs7UUFFQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0EseUNBQXlDLGlDQUFpQztRQUMxRSxnSEFBZ0gsbUJBQW1CLEVBQUU7UUFDckk7UUFDQTs7UUFFQTtRQUNBO1FBQ0E7UUFDQSwyQkFBMkIsMEJBQTBCLEVBQUU7UUFDdkQsaUNBQWlDLGVBQWU7UUFDaEQ7UUFDQTtRQUNBOztRQUVBO1FBQ0Esc0RBQXNELCtEQUErRDs7UUFFckg7UUFDQTs7O1FBR0E7UUFDQTs7Ozs7Ozs7Ozs7O0FDbEZBLElBQUlBLENBQUMsR0FBRyxDQUFSO0FBQ0EsSUFBSUMsQ0FBQyxHQUFHLENBQVI7QUFDQSxJQUFJQyxDQUFDLEdBQUcsQ0FBUixDIiwiZmlsZSI6ImJ1bmRsZS5qcyIsInNvdXJjZXNDb250ZW50IjpbIiBcdC8vIFRoZSBtb2R1bGUgY2FjaGVcbiBcdHZhciBpbnN0YWxsZWRNb2R1bGVzID0ge307XG5cbiBcdC8vIFRoZSByZXF1aXJlIGZ1bmN0aW9uXG4gXHRmdW5jdGlvbiBfX3dlYnBhY2tfcmVxdWlyZV9fKG1vZHVsZUlkKSB7XG5cbiBcdFx0Ly8gQ2hlY2sgaWYgbW9kdWxlIGlzIGluIGNhY2hlXG4gXHRcdGlmKGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdKSB7XG4gXHRcdFx0cmV0dXJuIGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdLmV4cG9ydHM7XG4gXHRcdH1cbiBcdFx0Ly8gQ3JlYXRlIGEgbmV3IG1vZHVsZSAoYW5kIHB1dCBpdCBpbnRvIHRoZSBjYWNoZSlcbiBcdFx0dmFyIG1vZHVsZSA9IGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdID0ge1xuIFx0XHRcdGk6IG1vZHVsZUlkLFxuIFx0XHRcdGw6IGZhbHNlLFxuIFx0XHRcdGV4cG9ydHM6IHt9XG4gXHRcdH07XG5cbiBcdFx0Ly8gRXhlY3V0ZSB0aGUgbW9kdWxlIGZ1bmN0aW9uXG4gXHRcdG1vZHVsZXNbbW9kdWxlSWRdLmNhbGwobW9kdWxlLmV4cG9ydHMsIG1vZHVsZSwgbW9kdWxlLmV4cG9ydHMsIF9fd2VicGFja19yZXF1aXJlX18pO1xuXG4gXHRcdC8vIEZsYWcgdGhlIG1vZHVsZSBhcyBsb2FkZWRcbiBcdFx0bW9kdWxlLmwgPSB0cnVlO1xuXG4gXHRcdC8vIFJldHVybiB0aGUgZXhwb3J0cyBvZiB0aGUgbW9kdWxlXG4gXHRcdHJldHVybiBtb2R1bGUuZXhwb3J0cztcbiBcdH1cblxuXG4gXHQvLyBleHBvc2UgdGhlIG1vZHVsZXMgb2JqZWN0IChfX3dlYnBhY2tfbW9kdWxlc19fKVxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5tID0gbW9kdWxlcztcblxuIFx0Ly8gZXhwb3NlIHRoZSBtb2R1bGUgY2FjaGVcbiBcdF9fd2VicGFja19yZXF1aXJlX18uYyA9IGluc3RhbGxlZE1vZHVsZXM7XG5cbiBcdC8vIGRlZmluZSBnZXR0ZXIgZnVuY3Rpb24gZm9yIGhhcm1vbnkgZXhwb3J0c1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5kID0gZnVuY3Rpb24oZXhwb3J0cywgbmFtZSwgZ2V0dGVyKSB7XG4gXHRcdGlmKCFfX3dlYnBhY2tfcmVxdWlyZV9fLm8oZXhwb3J0cywgbmFtZSkpIHtcbiBcdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgbmFtZSwgeyBlbnVtZXJhYmxlOiB0cnVlLCBnZXQ6IGdldHRlciB9KTtcbiBcdFx0fVxuIFx0fTtcblxuIFx0Ly8gZGVmaW5lIF9fZXNNb2R1bGUgb24gZXhwb3J0c1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5yID0gZnVuY3Rpb24oZXhwb3J0cykge1xuIFx0XHRpZih0eXBlb2YgU3ltYm9sICE9PSAndW5kZWZpbmVkJyAmJiBTeW1ib2wudG9TdHJpbmdUYWcpIHtcbiBcdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgU3ltYm9sLnRvU3RyaW5nVGFnLCB7IHZhbHVlOiAnTW9kdWxlJyB9KTtcbiBcdFx0fVxuIFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7IHZhbHVlOiB0cnVlIH0pO1xuIFx0fTtcblxuIFx0Ly8gY3JlYXRlIGEgZmFrZSBuYW1lc3BhY2Ugb2JqZWN0XG4gXHQvLyBtb2RlICYgMTogdmFsdWUgaXMgYSBtb2R1bGUgaWQsIHJlcXVpcmUgaXRcbiBcdC8vIG1vZGUgJiAyOiBtZXJnZSBhbGwgcHJvcGVydGllcyBvZiB2YWx1ZSBpbnRvIHRoZSBuc1xuIFx0Ly8gbW9kZSAmIDQ6IHJldHVybiB2YWx1ZSB3aGVuIGFscmVhZHkgbnMgb2JqZWN0XG4gXHQvLyBtb2RlICYgOHwxOiBiZWhhdmUgbGlrZSByZXF1aXJlXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLnQgPSBmdW5jdGlvbih2YWx1ZSwgbW9kZSkge1xuIFx0XHRpZihtb2RlICYgMSkgdmFsdWUgPSBfX3dlYnBhY2tfcmVxdWlyZV9fKHZhbHVlKTtcbiBcdFx0aWYobW9kZSAmIDgpIHJldHVybiB2YWx1ZTtcbiBcdFx0aWYoKG1vZGUgJiA0KSAmJiB0eXBlb2YgdmFsdWUgPT09ICdvYmplY3QnICYmIHZhbHVlICYmIHZhbHVlLl9fZXNNb2R1bGUpIHJldHVybiB2YWx1ZTtcbiBcdFx0dmFyIG5zID0gT2JqZWN0LmNyZWF0ZShudWxsKTtcbiBcdFx0X193ZWJwYWNrX3JlcXVpcmVfXy5yKG5zKTtcbiBcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KG5zLCAnZGVmYXVsdCcsIHsgZW51bWVyYWJsZTogdHJ1ZSwgdmFsdWU6IHZhbHVlIH0pO1xuIFx0XHRpZihtb2RlICYgMiAmJiB0eXBlb2YgdmFsdWUgIT0gJ3N0cmluZycpIGZvcih2YXIga2V5IGluIHZhbHVlKSBfX3dlYnBhY2tfcmVxdWlyZV9fLmQobnMsIGtleSwgZnVuY3Rpb24oa2V5KSB7IHJldHVybiB2YWx1ZVtrZXldOyB9LmJpbmQobnVsbCwga2V5KSk7XG4gXHRcdHJldHVybiBucztcbiBcdH07XG5cbiBcdC8vIGdldERlZmF1bHRFeHBvcnQgZnVuY3Rpb24gZm9yIGNvbXBhdGliaWxpdHkgd2l0aCBub24taGFybW9ueSBtb2R1bGVzXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLm4gPSBmdW5jdGlvbihtb2R1bGUpIHtcbiBcdFx0dmFyIGdldHRlciA9IG1vZHVsZSAmJiBtb2R1bGUuX19lc01vZHVsZSA/XG4gXHRcdFx0ZnVuY3Rpb24gZ2V0RGVmYXVsdCgpIHsgcmV0dXJuIG1vZHVsZVsnZGVmYXVsdCddOyB9IDpcbiBcdFx0XHRmdW5jdGlvbiBnZXRNb2R1bGVFeHBvcnRzKCkgeyByZXR1cm4gbW9kdWxlOyB9O1xuIFx0XHRfX3dlYnBhY2tfcmVxdWlyZV9fLmQoZ2V0dGVyLCAnYScsIGdldHRlcik7XG4gXHRcdHJldHVybiBnZXR0ZXI7XG4gXHR9O1xuXG4gXHQvLyBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGxcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubyA9IGZ1bmN0aW9uKG9iamVjdCwgcHJvcGVydHkpIHsgcmV0dXJuIE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbChvYmplY3QsIHByb3BlcnR5KTsgfTtcblxuIFx0Ly8gX193ZWJwYWNrX3B1YmxpY19wYXRoX19cbiBcdF9fd2VicGFja19yZXF1aXJlX18ucCA9IFwiXCI7XG5cblxuIFx0Ly8gTG9hZCBlbnRyeSBtb2R1bGUgYW5kIHJldHVybiBleHBvcnRzXG4gXHRyZXR1cm4gX193ZWJwYWNrX3JlcXVpcmVfXyhfX3dlYnBhY2tfcmVxdWlyZV9fLnMgPSBcIi4vc3JjL2luZGV4LmpzXCIpO1xuIiwibGV0IGEgPSAxO1xubGV0IGIgPSAyO1xubGV0IGMgPSAzOyJdLCJzb3VyY2VSb290IjoiIn0=
总结:将map作为DataURI嵌入,不单独生成.map文件。简单说,就是把source-map的内容内嵌到源代码bundle.js中。
cheap-source-map
src/index.js
let a = 1
let b = 2
let c = 3
dist/bundle.js
"./src/index.js":
(function(module, exports) {
var a = 1;
var b = 2;
var c = 3;
})
});
//# sourceMappingURL=bundle.js.map
dist/bundle.js.map
{"version":3,"file":"bundle.js","sources":["webpack:///webpack/bootstrap","webpack:///./src/index.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, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\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 = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./src/index.js\");\n","var a = 1;\nvar b = 2;\nvar c = 3;"],"mappings":";AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;AClFA;AACA;AACA;;;;A","sourceRoot":""}
总结:cheap(低开销)的sourcemap,因为它没有生成映射列,只是映射行数。
cheap-module-source-map
src/index.js
import { sum } from './title'
console.log(sum(1, 2))
src/title.js
let sum = (a, b) => a + b
debugger
export { sum }
总结:webpack会利用loader将所有非js模块转化为webpack可处理的js模块,而增加上面的cheap配置后也不会有loader模块之间对应的sourceMap。比如jsx文件会经历loader处理成js文件再混淆压缩,如果没有loader之间的sourceMap,在debug的时候定义到压缩前的js时,是不能追踪到jsx中的。所以为了映射到loader处理前的代码,一般会加上 module配置。
开发环境
我们在开发环境对sourceMap的要求是:快(eval)、信息全(module),且由于此时代码未压缩,我们并不那么在意代码列信息(cheap),所以开发环境比较推荐配置:cheap-module-eval-source-map。
生产环境
一般情况下,我们并不希望任何人都可以在浏览器直接看到我们未编译的源码,所以我们不应该直接提供sourceMap给浏览器。但我们又需要sourceMap来定位我们的错误信息,这时我们可以设置hidden-source-map。它不会为bundle.js添加引用注释,以避免浏览器使用。
关于.map文件中的mappings
compiler官方下载是java的一个编译包。
新建script.js
let a = 1
let b = 2
let c = 3
执行如下命令
java -jar compiler.jar --js script.js --create_source_map ./script-min.js.map --source_map_format=V3 --js_output_file script-min.js
执行后生成script-min.js
var a = 1,
b = 2,
c = 3
执行后生成script-min.js.map
{
"version":3,
"file":"script-min.js",
"lineCount":1,
"mappings":"AAAA,IAAIA,EAAI,CAAR,CACIC,EAAI,CADR,CAEIC,EAAI;",
"sources":["script.js"],
"names":["a","b","c"]
}
字段 | 含义 |
---|---|
version | sourceMap的版本,目前为3 |
file | 转换后的文件名 |
sourceRoot | 转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。 |
sources | 转换前的文件。该项是一个数组,表示可能存在多个文件合并。 |
names | 转换前的所有变量名和属性名 |
mappings | 记录位置信息的字符串 |
mappings属性
对应 | 含义 |
---|---|
第一层是行对应 | 以分号(;)表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。 |
第二层是位置对应 | 以逗号(,)表示,每个逗号对应转换后源码的一个位置。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。 |
第三层是位置转换 | 以VLQ编码表示,代表该位置对应的转换前的源码位置。 |
"mappings":"AAAA,IAAIA,EAAI,CAAR,CACIC,EAAI,CADR,CAEIC,EAAI;",
位置对应的原理
每个位置使用五位,表示五个字段
位置 | 含义 |
---|---|
第一位 | 表示这个位置在(转换后的代码的)的第几列 |
第二位 | 表示这个位置属于sources属性中的哪一个文件 |
第三位 | 表示这个位置属于转换前代码的第几行 |
第四位 | 表示这个位置属于转换前代码的第几列 |
第五位 | 表示这个位置属于names属性中的哪一个变量 |
如果某个位置是AAAAA,由于A在VLQ编码中表示0,因此这个位置的五个位实际上都是0。它的意思是,该位置在转换后代码的第0列,对应sources属性中第0个文件,属于转换前代码的第0行第0列,对应names属性中的第0个变量。
举例
假设现在有a.js
,内容为feel the force
,处理后为b.js
,内容为the force feel
以the
为例,它在输出中的位置是(0,0),a.js
是sources
的第1个(这里只是举例),输入中的位置是(0,5),the
是names
的第2个(这里只是举例)。
那么映射关系为: 0 1 0 5 2
最后将 01052 表示为 Base64 VLQ 即可。
说明
- 所有的值都是以0作为基数的
- 第五位不是必需的,如果该位置没有对应
names
属性中的变量,可以省略第五位 - 每一位都采用
VLQ
编码表示,由于VLQ
编码是可变长的,所以每一位可以由多个字符构成 - 为什么不保存转换后代码的行号,因为我们输出的文件总是一行,这样输出的行号就可以省略,因为都是0,没必要写出来
- 对于输出后的位置来说,到后边会发现它的列号特别大,为了避免这个问题,采用相对位置进行描述
相对位置是啥呢,看示意图:
第一次记录的输入位置和输出位置是绝对的,往后的输入位置和输出位置都是相对上一次的位置移动了多少,例如the
的输出位置为(0,-10),因为the
在feel
的左边数10下才能到这个位置。
VLQ编码
VLQ
是Variable-length quantity
的缩写,是一种通用的、使用任意位数的二进制来表示一个任意大的数字的一种编码方式。这种编码最早用于MIDI文件,后来被多种格式采用,它的特点就是可以非常精简地表示很大的数值,用来节省空间。
这种编码需要用最高位表示连续性,如果是1,代表这组字节后面的一组字节也属于同一个数;如果是0,表示该数值到这就结束了。
举例
对数值137进行
VLQ
编码
步骤 | 结果 |
---|---|
将137改写成二进制形式 | 10001001 |
七位一组做分组,不足的补0 | 0000001 0001001 |
最后一组开头补0,其余补1 | 10000001 00001001 |
所以,137的VLQ编码形式为10000001 00001001
代码实现
// 1.将137改写成二进制形式。 10001001
let binary = (137).toString(2)
console.log(binary) // 10001001
// 2.七位一组做分组,不足的补0。 0000001 0001001
let padded = binary.padStart(Math.ceil(binary.length / 7) * 7, '0')
console.log(padded) // 00000010001001
// 3.最后一组开头补0,其余补1。 10000001 00001001
let groups = padded.match(/\d{7}/g)
console.log(groups) // [ '0000001', '0001001' ]
groups = groups.map((item, index) => (index === groups.length - 1 ? '0' : '1') + item)
console.log(groups) // [ '10000001', '00001001' ]
let vlqCode = groups.join(' ')
console.log(vlqCode) // 10000001 00001001
Base64 VLQ
与一般的VLQ
的区别:
- 一个
Base64
字符只能表示6bit(2^6)
的数据 Base64 VLQ
需要能够表示负数,于是用最后一位来作为符号标志位(0为正,1为负)。- 由于只能用6位进行存储,而第一位表示是否连续的标志,最后一位表示正数/负数。中间只有4位,因此一个单元表示的范围为
[-15,15]
,如果超过了就要用连续标识位了。
举例
对数值137进行
Base64 VLQ
编码:
步骤 | 结果 |
---|---|
将137改写成二进制形式 | 10001001 |
127是正数,末位补0 | 100010010 |
五位一组做分组,不足的补0 | 01000 10010 |
将组倒序排序 | 10010 01000 |
最后一组开头补0,其余补1 | 110010 001000 |
转64进制 | y和I |
所以 137 通过Base64 VLQ
表示为yl
可以看出:
- 在
Base64 VLQ
中,编码顺序是从低位到高位 - 而在
VLQ
中,编码顺序是从高位到低位
代码实现
/**
* 任意一个数字转成一个base64格式的字符串
*/
let base64 = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'+',
'/',
]
function encode(num) {
// 1.将137改写成二进制形式 10001001 如果是负数的话还要转成绝对值
let binary = Math.abs(num).toString(2)
console.log(binary) // 10001001
// 2.137: 正数末尾补0 | 负数末尾补1
binary = num >= 0 ? binary + '0' : binary + '1'
// 3.五位一组做分组,不足的补0 01000 10010
let zero = 5 - (binary.length % 5) // 算出需要补多少个0
if (zero > 0) {
binary = binary.padStart(Math.ceil(binary.length / 5) * 5, '0')
}
// 4.将数组倒序排序 10010 01000
let parts = binary.match(/\d{5}/g)
parts.reverse()
console.log(parts) // [ '10010', '01000' ] 低位在前,高位在后
// 5.最后一组开头补0,其余补1 110010 001000 6个位
parts = parts.map((item, index) => (index === parts.length - 1 ? '0' : '1') + item)
console.log(parts) // [ '110010', '001000' ]
// 转成base64
let chars = []
for (let i = 0; i < parts.length; i++) {
let base64Index = parseInt(parts[i], 2) // 把二进制转10进制 6位 0-63
chars.push(base64[base64Index])
}
return chars.join('')
}
let result = encode(137)
console.log(result) // yI
sourceMap原理
计算位移
代码实现
let base64 = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'+',
'/',
]
function getValue(char) {
// 找到这个字符在base64里的索引
let index = base64.findIndex((item) => item === char) // 8
let str = index.toString(2) // 1000
str = str.padStart(6, '0') // 001000
let sign = str.slice(-1) === '0' ? 1 : -1
str = str.slice(1, -1) // 中间那4个bit的数才是真正的值
return parseInt(str, 2) * sign
}
// "AAAA,IAAIA,EAAI,CAAR,CACIC,EAAI,CADR,CAEIC,EAAI;"
let mappings = 'AAAA,IAAIA,EAAI,CAAR,CACIC,EAAI,CADR,CAEIC,EAAI'
function decode(values) {
let parts = values.split(',')
let positions = []
for (let i = 0; i < parts.length; i++) {
let part = parts[i]
let chars = part.split('') // [I,A,A,I,A]
let position = []
for (let j = 0; j < chars.length; j++) {
position.push(getValue(chars[j]))
}
positions.push(position)
}
return positions
}
let positions = decode(mappings)
console.log(positions)
// [转换前行, 转换前列, 转换后行, 转换后列]
let offsets = positions.map((item) => [item[2], item[3], 0, item[0]])
let origin = { row: 0, column: 0 }
let target = { row: 0, column: 0 }
let mapping = []
for (let i = 0; i < offsets.length; i++) {
let [originRowOffset, originColumnOffset, targetRowOffset, targetColumnOffset] = offsets[i]
origin.row += originRowOffset
origin.column += originColumnOffset
target.row += targetRowOffset
target.column += targetColumnOffset
mapping.push([`[${origin.row}, ${origin.column}] => [${target.row}, ${target.column}]`])
}
console.log(mapping)