1

开始

不知不觉加入工作已经有好一段时间,对于开发来说刚好就是到了十字路口的时候:走技术还是走管理路线,这确实是一个挺艰难的抉择,毕竟影响自己后面的路是否走得更宽更远。于本人来说,还是想继续往技术的路子一路走去,因为回顾自身为什么搞开发,做一只早出晚归的程序猿,更加多的是对技术的向往。而自我感觉这段时间,一直只关注如何做到一件事,却很少关注如何做好一件事;做好与做到虽然一字之差,但是付出的专注与努力,却是几倍于后者,更加考验自身的技术,全局观,沟通能力等等;所以现在就给自己一个目标吧,前5年在思考如何做到,那么后5年就思考如何做好每一件事。

温馨提示

下面的内容会夹杂着自己各种各样的想法,或者遇到的小问题,如果雷同的想法和场景,纯粹巧合。

后管项目如何做得更好

对于后台管理项目,感觉很多前端(包括我)都会觉得没必要浪费什么时间在上面:搞优化,搞布局,搞体验这些,业务一把梭,毕竟这些项目服务也就公司里面的几个运营和产品;有更多时间可以花在真正面对用户的业务或者新技术的学习上。
好吧,直到某一天,我们的测试同学很认真负责的测试了这些页面,给了报了一堆bug,我顿时陷入的沉思:这后管咋那么难搞!
抱怨归抱怨,也需要认真思考一下后管项目的问题,毕竟不想一直被这些后管bug缠身,下面就是个人的总结。

后管项目要快,稳,好

  • 建立好常用的正则表达式库,例如:电话,手机号码,姓名,地址等;
    最好跟后端的同学要协商一致,减少出现前端能通过,但是后端校验不通过的情况。
  • 确定好表单的交互形式:前置检查(不通过按钮置灰),后置检查(点击提交才去做检查提示);
    这两种检查,各有优缺点:前置检查可以减少挫败感,但是表单项输入多或者复杂的情况,反而容易引起困惑(按钮一直置灰,不知道哪个地方没有填写,需要来回查看);后置检查,则刚刚相反容易增加挫败感,有可能提交个三四次才能正确提交整个表单,但是在复杂的情况下,可以有明确的提示,哪些表单项没有填写遗漏或者格式错误。
    为什么要花时间纠结于这种地方了,因为在处理表单的时候,这两种交互的选择会影响代码的编写的,而且明显后置检查要比前置检查容易实现(前置检查要监听整个表单模型对象每个字段的值变化,每次变化都要校验表单);后置检查则可以更加讨巧一点,甚至可以依赖后端同学返回的错误信息来直接提示用户,毕竟后端同学那边已经做了更加完整的校验。
    而个人认为,对于后管项目应该更倾向于后置检查的,毕竟后管页面的表单很多时候都比较的复杂(但是测试莫名其妙的喜欢前置检查)。
  • 返回json结构必须得确定好,一般情况下都类似这种:

    {
        code: '0000',
        message: '',
        body: {}
    }

    但是后端有时候他们的数据很多时候都来自其他的系统,有时候他们会很取巧,直接把别人的返回结果扔到前端,也不做重新的封装,很明显这非常不好,所以返回的json结构必须是明确的。

  • 错误码也必须统一好;
    有时候后端返回的错误有时候几个错误码都代表一种场景,例如用户登陆失效的情况后端可能返回这样的几个错误码:612,606,401等,明显这些错误分别可能对应的是造成没有权限访问的几种情况,可能是cookie过期,用户不存在等情况。这时候对于前端的判断就比较麻烦了,而且有时候后端多增加一种状态码有时候也没有通知到前端去做相应的修改,这就容易导致页面出现问题了。
    所以我个人思考了一下,错误码的位数可以多增加一下,例如加到6为,前4位是错误的主类别,后边2位留作错误的细分,这样一个无权限访问的错误码可以是:400100,400101等,前端也更好对某些错误做个整体的判断,后端增加错误码也不容易导致前端出问题。
  • 多定义枚举,常量;
    这一条其实在其他项目也是一样的,更多的枚举和常量,然后搭配Typescript,对项目的可维护性和稳定性是有很大的意义的,毕竟字面值来回复制是很容易出错的,而且修改起来也相当不方便。
  • 请求的封装;
    页面的请求我们很经常都会写出这样的模板代码:

      function doRequest() {
          this.loading.show(); //开启loading动画
          this.request(url).then(()=> {
              //业务处理
          }).catch(()=> {
              //错误处理
          }).finally(()=> {
              this.loading.hide();//关闭loading动画
          })
      }

    而request方法可能会是这样的:

      axios.get(url).then((res)=> {
          if(res.code !== '0000') {
              return res.body;
          } else {
             //公共错误处理
             switch(res.code) {
              case '0001':
                  //错误处理
                  break;
              case '0002': 
                  //错误处理
                  break;
             }
             throw new Error(res.message);
          }
      })

    好,再回头思考一下对于请求的封装

    • 并不是所有请求都需要loading动画
    • 并不是所有请求都需要默认的错误处理

