Decorative image frame

利用visibilitychange对项目进行优化

  我目前做的项目中由于后台架构设计的不合理(后台架构本来应该设计成类似node的事件通知类型的,但是由于项目一开始选型的问题,导致后面很多操作都得依赖前端轮询来实现。) 同时我们的项目有一些跳转操作是直接打开一个新的标签页(方便使用人员比对、查看数据),所以会导致一个浏览器里面打开了好些相同的标签页,然后标签页又有许多没必要进行的轮询。当遇到visibilitychange事件时,我知道,就是它了。

  优化价值:减少CPU占用,减少不必要请求对带宽的占用,同时能比较大地减少服务器的压力。

  这次我总共尝试了三种不同的方法,下面对这三种进行描述,最后再进行对比总结。

   第一种是使用自定义生命周期的方法,在main中创建vue之前,往Vue.config.optionMergeStrategie中加入pageVisible跟pageHidden这两个生命周期,然后在创建Vue之后,在window上绑定一个visibilitychange的监听事件,每次事件被触发之后就利用递归去执行每个组件中主次的这两个生命周期。在组件中的具体使用方法是:在pageVisible中初始化轮询方法,在pageHidden中对定时器进行销毁。

   在每个使用轮询的组件中加入这两个生命周期其实是挺繁琐的事情,所以我试图寻找一种全局控制的方法,所以就有了第二种尝试。这一次我试图去对window中存到的timer的信息进行删除跟初始化,进行了一波骚操作之后我发现这种方法是不可行的——这种对window下的timer进行删除跟重新创建定时器的方法,可能会导致组件中原来保存的timer其实是过期的,这时候组件在beforeDestroy中清除的定时器是已经被清除的了,而真正的定时器还是存在的。

   第三种方法,也是我最后使用的方法。分析了一波项目,发现里面用的定时器都是有经过封装的intervalTask的实例,我只需要遍历组件,然后找出每个组件中intervalTask实例化的属性,对属性轮询的状态进行判断之后再调用其中的intervalTask的start、stop的方法,基本就能解决大部分情景的问题了。

   由于第二种方法是失败的,所以主要对比第一种跟第三种方法,第一种方法的话比较繁琐,所以编码的人可能经常忘记这种处理,但是这种方法对性能的影响会比较小。而第三种方法虽然集中统一地处理了,但是在递归对组件的属性进行遍历的时候,应该还是比较耗费性能的。

最后附上第三种方法具体实现的代码

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

import IntervalTask from 'src/util/interval_task';

const PAGE_VISIBLE = 'pageVisible',
PAGE_HIDDEN = 'pageHidden';

let goThroughAllAttr = (vm, callback) => {
for (let key in vm) {
if (vm.hasOwnProperty(key)) {

if (Object.prototype.toString.call(vm[key]) === '[object Object]') {
let attr = Object.getPrototypeOf(vm[key]);
if (attr && attr.constructor === IntervalTask) {
callback && callback(vm[key], vm[key].state);
}
}
}
}
};

// 通知所有组件页面状态发生了变化
let notifyVisibilityChange = (pageState, vm) => {

if (pageState === PAGE_VISIBLE) {
goThroughAllAttr(vm, (intervalTask, state) => {
if (state === IntervalTask.STATE.STOPPED_BY_ROOT) {
intervalTask.startByRoot();
}
});
}
if (pageState === PAGE_HIDDEN) {
goThroughAllAttr(vm, (intervalTask, state) => {
if (state === IntervalTask.RUNNING || state === IntervalTask.STATE.WAITING) {
intervalTask.stopByRoot();
}
});
}

// 遍历所有的子组件,然后依次递归执行
if (vm.$children && vm.$children.length) {
vm.$children.forEach(child => {
notifyVisibilityChange(pageState, child);
});
}
};

/**
* 将事件变化绑定到根节点上面
* @param {*} rootVm
*/
export function bindVisibilityHook (rootVm) {
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {

// 通知所有组件生命周期发生变化了
notifyVisibilityChange(PAGE_HIDDEN, rootVm);
} else if (document.visibilityState === 'visible') {
notifyVisibilityChange(PAGE_VISIBLE, rootVm);
}

});
}

找回删除分支中的commit

  之前一个项目的一个模块已经做的差不多了,然后规划那边又说不要了,这时候把代码上库也不是,删除了也不是(万一后面又要用了呢),所以我就习惯地把代码commit了,commit信息写的是“暂存”。但是过了很久这个分支还是没有用,我就把它删除了。后面又要了,其实找这个东西不难,就是因为我的commit里面有几个都是叫暂存,虽然“暂存”最后被不会被我推入库,但是就因为这过于随意的命名,使得我找回的过程略显曲折。

找回步骤分两步:
第一步: 查看分支的变更记录——这一步需要在记住分支名的基础上,所以个人本地的开发分支命名也得有规范

1
git reflog | grep '分支名'

