Decorative image frame

非相同请求被canceled的bug排查

BUG描述
  在工单的详情页有一个跳转到设备管理的按钮(到设备页后网页会发起两个请求——一个是detail,一个是dev),一开始这个detail会报错———无效的输入,这个时候dev接口还是可以正常请求的。结果后台把detail接口报错修复之后,发现dev接口一直是被canceled的状态。一开始还以为是后台的问题,因为我们封装的请求里面是网页发起两个相同的请求时,如果前一个请求没有返回时,网页继续发起相同的请求,代码里默认是就会把前一个请求abort。直到后面后台一直说他后台没有报错,这时我调试控制台,将网速降到slow 3G之后,发现dev请求不被canceled,这时候才把问题定位在前端。其实把请求给canceled的操作只能是前端操作的,后端设置接口时只能设置成超时。我不应该一开始就把后台拉下来定位问题的。。。

排查问题
  定位到前端问题后,我发现sf-table里面有一个v-if,我以为是v-if为false时把sf-table给隐藏了,导致发起的请求也被终止了,但是后面一想,模板的渲染跟请求没有啥关系,要排查问题,还是得从util/request中开始排查。

定位问题
  开始打断点,发现只要接口被canceled的时候,就会进入abortAll这个函数,于是全局搜索abortAll,发现Router.beforeEach中会去调用这个函数,这时才定位到原来dev被canceled是因为detail请求之后,路由发生了改变,此时查看detail的成功回调中,果然有操作路由的动作———router.push({query:{role:data.phase}});

问题剖析
  因为这个detail的请求是放在vuex的action里面去做的,每次只要客户管理的模块被加载,这个action就会在mounted里被触发,而之前要解决的bug是在客户管理的客户信息下面才有这个问题,现在如果把这个改变路由的代码放在客户信息下去做,就不会有这个问题了,因为客户信息本来也只有调用detail这个接口。所以最终的解决方案就是把这个router.push({query:{role:data.phase}})放在客户信息的路由下去做(监听路由,路由中包含info时,才在调用action中把操作路由的回调给传入,在detail请求成功的回调中执行)。

问题回顾
  此时查看代码提交记录,发现该代码是要解决一个td而添加的,这个td是用户打开了两个页面,在一个页面中去修改某个客户的服务项,但是在另外一个页面却没有更新客户管理模块中的客户信息的服务项,原因就是没有去加载detail的这个接口,于是mc之前改td的时候就是用watch,在路由中包含info的时候,就会去调用detial的接口,然后去更新路由的查询中的role。这样之后查询会把最新的role给带过去,写到这里我才发现,从表格进去的时候,用户可能有(3、5)这两种角色,在另外的网页中,用户可以把对应的(3、5)升级成(6、7),此时后台应该直接把前端请求的3映射成6,把请求的5映射成7,而不应该都依赖于前端去修改请求参数。要不然前端来做这种操作一般都会引发其他的问题。因为同一个页面,可能会有多个入口,没有办法保证每个入口都会给这个页面传一个role值,比如一开始的detail接口中报错———无效的参数,就是因为在详情中跳转过来没有带一个role值过来,请求的时候detail就直接报错了,这个的解决方法是后端判断role这个参数没有传值时,做了兼容的处理,同理我觉得客户信息页没有刷新也是同样的问题。
  因此,涉及到这种过于依赖前端查询参数的接口的,要尽可能让后端做映射、兼容处理。

  注意 之后遇到类似的交互场景、依靠前端这种添加路由的形式去做的要额外小心,因为后续一旦修改了路由参数的,可能又会发生类似的问题,特别是在整个大模块(下面可能会对应多个路由)mounted的时候就执行的场景。

用reflog拯救reset --hard造成的文件丢失

  依旧记得之前的一次reset –hard给我造成的心理阴影,要是早点get到这个点,或许当初会少些焦虑。
接下来还原一下reset –hard之后的场景以及记录下修复时需要用到什么指令。

首先用git log查看当前的commit记录

然后用git reset –hard来强制回退commit,此时再用git log查看了之前的reflog的提交记录已经不见了

此时如果要重新找回reflog的commit,需要用git reflog查看你在git上进行的每一步操作,然后找到之前的reflog的hash,再用git reset –hard 回退到丢失的commit。此时如果后面有新的commit,可以结合git cherry-pick来使用

此时查看commit已经成功拾回。

理解热更新原理

  今天学习了webpack-dev-server热更新的原理,大致的意思就是借助express启动的websocket让浏览器跟本地服务器建立双向的通信,然后通过webpack的compile的watch功能,监听文件的改变,然后通过websocker告诉浏览器最新的hash以及触发ok事件,触发ok事件之后,会通过webpack去检查更新的内容,从而去决定是reload还是进行模块替换,如果要模块替换,就调用hotApply函数进行新旧模块代码的替换,然后结合_webpack_require_去执行代码。因为这一切都涉及到当前运行环境,所以需要一开始时将websocket跟dev-server的代码塞进去bundle中,让客户端中也能执行这一块的代码。
  另外两个需要注意的点就是:
