问题背景:在使用
element-ui
的form
组件时,用到了表单的validate
数据规则校验能力。当在本地开发测试时,必填项没填时可以正确提示错误,而在打包发上测试环境时,却失效了,必填项没数据依然能正确的提交通过
作为一个合格的程序员,既然碰到问题,我们就要找出问题归因,并把问题解决掉。
创建问题排查环境,提高排查效率
因为项目代码比较庞大,影响因素也多,且打包构建也比较费时,严重影响调试排查效率。所以把有问题的功能抽出来,独立成demo,而其他配置则保留跟原项目一样,相当于完整项目的一个小型版本。
断点调试,直面问题源头
分析代码逻辑,使用表单校验时用的是form组件的validate方法,所以首先把问题定位在这个方法上。
复制代码
form
的validate
内部实现实际上是使用的另一个库async-validator
的能力。它是在这个库的基础上做了一些封装,使适用于element-ui的表单组件。
通过跟踪代码执行逻辑发现了一处可疑之处,格式化后的代码如下
function za(e, t) {
return (
null == e ||
!("array" !== t || !Array.isArray(e) || e.length) ||
!(
!(function (e) {
return (
"string" === e ||
"url" === e ||
"hex" === e ||
"email" === e ||
"pattern" === e
)
})(t) ||
"string" != typeof e ||
e
)
)
}
复制代码
这个方法对应的未编译前源码为:
function isNativeStringType(type: string) {
return (
type === 'string' ||
type === 'url' ||
type === 'hex' ||
type === 'email' ||
type === 'date' ||
type === 'pattern'
);
}
export function isEmptyValue(value: Value, type?: string) {
if (value === undefined || value === null) {
return true;
}
if (type === 'array' && Array.isArray(value) && !value.length) {
return true;
}
if (isNativeStringType(type) && typeof value === 'string' && !value) {
return true;
}
return false;
}
复制代码
此方法用来判断值是否为空,为空则返回true
。
在chrome的开发者工具的Sources面板里面查看za()
返回的值,发现为false
,实际应该为true
。为了进一步验证一个事实,打印出如下的结果
// t = 'string'
console.log(
function(e) {
console.log('e %s', e, typeof e)
return "string" === e || "url" === e || "hex" === e || "email" === e || "pattern" === e
}(t),
function(e) {
return "string" === e || "url" === e || "hex" === e || "email" === e || "pattern" === e
}(t),
!function(e) {
console.log('e %s', e, typeof e)
return "string" === e || "url" === e || "hex" === e || "email" === e || "pattern" === e
}(t),
!function(e) {
return "string" === e || "url" === e || "hex" === e || "email" === e || "pattern" === e
}(t),
!function(e) {
return "string" === e || "url" === e || "hex" === e || "email" === e || "pattern" === e
}('string')
)
复制代码
打印输出结果:true false false true false
问题就出在此处
// t = string
function(e) {
return "string" === e || "url" === e || "hex" === e || "email" === e || "pattern" === e
}(t)
复制代码
正常执行应该是返回true
的,而这里是返回false
,肯定是哪里出了问题。
所以这里就已经掉进了一个陷阱了,看代码是没问题的,无论在哪里执行都可以正常返回结果,但在这里却返回异常的结果。
那么问题出在哪里呢?
复制代码
我们在开发时都有开启chrome开发者工具的sourcemap功能,在断点调试看代码时,工具给我们看到的是混淆压缩前的代码,所以所见的代码并不跟运行的代码是一模一样的,运行时的代码片段是经过压缩混淆后的代码片段。
关闭控制台的Enable JavaScript source maps
功能,调试进去看实际的代码
console.log(
(function (e) {
return (
console.log("e %s", e, typeof e),
"string" === e ||
"url" === e ||
"hex" === e ||
"email" === e ||
"pattern" === e
)
})(t),
!1, // 注意看这里
!(function (e) {
return (
console.log("e %s", e, typeof e),
"string" === e ||
"url" === e ||
"hex" === e ||
"email" === e ||
"pattern" === e
)
})(t),
!0, // 注意看这里
!1 // 注意看这里
)
复制代码
从以上代码发现,部分函数直接被编译成了!0 !1 !0
这样的固定值。也就是说打包压缩时,如果方法里面只是返回值,没有其他操作(如console.log),那么能推测出返回结果的,会直接编译成最终的返回结果。
function(e) {
return "string" === e || "url" === e || "hex" === e || "email" === e || "pattern" === e
}(t)
// 此立即执行函数会被编译成 !1
复制代码
问题总结
综上分析,问题就出在压缩工具在混淆压缩时,把部分立即执行函数编译成错误的推测出来的固定值。而我们在开发调试时又是使用source maps模式,看到的是工具编译前的源码,所以掉进了代码陷阱,没有及时觉悟到编译后的代码会修改编译前的代码逻辑这事。
一般本地环境看到的效果和测试环境看到的效果不一样时,排除掉其他外因的情况下,可以想想目前测试包和本地包的差异在哪,差异就在压缩混淆,和开启了production模式,而这两个是会改变代码的结构的,在特殊情况下可能会引起不必要的bug。
解决方案
项目打包时是使用webpack-parallel-uglify-plugin
插件做压缩的,把压缩工具改成terser-webpack-plugin
即可解决问题