驯服ClojureScript项目中的高级编译错误

2020-07-01 10:42:47

想象一下:您有一个大型的多模块ClojureScript项目,并且您计划在生产中进行新的部署,您的项目使用的是CLJS编译器的高级优化模式,一切似乎都很顺利。在发布之前,您正在执行一些最后的E2E测试。然后,加载有故障的模块。砰!您会被打耳光,并出现类似如下的错误:

未捕获错误:没有协议方法IMultiFn.-Add-为类型函数定义的方法:函数xr(){[本机代码]}位于Nb((INDEX):964),位于Yh((INDEX):443at(INDEX):236(INDEX):143)。未捕获类型错误:无法读取位于Nu((INDEX):143)处未定义的、位于Nu((INDEX):143)的属性';j';login-a5a67c0fffdfa158b7220c8c2553253b645e4e2e.js:169。

你没料到会这样。自上一次构建以来没有重大的新变化,单元测试也都是完美无缺的。此外,在使用Firefox时一切正常,但现在编译后的代码在Google Chrome上崩溃了。怎么一回事?发布的最后期限迫在眉睫,你必须尽快解决这个问题。

当您使用CLJS编译器构建代码时,它会发出与Google闭包编译器的ADVANCED_OPTIMIZATIONS级别兼容的JavaScript代码。然后,如果启用了";:Optimations:Advanced";编译器选项,CLJS编译器将使用Close Compiler的高级优化缩小结果。

闭包编译器的高级优化模式可能会在您的构建中导致一些错误,如果您是Closure编译器的新手,跟踪这些错误可能会很不舒服。最重要的是,如果您试图搜索带有错误消息的Web,可能很难找到帮助,因为函数名称都已损坏。

最好从项目开始时就牢记高级编译模式来避免这些错误。我将描述一些帮助您避免和解决高级编译问题的常用方法,并展示如何解决开头描述的更不常见的问题及其背后的原因。

建议对您的生产构建使用";:优化:高级CLJS编译器选项。通过使用更积极的高级转换(如删除死代码和积极重命名),您将极大地减小最终的构建大小。在此处阅读有关高级转换的更多信息:闭包编译器编译级别。

不同的构建工具(如lein-cljsbuild和dow-cljs)可能会将不同的默认选项传递给Close Compiler。我不会在这篇博客文章中讨论这些不同之处,但我会提供一些关于如何避免在项目中使用高级编译的一般指导原则。

extern是一种声明不应该被闭包编译器忽略(重命名)的名称的机制。当您使用外部库时,如果您使用的是lein-cljsbuild这样的构建工具,请确保它们与externs捆绑在一起。或者,如果需要,提供您自己的externs文件。否则,闭包编译器将在高级编译期间无意中忽略对外部定义符号的引用,从而导致只有在稍后运行编译代码时才会发现错误。

要添加您自己的externs,请使用编译器选项,例如:";:externs[";exters.js";]";并在您的工作目录中提供一个exters.js文件。

此外,使用";:infer-externs true";编译器选项也很好。此选项将启用为JavaScript互操作调用自动生成外部变量。

值得一提的是,闭包编译器包含稳定JavaScriptAPI外部变量,但可能还没有包含处于试验状态的新特性。实验API变化非常快,因此将它们包含在闭包编译器工具中是没有意义的。因此,如果您计划使用一些实验特性,请确保将它们添加到您自己的externs文件中。

如果要将CLJS代码拆分为:模块,则可能需要使用:rename-prefix";.";编译器选项。拆分模块在全局JavaScript作用域中运行,因此它们可能会干扰同一页上加载的其他代码(例如Google Analytics),并在发生名称冲突时导致不可预测的错误。使用";:rename-prefix";时,最好使用非常短的字符串作为前缀,例如::rename-prefix";r_";

这将略微增加最终(Gzided)构建大小,因为现在每个被忽略的全局变量都将以“r_”为前缀。

