让前端飞

debug 工具 —— source-map

2018-09-29  本文已影响30人  ebfc7d0362e4

一直都或多或少听说过 source-map,但却从未仔细学习过其具体内容。
谨以此篇文章,鞭策自己,砥砺前行。


一、why source map ?

随着前端的不断发展,前端技术栈中,JavaScript 所占比重越来越大,与之对应的在网络中传递的 JS 文件也随之变得越来越大,尤其是单页 web 应用(SPA)中打包出来的 JS 文件,往往更是会达到以 M 为单位的大小。为了提高用户体验,加快网络请求速度,作为开发者,我们不得不针对这一问题进行优化,常见的方式有 Gzip 压缩、合理利用缓存、Tree-shaking、文件(请求)合并、代码压缩等方式。其中,文件压缩、文件合并等操作则会导致线上代码与我们开发的代码呈现出一定的差异性,此外,文件/语言的编译(如 Less ->
CSS, TypeScript -> JavaScript)也会导致类似的结果,使得我们线上代码的难于维护和调试,于是 source map 便应运而生了。

二、source map 的适用场景

三、source map 的实际效果

首先,source map 功能需要浏览器的支持,较新的 firefox/chrome 的现代浏览器大都自动开启了 source map 功能,比如在 chrome 中,点击勾选上 Settings 里的 Enable JavaScript source maps 选项即可,较新版本的 chrome 是默认开启的。

step-1 step-2

然后,对于如下的一小段 js 源代码(请智能忽略掉一些细节):

(function () {
    var data = 1;
    function foo() {
        var data = 2;
        console.log(data);
        console.log(2 + foo2(data));
        return data + data;
    }
    function foo2(data) {
        return data + 2;
    }
    console.log(data);
    console.log(foo());
})()

总的来说,功能上还是比较简单的,但当它被 uglify 压缩之后,大致会变成如下的样子:

console.log(1),console.log((console.log(2),console.log(6),4));

你还能看出它的原本的样子和作用么?
就算使用了类似 chrome 中的 pretty print 之类的工具,也大致只能变成下面的样子:

pretty print 版本

跟源代码仍旧相去甚远。

而当我们打开 source map 处理之后的文件(一般存放在同目录下,chrome 中在其文件名后面添加了一个 [sm],应该是 source map 的缩写):

source map 版本

在这个虚拟出来的文件上,你可以正常地进行断点调试等任何工作,相比于压缩后的代码,这跟源文件基本无异的代码无疑方便很多。

四、怎么使用 source map ?

代码层面,要做的其实很少,只需要在编译之后的代码的最后,加上一行

//@ sourceMappingURL=/path/to/file.js.map

就可以了,但实际上,通常情况下,连这一步都是由编译打包工具完成的,常见的 gulp、webpack 等工具,里面都有支持 source map 的插件、中间件等。以 gulp 为例,我们就可以在其配置文件 gulpfile.js 中,增加 gulp-sourcemaps 插件相关配置,配合 gulp-uglify,我们可以轻松实现代码的压缩、以及 source map 支持。具体配置可以参考:点击这里,具体效果参考:点击这里。需要特别注意的一点是,gulp-sourcemaps 需要一定的 插件支持

五、source map 具体原理

source map,source 指源头、来源,而 map 意为映射、投影,其命名方式就已经基本说明了其具体原理——通过某种方式,将压缩、编译处理过的线上文件映射成文源文件。


source map

而 source map 主要分两个步骤:

  1. 在生成线上文件的同时,生成一个映射规则;
  2. 根据映射规则,将线上文件映射成源文件。
    第一步一般由开发者通过自己的某些工具(如 gulp/webpack)等生成一个 .map 文件:
{
    // source map的版本,目前为 3
    "version": 3,
    // 处理前的文件名数组(可能由多个文件进行合并)
    "sources": ["index.js"],
    // (可选)处理前的所有变量名和属性名
    "names": ["console", "log", "data"],
    // VLQ 编码的文件内容位置映射规则
    "mappings": "AAWIA,QAAQC,IAVG,GAWXD,QAAQC,KARJD,QAAQC,IADG,GAEXD,QAAQC,IAAI,GACLC",
    // 处理后的文件名
    "file": "index.js",
    // (可选)所有源文件的内容
    "sourcesContent": ["(function () {\n    var data = 1;\n    function foo() {\n        var data = 2;\n        console.log(data);\n        console.log(2 + foo2(data));\n        return data + data;\n    }\n    function foo2(data) {\n        return data + 2;\n    }\n    console.log(data);\n    console.log(foo());\n})()"]
}

不难看出,这其实就是一个 json 格式的文本文件。在线上文件的末尾添加上

//@ sourceMappingURL=/path/to/file.js.map

就指明了线上的这个文件,可以通过 /path/to/file.js.map 进行 source map 处理。


第二步,解析映射规则,并将线上文件通过它映射成源文件,这个操作一般由支持 source map 的浏览器来完成。

整个映射规则中,最重要的就是 mappings 属性。它是一个比较长的字符串,总的分为三层:

此外,每个位置使用五个字符,分别表示五个字段:

上面的每一位,同时都使用的 VLQ 编码,它使用的是与 base64 相同的码表。


base64 VLQ

它以 [A-Z]、[a-z]、[0-9] 以及 “+”、“/”来表示一个 64 进制数。

例如上面的“AAWIA”,如果以 10 进制数则可以表示成 “0, 0, 22, 8, 0”(逗号只是为了方便查看添加的,实际上不存在)。但事实上,我们知道,数据在计算机中,都是以二进制数存储和计算的,而一位 64 进制数,可以表示为 6 位 2 进制数,所以这里又可以按二级制数表示成“000000, 000000, 010110, 001000, 000000”。事实上,VLQ 也是按照二进制生效的。这 6 位 2 进制数中,

  Continuation
  |     Sign
  |     |
  V     V
  10101 1
demo

更多关于VLQ 编码的内容,请参考 wiki

上一篇下一篇

猜你喜欢

热点阅读