「不容错过」34条 Vue 高质量实战技巧

共 21492字,需浏览 43分钟

 ·

2020-10-08 20:18

这是我学习整理的关于 Vue.js 系列文章的第一篇,另外还有两篇分别是关于优化和原理的。希望读完这3篇文章,你能对 Vue 有个更深入的认识。

7种组件通信方式随你选

组件通信是 Vue 的核心知识,掌握这几个知识点,面试开发一点问题都没有。

props/@on+$emit

用于实现父子组件间通信。通过 props 可以把父组件的消息传递给子组件:

// parent.vue    
<child :title="title">child>
// child.vue
props: {
    title: {
        typeString,
        default'',
    }
}

这样一来 this.title 就直接拿到从父组件中传过来的 title 的值了。注意,你不应该在子组件内部直接改变 prop,这里就不多赘述,可以直接看官网介绍。

而通过 @on+$emit 组合可以实现子组件给父组件传递信息:

// parent.vue
<child @changeTitle="changeTitle">child>
// child.vue
this.$emit('changeTitle''bubuzou.com')

listeners

Vue_2.4 中新增的 $attrs/$listeners 可以进行跨级的组件通信。$attrs 包含了父级作用域中不作为 prop 的属性绑定(classstyle 除外),好像听起来有些不好理解?没事,看下代码就知道是什么意思了:

// 父组件 index.vue
<list class="list-box" title="标题" desc="描述" :list="list">list>
// 子组件 list.vue
props: {
    list: [],
},
mounted() {
    console.log(this.$attrs)  // {title: "标题", desc: "描述"}
}

在上面的父组件 index.vue 中我们给子组件 list.vue 传递了4个参数,但是在子组件内部 props 里只定义了一个 list,那么此时 this.$attrs 的值是什么呢?首先要去除 props 中已经绑定了的,然后再去除 classstyle,最后剩下 titledesc 结果和打印的是一致的。基于上面代码的基础上,我们在给 list.vue 中加一个子组件:

// 子组件 list.vue

// 孙子组件 detail.vue
// 不定义props,直接打印 $attrs
mounted() {
    console.log(this.$attrs)  // {title: "标题", desc: "描述"}
}

在子组件中我们定义了一个 v-bind="$attrs" 可以把父级传过来的参数,去除 propsclassstyle 之后剩下的继续往下级传递,这样就实现了跨级的组件通信。

$attrs 是可以进行跨级的参数传递,实现父到子的通信;同样的,通过 $listeners 用类似的操作方式可以进行跨级的事件传递,实现子到父的通信。$listeners 包含了父作用域中不含 .native 修饰的 v-on 事件监听器,通过 v-on="$listeners" 传递到子组件内部。

// 父组件 index.vue
<list @change="change" @update.native="update">list>

// 子组件 list.vue
<detail v-on="$listeners">detail>
// 孙子组件 detail.vue
mounted() {
    this.$listeners.change()
    this.$listeners.update() // TypeError: this.$listeners.update is not a function
}

provide/inject组合拳

provide/inject 组合以允许一个祖先组件向其所有子孙后代注入一个依赖,可以注入属性和方法,从而实现跨级父子组件通信。在开发高阶组件和组件库的时候尤其好用。

// 父组件 index.vue
data() {
    return {
        title'bubuzou.com',
    }
}
provide() {
    return {
        detail: {
            titlethis.title,
            change(val) => {
                console.log( val )
            }
        }
    }
}

// 孙子组件 detail.vue
inject: ['detail'],
mounted() {
    console.log(this.detail.title)  // bubuzou.com
    this.detail.title = 'hello world'  // 虽然值被改变了,但是父组件中 title 并不会重新渲染
    this.detail.change('改变后的值')  // 执行这句后将打印:改变后的值 
}

provideinject 的绑定对于原始类型来说并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。这也就是为什么在孙子组件中改变了 title,但是父组件不会重新渲染的原因。

EventBus

以上三种方式都是只能从父到子方向或者子到父方向进行组件的通信,而我就比较牛逼了?,我还能进行兄弟组件之间的通信,甚至任意2个组件间通信。利用 Vue 实例实现一个 EventBus 进行信息的发布和订阅,可以实现在任意2个组件之间通信。有两种写法都可以初始化一个 eventBus 对象:

  1. 通过导出一个 Vue 实例,然后再需要的地方引入:

    // eventBus.js
    import Vue from 'vue'
    export const EventBus = new Vue()

    使用 EventBus 订阅和发布消息:

    import {EventBus} from '../utils/eventBus.js'

    // 订阅处
    EventBus.$on('update', val => {})

    // 发布处
    EventBus.$emit('update''更新信息')
  2. main.js 中初始化一个全局的事件总线:

    // main.js
    Vue.prototype.$eventBus = new Vue()

    使用:

    // 需要订阅的地方
    this.$eventBus.$on('update', val => {})

    // 需要发布信息的地方
    this.$eventBus.$emit('update''更新信息')