对于第一个问题,因为有些请求都是在用户不用感知的情况下发送的,所以我们可以加多一个参数控制这个loading动画,例如调用时:

  this.request(url, {slient: true})

而doRequest方法,则:

  if(!opts.slient) {
      this.loading.show();
  }
  axios.get(url).then((res)=> {
      ...
  }).finally(()=> {
      if(!opts.slient) {
          this.loading.hide();
      }
  })

但是这里也会引申另外一个问题,关于loading动画的关闭,如果loading动画关闭时没有做任何处理直接关闭,则在多个需要loading动画的请求并发的时候,一旦其他请求完成就会立马关掉loading,其实还有其他请求还没完成的,还是需要用户等待。所以loading动画需要加入计数器,当计算器归零的时候才是真正的关闭loading,那也必须要求开发的时候show和close方法必须是成对调用的。
至于另外的一个问题:默认处理逻辑,有时候我们的请求可能需要对某个错误码做一些特殊处理,例如登陆失效的时候,公共的处理器可能是直接跳到登陆页面,但是某个业务逻辑可能需要弹出对话框让用户确认跳转。所以:
[我们的错误处理逻辑]-> 公共的错误处理逻辑
但是正如之前代码所示我们的公共的错误处理逻辑是直接加入请求完成后,完全没有机会把我们自己的错误处理逻辑插入到公共的处理逻辑前面,所以一旦我们出现一些刚刚那种场景就比较麻烦了。
所以一种其中一种封装的方式是使用unhandledrejection事件,因为我们都是使用promise,uncatch的promise rejection会触发这个事件,所以可以把公共的错误处理逻辑迁移到这里处理,但是这个事件有兼容性问题。
所以再思考另外一种方式:

  this.callRequestWithErrorHandler(
      this.request().then(()=> {
          // 业务处理
      }).catch((err)=> {
          // 特殊处理
          throw err; //重新抛出到公共处理器
      })
  );

callRequestWithErrorHandler的实现:

  function callRequestWithErrorHandler(request, opts) {
      if(!opts.slient) {
          this.loading.show();
      }
      return request.catch((err)=> {
          //公共处理逻辑
      }).finally(()=> {
          if(!opts.slient) {
              this.loading.close();
          }
      })
  }
  • 防止用户二次触发;例如对话框弹出,如果是对话框是立即显示的一般是不需要的,因为用户没有时间做其他操作,但是有些情况,例如访问一个接口之后才显示对话框,这就有必要防止用户二次触发了,因为用户有时间去做其他操作,可以触发两次对话框显示,如果对话框不是单例,还是会弹出多个对话框;当然解决方案也有很多:例如全局loading,而且需要mask屏蔽操作,要么就是对触发的按钮添加标记,防止二次触发。

列表

后台项目最多就是列表,几乎所有业务都是从一个列表开始,那么一个列表又有哪些细节需要注意的尼,
例如,关键字模糊查询,分页类似的参数应该是要添加到url的查询参数上的,这样用户可以跳去其他页面的时候,也能返回到之前的分页;
所以分页和模糊查询首先应该改变url,但是这样的话需要能够感知url变化然后重新读取分页和关键字参数,然后触发请求,而且页面进入的时候也要首先从url中取出参数,再请求。

H5项目用户至上

