new Function source-map

Published on
发布于·预估阅读13分钟
Authors
  • Name
    willson-wang
    Twitter

目录

  • 动态执行js字符串方式
  • source map
  • 错误监听
  • 总结

动态执行js字符串方式

动态执行js字符串的能力,有利于我们去做一些热更新,劫持子应用js并渲染子应用js等,常用的动态执行js字符串有三种方式

  • 内联script
  • eval
  • new Function

内联script

比如通过fetch or ajax获取到可执行的js字符串之后,通过创建一个script标签,然后将内容设置到script标签,最后在append到dom中,即可达到动态执行字符串的目的,伪代码如下所示

const code = await fetch('xxxx')

const scriptEle = document.createElement('script');
scriptEle.textContent = code
document.bbody.appendChild(scriptEle)

注意这种执行方式,js的作用域是全局作用域

eval

比如通过fetch or ajax获取到可执行的js字符串之后,通过eval来执行可以执行的js字符串,即可达到动态执行字符串的目的,伪代码如下

const code = await fetch('xxxx')

eval(code);

注意点: eval执行的作用域为当前作用域,而间接执行的eval作用域为全局作用域

var x = 1;
function fn() {
  var x = 2;
  eval('alert(x)'); // alert 2  当前作用域
  (0, eval)('alert(x)') // alert 1 全局作用域
}

所以我们需要根据场景来决定怎么使用eval

new Function

比如通过fetch or ajax获取到可执行的js字符串之后,通过new Function()()来执行可以执行的js字符串,即可达到动态执行字符串的目的,伪代码如下

const code = await fetch('xxxx')

new Function(code)();

注意点:new Function()的作用域是全局作用域,不存在当前作用域的情况;所以推荐使用new Function来替换eval的原因就是,eval如果是直接调用的场景,那么js引擎在执行eval内代码的时候需要判断变量是当前最用域内的变量还是全局变量,所以相对new Function 只有全局作用域会慢;

soure map

为什么需要source map? 便于调试压缩后的代码及还原压缩后代码真实的源码报错信息

source map会不会影响性能? 不会,只有开启了浏览器的devtool的时候浏览器才会去根据js文件内是否有souceMapUrl的标识去加载对应的source map文件

source map有没有标准? 有,目前的版本是3,标准包含了source map应该包含的信息,及怎么去设置source map,具体内容请查看sourcemaps spec

webpack为什么有那么多种source-map格式? 因为webpack基于安全、大小(性能)生成了不同方式的source map,但是本质上包含的内容都是标准规定的内容,只是做了部分增减

webpack source map

webpack生成的source map虽然有很多类型,但是归根结底就是几个场景的组合,具体场景如下所示

  • eval: 使用eval包裹模块,通过sourceURL来设置eval模块的名称,不会生成source map文件,即无法还原源代码
  • cheap:生成source map,不包含列信息、loader的souce map
  • source-map: 生产完整的source map文件
  • module:生成source map,不包含列信息,包含loader的source map
  • inline: 生成内联的source map
  • nosources: 生成source map,不含源码,仅包含行列信息
  • hidden: 仅生成source map, 不主动添加sourceMapUrl