如果想要移除事件监听,可以这样来:

this.$eventBus.$off('update', {})

上面介绍了两种写法,推荐使用第二种全局定义的方式,可以避免在多处导入 EventBus 对象。这种组件通信方式只要订阅和发布的顺序得当,且事件名称保持唯一性,理论上可以在任何 2 个组件之间进行通信,相当的强大。但是方法虽好,可不要滥用,建议只用于简单、少量业务的项目中,如果在一个大型繁杂的项目中无休止的使用该方法,将会导致项目难以维护。

Vuex进行全局的数据管理

Vuex 是一个专门服务于 Vue.js 应用的状态管理工具。适用于中大型应用。Vuex 中有一些专有概念需要先了解下:

  • State:用于数据的存储,是 store 中的唯一数据源;
  • Getter:类似于计算属性,就是对 State 中的数据进行二次的处理,比如筛选和对多个数据进行求值等;
  • Mutation:类似事件,是改变 Store 中数据的唯一途径,只能进行同步操作;
  • Action:类似 Mutation,通过提交 Mutation 来改变数据,而不直接操作 State,可以进行异步操作;
  • Module:当业务复杂的时候,可以把 store 分成多个模块,便于维护;

对于这几个概念有各种对应的 map 辅助函数用来简化操作,比如 mapState,如下三种写法其实是一个意思,都是为了从 state 中获取数据,并且通过计算属性返回给组件使用。

computed: {
    count() {
        return this.$store.state.count
    },
    ...mapState({
        countstate => state.count
    }),
    ...mapState(['count']),
},

又比如 mapMutations, 以下两种函数的定义方式要实现的功能是一样的,都是要提交一个 mutation 去改变 state 中的数据:

methods: {
    increment() {
        this.$store.commit('increment')
    },
    ...mapMutations(['increment']),
}

接下来就用一个极简的例子来展示 Vuex 中任意2个组件间的状态管理。1、 新建 store.js

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
    
export default new Vuex.Store({
    state: {
        count0,
    },
    mutations: {
        increment(state) {
            state.count++
        },
        decrement(state) {
            state.count--
        }
    },
})

2、 创建一个带 storeVue 实例

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './utils/store'
    
new Vue({
    router,
    store,
    renderh => h(App)
}).$mount('#app')

3、 任意组件 A 实现点击递增

<template>
    <p @click="increment">click to increment:{{count}}p>
template>
<script>
import {mapState, mapMutations} from 'vuex'
export default {
    computed: {
        ...mapState(['count'])
    },
    methods: {
        ...mapMutations(['increment'])
    },
}
script>

4、 任意组件 B 实现点击递减

<template>
    <p @click="decrement">click to decrement:{{count}}p>
template>
<script>
import {mapState, mapMutations} from 'vuex'
export default {
    computed: {
        ...mapState(['count'])
    },
    methods: {
        ...mapMutations(['decrement'])
    },
}
script>

以上只是用最简单的 vuex 配置去实现组件通信,当然真实项目中的配置肯定会更复杂,比如需要对 State 数据进行二次筛选会用到 Getter,然后如果需要异步的提交那么需要使用 Action,再比如如果模块很多,可以将 store 分模块进行状态管理。对于 Vuex 更多复杂的操作还是建议去看Vuex 官方文档,然后多写例子。

Vue.observable实现mini vuex

这是一个 Vue2.6 中新增的 API,用来让一个对象可以响应。我们可以利用这个特点来实现一个小型的状态管理器。

// store.js
import Vue from 'vue'
 
export const state = Vue.observable({
    count0,
})

export const mutations = {
    increment() {
        state.count++
    }
    decrement() {
        state.count--
    }
}
// parent.vue
<template>
    <p>{{ count }}p>
template>
<script>
import { state } from '../store'
export default {
    computed: {
        count() {
            return state.count
        }
    }
}
script>
// child.vue
import  { mutations } from '../store'
export default {
    methods: {
        handleClick() {
            mutations.increment()
        }
    }
}

children/root

通过给子组件定义 ref 属性可以使用 $refs 来直接操作子组件的方法和属性。

<child ref="list">child>