这里收集的是H5的最佳实践还有优化建议。

用户退出确认

最近在h5上做的一个业务,页面是一个spa,有这么一个场景,需要用户填写地址,当然对于表单在用户退出的时候是要给于适当的提示的。
在Vue里面,用户退出确认明显都应该在beforeRouteLeave钩子里面做处理,先看我第一种处理:

    beforeRouteLeave(from, to, next) {
        if(!this.dialogVisibled) {
            this.$alert('...').then(()=> {
                next();
            }).catch((err)=> {
                next(false);
            });
            this.dialogVisibled = true;
        }
    }

这样子对话框确实也正常显示出来了,用户确认之后也确实能够返回上一个页面,感觉功能也完成了。
但是问题来了,如果用户在弹出对话框之后如果再点一下后退那又会怎样的尼,点了第一次后退,虽然我们没有调起next方法,但是路由确实是变化了的,变成上一个页面的路由,再点一下后退有会从上一个页面的路由跳去上上一个页面的路由,甚至是直接退出整个页面,很明显这不是我们所期待的交互。
其实这里的问题主要是,前端没有什么办法阻止历史的记录的后退操作,所以这里的next很有迷惑性,初期以为只要没有调起next方法路由是不会变化,因为我们在beforeRouteEnter的时候next方法不调起是不会改变路由的。
知道问题的怎么来的,那就想办法去解决吧:

    beforeRouteLeave(from, to, next) {
        if(this.shouldLeave) {
            next(); //直接离开
            return;
        }
        if(!this.dialogVisibled) {
            this.$alert('...').then(()=> {
                if(to是上一级页面) {
                    this.$router.back();
                    this.shouldLeave = true; //用户确认离开不用弹出对话框了
                } else {
                    this.$router.push({name: to.name});
                    this.shouldLeave = true; //用户确认离开不用弹出对话框了
                }
            }).catch((err)=> {});
            next(false); //保持路由原样不变
            this.dialogVisibled = true;
        } else {
            next(false); //保持路由原样不变
        }
    }

这里用了一个shouldLeave来标出用户是否确认离开,如果是为true那么就直接离开路由了,如果为false,就弹出对话框,然后立马调用next(false)保持路由原样不变(虽然点击后退的时候路由是立马改变了,触发popstate事件,但是next(false)会立马把旧的路由压回去,保持路由不变),所以现在用户不停点击后退也是不可以退出页面的,必须取消或者确认。
那么问题又来了,是真的不可以直接退出页面吗?,那肯定不是,如果这个页面刚好是路由历史记录里面唯一的记录,那它还是会直接退出页面的,因为h5在webview的容器里面beforeunload事件是不可用的,真的没想到还有啥办法可以触发这个退出提示。所以刚刚的解决方案在二级路由的时候这个处理才是可行的。
而另外一个解决方法就是客户端在用户点击返回的,先触发前端一个回调,当前端返回为true的时候才可以退出路由。

页面来回滚动多次部分页面元素会消失

前段时间有同事做的一个活动页面遇到了一个问题,就是页面在来回快速滚动之后,页面的元素(头像,文字这些)会消失,而这个问题只能在移动端才能复现,在pc上无法重现出来。
第一次遇到这样的问题,首先怀疑是不是这部分部分元素是不是被隐藏或者移除,但是从vConsole上dom的节点元素都在,也没有被隐藏;好吧,继续看是不是触发vue组件的更新,打印了一下,vue组件也完全没有触发更新,所以也不存在由于页面更新出现了闪烁。
感觉一下子毫无头绪,难道由遇到兼容性问题吗,再仔细深入一下,最后在chrome dev tool上看了一下页面layers,发现这个活动单页,竟然存在多个layer,这明显不合常理的;继续看一下这些layer的触发原因:

Reasons with a "backface-visibility: hidden" style

好明显这些layer是由-webkit-backface-visibility这个样式属性触发的;
那么很快就发现这个元素匹配的一个样式:

    * {
        ...
        -webkit-backface-visibility: hidden;
        ...
    }

