当前位置: 首页 > 科技观察

Webpack实战系列一:正确使用Sourcemap

时间:2023-03-11 20:36:25 科技观察

一、什么是Sourcemap?Sourcemap协议最初由Google设计并首先在ClosureInspector中实现。查明环境中出现问题的行和列。到目前为止,Sourcemap已经得到了Webpack、Rollup、Babel、Less、Typescript、Chrome、Safari、VSCode等工具的广泛支持。参考:https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k在实现上,Sourcemap由三部分组成:开发者编写的原始代码,通过Webpack等工程工具进行压缩、转换、合并和Rollup,产品中必须包含//#sourceMappingURL=https://xxxx/bundle.js.map指向Sourcemap文件地址,用于记录原代码与工程代码的位置映射关系。Map文件页面只会加载并编译构建产品,直到特定事件发生——例如Chrome打开Devtool面板时,会根据//#sourceMappingURL的内容自动加载Map文件,代码如下按照Sourcemap协议约定的映射规则重构回原来的形式。既能保证终端用户的性能体验,又能帮助开发者快速还原场景,提高在线问题定位调试效率。1.1示例以Webpack为例,设置devtool='source-map'同时打包代码产品xxx.js文件和同名xxx.js.map文件,Map文件一般为JSON格式,内容如下:{"version":3,"sources":["webpack:///./src/index.js"],"names":["name","console","log"]“映射”:“;;;;AAAA、IAAMA、IAAI、GAAG、QAAb;AAEAC、OAAO、CAACC、GAAR、CAAYF、IAAZ、E”,“文件”:“main.js”,“sourcesContent”:["constname='tecvan';\n\nconsole.log(name)"],"sourceRoot":""}各字段含义为:version:指sourcemap版本,最新版本为3names:astring数组,记录原始代码中出现的变量名文件:String,Sourcemap文件对应的编译产物文件名。sourcesContent:字符串数组,原始代码的内容。字符串数组,记录打包后的产品与原代码的位置映射关系。使用时,浏览器会根据mappings中记录的数值关系,将产品代码映射回sourcesContent数组中记录的原始代码文件、行列位置。这是最复杂的难点,就是mappings字段的规则。1.2Sourcemap和VLQSourcemap初始版本生成的.map文件非常大,大约是编译产品体积的10倍;V2引入base64编码等算法,降低20%~30%;而最新版本V3在V2的基础上引入了VLQ等算法,体积进一步压缩了50%。这一系列的演进创造了一个高效的Sourcemap系统,但伴随而来的是更复杂的mappings编码规则。1.2.1Mappings编码规则例如如下代码:当devtool='source-map'时,Webpack生成的mappings字段为:;;;;;AAAA,IAAMA,IAAI,GAAG,QAAb;AAEAC,OAAO,CAACC,GAAR,CAAYF,IAAZ,E字段内容包含三层结构:“线映射”由;分隔,各;对应编译后的产品每一行到源码的映射,上面的例子划分后:[//产品号1-5行的内容是Webpack生成的runtime,不需要记录映射关系'','','','','',//产品第6行的映射信息'AAAA,IAAMA,IAAI,GAAG,QAAb',//产品第7行的映射信息product'AAEAC,OAAO,CAACC,GAAR,CAAYF,IAAZ,E']用分隔的“段映射”,每个对应行中的每个代码段到源码的映射,上面的例子划分后:[//product第1-5行内容为Webpack生成的runtime,无需记录映射关系'','','','','',//product第6行映射信息[//将`var`分段到`const`映射'AAAA',//将`name`分段到`name`映射'IAAMA',//等等'IAAI','GAAG','QAAb'],//产品第7行的映射信息['AAEAC','OAAO','CAACC','GAAR','CAAYF','IAAZ','E']]第三层逻辑是片段映射到源码的具体位置,以上例IAAMA为例:第一个数字I中列出了代码片段product,第二位A代表源代码文件的索引,即片段索引到sources数组的元素下标第三位A代表片段在源代码中的行号第四位Mfile表示源码文件中分片的列号第5位A,表示分片对应的name索引,即分片索引到names数组的第一层和第二层以上的元素下标logic比较简单。唯一需要注意的是,片段之间存在相对偏移关系。例如,对于上例的第六行,映射值为:AAAA、IAAMA、IAAI、GAAG、QAAb,每个分片的第一位——即分片的列数为A、I、I、G,Q,分别代表:A:列A列I:列A+I列I:列A+I+IColumnG:ColumnA+I+I+GColumnQ:ColumnA+I+I+G+Q这种相对偏移可以减少Sourcemap积的大小,提高整体性能,同时第三层分片位置映射使用更高效的数值编码算法——VLQ(Variable-lengthQuantity)。1.2.2VLQ编码参考:https://en.wikipedia.org/wiki/Variable-length_quantityVLQ本质上是一种将整数值转换为Base64的编码算法。它首先将任意大整数转换成一系列的六位字节码,然后根据Base64规则转换成一串可见的字符。VLQ用6位来存储一个编码组,例如:数字7经过VLQ编码后,结果为001110,其中:第1位为连续标志位,标识后面的组是否为同一个数;第6位表示数字Symbol的符号,0为正整数,1为负整数;中间的2-5是实际值。这样一个六位编码组可以根据Base64的映射规则转换成ABC等可见字符。比如上面数字7的编码结果是001110,十进制等于14,根据Base64码表可以映射到字母O。但是组中只有中间4个字节用来表示值,所以单个组只能表示“-15~15”之间的取值范围。对于超出这个范围的整数,需要组合多个组来表示同一个数。组合规则:第一组最后一位为符号位,其他组为2-6的数字位。二进制值的最后四位为第一组值,然后从后往前,每5位为一个除最后一组外的一组,其余组的连续标志位全部置1。对于例如十进制-17,其二进制值为10001(取二进制值为17),共5位,先从后向前分成两组,后四位0001为第一组,连续标志位为1,符号位为1,结果为1,0001,1;剩下的1分配给第二组——最后一组,连续标志位为0,结果为0,00001。根据Base64规则[100011,000001]最终映射到jA。DecimalbinaryVLQBase64-17=>1,0001=>100011,000001=>jA同样对于更大的数字,比如1200,它的二进制是10010110000,分组为[10,01011,0000],从后向前编码,第一个分组为1,0000,0;第二组是1,01011;最后一组是0,00010。通过Base64映射为grC。DecimalbinaryVLQBase641200=>10;01011;0000=>100000,101011,000010=>grC1.2.3Decodingmappings结合VLQ编码知识,我们回过头来解读本章开头的例子。对于代码:编译并生成映射:;;;;;AAAA,IAAMA,IAAI,GAAG,QAAb;AAEAC,OAAO,CAACC,GAAR,CAAYF,IAAZ,E按照行分片规则划分,得到如下分片:[//内容product第1-5行是Webpack生成的runtime不需要记录映射关系'','','','','',//product第6行的映射信息['AAAA','IAAMA','IAAI','GAAG','QAAb'],//产品第7行的映射信息['AAEAC','OAAO','CAACC','GAAR','CAAYF','IAAZ','E']]以第6行结束['AAAA','IAAMA','IAAI','GAAG','QAAb']为例:AAAA的解码结果为[000000,000000,000000,000000],即乘积的第六行“第0列”映射到sources[0]文件的“第0行”和“第0列”实际上对应的是从var到const的位置映射。IAAMA解码结果为[001000,000000,000000,001100,000000],即乘积的第6行第4列映射到sources[0]文件的“第0行”和“第6列”,实际对应的产品名称从源代码名称的位置映射到其他片段,依此类推。2.使用SourcemapWebpack提供了两种设置Sourcemap的方式,一种是通过devtool配置项设置Sourcemap规则短语;另一种是直接使用SourceMapDevToolPlugin或EvalSourceMapDevToolPlugin插件深度定制Sourcemap的生成逻辑。先介绍一下比较晦涩的devtool配置项,了解一下Webpack提供的各种Sourcemap功能规则。2.1使用devtooldevtool支持25种字符串枚举值,包括eval、source-map、eval-source-map等,单独看特别晦涩,但是可以发现这些值都是由inline,eval,source-map,nosources,hidden,cheap,module由七个关键字组成,每个关键字代表一条Sourcemap规则。2.1.1eval当devtool值中包含eval时,生成的模块代码会被包装成一个eval函数,模块的sourcemap信息通过//#sourceURL直接挂载到模块代码中。例如:eval("varfoo='bar'\n\n\n//#sourceURL=webpack:///./src/index.ts?")eval方式的编译速度通常比较快,但是乘积直接包含Sourcemap信息,所以只推荐在开发环境中使用。2.1.2source-map当devtool包含source-map时,Webpack会生成Sourcemap内容。例如,对于devtool='source-map',该产品将生成额外的.map文件,例如:{"version":3,"sources":["webpack:///./src/index.ts"],"names":["console","log"],"mappings":"AACAA,QAAQC,IADI","file":"bundle.js","sourcesContent":["constfoo='bar';\nconsole.log(foo);"],"sourceRoot":""}其实除了eval之外的其他枚举值都包含这个字段。2.1.3cheap当devtool包含cheap时,生成的Sourcemap内容会丢弃“列”维度的信息,也就是说浏览器只能映射到代码行维度。例如,当devtool='cheap-source-map'时,产品:{"version":3,"file":"bundle.js","sources":["webpack:///bundle.js"],"sourcesContent":["console.log(\"bar\");"],//具有廉价效果:"mappings":"AAAA",//没有廉价效果://"mappings":"AACAA,QAAQC,IADI","sourceRoot":""}浏览器映射效果:虽然Sourcemap提供的映射功能可以准确定位文件、行、列,但有时“行”级别就足以帮助我们达到调试和调试的目的定位。您可以选择使用cheap关键字来简化Sourcemap内容并减小Sourcemap文件的大小。2.1.4module模块关键字只在廉价场景下生效,比如cheap-module-source-map,eval-cheap-module-source-map。当devtool包含cheap时,webpack根据module关键字判断是以loader联调处理结果为源还是以处理前的代码为源。例如:注意上例中的sourcesContent字段,左边的devtool有module关键字,所以原来包含类Person的代码映射到这里;右边生成的sourcesContent是babel-loader编译处理后的内容。2.1.5nosources当devtool包含nosources时,生成的Sourcemap内容不包含源码内容——即sourcesContent字段。例如,当devtool='nosources-source-map'时,产品:{"version":3,"sources":["webpack:///./src/index.ts"],"names":["console","log"],"mappings":"AACAA,QAAQC,IADI","file":"bundle.js","sourceRoot":""}虽然不包含源代码,但是.map产品还包含filesName、mappings字段、变量名等信息,依然可以帮助开发者定位代码的原始位置,配合sentry等工具提供的源码映射功能,可以在异地还原错误堆栈等信息.2.1.6inline当devtool包含inline时,Webpack会将Sourcemap内容编码成Base64DataURL,直接追加到产品文件中。例如对于devtool='inline-source-map',产品:console.log("bar");//#sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOlsiY29uc29sZSIsImxvZyJdLCJtYXBwaW5ncyI6IkFBQ0FBLFFBQVFDLElBREkiLCJmaWxlIjoiYnVuZGxlLmpzIiwic291cmNlc0NvbnRlbnQiOlsiY29uc3QgZm9vID0gJ2Jhcic7XG5jb25zb2xlLmxvZyhmb28pOyJdLCJzb3VyY2VSb290IjoiIn0=inline模式编译速度较慢,andtheproductvolumeisverylarge,itisonlysuitableforuseinthedevelopmentenvironment.2.1.7hidden正常情况下,产品必须携带//#sourceMappingURL=指令,这样浏览器才能正确找到Sourcemap文件。当devtool包含hidden时,编译后的产品不包含//#sourceMappingURL=指令。例如:两者的区别仅在于编译后的产品最后一行有//#sourceMappingURL=指令。当你需要Sourcemap功能,又不想让浏览器Devtool工具自动加载时,可以使用这个选项。您也可以通过以下方式手动打开Sourcemap:2.1.8总结综上所述,Webpack的devtool值由以上七个关键字中的一个或多个组成。虽然提供了27个候选,但逻辑上都是由上面的规则叠加而成,例如:cheap-source-map:代表Sourcemap“withoutcolumnmapping”eval-nosources-cheap-source-map:代表“with”**eval**"wrappingmodulecode",and**.map**"Nosourcecodeinthemappingfile"and"Nocolumnmapping"Sourcemap等选项类推。最后总结一下:对于开发环境,适合使用:eval:速度极快,但只能看到原始文件结构,看不到打包前的代码内容cheap-eval-source-map:比较快,可以see打包前的代码内容,但是看不到loader处理前的源码cheap-module-eval-source-map:速度比较快,可以看到loader处理前的源码,但是你cannotlocatethecolumn-leveleval-source-map:初始编译速度慢,但定位精度最高。对于生产环境,适合使用:source-map:资料最全,但安全性最低。外部用户可以轻松获取压缩和混淆前的源代码。慎用hidden-source-map:信息比较全,安全性低。外部用户在获取.map文件的地址后,仍然可以获取到源代码。2.2使用插件上面介绍的devtool配置项本质上只是一个方便记忆和使用的正则缩写短语。Sourcemap的底层处理逻辑实际上是由SourceMapDevToolPlugin和EvalSourceMapDevToolPlugin插件实现的。参考:https://webpack.js.org/plugins/source-map-dev-tool-plugin/该插件在devtool的基础上还提供了更细粒度的配置项,以满足更复杂的需求场景,包括:使用test、include和exclude配置项为这些包设置sourcemap使用append、filename、moduleFilenameTemplate和publicPath配置项设置Sourcemap文件的文件名和URL。使用方法和其他插件一样,如:constwebpack=require('webpack');module.exports={//...devtool:false,plugins:[newwebpack.SourceMapDevToolPlugin({exclude:['vendor.js']})],};插件配置规则比较简单,这里就不赘述了。3.总结至此,关于Sourcemap的大部分内容已经讲解完毕。读者需要了解Sourcemap是一种高效的位置映射算法,它将产品与源代码之间的位置关系表达为映射的层次设计和VLQ编码规则。然后使用Chrome、Safari、VSCode、Sentry等工具将其恢复到接近开发状态的源代码形式。在Webpack场景下,通常只需要选择合适的devtool短语就可以满足大部分场景的需求。在特殊情况下,也可以直接使用SourceMapDevToolPlugin进行更深层次的定制。