比如子组件有一个 getList 方法,可以通过如下方式进行调用,实现父到子的通信:

this.$refs.list.getList()

除了 $refs 外,其他3个都是自 Vue 实例创建后就会自动包含的属性,使用和上面的类似。

6类可以掌握的修饰符

表单修饰符

表单类的修饰符都是和 v-model 搭配使用的,比如:v-model.lazyv-model-trim 以及 v-model.number 等。

  • .lazy:对表单输入的结果进行延迟响应,通常和 v-model 搭配使用。正常情况下在 input 里输入内容会在 p 标签里实时的展示出来,但是加上 .lazy 后则需要在输入框失去焦点的时候才触发响应。
    <input type="text" v-model.lazy="name" />
    <p>{{ name }}p>
  • .trim:过滤输入内容的首尾空格,这个和直接拿到字符串然后通过 str.trim() 去除字符串首尾空格是一个意思。
  • .number:如果输入的第一个字符是数字,那就只能输入数字,否则他输入的就是普通字符串。

事件修饰符

Vue 的事件修饰符是专门为 v-on 设计的,可以这样使用:@click.stop="handleClick",还能串联使用:@click.stop.prevent="handleClick"

<div @click="doDiv">
    click div
    <p @click="doP">click pp>
div>
  • .stop:阻止事件冒泡,和原生 event.stopPropagation() 是一样的效果。如上代码,当点击 p 标签的时候,div 上的点击事件也会触发,加上 .stop 后事件就不会往父级传递,那父级的事件就不会触发了。
  • .prevent:阻止默认事件,和原生的 event.preventDefault() 是一样的效果。比如一个带有 href 的链接上添加了点击事件,那么事件触发的时候也会触发链接的跳转,但是加上 .prevent 后就不会触发链接跳转了。
  • .capture:默认的事件流是:捕获阶段-目标阶段-冒泡阶段,即事件从最具体目标元素开始触发,然后往上冒泡。而加上 .capture 后则是反过来,外层元素先触发事件,然后往深层传递。
  • .self:只触发自身的事件,不会传递到父级,和 .stop 的作用有点类似。
  • .once:只会触发一次该事件。
  • .passive:当页面滚动的时候就会一直触发 onScroll 事件,这个其实是存在性能问题的,尤其是在移动端,当给他加上 .passive 后触发的就不会那么频繁了。
  • .native:现在在组件上使用 v-on 只会监听自定义事件 (组件用 $emit 触发的事件)。如果要监听根元素的原生事件,可以使用 .native 修饰符,比如如下的 el-input,如果不加 .native 当回车的时候就不会触发 search 函数。
    <el-input type="text" v-model="name" @keyup.enter.native="search">el-input>

串联使用事件修饰符的时候,需要注意其顺序,同样2个修饰符进行串联使用,顺序不同,结果大不一样。@click.prevent.self 会阻止所有的点击事件,而 @click.self.prevent 只会阻止对自身元素的点击。

鼠标按钮修饰符

  • .left:鼠标左键点击;
  • .right:鼠标右键点击;
  • .middle:鼠标中键点击;

键盘按键修饰符

