记录我在使用 Vue 中发现的一些好的代码实践,希望能够保持更新。
this 引用
在组件作用域内使用箭头函数可以保证 this 永远指向组件本身。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export default { data() { return { msg: 'hello' } }, methods: { hello() { setTimeout(function ( ) { console .log(this .msg) }) } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // goodexport default { data() { return { msg: 'hello' } }, methods: { hello() { setTimeout(() => { console .log(this .msg) // this 指向组件 }) } } }
属性绑定
绑定字符串不需要加冒号。
1 2 3 4 <component :str ="'hello'" > </component > <component str ="hello" > </component >
布尔属性省略值时默认为 true。
1 2 3 <my-modal visible > </my-modal >
绑定无参函数不需要加括号。
1 2 3 4 5 <button @click ="onClick()" > </button > <button @click ="onClick" > </button >
只有一行代码的事件函数,可以直接写标签上。
1 <button @click ="visible = true" > </button >
双向绑定
表单组件一般都支持双向绑定,实际场景中表单组件值发生变化往往要在 POST or PUT 请求之后。如果直接在 v-model 绑定原始值往往会打破单向数据流。
使用计算属性的 get/set 方式可以解决这个问题。(也适用 .sync)
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 export default { template: ` <div> <input type="radio" v-model="nameVal" value="1" > <input type="radio" v-model="nameVal" value="2" > </div>`, data () { return { name: '' } }, computed: { nameVal: { get () { return this .name }, set (val ) { this .edit(val ) } } }, methods: { edit(name) { this .$http.put('/name' , { name }).then(data => { this .name = name }) } }, created() { this .$http.get ('/name' ).then(data => { this .name = data .name }) } }
释放资源
善用 destory 释放原生事件、第三方组件、全局事件总线等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import bus from 'event-bus' import plugin from 'plugin' export default { created() { bus.$on('hello' , this .hello) window .addEventListener('resize' , this .onResize) plugin.init() }, destoryed() { bus.$off('hello' , this .hello) window .removeEventListener('resize' , this .onResize) plugin.destory() } }
修饰符
Vue 内置了许多常用修饰符可以让你少写几行代码,提高开发效率。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <input type ="text" v-model.number ="value" > <input type ="text" v-model.trim ="value" > <button @click.left ="onLeftClick" > 点击鼠标左键</button > <button @click.right ="onRightClick" > 点击鼠标右键</button > <button @click.stop.prevent ="doThis" > </button > <input @keyup.13 ="onEnter" > <button @click.once ="doThis" > </button > <el-button @click.native ="doThis" > </el-button >
以上是一些常用的修饰符,更多用法可以去文档上找找。
数据请求
切换路由请求数据时,一般都需要兼容两种视图打开方式:路由跳转和直接 URL 输入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export default { watch: { $route() { this .fetchData() }, }, methods() { fetchData() { if (this .isLoading) return this .isLoading = true } }, created() { this .fetchData() } }
路由跳转会触发 watch -> $route,如果是未创建的组件还会触发 create,直接 URL 只会触发 created 钩子。一般在两个位置都执行数据请求,再通过判断避免重复请求,还可以利用 isLoading 标记做加载动画。如果使用了 keep-alive 组件,还需要考虑 activated 钩子。
减少嵌套层级
组件即使未在 props 声明,也可以传递一些原生 DOM 属性。
1 2 3 4 5 6 <div class ="content-view" > <router-view > </router-view > </div > <router-view class ="content-view" > </router-view >
命名插槽中需要放置多个块时,可以利用 template 组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <my-component > <div slot ="hello" > <div class ="block1" > </div > <div class ="block2" > </div > </div > </my-component > <my-component > <template slot ="hello" > <div class ="block1" > </div > <div class ="block2" > </div > </template > </my-component >
不管是内置组件还是自己的组件,有时候不需要多一层包裹去添加样式,反而因此增加了嵌套层级。
过滤器
过滤器的最佳应用场景应该是值的转换,比如:Date 类型日期转字符串、货币、字符截断、markdown 等等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const cnReg = /[\u4e00-\u9fa5]/Vue.filter ('ellipsis' , (str , len = 10 ) => { let i = 0 let j = 0 let ret = '' const text = String (str ).trim () const max = text .length while (j < max && i < len) { const c = text .charAt(j) ret += c j += 1 i = cnReg.test(c) ? i + 2 : i + 1 } return ret === text ? text : `${ret}...` }) Vue.filter ('calendar' , value => moment(value).calendar())
Props
布尔属性默认值为 false 可以省略 数组最好声明默认值 [],保证数据请求成功前模版里的 v-for 不会出错 对象也需要注意是否声明了默认值 {},避免模版中使用 obj.xx 报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { props: { visible: Boolen, data: Array, data2: { type: Array, default: [] }, obj: Object, obj2: { type: Object, default() { return {} } } } }
v-if
如果模版中绑定了 obj.xx 时,需要注意 obj 是否是异步数据,默认值是否为 null。安全起见,可在组件最外层加 v-if 判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template > <div v-if ="!!obj" > <p > {{obj.name}} </p > <p > {{obj.age}} </p > </div > </template > <script > export default { data() { return { obj: null } } } </script >
路由
对于经常发生变化的一级、二级菜单导航,可以和路由数据结合起来,按模块划分,视图直接引用对应模块的路由数据来生成导航,减少维护成本。
1 2 3 4 export const settingRoutes = []export const userRoutes = []export default [...settingRoutes, ...userRoutes]
菜单组件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template > <ul > <li v-for ="item in menus" :key ="item.name" > <router-link :to ="item" > {{item.text} }</router-link > </li > </ul > </template > <script > import { settingRoutes } from '../routes' export default { data() { menus: settingRoutes } } </script >
继承和混合
用过ElementUI的同学,都知道其 Dialog 组件 是不支持垂直居中,只提供了一个top属性用于设置组件内容节点到顶部的距离。早期 1.x 版本时 Dialog 组件也不支持append-to-body。我们可以通过继承和混合来扩展这些需要的特性。
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 import { Dialog } from 'element-ui' export default { name: 'ElDialogEx' , extends: Dialog, props: { appendToBody: { type: Boolean , default : true }, center: Boolean }, computed: { sizeClass() { return `el-dialog--${this .size}` + this .center ? ' dialog-center ' : '' } }, mounted() { if (this .appendToBody) document.body.appendChild(this .$el) }, beforeDestroy() { if (this .appendToBody) this .$el.parentNode.remove(this .$el) } }
之后你又发现,在其他的一些组件中也需要appendToBody这个特性,那么就可以把相关的代码写成mixins。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export default { props: { appendToBody: { type: Boolean , default : true } }, mounted() { if (this .appendToBody) document.body.appendChild(this .$el) }, beforeDestroy() { if (this .appendToBody) this .$el.parentNode.remove(this .$el) } }
现在dialogEx组件可以写的更简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { Dialog } from 'element-ui' import appendToBody from 'mixins/appendToBody' export default { name: 'ElDialogEx' , extends: Dialog, mixins: [appendToBody], props: { center: Boolean }, computed: { sizeClass() { return `el-dialog--${this .size} ` + this .center ? ' dialog-center ' : '' } } }
第三方库的集成
第三方库一般是传统的基于 DOM 和原生 js。它们虽然写起来没有使用任何的代码模版,但出于作者的编程经验其实都符合了大众使用预期。
任何一个库一般都会提供以下的接口:
使用自定义配置初始化 可访问的属性 可调用的功能函数 事件绑定 良好的生命周期钩子
如果没有足够的编程经验用原生 js 去写一个插件可能最后就是一团乱麻。这也是 Vue 等众多前端框架的作用,它们约束了一个模块的代码模版,提供了事件管理、生命周期运行、属性和函数的定义,使即使经验不足的人也能写出一个看得过去的模块。
把第三方库转换为一个 Vue 组件,其实就是把这个库的接口挂到 Vue 组件对应的组件选项上去。
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 import Lib from 'lib' export default { props: { options: Object }, data () { return { instance: null } }, methods: { doSomething(xxx) { this .instance.doSomething(xxx) } }, computed: { libProp() { return this .instance.prop } }, watch: { options(val ) { if (val ) this .instance.updateOptions(val ) } }, mounted() { this .instance = new Lib(this .$el, this .options) this .instance.on('update' , (...args) => { this .$emit('update' , ...args) }) }, destroyed() { this .instance.destroy() } }
也可能你想把一个库变为一个 Vue 指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import Lib from 'lib' export default { install(Vue, option = {}) { const defaults = option Vue.directive('my-directive' , { bind(el, { value }) { const options = Object.assign({}, defaults, value ) const lib = new Lib(el, options) el._libInstace = lib }, update(el, { value }, vnode) { el._libInstace.setOptions(value ) }, unbind(el) { el._libInstace.destroy() delete el._libInstace } }) } }
指令有着完善的生命周期钩子,但在数据管理上偏弱。一般用于单一功能的集成,或者只需要一次初始化的插件。
指令中可通过 el 或 el.dataset 进行生命周期间的数据共享。