Fork me on GitHub

关于Vue的一次技术分享

本篇博客,是我在公司内部的一次关于vue的技术分享中分享的内容,特意整理一下,放在博客中,做个记录。

概述

对于vue框架来说,并没有太多特别规范化,大家都认可的特别约定俗成的最佳实践,当然,很多软件开发领域的通识规范,例如命名,文件划分,框架结构的分层界限等等,更多的也是通用于各类项目,也不好说这些就是vue的最佳实践。

更多的,也是追求一个能符合自己项目的需求,同时也保证一定开发效率的标准和良好实践,就非常ok了。所以呢,今天也是更多的就跟大家分享一下我在写vue这些时间里自己的一些心得。

最佳实践,大致的,可以分为风格类和功能操作类。

具体来说,风格类,例如缩进规则,命名规则等这些,vue在官方文档里提供了一个风格指南,主要就是关于这方面的建议,例如组件名,prop的定义和使用方式等等这些,大家闲了没事可以去瞄几眼。

功能操作类的最佳实践,例如组件的组织方式,全局状态的管理方式,类似axios请求层的封装调用方式,或者组织vuex这样的数据层的方式,包括如何处理自定义插件指令过滤器等等,官方并没有给出这方面的建议和最佳实践,网上关于这方面的东西也是争论多于讨论。所以要细说vue的最佳实践其实比较还蛮难的,可能大部分地方都得针对项目的具体情况和实际需求,来综合考虑。

当然,其实最佳实践这种东西,在项目中来说,最重要的还是统一就好