大概问题的元凶发现了,看一下这个样式的前后影响,没有注释之前:
注释前
而注释后:
注释后
layer瞬间少了很多,再经过测试,问题也没有再发现,估计是移动端浏览器对layer的数量有限制。
再温故而知新,layer到底是什么来的,参考了一下其他博客的文章:
浏览器layer知识
浏览器layer知识
再简单总结一下,浏览器主要有两种RenderLayer和GraphicLayer;而我们在开发者工具上看到的layer都是GraphicLayer,它们整个关系逻辑如下:
图层关系
而我们在开发者工具看到的都是GraphicLayer;那么是如何触发这些layer的生成的尼;
触发RenderLayer的原因主要有:
(1)生成普通PaintLayer(SelfPaintingLayers)的原因:

 1.document
 2.非static的position属性
 3.opacity小于1
 4.有css filter属性
 5.有css mask属性
 6.css mix-blend-mod属性
 7.有css transform属性
 8.backface-visibility为hidden
 9.有css reflection属性
 10.有css column-count属性或者column-width属性
 11.动画改变 opacity, transform, filter, and backdrop-filter.

(2) OverflowClipPaintLayer:overflow非visible
(3) NoPaintLayer:没有需要paint的LayoutObject
其他LayoutObject与最近的祖先节点分享PaintLayer。

所以我们经常可能就是由于opacity或者设置绝对/固定定位还有transform属性而触发RenderLayer的生成了;而这里也可以看到刚刚遇到的backface-visibility属性毫无疑问它也会触发一个RenderLayer的生成。

再看GraphicLayer
(1)本身节点原因

 1.拥有硬件加速属性节点的iframe,如果一个iframe没有CompositingLayer,则该iframe会与父document分享CompositingLayer
 2.Video节点
 3.Video内的控制条
 4.3D或者硬件加速2D的canvas节点,getContext(‘2d’)是不会升级的
 5.硬件加速的插件,如flash
 6.在高DPI的设备里,fixed节点会自动升级为CompositingLayer,由于PaintLayer升级会改变font的渲染模式(测试在pc chrome fixed元素也会升级)
 7.3d transform
 8.backface-visibility为hidden
 9.动画或者缓动改变 opacity, transform, filter, and backdrop-filter,当动画停止的时候则恢复PaintLayer.
 10.will-change设置为 opacity, transform, top, left, bottom, or right. 
 11.position为fixed或者sticky

(2)重叠原因

 1.一个CompositingLayer被覆盖,则该覆盖者自动升级(squashing,该覆盖者升级的CompositingLayer是被覆盖的CompositingLayer衍生出来的,两者同级)
 2.一个CompositingLayer被有filter属性的filter部分覆盖(测试没有发现有升级)
 3.被transformed元素覆盖(squashing)
 4.被overflow:scroll or auto节点覆盖
 5.兄弟节点有动画或者缓动改变opacity, transform, filter, and backdrop-filter.

(3)Layer Squashing 层级压缩
如果有多个PaintLayer与一个CompositingLayer重叠,这这些PaintLayer公用一个CompositingLayer
但是,有些情况不会公用

 1.使用了mask属性,子节点覆盖与父节点同级的CompositingLayer A,此时该子节点squashingWouldBreakPaintOrder的squashingDisallowed,不能被A衍生一个CompositingLayer公用,而是自己独立一个CompositingLayer。
 就是覆盖者存在CompositingLayer的祖先节点,而被覆盖者在该祖先节点之外,则会独立一个CompositingLayer
 2.升级为CompositingLayer的iframe不会与任何节点压缩一起squashingLayoutPartIsDisallowed
 3.有reflection的PaintLayer不会与任何节点压缩在一起,会独立升级squashingReflectionDisallowed
 4.当覆盖者和CompositingLayer不是同一个剪贴容器,比如CompositingLayer被一个overflow:hidden节点包裹
 5.当覆盖者和CompositingLayer存在一个不同祖先节点,而这个祖先节点有opacity小于1
 6.当覆盖者和CompositingLayer存在一个不同祖先节点,而这个祖先节点有filter
 7.当覆盖者正在缓动或者正在动画,结束后恢复squash
 

说真的内容还是挺多的,幸好现在的开发工具会有触发layer的原因提示,可以让我们快速定位问题


tain335
576 声望196 粉丝

Keep it simple.