目前正在开发一个低代码平台,主要用于运营和构建H5活动。这就涉及到第三方组件的开发,第三方组件要接入平台需要通过我们特定的打包工具来构建。构建后的组件会合并成一个单独的js文件,代码会被压缩和混淆。如果这时候需要调试,那将是极其痛苦的。想要有一个好的调试环境,就需要涉及到SourceMap的输出,而Webpack的devtools字段就是用来控制SourceMap的。SourceMap的原理在详细讲解devtools配置之前,我们先来看看SourceMap的原理。SourceMap的主要功能是还原代码,将编译压缩后的代码还原成之前的代码。下图左侧代码为Webpack打包前,右侧代码为打包后。打开chrome,导入dist.js,你会发现浏览器会自动还原压缩后的代码。那么这个SourceMap是如何将右边的代码还原成左边的呢?我们先看一下dist.js.map的结构。{//版本号"version":3,//输出文件名"file":"dist.js",//输出代码与源代码的映射关系"mappings":"MAAA,IAAMA,EAAM,CACVC,KAAM,KACNC,OAAQ,KAGV,SAASC,IACPH,EAAIE,QAAU,EAGhB,SAASE,IACPJ,EAAIE,QAAU,EACdG,QAAQC,IAAIN,EAAIC,KAAM,OAGxBE,IACAC,IACAA,IACAD,K",//原创Some代码中的变量名"names":["dog","name","weight","eat","call","console","log"],//源文件列表//我们打包的Sometimes多个js文件合并为一个,所以源文件有多个"sources":["webpack:///./src/index.ts"],//源文件内容列表,对应sources字段"sourcesContent":["constdog={\nname:'旺菜',\nweight:100\n}\n\nfunctioneat(){\ndog.weight+=1\n}\n\nfunctioncall(){\ndog.weight-=1\nconsole.log(dog.name,'Wowwoof')\n}\n\neat()\ncall()\ncall()\neat()"],}其他字段应该很容易理解。比较难的是mappings字段,看起来就是一堆乱码。这是一串使用VLQ编码的字符串,规则比较复杂。我们可以直接在github上找一个VLQ(https://github.com/Rich-Harris/vlq/blob/master/src/index.js)编码库来解码这串字符。/**@type{Record}*/letchar_to_integer={};/**@type{Record}*/letinteger_to_char={};'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split('').forEach(function(char,i){char_to_integer[char]=i;integer_to_char[i]=char;});/**@param{string}string*/functiondecode(string){/**@type{number[]}*/letresult=[];让移位=0;让价值=0;for(leti=0;i>>=1;if(should_negate){result.push(value===0?-0x80000000:-value);}else{result.push(value);}//重置值=shift=0;}}returnresult;}mappingscharacters字符串一般用分号(;)和逗号(,)分隔,每个分号分隔的部分对应压缩代码的每一行。因为上面打包的代码已经压缩,只有一行代码,所以这个mappings中没有分号。逗号分隔的部分表示压缩代码当前行的某列与源代码的对应关系。我们尝试通过上面的代码解码前面部分的映射。'MAAA,IAAMA,EAAM,CAVCC,KAAM'.split(',').forEach((str)=>{console.log(decode(str))})解码结果如下:[6,0,0,0]//MAAA[4,0,0,6,0]//IAAMA[2,0,0,6]//EAAM[1,0,1,-10,1]//CACVC[5,0,0,6]//KAAM中每串字符对应五个数字,这五个数字对应的含义如下:第一个数字表示该位置压缩码所在的列(与以前的数字)。第二位指示此位置属于源属性中的哪个文件。第三位表示这个位置属于源代码的哪一行(与前面的数字累加)。第四位表示该位置属于源代码的哪一列(与前面的数字累加)。第五位表示这个位置属于names属性中的哪个变量。然后MAAA:[6,0,0,0]:对应压缩代码第一行第7列(PS。从0开始计数,所以数字6应该对应第7列,后面的数字是相同),对应sources中第一个文件第一行的第一列。查看代码,我们可以看到压缩后的var声明对应于源代码的const。看IAAMA:[4,0,0,6,0],表示压缩后的代码的第11列(这里的4表示从前面计算的列开始算4列,也就是第11列),对应到第7列源码第一行的column(这里同理,也是倒数6列),对应names属性的第一个变量名,即“dog”。这里对代码进行了混淆处理,所以有一个names字段专门用来记录压缩前的变量名。简单翻译一下之前的解码结果:[6,0,0,0]//压缩后的第7列对应源码第1行第1列[4,0,0,6,0]//压缩后的代码源码第11列对应源码第一行第7列,对应names的第一个变量("dog")[2,0,0,6]//第13个压缩代码的列对应源代码的第一行Column13[1,0,1,-10,1]//压缩代码的第14列,对应源代码第2行的第3列,对应tothesecondvariableofnames("name")[5,0,0,6]//压缩代码的第19列对应源代码第2行的第9列。你可以看到里面有一个负数。这是因为对应关系从源代码的第1行跳到了第2行。新一行的列数要从前面计算,列数是根据前面的结果累加的,所以这里需要回滚列数,所以出现负数,而列数列被回滚。以上是代码经过压缩的情况。如果我们只使用webpack打包,不压缩,生成的映射如下:可以看到dist.js中的前5行代码是webpack运行时生成的,与源码无关。所以mappings前面有5个分号(;),表示前5行与源代码没有对应关系,后面的AAAA、IAAMA、GAAG、GAAG;是dist.js第六行和源码的对应关系。devtools配置项了解了SourceMap的原理之后,我们来看一下devtools的配置项。如果你看Webpack的官方文档,你会发现devtools这个配置项是一张十几行的表格,有点唬人。如果仔细观察,你会发现devtools的配置是基于“source-map”,然后加上各种前缀。格式如下:[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map不同的配置会生成不同的产品。在webpack的github仓库中,有一个专门的demo用于展示不同参数打包的产品:https://github.com/webpack/webpack/tree/main/examples/source-map。source-map先看最基本的配置(devtools:"source-map"),就是单独生成一个.map文件,然后在打包代码的最后一行添加注释,指明生成SourceMap的路径,方便浏览器阅读Pick。//#sourceMappingURL=SourceMap文件路径inline-source-map看名字就很容易理解。在内联SourceMap前面加上inline-,就是对SourceMap的内容进行base64-escap,直接放在打包代码的最后一行。//#sourceMappingURL=data:application/json;charset=utf-8;......eval/eval-source-mapeval-source-map会把对应模块的代码放到eval()中执行,如果加上//#sourceURL=xxx,浏览器会自动将eval中的代码放到sources中。通过eval生成代码的好处是,如果更改了某个模块,只需要重新eval某个模块的代码,可以提高二次编译的效率。官方文档也说eval的rebuild效率基本上是最高的。cheap-source-map/cheap-module-source-map//source-map"mappings":";;;;;AAAA,IAAMA,GAGL,GAAG;EACFC,IAAI,EAAE,IADJ;EAEFC,MAAM,EAAE;AAFN,CAHJ;;AAQA,SAASC,GAAT,CAAaD,MAAb,EAA6B;EAC3BF,GAAG,CAACE,MAAJ,IAAcA,MAAd;AACD;;AAED,SAASE,IAAT,GAAgB;EACdJ,GAAG,CAACE,MAAJ,IAAc,CAAd;EACAG,OAAO,CAACC,GAAR,CAAYN,GAAG,CAACC,IAAhB,EAAsB,KAAtB;AACD;AAEDE,GAAG,CAAC,EAAD,CAAH;AACAC,IAAI;AACJA,IAAI;AACJD,GAAG,CAAC,CAAD,CAAH,C"//cheap-source-map"mappings":";;;;;;AACA;AACA;AACA"以上是source-map和cheap-source-map生成的映射的区别。您可以看到Cheap-Source-Map生成的映射大大简化了。因为cheap-source-map去除了列信息,所以可以大大提高sourcemap的生成效率。在打包webpack的过程中,对应的代码映射关系可能会在loader处理过程中发生变化,而cheap-module-source-map的作用是将打包后的代码与初始对应的代码对应起来,而不是处理后的代码装载机。我们先写一段typescript代码,如下:constdog:{name:string,weight:number}={name:'旺菜',weight:100}functioneat(weight:number){dog.weight+=weight}functioncall(){dog.weight-=1console.log(`${dog.name}:woofwoof`)}eat(10)call()call()eat(5)我们来看看,用便宜的源-map还原的代码:看cheap-module-source-map还原的代码:hidden-source-map和source-map的配置一样,会单独生成一个.map文件,但不包含在打包代码的末尾进行关联注释,制作发布时将.map文件上传到报错平台(例如:sentry)。另外,如果配置了多个loader,上线时可以考虑将devtools配置为hidden-cheap-module-source-map。小结以上介绍了各种配置输出代码的特点,各有各的排列组合。比如在开发环境中,为了尽可能看到没有经过loader转换的原始代码,可以配置为cheap-module-source-map。如果需要进一步提高编译速度,可以配置为eval-cheap-module-source-map。发布上线时,可以调整配置为hidden-cheap-module-source-map。本文由mdnice多平台发布