第二步: 查看之前commit的信息,找到对应的要回退的id

1
git checkout -b 分支名 HEAD@{id}

IE下get请求报错排查

  报错原因:在ie下pathInfo的编码方式为UTF-8,而queryString的编码方式是GB2312编码(谷歌、火狐都是UTF-8),所以后台解析不了queryString中的内容,也就报错了。
  解决方法:使用encodeURI对url进行包裹,或者是将get请求改成post。

页面加载过多DOM节点导致卡顿的优化实践

  安全运营中心新工单首页的工单详情——误判确认环节在开发时,确认的该节点展示的ioc条数最多为20条,但是实际上线后却发现展示的条数最多为1000条(这1000条数据展开来还有表格、图表等节点),所以当某条工单拥有1000条ioc节点时,不仅页面空白时间长,而且当数据加载完之后会直接导致网页崩溃。。

  之前优化的方案:1000条数据不一次性加载,而是10条10条加载,这样子加载可以减少空白的时间,更快地渲染数据,但是这样优化的缺点就是在数据加载的过程中,滚动条会一直缩小,导致数据加载过程中滚动条几乎滚动不了,等到数据加载完成后,页面依旧会卡顿。

  目前的优化方案主要有两方面,交互层面控制ioc展示的数量(最多只展示100条),前端代码层面的优化主要有如下两点:

一、一次性加载所有的数据,但是不提前渲染没打开的ioc内部Dom节点,只渲染需要提交给后台的节点。

二、用对象映射+render渲染的方式取代在template中用大量的v-else-if判断渲染哪个组件。

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

map对象写法:
[ORDER_STAGE_TYPE.notReady]: {
name: 'not-ready',
ref: REF_NAME,
props: {
orderId: this.orderId,
stageName: this.stageName
},
on: {
'is-ready': function (stageName, data) {
vm.dataIsReady(stageName, data, index);
}
}
}


render函数写法:

renderTask (renderCallback) {
let vm = this;
return vm.tasks.map(function (item, index) {
if (vm.shouldShowTask(item)) {
const MAP_ITEM = vm.getHandledMapItem(item, index);

return renderCallback(MAP_ITEM.name, {
class: MAP_ITEM.class,
props: MAP_ITEM.props,
ref: MAP_ITEM.ref,
refInFor: true,
on: MAP_ITEM.on
});
}
});
}

其中rener中的refInFor:true是很重要的一个属性,如果没有配置该属性,则用$refs获取的是单个对象,如果配置了则是一个数组,对于遍历dom节点获取提交给后台数据的,获取的是数组这点可是至关重要。

bug解决记录——页面切换后定时器没有销毁

  前几天我给首页(路由为/home)添加了一个定时器,但是我发现“/home”下跳转到“/home/order-manage”时该定时器不仅没有被清除,反而定时器被触发的时间间隔从我设定的5秒,变成了2、3秒,一开始我以为是调用定时器的库有问题,但是回想到没有改变路由之前,定时器调用的时间间隔一直是准确的。。此时我一直在调试定时器封装库的代码,而后发现created钩子居然被调用了两次,但是destroy却没有被触发,这个时候我明白了为什么定时器触发的时间间隔会变短,原因就在于创建多了一个定时器,所以两个定时器被间隔触发,看起来就好像间隔变短了。
  destroy没有被触发的原因:一开始我定位的是在同个页面下,destroy中的代码没有被执行(因为当时注意到的是这两个路由指向的是同个页面,只是内容不同,现在想来这个想法好愚蠢,路由都变了,组件肯定是有被重新调用的,至于/home下的destroy没有被执行,这是因为使用了keep-alive)。
  所以之后在公共组件(被两个页面同时使用)中使用定时器,需要监听路由的变化来判断是否要创建、销毁定时器,而不能直接创建定时器。同时,在涉及到页面之间跳转的bug时,要多考虑路由方面的问题

bug解决记录——给远程搜索框赋值后显示的是id而不是name

  td描述:点击重置按钮后(前端会调用接口给表单赋值),原本显示名字的搜索框居然显示成id,而且该现象是偶现。
  一开始我一直花时间在复现该td,但是我发现一直复现不了。于是直接去看代码逻辑,我起初怀疑是$set的问题,但是如果是$set的话,该问题应该是必现。后面我把问题定位在是给选择框赋值的时候,下拉框中的数据还没加载好,搜索框会因为没找到匹配的项而直接展示成id。但是当我把加载下拉框中的数据放在setTimeout中执行时,居然还没办法复现这个td时,我对这个方向的定位产生了怀疑。后面我直接把加载下拉框中的数据的代码给注释了,于是问题就复现了。。。讲真,我在复现这个问题的时候,测试跟我说的是只有线上环境的火狐浏览器才会,于是我为为了直接复现这个td花了几个小时,现在看来这样做真的傻的一匹,原因有如下两点:
  1、测试的话并不完全可信,因为他们对于逻辑不严谨的代码产生的bug不会测试的面面俱到;
  2、很多时候,留心代码的逻辑,多调试,总是能看出漏洞在哪里

  那么为什么这个td是偶现呢,问题就出在了缓存。在还没重新加载下拉框中列表的时候,这个时候网页中保存的下拉框列表的数据是上一次搜索返回的,因此只要你上一次搜索的下拉列表包含了重置操作中返回的数据,就能够在下拉框中正常显示name。相反,如果上一次搜索框查询的数据不包括重置返回的数据,则会复现该td。(普通选择器没出现该bug的原因就在于普通的选择器下拉框的数据基本都是固定的,基本不存在从接口中取回的数据是下拉框中没有的情况。)所以我感觉这种不是必现的bug其实可以直接定位是不是缓存数据的问题,如果不是,再定位到其他方向上。
  这个td最后的解决方案是把给选择框赋值的操作放在$nextTick中去完成。普通选择器没出现该bug的原因就在于普通的选择器下拉框的数据基本都是固定的,基本不存在从接口中取回的数据是下拉框中没有的情况。