(一)获取当前更新模块id时,是通过jsonp,可以立即直接脚本
(二)当webpack监听到文件变化之后,是写进内存而不是写进文件系统,这样可以加快速度。
自己理解画的图:

掘金跟知乎上的图:

封装load-btn

  之前由于公司组件库使用的load-btn是一直有load这个icon的,只是当loading的值为true时这个图标才会转动。但是产品线是要求该icon只在load的时候才出现。于是每次使用这个按钮,都需要使用v-if去写两次。所以需要自己重新对该load按钮进行封装。

  接下来阐述下封装这个按钮的几个过程。

version 1.0 (接收参数:text、loading)

1
2
<load-sfv-btn v-if="loading">{{text}}</load-sfv-btn>
<sfv-primary-btn v-else>{{text}}</sfv-primary-btn>

这种看着感觉没啥问题,但是等到我用另外种按钮的时候就需要额外拓展。

version 1.2 (接收参数:text、loading、type(按钮的类型有primary、differ、primary、 danger、 blank、secondary、 circle))

1
2
3
4
5
6
7
8
9
10
11
12
13
<span v-if="isPrimaryBtn">
<sfv-button-primary @click="handleClick"
v-if="!showLoading">{{text}}</sfv-button-primary>
<sfv-button-load v-else
loading
class="sfv-btn-primary">{{text}}</sfv-button-load>
</span>
<span v-if="isNormalBtn">
<sfv-button @click="handleClick"
v-if="!showLoading">{{text}}</sfv-button>
<sfv-button-load v-else
loading>{{text}}</sfv-button-load>
</span>

此时代码有点相似而冗余了,此时代码审核人mc建议这种根据不同状态展示相似组件的可以考虑用render去实现

version 1.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
render (createElement, context)  {
let {text, loading, type} = context.props,
domName,
btnClass;

if (type === 'primary') {
domName = 'sfv-button-primary';
btnClass = 'sfv-btn-primary';
} else {
domName = 'sfv-button';
}


if (loading) {
domName = 'sfv-button-load';

return createElement(
domName, text, {
props:{
loading:true
},
class: btnClass
}
);

}

return createElement(
domName, {
on:{
click:context.data.on['on-click']
},
domProps: {
innerText: text
}
}
);
}

此时是可以看到感觉两个return应该是可以写成一个的,但是不知道为啥那个load-btn中text用domProps的形式传进去总是会有问题。此时mc觉得这种写法既不可以传slot、又不可以传其他类型的btn,感觉整个组件的灵活性比较差。此时重新看了组件源码,发现组件里面其实是通过不同的类名去区分组件的。因此有了版本1.4。

version1.4

1
2
3
4
5
6
7
8
9
10
11
12
<sfv-button @click="handleClick" 
v-if="!showLoading"
:class="typeCls">
{{text}}
<slot name="content"></slot>
</sfv-button>
<sfv-button-load v-else
loading
:class="typeCls">
{{text}}
<slot name="loadContent"></slot>
</sfv-button-load>

此时感觉有点兜兜转转倒回去的感觉,只是加多了多种按钮类型的拓展、同时添加了slot。但是乍一看才知道这种写法传slot的话还得传两个,既不够简洁、反而使用起来还麻烦了。
version1.5

1
2
3
4
5
6
7
<sfv-button @click="handleClick"
:class="typeCls">
<i slot="icon" :class="iconCls"></i>
<span v-if="text">{{text}}</span>
<slot v-else
name="content"></slot>
</sfv-button>

这次是通过动态class(公司组件中icon的展示用的是类名),感觉比上次简洁了点,但是此时还需考虑不直接使用封装好的load按钮,而是直接使用的图标控制,会不会造成原来组件功能不能使用。此时发现了一个bug,就是调用这个封装的组件时,配置disable属性是不生效的,此时有了version1.6
version1.6

1
2
3
4
5
6
<sfv-button @click="handleClick"
:class="typeCls"
:default-disable="disabled">
<i slot="icon" :class="iconCls"></i>
<slot name="content">{{text}}</slot>
</sfv-button>

此时问题又来了,我发现这个btn组件除了disable,应该还有其他属性的,所以有了目前的终极版本,希望以后还能继续改进。

1
2
3
4
5
6
7
<sfv-button @click="handleClick"
:class="typeCls"
v-bind="$attrs"
v-on="$listeners">
<i slot="icon" :class="iconCls"></i>
<slot name="content">{{text}}</slot>
</sfv-button>

一天花了不少时间在优化这个组件,从写重复的html,到写render函数,到写回html(加动态类型、动态icon、slot、$attr),到考虑组件库升级之后可能会发生的问题(用类型去添加icon,如果新版本把类名改了就容易出问题),感觉自己造轮子的能力还是有很大的提升空间,应该多去学习一些优秀源码的写法、设计思想。