Vue 提供了一些常用的按键码:

  • .enter
  • .tab
  • .delete (捕获“删除”和“退格”键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

另外,你也可以直接将 KeyboardEvent.key 暴露的任意有效按键名转换为 kebab-case 来作为修饰符,比如可以通过如下的代码来查看具体按键的键名是什么:

<input @keyup="onKeyUp">
onKeyUp(event) {
    console.log(event.key)  // 比如键盘的方向键向下就是 ArrowDown
}

.exact修饰符

.exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。


<button v-on:click.ctrl="onClick">Abutton>


<button v-on:click.ctrl.exact="onCtrlClick">Abutton>


<button v-on:click.exact="onClick">Abutton>

.sync修饰符

.sync 修饰符常被用于子组件更新父组件数据。直接看下面的代码:

// parent.vue
<child :title.sync="title">child>
// child.vue
this.$emit('update:title''hello')

子组件可以直接通过 update:title 的形式进行更新父组件中声明了 .syncprop。上面父组件中的写法其实是下面这种写法的简写:

<child :title="title" @update:title="title = $event">child>

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用

如果需要设置多个 prop,比如:

<child :name.sync="name" :age.sync="age" :sex.sync="sex">child>

可以通过 v-bind.sync 简写成这样:

<child v-bind.sync="person">child>
person: {
    name'bubuzou',
    age21,
    sex'male',
}

Vue 内部会自行进行解析把 person 对象里的每个属性都作为独立的 prop 传递进去,各自添加用于更新的 v-on 监听器。而从子组件进行更新的时候还是保持不变,比如:

this.$emit('update:name''hello')

6种方式编写可复用模块

今天需求评审了一个需求,需要实现一个详情页,这个详情页普通用户和管理员都能进去,但是展示的数据有稍有不同,但绝大部分是一样的;最主要的区别是详情对于普通用户是纯展示,而对于管理员要求能够编辑,然后管理员还有一些别的按钮权限等。需求看到这里,如果在排期的时候把用户的详情分给开发A做,而把管理员的详情分给B去做,那这样做的结果就是开发A写了一个详情页,开发B写了一个详情页,这在开发阶段、提测后的修改 bug 阶段以及后期迭代阶段,都需要同时维护这 2 个文件,浪费了时间浪费了人力,所以你可以从中意识到编写可复用模块的重要性。

Vue 作者尤大为了让开发者更好的编写可复用模块,提供了很多的手段,比如:组件、自定义指令、渲染函数、插件以及过滤器等。

组件

组件是 Vue 中最精髓的地方,也是我们平时编写可复用模块最常用的手段,但是由于这块内容篇幅很多,所以不在这里展开,后续会写相关的内容进行详述。

使用混入mixins

什么是混入呢?从代码结构上来看,混入其实就是半个组件,一个 Vue 组件可以包括 templatescriptstyle 三部分,而混入其实就是 script 里面的内容。一个混入对象包含任意组件选项,比如 datamethodscomputedwatch 、生命周期钩子函数、甚至是 mixins 自己等,混入被设计出来就是旨在提高代码的灵活性、可复用性。

什么时候应该使用混入呢?当可复用逻辑只是 JS 代码层面的,而无 template 的时候就可以考虑用混入了。比如需要记录用户在页面的停留的时间,那我们就可以把这段逻辑抽出来放在 mixins 里:

// mixins.js
export const statMixin = {
    methods: {
        enterPage() {},
        leavePage() {},
    },
    mounted() {
        this.enterPage()
    },
    beforeDestroyed() {
        this.leavePage()
    }
}

然后在需要统计页面停留时间的地方加上:


import { statMixin } from '../common/mixins'
export default {
    mixins: [statMixin]
}

使用混入的时候要注意和组件选项的合并规则,可以分为如下三类:

  • data 将进行递归合并,对于键名冲突的以组件数据为准:

    // mixinA 的 data
    data() {
        obj: {
            name'bubuzou',
        },
    }

    // component A
    export default {
        mixins: [mixinA],
        data(){
            obj: {
                name'hello',
                age21
            },
        },
        mounted() {
            console.log( this.obj )  // { name: 'bubuzou', 'age': 21 }    
        }
    }
  • 对于生命周期钩子函数将会合并成一个数组,混入对象的钩子将先被执行:

    // mixin A
    const mixinA = {
        created() {
            console.log( '第一个执行' )
        }
    }

    // mixin B
    const mixinB = {
        mixins: [mixinA]
        created() {
            console.log( '第二个执行' )
        }
    }

    // component A
    export default {
        mixins: [mixinB]
        created() {
            console.log( '最后一个执行' )
        }
    }
  • 值为对象的选项,例如 methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

自定义指令

除了 Vue 内置的一些指令比如 v-modelv-if 等,Vue 还允许我们自定义指令。在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。比如我们可以通过自定义一个指令来控制按钮的权限。我们期望设计一个如下形式的指令来控制按钮权限:

<button v-auth="['user']">提交button>

通过在按钮的指令里传入一组权限,如果该按钮只有 admin 权限才可以提交,而我们传入一个别的权限,比如 user,那这个按钮就不应该显示了。接下来我们去注册一个全局的指令:

// auth.js
const AUTH_LIST = ['admin']

function checkAuth(auths{
    return AUTH_LIST.some(item => auths.includes(item))
}

function install(Vue, options = {}{
    Vue.directive('auth', {
        inserted(el, binding) {
            if (!checkAuth(binding.value)) {
                el.parentNode && el.parentNode.removeChild(el)
            }
        }
    })
}

export default { install }

然后我们需要在 main.js 里通过安装插件的方式来启用这个指令:

import Auth from './utils/auth'
Vue.use(Auth)

使用渲染函数

这里将使用渲染函数实现上面介绍过的的权限按钮。使用方式如下,把需要控制权限的按钮包在权限组件 authority 里面,如果有该权限就显示,没有就不显示。

<authority :auth="['admin']">
    <button>提交button>
authority>

然后我们用渲染函数去实现一个 authority 组件: