sourceMap的原理

1,141
什么是sourceMap?

sourceMap的主要作用是为了方便调试,因为现在的前端代码都是模块化、组件化的,在上线前会对js和css文件进行合并、压缩混淆,如果对这样的线上代码进行调试,肯定痛苦万分,sourceMap的作用就是能让浏览器的调试面板将生成后的代码映射到源码文件中,开发者可以在源码文件中debug。简单的说,sourceMap就是一个文件,里面储存着位置信息。它跟构建后的文件同在一个目录下。


sourceMap的配置项

devtool.png

配置项其实只是五个关键字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

dist.png

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

eval-source-map.png

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

eval-source-map.png

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

source-map.png

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-source-map.png

source-map_v1.png

总结: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 }

flow.jpg

总结: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的一个编译包。

compiler.png

新建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:SourcesourceMap的版本,目前为3
file转换后的文件名
sourceRoot转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。
sources转换前的文件。该项是一个数组,表示可能存在多个文件合并。
names转换前的所有变量名和属性名
mappings记录位置信息的字符串

mappings属性

对应含义
第一层是行对应以分号(;)表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。
第二层是位置对应以逗号(,)表示,每个逗号对应转换后源码的一个位置。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。
第三层是位置转换以VLQ编码表示,代表该位置对应的转换前的源码位置。
"mappings":"AAAA,IAAIA,EAAI,CAAR,CACIC,EAAI,CADR,CAEIC,EAAI;",

base64vlq在线转换

位置对应的原理

每个位置使用五位,表示五个字段

位置含义
第一位表示这个位置在(转换后的代码的)的第几列
第二位表示这个位置属于sources属性中的哪一个文件
第三位表示这个位置属于转换前代码的第几行
第四位表示这个位置属于转换前代码的第几列
第五位表示这个位置属于names属性中的哪一个变量

如果某个位置是AAAAA,由于A在VLQ编码中表示0,因此这个位置的五个位实际上都是0。它的意思是,该位置在转换后代码的第0列,对应sources属性中第0个文件,属于转换前代码的第0行第0列,对应names属性中的第0个变量。


举例

假设现在有a.js,内容为feel the force,处理后为b.js,内容为the force feel

nxig1pq9o4.png

the为例,它在输出中的位置是(0,0),a.jssources的第1个(这里只是举例),输入中的位置是(0,5),thenames的第2个(这里只是举例)。

那么映射关系为: 0 1 0 5 2

最后将 01052 表示为 Base64 VLQ 即可。


说明

  • 所有的值都是以0作为基数的
  • 第五位不是必需的,如果该位置没有对应names属性中的变量,可以省略第五位
  • 每一位都采用VLQ编码表示,由于VLQ编码是可变长的,所以每一位可以由多个字符构成
  • 为什么不保存转换后代码的行号,因为我们输出的文件总是一行,这样输出的行号就可以省略,因为都是0,没必要写出来
  • 对于输出后的位置来说,到后边会发现它的列号特别大,为了避免这个问题,采用相对位置进行描述

相对位置是啥呢,看示意图:

lefr0nmayj.png

第一次记录的输入位置和输出位置是绝对的,往后的输入位置和输出位置都是相对上一次的位置移动了多少,例如the的输出位置为(0,-10),因为thefeel的左边数10下才能到这个位置。


VLQ编码

VLQVariable-length quantity 的缩写,是一种通用的、使用任意位数的二进制来表示一个任意大的数字的一种编码方式。这种编码最早用于MIDI文件,后来被多种格式采用,它的特点就是可以非常精简地表示很大的数值,用来节省空间。

这种编码需要用最高位表示连续性,如果是1,代表这组字节后面的一组字节也属于同一个数;如果是0,表示该数值到这就结束了。

举例

对数值137进行VLQ编码

步骤结果
将137改写成二进制形式10001001
七位一组做分组,不足的补00000001 0001001
最后一组开头补0,其余补110000001 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); // 1000000100001001

Base64 VLQ

与一般的VLQ的区别:

  • 一个Base64字符只能表示 6bit(2^6)的数据
  • Base64 VLQ需要能够表示负数,于是用最后一位来作为符号标志位。
  • 由于只能用6位进行存储,而第一位表示是否连续的标志,最后一位表示正数/负数。中间只有4位,因此一个单元表示的范围为[-15,15],如果超过了就要用连续标识位了。

表示正负的方式:

  • 如果这组数是某个数值的VLQ编码的第一组字节,那它的最后一位代表"符号",0为正,1为负;
  • 如果不是,这个位没有特殊含义,被算作数值的一部分。

base64.png

举例

对数值137进行Base64 VLQ编码:

步骤结果
将137改写成二进制形式10001001
127是正数,末位补0100010010
五位一组做分组,不足的补001000 10010
将组倒序排序10010 01000
最后一组开头补0,其余补1110010 001000
转64进制y和I

所以 137 通过Base64 VLQ表示为yI

可以看出:

  • 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原理

计算位移

sourcemapmove.png

代码实现

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);

参考文章:你知道source-map的原理是什么吗?