组件命名

  • 组件文件名的大小写问题
    (前几天我在linux下跑了某个前端项目,发现竟然是跑不起来的, 最后翻报错发现,原来存在几个组件命名时采用了首字母小写的驼峰命名,但是import时用了首字母大写的帕斯卡命名。
    这在不识别文件名大小写的windowsmac下面是可以的,但是在linux下对文件大小写是敏感的,就会报找不到路径文件的错误,无法打包。
    当然,因为现在大家大部分都是在mac打包编译之后部署的,所以没什么影响。但比如ssr类型的项目,因为是在linux服务器中编译的,如果存在组件命名大小写不统一错误的话,就会出问题。
    对于组件的命名和使用,因为Vue的模板可以是单文件,可以是字符串,可以是原生DOM元素,模板形式不同,命名的最佳规则也不同,只要命名方式统一,就是比较好的。

vue实例对象属性顺序

虽然对象的属性是不区分大小写的,但是我们在写的时候,遵照特定的顺序,可以让代码更清晰一些。这种顺序风格也比较多,我总结了一下官方和iview库的风格,具体特别细的顺序其实也不用规定的太死,大概遵照下面的顺序,会使得组件的内部结构更清晰和易懂。组件特性,包括组件的挂载元素,名称等组件自身特质的一些东西。组件依赖项,包括组件依赖的一些在内部使用的外部资源。组件接口,例如props,双向绑定的model。组件自身的数据,包括data相应数据,compute计算属性组件的方法,包括组件的生命周期钩子函数,watch监听器,methods。我们在写代码的过程里,可以按照类似下面的分类来加上一些空行,使组件的内部结构更清晰明了。

  • 组件特性,例如 el, name
  • 组件的依赖项 directive, filter, componets
  • 组件的接口 props, model
  • 组件自身数据 data,computed
  • 组件自身方法
    事件函数:生命周期函数,watch监听器
    方法函数: methods

这也是element和iview都使用的一种组件对象属性的顺序,在组件比较复杂,组件内属性比较多的时候,也可以根据这种归类使用空行分割,会比较清晰和明了。

模板内的表达式

模板内不要写太复杂的表达式。在模板内的表达式如果比较复杂的话,有几个缺点:

  • 难以阅读
  • 将数据逻辑和渲染层混在了一起,结构不清晰
  • 没法复用
  • eslint无法准确的进行语法检查

因为在模板内的表达式是拿双引号扩起来的,很多时候都堆在长长的一行里,阅读起来比较困难
模板内的表达式将数据逻辑和视图渲染层混在了一起,容易造成混乱。同时在一个地方写了一长串逻辑之后,在其他的地方是没办法用的。同时,eslint也没办法对它进行准确的语法检查,可能造成潜在的bug。

对于这种表达式,我们可以使用methods或者计算属性computed来代替。

页面初始化的数据获取

进入页面后获取

比较常见的一个场景就是,我们进入页面时需要根据某个参数获取一次数据,
随后watch这个参数,例如筛选条件参数,路由参数等,当参数变化时,去再次获取数据。
最基础的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建时获取数据
created () {
this.getList()
},

watch: {
'$route': function () {
this.getList()
}
},

methods: {
// 获取数据的方法
getList () {
 const id = this.$route.params.id
api.getList(id)
this.pageData = pageData
}
},

我们还可以使用watch的immdiate属性,在页面初始化完成,监听器开始工作的时候立即调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建时获取数据
watch: {
'$route': {
immediate: true,
handler: 'getList'
}
},

methods: {
// 获取数据的方法
async getList () {
const id = this.$route.params.id
const pageData = await api.getList(id)
this.pageData = pageData
}
},

顺便插一句let和const的使用,最好遵循最小特权原则,如果不需要某个变量,对象,类做某件事,就不要给它可以做这件事的功能。

在页面加载完成之后开始获取数据,然后让用户等待loading,这种方式不一定是我们想要的。有时候我们想让用户进来的时候,数据就已经展现好了,当页面呈现在用户面前时,就已经是加载好的状态了,我们不需要再显示一个loading状态。就可以通过下面一种方式

页面导航完成前获取

这种方法不是特别常用,但是在某些时候还比较有用的另一种数据获取方式。
就是在导航完成前获取数据,这样
我们不在组件的生命周期里或者监听器里获取数据,而是到路由守卫里获取数据。

1
2
3
4
5
beforeRouteEnter (to, from, next) {
const id = to.params.id
const pageData = await api.getList(id)
next(vm => vm.pageData = pageData)
}

路由守卫中是不能获取vue实例this的,因为这个时候组件实例还没有生成。next是用来resolve这个路由钩子的,调用resolve,就表示这个钩子已经完成,可以继续进入导航下一步了。对于这个钩子来说,就是表示可以开始组件的声明周期了。
通过向next传递一个回调函数,来将我们在路由导航前获取的数据,传递给组件。
这样,这个组件一呈现在用户面前,就是数据已经加载完毕的状态。
从另一个角度来说,这种方式,我们不用在组件页面展示loading状态了,需要在离开的页面展示一个进度条或加载中提示。
当然,这两种方式,还是看需求使用,看需要哪种用户体验。。

key

前端页面的性能,主要就是消耗在页面DOM的插入,更新,删除这些操作导致页面不断的重新生成DOM树,重新布局,重新绘制。

Vitual DOM是基于javascript实现的一种虚拟DOM,将dom对象抽象成保留了页面元素层级关系和基本属性,例如id,class这些的javascript对象,javascript对象更简单,处理速度更快,dom树的结构,属性信息,以及文本内容都可以很容易的用javascript对象来表示。在vue中的虚拟DOM对象,也被称为VNode,是vue将真实的页面结构和数据抽象成一个个包含元素结构信息和数据的节点。

当页面数据更新的时候,vue会重新生成一次虚拟DOM,然后去跟上一次生成的虚拟DOM进行diff,得到两次的虚拟DOM不相同的地方,用专业的语言来说,就叫patch,得到patch之后,vue会将patch打到DOM上去,然后页面再去重新渲染,完成更新。

但是diff的时候,比较前后两次虚拟DOM的不同,如果从头到尾循环往复的一个个比较的话,也是非常消耗性能的,所以vue的diff算法有两个比较通用的规则:

  • 只进行同一层级的比较

  • 如果比较到相同层级的节点存在不同,直接删除旧的,插入新的节点。不会再去比较后面有没有可以复用的。

  • 如果渲染前后节点结构相同的话,就直接复用,继续比较两个节点的字节点,直到结束。
    这样的话,在列表渲染的时候,就会高效的复用DOM结构,大部分时间,只需要更新每个节点所绑定的列表数据就行了。

    通过示意图来说明一下:

    我们打算更新一个列表,向列表插入一条数据
    更新列表

    在更新完成之后,它是这样的:
    withkey

    上面这一排是旧的页面结构对应的vnode节点,下面是vue根据更新后的数据生成的虚拟vnode节点,通过diff算法来比较它们从而决定如何更新页面DOM结构。

    需要注意的一点是,vue只会更新节点绑定的列表数据,对于节点自身的数据和状态,也是跟结构一样,直接复用的。比如我们渲染一个select选择器的option属性,如果原来相同位置的option有个自己的状态disable,那它的结构被复用的话,它的disable数据同样也会被保留。可能会造成比较奇怪的bug,比如列表渲染的第二个元素是disabled的。这也是vue推荐除非特殊情况,一般都应该加上key的原因。
    那如果我们给v-for绑定上key的话。这个key就相当于节点的id,vue在渲染时,对于存在前后key相同的元素,它就会直接复用,如下

    这样,vue通过key,就可以不单单只复用节点的结构,还可以复用节点绑定的列表数据,性能上会优势,同时它一起复用结构和数据,也会避免一些怪异的bug。

    另外提一点,通过上面的解读,大家也可以发现一点,用列表的index来做key的话其实也存在一点点问题,因为这样会导致vue把明明前后不是同一个的节点当成同一个。也就是说,使用index做key的话,大部分时间,其实跟不加key是一样的效果,只不过vue不会报警告。但总的来说,因为大家都习惯了拿index当key,在大部分时间列表项的唯一id也不是那么好找,所以大概了解key的原理,遇到列表渲染奇怪的表现时也好处理一些。

当然,key的作用也不止v-for这些,它另外的作用就是阻止组件复用。

比较典型的, 我们有一个展示 poi 的组件,它的页面路由是poi/${id},我们在它的created函数里根据id获取数据, 现在我们在 poi/22 页面,然后我们通过修改地址栏路由也好,或者通过$router的路由跳转方法也好,将路由改成 poi/23 ,

我们会发现页面并没有变化,这就是因为vue在判断前后节点结构相同,就直接复用了原有的节点,也就不会重新生成和插入节点,当然就不会触发组件的生命周期钩子。

当然,这种时候我们可以通过watch $route来处理,但还有另一种比较简便的做法,就是为路由出口加上key

1
2
<router-view :key="$route.fullpath">
</router-view>

拿路由当做key,这样一来,vue在diff的时候发现前后key不同,就不会再重用组件了,生命周期钩子函数也就会按照我们预期的来触发了。

通过为元素加上key,来保证元素动画的正常触发,也是同样的道理。

总结

回顾一下,对于列表渲染来说,我们可以通过key来增加元素的复用程度,让它不光顺序复用结构,还可以根据节点id身份来复用,但对于路由来说,我们又可以通过key来让它不复用组件结构,从而正常触发组件的生命周期。在组件里呢,我们可以通过生命周期来获取数据,也可以通过增加watch的immediate选项来不使用生命周期就获取到初始数据,我们甚至可以在进入页面前就通过路由守卫先把数据获取了,这也就是vue的灵活性的体现,你想怎么来都行,官方也并没有给如何完成某个功能给出明确的建议,但不管什么框架,保证开发效率高效的同时,避免混乱,都是最佳实践的不变主题。

----本文结束感谢阅读----