前端不要轻易轮询

  如非必要,尽量不要轻易轮询。盲目地用轮询往往会增加工作量、给服务器造成负担、没有实现实际功能。这里阐述下几种本来需要前端轮询、后面变成后端控制的场景。

  一、后端操作有延迟、比如需要社保苏醒时间等
  执行某种编辑操作之后,前端在成功回调中刷新页面,结果数据不刷新。后端有某些操作是会延迟1~3秒才能真正地生效,这种情况,不要轮询、不要前端去瞎setTimeout,而是要让后端在操作真正完成之前将接口处于挂起(pending状态)。这样子才能将页面刷新的时间缩短至最小。

  二、后端也去轮询查询
  场景:后端去轮询某个接口,最长时间不超过一分钟(其实一般如果请求封装的时候有设置超时时间,可能需要考虑轮询,或者看看能不能将请求的超时时间改成可配置,默认是设置的时间,但是可以针对特定接口进行修改)。如果项目没有设置或者设置的超时时间比后端(后端去轮询一般也有设置超时时间)的要长,那么应当让后端将接口挂起,而不是轮询。

  三、业务需求:实时更新服务截止日期。
  这种轮询需要明确是否有必要,像我们的webui项目的token过期时间就是半小时,这种一般重新登录之后,会自己重新去请求服务是否过期的接口,可以不再轮询。这种情况,如果要轮询,需要尽量减少轮询的时间,比如距离服务过期还剩下一天才开始轮询或者是其他方法

var location = window.location引发的坑

  之前项目修改了一个东西,就是将之前的login_url由指定的字符串改成了动态的ip,在使用了location去保存window.location之后,使得网页每次加载时都一直疯狂地请求资源,感觉是陷入了循环。这个bug我找了几个小时都没找到。当时是有发现当login_url后面加一些会报错的符号时,页面能正常加载,所以就把错误定位在了url上,可是后面一直把错误定位在后端对请求的转发造成没成功请求到资源的问题,现在仔细想,当时定位bug的思路是有很大问题的。

  首先,我应该意识到有代码造成了循环——因为查看了网络资源确实不断地在加载。其次,我应该意识到login_url的拼写错误或者是资源定位错误都不可能会有不断加载资源的问题。所以之后想问题纯粹点,别想那些让人感觉困惑的点。如果一开始就想到循环的问题,或许能早点解决问题。

  最后的还需要学习的就是不要用关键字作为变量名——js关键字包括,这次var location其实指代的就是window.location,如果我有在控制台进行尝试,应该是可以非常快速的发现问题的,尽管这个代码不会报错,但是在控制台造成的问题确可以让我很快地意识到问题在哪里。

  附上一个有保留关键字的链接

Vue中的data函数不会对数据进行深拷贝

  今天代码rv的时候,审核人说不能直接修改父组件传的值,我代码里写的是 data() {return {commonForm: form}},然后通过修改commonForm的值,最后提交的也是commonform。按照常理来说,我改变commonForm确实form也会改变,但是我想起来我写了不少代码都是这种操作,但是控制台也没有警告说我修改了父组件的值,此时我以为Vue中的data函数会对数据进行深拷贝。结果后面在父组件进行监听,当加上deep:true时,发现子组件的commonForm进行修改时,父组件也会被修改,但是当没有加上deep:true时,则监听不到,由此推断出控制台监听的那种是默认为deep:false的,这种情况下,只有改变this.form对象的地址,控制台才会报错。由于这种操作确实改变了父组件的值,违反了单向数据流,所以需要使用深拷贝!!
  

forEach不能采用break或者returen false终止循环

  前几天写PPT项目代码的时候,想要找到符合条件的一项,就跳出循环,不要继续循环,结果我用了break发现没用,查了一下原来forEach要终止循环需要通过try catch。。。于是发现some是提前终止循环的。但是some也有个坑要提防,那就是some去遍历一个空数组,会返回false;

字段值可能为undefined的处理

  前几天在写代码,后端传一个code值给前端,前端根据常量表决定展示的文案以及字体颜色,当code为-1时,不展示内容。因为后台有做不展示的处理,所以我误以为该字段是必定会返回的。后面出现bug才知道该字段是在某种特定类型的工单才会进行返回。于是我在页面渲染的时候用v-if=”data.code && data.code !== -1”去进行判断,此时问题又来了。此前约定的code里面包含了0,而0也是要展示的,如果用这种方式进行判断,则code为0时这里的v-if的结果为false。所以之后开发应当明确两点,最大可能地避免bug。
  一、明确字段是否返回,尽可能地处理字段为undifined的情况
  二、处理字段为undifined时,分情况,如果该字段是一个object类型的,用loadash中的get进行处理(此处要注意loadash的按需引入方法);如果该字段是某个对象的属性,可以用obj.hasOwnProperty进行判断,而不是用data.code && xxx 去进行判断,因为这里的code可能为0或者是空字符串。