在高级编译过程中,闭包编译器将生成大量全局变量,这些变量可能会导致与同一页上运行的其他代码发生名称冲突。如果您在同一页上加载其他代码,并且没有将代码拆分成多个模块,则可以使用";:output-wrapper true";编译器选项。编译器将使用默认值";(Function(){…}包装输出的JavaScript代码。​};)()";。这将防止污染全局JavaScript作用域,从而防止与其他外部代码发生冲突。

但是,如果项目中有多个模块,则必须添加编译器选项:Rename-Prefix-Namespace";.";。这使每个模块都可以访问它所依赖的其他模块中定义的变量。这与";:Rename-Prefix";选项的工作方式类似。不同之处在于,现在每个全局变量的作用域都是一个全局变量,而不是多个全局变量。例如,如果使用这样的前缀命名空间::rename-prefix-nampace";P";,编译后的代码将引用变量";foo";like";p.foo";。此选项还会像";:rename-prefix";选项一样增加最终的构建大小,因此在使用它之前请三思。

还有其他与高级优化没有直接关系的有趣编译器选项,例如";:fn-Invoke-direct";,它们对于优化性能关键型代码很有用。您可以在CLJS文档中阅读有关它们的更多信息。

如果您怀疑高级编译可能有问题,浏览器控制台中可能没有意义的错误通常会提示您,最好通过以下方式解决问题。

为您的高级编译构建添加两个额外的编译器选项";:伪名称true";和";:Pretty-print true";。您的错误现在将显示与源代码中的名称相对应的可读名称。这将帮助您推断是否缺少外部定义。

此外,如果在启用上述编译器选项后错误消失,它会提示您可能存在名称冲突问题。如果强制变量名与外部变量名冲突出现问题,则当强制变量名更改时,该问题就会消失,就像启用:Pseudo-Names选项时发生的情况一样。通常,如果没有正确修复,名称冲突问题可能会出现,然后在添加新更改时神奇地“修复”,这反过来会导致构建输出中的参数变量名称发生更改。最好是彻底消除这些问题。

在极少数情况下,可能有一些高级编译问题不是那么容易避免的。让我们回到开头。在Chrome中运行我们构建的Web应用程序时出现错误。火狐没有任何问题。

未捕获错误:没有为类型函数定义协议方法IMultiFn.-add-method:函数xr(){[本机代码]},位于Nb((Index):964),位于yh((Index):443),位于(Index):236。

堆栈跟踪指向CLJS源代码中的某个多方法。乍一看,这个错误可能毫无意义。编译后的代码正在尝试为名为xr的函数调用-add-method IMultiFn协议方法。在构建输出中搜索强制的XR时,一切似乎都很正常。XR看起来不错,应该可以工作,对吧?错误消息中需要注意的关键一点是";[本机代码]";部分";函数xr(){[本机代码]}";。这告诉我们,编译后的代码正在尝试调用本机浏览器函数xr,而不是我们的参数xr。一次偶然的机会,闭包编译器将我们的Multimethod命名为XR,这恰好与浏览器提供的XR函数冲突。

在使用较旧版本的闭包编译器时,偶尔会遇到这样的错误。开发人员一直在向Web浏览器添加新功能。当用户升级他/她的网络浏览器时,有可能添加了新的保留字,该保留字与被忽略的变量冲突。

原来,xr是WebXR平台API添加的保留字。当错误发生时,“xr”被添加到最新版本的Google Chrome中,但还没有添加到Firefox中。较新的闭包编译器版本通过为其提供外部元素来考虑到这一点。升级ClojureScript版本以及项目中使用的闭包编译器并不总是很容易。在这种情况下,您可以通过添加自己的自定义外部来快速解决问题:

很多时候,如果准备得当,CLJS的高级编译问题很容易避免。在极少数情况下,可能会发生一些奇怪的错误。因此,使用当前浏览器版本测试您的构建,并学习快速调试高级编译问题,以避免浪费宝贵的时间。