关注我们 文末有福利


转转|又写bug了?没时间生活?腹肌小哥哥教你一个函数搞定线上bug_业务逻辑

袁小龙

​ 商业客服前端负责人,8年前端开发经验,西小口第一虞姬,在性能统计,前端工程化方面有一定的研究,平时喜欢运动,不定时会冒出一个点子来解决一下效率和质量问题。


背景

我们通过 ​​sentry​​​ 异常监控平台和用户反馈的错误进行分析发现,前端有将近 ​​80%​​​ 的线上错误都是可以避免的,都可以归类为同一类型错误:​​判断不严谨​

所谓的​​判断不严谨​​​就是,前端在判断是否存在时没有做很好的 ​​if else​​ 判断导致,例如如下错误:

转转|又写bug了?没时间生活?腹肌小哥哥教你一个函数搞定线上bug_默认值_02image

此类错误处理不好经常导致用户白屏体验(在《转转商业前端错误监控系统(Sentry)策略升级》中有说过我们对白屏体验的处理,这里就不再复述了,有需要的同学可以翻过去浏览一下),就算白屏问题解决了,依然影响用户操作体验。

基于想完全解决此种报错,又不想影响用户的视觉及操作体验,诞生了此需求:​​判断是否存在的,如果不存在赋值默认值​

常规处理

示例对象如下:

const deepObj = {
respCode: 0,
respData: {
status: 0,
data: {
useInfo: {
userName: '张一',
sellerAddress: {
city: '北京市'
}
}
}
},
data: [{
name: '你长得真帅'
}, {
name: '你就是那么帅'
}]
};

if else

要判断 ​​city​​ 字段是否存在,必须做如下判断才能完全解决中间某个字段不存在时导致的页面报错

if (
deepObj &&
deepObj.respData &&
deepObj.respData.data &&
deepObj.respData.data.useInfo &&
deepObj.respData.data.useInfo.sellerAddress
) {
deepObj.respData.data.useInfo.sellerAddress.city = deepObj.respData.data.useInfo.sellerAddress.city || '北京';
// ... 业务逻辑
}

此种判断


  1. 不小心会有遗漏一个判断条件就会导致报错
  2. 通篇的判断语句导致业务代码不优雅,维护困难

ES6对象解构

// 正确的写法,要每一步都有默认值处理
let { respData: { data : { useInfo: { sellerAddress: { city = '天津市' } = {} } = {} } = {} } = {} } = deepObj;
// 错误的写法,如果sellerAddress不存在,此方式就会报错,
let { respData: { data : { useInfo: { sellerAddress: { city = '天津市' } } } } } = deepObj;
// 业务逻辑代码
console.log(city);

此方法要小心赋值默认值,不然一开始就会报错,层级越深,嵌套会有回调地狱问题

proxy代理

如果我们不考虑兼容性的话,​​proxy​​​监听​​get​​的方式会特别简单

// 以下代码为网上提供的的例子进行改造
/**
* @param {object} target 目标对象
* @param {any} defaultValue 默认值
* @param {string} exec 结尾属性字符
*/
function getter(target, defaultValue, exec = '_') {
return new Proxy({}, {
get: (o, n) => {
const result = target || defaultValue;
if (n === exec) {
return result;
} else {
return getter(typeof target === 'undefined' ? result : target[n], defaultValue, exec);
}
}
})
}
const city = getter(deepObj, '天津市').respData.data.useInfo.sellerAddress.city._;
// ... 业务逻辑代码

此方式必须设置结尾符号,告诉​​proxy​​已经到最后一步了,使用上看着不太优雅

此外,​​Babel​​​ 官方也提到过 ​​proxy​​​ 是不能转换成 ​​ES5​​ 的写法的,如下:

Due to the limitations of ES5, Proxies cannot be transpiled or polyfilled. See support in various JavaScript engines.
// 由于ES5的局限性,没办法对proxy进行换成。只能依赖js引擎的支持。

不过如果不考虑 ​​IOS8​​​ 及以下的兼容的话,可以考虑引入 ​​proxy-polyfill​​​ 来代替 ​​proxy​​,如下:

// commonJS require
const proxyPolyfill = require('proxy-polyfill/src/proxy')();

// Your environment may also support transparent rewriting of commonJS to ES6:
import ProxyPolyfillBuilder from 'proxy-polyfill/src/proxy';
const proxyPolyfill = ProxyPolyfillBuilder();

// Then use...
const myProxy = new proxyPolyfill(...);

链判断运算符

  • 使用
let city = '';
if (!deepObj?.respData?.data?.userInfo?.sellerAddress?.city) {
city = '天津市';
}
// 业务逻辑代码
  • babel配置
{
// ...
plugins: [
"@babel/plugin-proposal-optional-chaining"
]
}

此方式 ​​ES6​​​ 规范已经支持,只需要安装一个 ​​babel​​ 插件就能兼容所有浏览器

但是也有一点小问题, 在我们使用的时候,​​TypeScript​​​ 并不支持 ​​Optional Chaining​​​,在 ​​TypeScript3.7​​ 以后才正式支持的。

如果你的项目没有用到 ​​TypeScript​​​,或者 ​​> TypeScript3.7​​ 的话,强烈推荐使用此方法,使用简单,而且还能做函数的判断,例如:

转转|又写bug了?没时间生活?腹肌小哥哥教你一个函数搞定线上bug_业务逻辑_03image

我们的方案

由于 ​​Proxy​​​ 的局限,及 ​​TypeScript3.7​​​ 版本以下不支持 ​​Optional Chaining​​,我们选择自己设计一个兼容性较好的函数来做处理。

  • 代码
function getIn (deepObj, path, defaultVal) {
try {
if (!path) {
return deepObj || defaultValue;
}
if (typeof path === 'string' || path instanceof Array) {
const pathArr = typeof path === 'string' ? path.split('.') : [...path];
if (!pathArr.length) {
return deepObj || defaultVal;
}
let currValue = deepObj[pathArr[0]];
if (currValue === undefined) {
return defaultVal;
} else {
const restPath = pathArr.slice(1);
return restPath.length === 0 ? currValue : getIn(currValue, restPath, defaultVal);
}
} else {
throw 'path 类型只能为字符串或数组';
}
} catch (err) {
console.warn(err);
return defaultVal;
}
}

PS. 感谢前同事提供的代码

  • 使用
import { getIn } from '@zz-biz/utils';

// 判断city属性是否存在
if (getIn(deepObj,'respData.data.useInfo.sellerAddress.city')) {}
// vs 旧的判断,代码更清晰明了
if (
deepObj &&
deepObj.respData &&
deepObj.respData.data &&
deepObj.respData.data.useInfo &&
deepObj.respData.data.useInfo.sellerAddress &&
deepObj.respData.data.useInfo.sellerAddress.city
) {}
// 设置默认值
getIn(deepObj,'respData.data.useInfo.sellerAddress.city','天津市');
// return 北京市
getIn(deepObj,'respData.data.useInfoSS.sellerAddress.city','默认数据');
// return 默认数据
getIn(deepObj,'data.0.name', '你秃了');
// return 你长得真帅
getIn(deepObj, 'data.1.name', '你又秃了');
// return 你就是那么帅
getIn(deepObj, ['data', 1, 'name'], '你又秃了');
// return 你就是那么帅
getIn(deepObj, {}, '你怎么秃了');
// console.warn(path 类型只能为字符串或数组)
// return 我不爱你

不过,​​lodash​​ 中也实现了此方法,如果感兴趣的话可以查看 lodash/get 源码,看看实现方式。

借用之前分享 ​​Sentry 错误搜集策略升级​​ 里的一张bug图回顾一下看出,线上bug数巨降

转转|又写bug了?没时间生活?腹肌小哥哥教你一个函数搞定线上bug_业务逻辑_04image

结语

前端异常监控是个好东西,能帮我们发现很多细问题,帮助我们代码书写更严谨,减少线上常规错误,实现前端线上零bug的目标

代码不在乎简单,只要能解决业务问题那么就是最好的方法。

如果有写的不好的地方,欢迎留言指出,一起学习。

福利来了

本文并留下评论,我们将抽取第10名留言者(依据公众号后台顺序)送出转转纪念T恤一件:转转|又写bug了?没时间生活?腹肌小哥哥教你一个函数搞定线上bug_业务逻辑_05

大转转FE招贤纳士,需要内推的同学可以投递此邮箱,

想了解我们作者小哥哥更多信息,关注我们公众号哦!!!





转转|又写bug了?没时间生活?腹肌小哥哥教你一个函数搞定线上bug_默认值_06

扫二维码|关注我们


 微信号|zhuanzhuanfe