IE缓存策略带来的问题

  今天测试在ie11中发现新工单中提交到下一环节后,上一个环节还是原来的样子。因为这一块涉及了好几个接口,然后业务逻辑又比较复杂,同时因为谷歌下是正常的,所以我一开始就排除了是数据的问题,这让我几乎浪费了一天。。。
  请教同事后才发现原来IE对同一个get请求,如果请求的参数是没有发生变化的话,默认会用缓存(尽管此时浏览器请求的接口是返回200而不是304),(同时我发现如果打开调试工具,则默认会勾选 从服务器读取最新数据,但是如果没打开调试工具,则是按照上面说的缓存策略执行)最后在请求中加上如下代码即可解决:
  xhr.setRequestHeader(‘Cache-Control’,’no-cache’);
  xhr.setRequestHeader(‘Pragma’,’no-cache’);

  其实有另外一种方法就是在请求后加一个时间戳,但是这种方式使得缓存不可用,而且这样可能需要在好几个特定的请求中去写一些额外的代码。

  感觉IE真是不好惹,但是下次排查这种还是得从接口去入手,如果觉得接口不熟悉,可以先询问对这块比较熟悉的人,这样可以大大提高效率。

provisional headers are shown

采坑记录:本地开发调用接口时显示请求头显示provisional headers are shown ;

原因是浏览器证书报错:NET::ERR_CERT_AUTHORITY_INVALID,导致请求被拦截;

解决办法:在chrome://flags/#allow-insecure-localhost中enabled #

路由控制权限控制

  工单的权限控制中前端的控制分为两部分,一部分是导航栏中的入口,另一部分是隐藏注册的路由(防止用户记住路由后直接输入路径去获取资源)。工单项目中路由的隐藏主要是通过路由表的配置 + 递归处理路由表 + ROUTER.addRoutes(处理完的路由数组)来实现的。
  一、目前项目权限如下所示

专家类型 菜单权限 读写权限 备注
T1,T2,T3 除了xx管理,xx设置,其它全有 有所有权限 可查看所有客户的信息,只能查看分配给自己的工单
T5 所有权限 所有权限 -
渠道 只有工单管理(不包括xx),客户管理(不包括xx) 工单只有查看权限 只能查看自己所属渠道的客户和所属渠道客户的工单

由于这个项目的隐藏需求都是对某个用户开放大的模块,然后再关闭里面小模块的权限,或者是关闭整个大模块的权限,所以我目前采取的配置是,一级模块可以在meta中配置允许访问的roles,然后在子级模块中配置一级路由放通权限,但是子级路由需要关闭权限的用户——在子级路由的meta中配置forbidenRoles。(这种配置方式比较适用于一级二级、三级路由有写成那种父子级的路由配置表)

  二、具体步骤:首先用使用同步请求获取用户权限信息,然后用filter筛选出当前用户允许访问的一级路由,然后递归删除(此处的删除依旧是用的filter)子路由中不让当前用户访问的子路由。

  三、递归删除的方法

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
function deleteForbiddenRoutes (routerObj, role) {  //删除不允许被当前角色访问的路由
if (get(routerObj, 'meta.forbiddenRoles', []).includes(role)) {
return false;
}

if (Array.isArray(routerObj.children)) {
const ROUTES_ARR = routerObj.children;
let filteredArr = ROUTES_ARR.filter(item => {
return !get(item, 'meta.forbiddenRoles', []).includes(role);
});

filteredArr.forEach((item, index) => {
if (Array.isArray(item.children)) {
filteredArr[index].children = delChildForbiddenRoutes(item, role);
}
});
routerObj.children = filteredArr;
}

return routerObj;
}

function delChildForbiddenRoutes (item, role) { //递归删除不允许被当前角色访问的路由
let arr = [];

arr = item.children.filter((childItem) => {
return deleteForbiddenRoutes(childItem, role);
});

return arr;
}