具体组合规则如下所示 [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

下面以如下源码为例

document.write(`broker`)

source-map

document.write("broker");
//# sourceMappingURL=broker.0af4c32c350c702937dc.js.map

eval

eval('document.write("broker");\n\n//# sourceURL=webpack://webpack5/./src/broker.js?')

image.png

相对于source-map,eval 不生成source-map,仅添加sourceURL,用于提示错误文件来源

inline-source-map

document.write("broker");
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnJva2VyLjBhZjRjMzJjMzUwYzcwMjkzN2RjLmpzIiwibWFwcGluZ3MiOiJBQUFBQSxTQUFTQyxNQUFUIiwic291cmNlcyI6WyJ3ZWJwYWNrOi8vd2VicGFjazUvLi9zcmMvYnJva2VyLmpzIl0sInNvdXJjZXNDb250ZW50IjpbImRvY3VtZW50LndyaXRlKGBicm9rZXJgKSJdLCJuYW1lcyI6WyJkb2N1bWVudCIsIndyaXRlIl0sInNvdXJjZVJvb3QiOiIifQ==

image.png

相对于source-map、inline-source-map将单独的source map文件,通过base64的方式添加到源文件内

nosources-source-map


// 少了sourcesContent包含源码的字段
{"version":3,"file":"broker.0af4c32c350c702937dc.js","mappings":"AAAAA,SAASC,MAAT","sources":["webpack://webpack5/./src/broker.js"],"names":["document","write"],"sourceRoot":""}

image.png 相对于source-map、nosources-source-map生成的map文件缺少了源代码,只能用于定位错误的文件及行列号,无法查看源码信息

hidden-source-map

document.write("broker");

image.png 相对于source-map、hidden-source-map不在源码中添加顶级注释//# sourceMappingURL

cheap-source-map

image.png image.png source-map 完整的行列信息,点击错误信息的时候光标会直接定位到错误行列

image.png

image.png cheap-source-map 完整的行信息,点击错误信息的时候光标会直接定位到错误行 cheap-source-map 相对于source-map少了列信息,少了loader转化的source-map,也就是定位的文件是loader转化后的代码,而不是源文件

cheap-module-source-map

image.png

cheap-module-source-map 相对于source-map少了列信息,定位的文件是源文件

注意点

webpack mode production 开启压缩的场景下,devtoo仅支持source-map,inline-source-map,hidden-source-map 和 nosources-source-map,其它模式不会按照预期生成source map,具体可以查看note-about-source-maps

总结

其实webpack的source map的生成,主要围绕的是安全、大小(也就是速度)两个维度来生成不同种类的source map 首先是最完整的source-map 然后从安全角度看

  • nosources-source-map
  • hidden-source-map

从大小(速度)角度看

  • eval
  • cheap
  • cheap-module

所以只要理解了source map这么多种类的目的,那么我们就可以结合实际场景做出最适合当前项目的source map

内联script、eval、new Function错误定位

在微前端或者一些动态执行脚本的场景需要先fetch js代码,然后在通过内联script or eval or new Function的方式来执行js,这时候怎么去快速定位错误及记录错误来源是很重要与关键的能力,那么分别看下这三种场景下的错误应该怎么处理才是最佳

在看内联script、eval、new Function三种执行js代码报错之前,我们先看下通过script src脚本执行报错,有source map与无source map的场景是怎么样的

script src

无source map

image.png

可以看出js出错的文件名,不能直接映射到源文件

有source map

image.png 可以看出js出错的源文件名,能直接映射到源文件

内联script

无source map

image.png

image.png 内联脚本名是anonymous, 不能够看出是哪个js文件的报错,不能映射到原文件

有source map

image.png

image.png

image.png 内联脚本名是anonymous,能看到报错源文件名, 能映射到原文件,但是在微前端场景,不能够知道报错的这段js来自于哪个子应用

有sourceURL

image.png

image.png

image.png

内联脚本名是anonymous,能看到报错源文件名, 不能映射到源文件,但是在微前端场景,能够知道报错的这段js来自于哪个子应用

eval

无source map

image.png

image.png

会看到代码是eval执行, 不能够看出是哪个js文件的报错,不能映射到源文件

有source map

image.png

image.png

image.png

会看到代码是通过eval执行, 能够看出是哪个js文件的报错,能映射到源文件,但是在微前端场景,不能够知道报错的这段js来自于哪个子应用

有source url

image.png

image.png

image.png 会看到代码是通过eval执行, 能够看出是哪个js文件的报错,不能映射到源文件,但是在微前端场景,能够知道报错的这段js来自于哪个子应用

new Function

无source map

image.png

image.png

会看到代码是eval执行, 不能够看出是哪个js文件的报错,不能映射到源文件

有source map

image.png

image.png

image.png

会看到代码是通过eval执行, 不能够看出是哪个js文件的报错,不能映射到源文件

有source url

image.png

image.png

image.png 会看到代码是通过eval执行, 能够看出是哪个js文件的报错,不能映射到源文件,但是在微前端场景,能够知道报错的这段js来自于哪个子应用

错误监听

在微前端的场景下,比如micrpApp是通过new Function or 内联script来执行子应用js代码,那么怎么区分错误来源是哪个子应用,有两种三种思路

  • 通过Object.defineProperty + Proxy的方式改写Error类
  • 使用try catch包裹代码块、重写xhr、fetch、promise、addEventListen等方法,然后对callback进行try catch
  • 添加sourceURL,通过sourceURL来执行js文件名与子应用名称

最终选择添加sourceURL方式,原因是Object.defineProperty + Proxy的方式改写Error类,只能影响到代码内主动throw 的Error,对于语法错误等js引擎执行过程中抛出的错误是影响不到的;而使用try catch的方式改动量更大,且更容易出bug下,相对来说选择sourceURL方式成本更低,改动更小

无sourceURL

addEventlisten error no sourceURL image.png

filename来自url,不知道错误来源于哪个js文件,及哪个子应用

有sourceURL

addEventlisten error sourceURL image.png

filename来自原本的js文件,准备知道错误来源于哪个js文件,及哪个子应用

总结

image

在微前端场景,通过添加sourceURL可以区分错误来源是哪个子应用,通过添加sourceMappingURL便于排查错误

参考链接 eval note-about-source-maps chrome-development-tool-vm-file-from-javascript Support //# sourceURL= and //# sourceMappingURL= in v8's parser Introduction to JavaScript Source Maps Source Map Revision 3 Proposal Source map for a dynamically created function Bug: Source maps don't work with hot reload Webpack devtool source map Source Maps