Vue2常用技巧总结

| 前端 | Vue / Vue2 / 技巧 | 36k | 1:01

挥泪回顾告别 Vue2,准备拥抱 Vue3

一开始偏后端开发,后来准备搞副业转身完全投入到了前端领域,当时正值 Vue 大火,于是便和 Vue 结下了不解之缘。时间如梭转眼已是几个春秋,学的东西越多,不会的也就越多,今天学算法,明天学小程序开发,只能直呼学不动学不动,一入代码深似海,从此头发是路人。

本文大体分为四部分:

  • Vue2的基础语法
  • Vuex的使用
  • VueRouter的使用
  • Axios

文中的部分代码示例摘自Vue官网,引用时并未全部指出。

数据绑定Mustache语法

<!-- Mustache语法(双括号) -->
<!-- 双括号里面可以写表达式 -->
<!-- 在组件实例上面定义一个msg变量,然后动态输出 -->
<!-- vue的template中,对变量的引用是在vue的实例上面查找 -->
<span>Message: {{ msg }}</span>

<!-- 支持表达式 -->
<span>Message: {{ msg || '-' }}</span>
<span>Message: {{ msg ? msg : '-' }}</span>

<!-- 支持管道符构成过滤器链 -->
<span>Message: {{ msg | filter1 | filter2 }}</span>

<!-- 还可以使用指令指定语法工作的次数 -->
<!-- v-once标识语法块只会动态改变一次,数据再次改变时,插值处的内容不会更新 -->
<span v-once>Message: {{ msg }}</span>

model

我们最开始学习 vue 的时候,都是拿着input v-model=""来研究的。v-model 是父子组件传值的语法糖,可以在子组件上面定义 model 属性来更改它的使用方式。

export default {
  name: "Model",
  model: {
    // 父组件使用v-model的值会传给子组件的对应的prop
    // 默认v-model会传递给子组件props的value字段
    prop: "value",
    // 约定由子组件的哪个emit事件来更改v-model值
    // 默认为input事件
    event: "input"
  }
}

通常我们封装表单组件的时候都会使用到 v-model 语法

<!-- 子组件 x-input -->
<template>
  <input :value="value" @input="val => $emit('input', val)" />
</template>

<script>
export default {
  name: 'XInput'
  props: {
    value: {
      type: [String, Number],
      default: ''
    }
  }
}
</script>

<!-- 父组件 -->
<x-input v-model="form.value"></x-input>

<!-- 等价于上面写法 -->
<x-input :value="form.value" @input="val => form.value = val"></x-input>

props

定义一个组件,它接收一些什么参数传入,以及设置参数的类型、默认值和对参数进行校验

export default {
  name: 'Props'
  props: {
    value: {
      type: [String, Number],
      default: ''
    },
    list: {
      type: Array,
      // 引用类型需要用函数返回默认值,否则会被组件共享引用
      default: () => []
    },
    type: {
      type: Number,
      required: true,
      validator: v => [1, 2, 3].includes(v)
    }
  }
}

provide/inject

provide能够让父组件传递值给所有定义了inject来接收的子组件,能够消除当父子组件嵌套过深时傻瓜式的prop传递场景。


// 定义枚举
// export const common = Symbol('common')

// 父组件
const common = Symbol('common')
export default {
  // provide是一个对象或者是一个返回对象的函数
  provide: () => {
    return { [common]: 'aaa' }
  },
  // or
  provide: {
    [common]: 'aaa'
  }
}

// 子组件
import { common } from '....'
export default {
  inject: { common },
  mounted () {
    console.info(this[common])
  }
}

mixin

mixin可以让组件继承一些options配置,支持全局混入和组件混入。用Vue.mixin定义的属性会影响到全部组件,需要根据场景使用。vue实例上也可以接收指定的混入配置。

Vue.mixin({
  created () {
    // 接下来所有组件在创建的时候都会打印下面的语句
    console.info('组件被创建')
  }
})

// 或者在创建vue实例的时候指定mixin的选项
const mixin = {
  created () {
    console.info('组件被创建1')
  }
}
const mixin2 = {
  created () {
    console.info('组件被创建2')
  }
}
export default {
  mixins: [ mixin, mixin2 ],
  created () {
    console.info('组件被创建3')
  }
  // 注意调用顺序,按照mixin的顺序调用,最后调用组件实例上面的方法
}

extends

extend可以用来定义组件和继承某组件的实现。一个组件上面只能extends一个选项,mixins接收的是多个选项,extend的优先级别比mixin高。

const superExtend = {
  ...
}
export default {
  extends: superExtend
}

watch

当我们在一些数据发生变化时需要执行一些逻辑,就需要使用到watch选项。


export default {
  data () {
    return {
      working: true,
      status: 1
    }
  },
  watch: {
    // 注意别重复触发watch的监听,避免造成死循环
    working (newValue, oldValue) {
      if (!newValue) {
        console.info('别偷懒...继续工作')
      }
    },
    status: {
      // 是否会初始调用一次watch逻辑,适用在mounted里面手动再次调用watch逻辑的场景
      immediate: true,
      // 是否深度监听该数据
      deep: true,
      hander (newValue, oldValue) {
        if (status === 2) {
          // do someting...
        }
      }
    },
    // 如果需要监听对象的指定字段
    'obj.aaa' () {

    }
  }
}

computed

如果有些特定值依赖于某些值的计算,我们当然不会在data里面再定义一个字段,然后在watch中计算那个特定值。computed为我们完成了这种功能,computed会观测函数块里依赖了哪些值,当这些值发生变化时会就会自动计算出新值。

export default {
  data () {
    return {
      list: [
        { name: 1, show: true },
        { name: 2 },
      ]
    }
  },
  computed: {
    // 可以很方便的自动完成一些条件数据的刷新
    showData () {
      return this.list.filter(item => item.show)
    }
  }

filter

我们在展示数据的时候可能需要格式化数据,这个时候就需要用到filter和管道符“|”,filter能全局定义也支持在局部定义。

// 统一保留两位小数
Vue.filter('f2', function (value) {
  // 未处理边界清空和异常情况,请勿在实际项目中如此写
  return Number(value).toFixed(2)
})

export default {
  filters: {
    f2 (value) {
      return Number(value).toFixed(2)
    }
  }
}

使用

<!-- 管道符前的值会被传递给后面的函数,支持 a | b | c | d 的方式 -->
<span>{{ originValue | f2 }}</span>
<!-- 假如 originValue 的值为 2 ,则实际展示的值为 2.00-->

directives

Vue默认提供了一些指令,用来附加特定的逻辑。支持全局定义指令和局部定义指令。

默认指令:

v-bind

动态绑定一个表达式到attribute或者组件的prop,以下代码示例摘自Vue官网


<!-- 如果目标是组件,并且定义了prop,则是传递给prop,否则根据vue的默认行为会绑定到attribute -->
<!-- 可以修改子组件的inheritAttrs控制这个行为 -->

<!--  已下摘自Vue官网 -->

<!-- 绑定一个 attribute -->
<img v-bind:src="imageSrc">

<!-- 动态 attribute 名 (2.6.0+) -->
<button v-bind:[key]="value"></button>

<!-- 缩写 -->
<img :src="imageSrc">

<!-- 动态 attribute 名缩写 (2.6.0+) -->
<button :[key]="value"></button>

<!-- 内联字符串拼接 -->
<img :src="'/path/to/images/' + fileName">

<!-- class 绑定 -->
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]">

<!-- style 绑定 -->
<div :style="{ fontSize: size + 'px' }"></div>
<div :style="[styleObjectA, styleObjectB]"></div>

<!-- 绑定一个全是 attribute 的对象 -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>

<!-- 通过 prop 修饰符绑定 DOM attribute -->
<div v-bind:text-content.prop="text"></div>

<!-- prop 绑定。“prop”必须在 my-component 中声明。-->
<my-component :prop="someThing"></my-component>

<!-- 通过 $props 将父组件的 props 一起传给子组件 -->
<child-component v-bind="$props"></child-component>

<!-- XLink -->
<svg><a :xlink:special="foo"></a></svg>

v-on

支持绑定一个或多个函数到dom上或者组件上。以下代码示例摘自Vue官网

<!-- 注意v-on:click="doThis"会默认传递事件参数给doThis -->
<!-- 注意v-on:click="doThis()"不会自动传递事件参数给doThis -->
<!-- 如果doThis(row, ...自定义传参)需要传递事件参数,事件参数的字段是$event -->

<!-- 方法处理器 -->
<button v-on:click="doThis"></button>

<!-- 动态事件 (2.6.0+) -->
<button v-on:[event]="doThis"></button>

<!-- 内联语句 -->
<button v-on:click="doThat('hello', $event)"></button>

<!-- 缩写 -->
<button @click="doThis"></button>

<!-- 动态事件缩写 (2.6.0+) -->
<button @[event]="doThis"></button>

<!-- 停止冒泡 -->
<button @click.stop="doThis"></button>

<!-- 阻止默认行为 -->
<button @click.prevent="doThis"></button>

<!-- 阻止默认行为,没有表达式 -->
<form @submit.prevent></form>

<!--  串联修饰符 -->
<button @click.stop.prevent="doThis"></button>

<!-- 键修饰符,键别名 -->
<input @keyup.enter="onEnter">

<!-- 键修饰符,键代码 -->
<input @keyup.13="onEnter">

<!-- 点击回调只会触发一次 -->
<button v-on:click.once="doThis"></button>

<!-- 对象语法 (2.4.0+) -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>

v-model

v-model是接收一个引用(不能接收基础数据类型),默认传递给绑定对象的value值,然后会默认接收绑定对象的input事件,用input事件的输入值修改原引用的值。


<!-- text是指向一个引用 -->
<input v-model="text" />

<input type="checkbox" v-model="check" />

v-if

v-if接收一个逻辑表达式,用来控制绑定目标在符合某种条件才真实创建,可以用来切换展示的内容或者控制目标创建的时机。


<!-- awesome是this实例上面定义的一个布尔值 -->
<h1 v-if="awesome">Vue is awesome!</h1>

<!-- 如果绑定的是一个引用,可以用双非将其转成布尔值 -->
<!-- v-if接收非布尔值会产生一个警告 -->
<h1 v-if="!!obj">Vue is awesome!</h1>

<!-- 可以用template来聚合一组相同条件的元素 -->
<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

v-else-if

v-else-if 紧跟在 v-if 绑定的元素后面,表示当 v-if 不成立的时候再执行 v-else-if 目标元素上面的判断,如果成立则创建 v-else-if 的目标元素


<!-- 根据条件决定内容的展示 -->
<div v-if="type === 'A'">
  A
</div>
<div v-else-if="type === 'B'">
  B
</div>
<div v-else-if="type === 'C'">
  C
</div>

v-else

v-else 必须紧跟在带 v-if 或者 v-else-if 的元素的后面,表示当前者为 false 的时候则创建 v-else 的目标元素。

<div v-if="grade === 100">
  满分小能手
</div>
<div v-else-if="grade >= 60">
  及格了,回家不用被打屁股了
</div>
<div v-else>
  emmm /(ㄒoㄒ)/~~
</div>

v-show

v-showv-if 的区别是前者控制的是目标元素的是否显示,后者控制是否创建。一个是没有创建消耗和不会引发元素内的逻辑,一个是创建了该元素,然后隐藏了该元素。

一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好

<!-- 带有 v-show 的元素始终会被渲染并保留在 DOM 中。 -->
<!-- v-show 只是简单地切换元素的 CSS property display -->
<h1 v-show="ok">Hello!</h1>

v-for

v-for 会遍历一个可遍历的对象以循环创建一组元素。


<ul>
  <li v-for="item in [1, 2, 3]">
    {{ item }}
  </li>
</ul>

<!-- 如果需要用到遍历的索引 -->
<ul>
  <li v-for="(item, index) in ['red', 'white', 'black']">
    {{ `第${index + 1}个是${item}` }}
  </li>
</ul>

<!-- 每次使用v-for都需要指定key属性,用来优化列表渲染时的diff决策 -->
<!-- 在当遍历源产生变化时,如果key不变则会直接复用上次的同key元素 -->
<ul>
  <li v-for="(item, index) in ['red', 'white', 'black']" :key="index">
    {{ `第${index + 1}个是${item}` }}
  </li>
</ul>

v-html

使用 v-html 可以将一段Html代码覆盖式的添加到目标dom的下面。

在网站上动态渲染任意 HTML 是非常危险的,因为容易导致 XSS 攻击。只在可信内容上使用 v-html,永不用在用户提交的内容上。
<div v-html="html"></div>

<!-- 如果html等于<span>你好</span> -->
<!-- 则最终渲染如下 -->
<div>
  <span>你好</span>
</div>

v-text

v-html 更新的是绑定元素的 innerHtml,而 v-text 则更新的是绑定元素的 textContent

<span v-text="msg"></span>
<!-- 和下面的一样 -->
<span>{{msg}}</span>

v-pre

v-pre 会使得程序忽略该元素的编译,可以用来显示原始 Mustache 标签,跳过大量没有指令的节点会加快编译。

<span v-pre>{{ 这里的Mustache语法将会被忽略 }}</span>

v-once

v-once 使得绑定目标只会更新一次,随后的重新渲染,目标及其所有的子节点将被视为静态内容并跳过,这可以用于优化更新性能,比如可以预料不会更新的静态展示组件可以使用 v-once

<!-- 单个元素 -->
<span v-once>This will never change: {{msg}}</span>
<!-- 有子元素 -->
<div v-once>
  <h1>comment</h1>
  <p>{{msg}}</p>
</div>
<!-- 组件 -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` 指令-->
<ul>
  <li v-for="i in list" v-once>{{i}}</li>
</ul>

v-cloak

使用 v-cloak 配合 CSS 可以避免更新时的数据闪动。这个指令将保持在目标上直到该目标结束渲染(换句话说这个指令存在时则标识该目标正在渲染)。

下面的消息内容会在渲染完成后才会展现。

[v-cloak] {
  display: none;
}
<div v-cloak>
  {{ message }}
</div>

v-slot

使用 v-slot 来具名一个插槽和传递插槽参数。以下代码示例摘自Vue官网

<!-- 具名插槽 -->
<base-layout>
  <template v-slot:header>
    Header content
  </template>

  Default slot content

  <template v-slot:footer>
    Footer content
  </template>
</base-layout>

<!-- 接收 prop 的具名插槽 -->
<infinite-scroll>
  <template v-slot:item="slotProps">
    <div class="item">
      {{ slotProps.item.text }}
    </div>
  </template>
</infinite-scroll>

<!-- 接收 prop 的默认插槽,使用了解构 -->
<mouse-position v-slot="{ x, y }">
  Mouse position: {{ x }}, {{ y }}
</mouse-position>

生命周期

可以在组件的不同生命周期来执行对应的逻辑

export default {
  // 在对数据绑定observer之前执行, 这里的this不指向vue的实例
  beforeCreate () {},
  // vm实例创建完成,此时数据观测和初始赋值等均已经完成
  created () {},
  // 在render函数之前调用,此时dom等还均未挂载
  beforeMounted () {},
  // dom挂载已经完成
  // 官网原文: 注意 mounted 不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted 内部使用 vm.$nextTick
  mounted () {},
  // 在update之前被调用
  beforeUpdate () {},
  // 组件在监测到一些数据变化后更新组件信息
  updated () {}
  // 当组件被keep-alive缓存后,当组件被激活后触发
  activated () {},
  // 注意activated和mounted的关系
  // 当组件被keep-alive缓存后,当组件被取消调用时触发
  deactivated () {},
  // 组件被销毁前触发,此时this还指向vm实例
  beforeDestroy () {},
  // 组件已经销毁
  // 此时this已经不存在
  destroyed () {}


}

capture

vue提供了全局error捕捉器,也提供了配置组件内部的error捕捉器


// 全局定义捕捉未被捕获的错误
Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
  // 只在 2.2.0+ 可用
}

// 捕获未被捕获的warn
Vue.config.warnHandler = function (msg, vm, trace) {
  // `trace` 是组件的继承关系追踪
}

// 组件内部捕获
export default {
  // 当捕获一个来自子孙组件的错误时被调用
  // 注意errorCaptured的传播规则
  errorCaptured (err, vm, info) {
    // 2.5.0+
  }
}

下面摘自Vue官网

  • 默认情况下,如果全局的 config.errorHandler 被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报。
  • 如果一个组件的继承或父级从属链路中存在多个 errorCaptured 钩子,则它们将会被相同的错误逐个唤起。
  • 如果此 errorCaptured 钩子自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局的 config.errorHandler
  • 一个 errorCaptured 钩子能够返回 false 以阻止错误继续向上传播。本质上是说“这个错误已经被搞定了且应该被忽略”。它会阻止其它任何会被这个错误唤起的 errorCaptured 钩子和全局的 config.errorHandler

修饰符

Vue提供了一些修饰符,使得我们可以很方便的处理一些值和处理一些事件。

事件修饰符

以下代码摘自Vue官网

<!-- Vue的事件修饰符 -->

<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>

<!-- 点击事件将只会触发一次 -->
<!-- 2.1.4 + -->
<a v-on:click.once="doThis"></a>

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成  -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<!-- 2.3.0 + -->
<div v-on:scroll.passive="onScroll">...</div>

<!-- .native -->
<!-- 监听组件根元素上的原生事件 -->
<x-input v-on:focus.native="onFocus" />

<!-- 如果x-input封装的根元素没有对应事件,则监听会失效 -->
<!-- 可以利用实例上面的 $listeners 属性将其挂载到子组件中的对应元素上 -->

按键修饰符

在监听键盘事件时,我们经常需要检查详细的按键。Vue 允许为 v-on 在监听键盘事件时添加按键修饰符。你可以直接将 KeyboardEvent.key 暴露的任意有效按键名转换为 kebab-case 来作为修饰符,你还可以通过全局 config.keyCodes 对象自定义按键修饰符别名。

<!-- Vue的按键修饰符 -->

<!-- 按下键盘的Enter按钮触发 -->
<input v-on:keyup.enter="submit" />

<!-- 按下键盘的PageDown按钮触发 -->
<input v-on:keyup.page-down="onPageDown" />

<!-- 支持keycode码的写法 -->
<!-- enter的keycode码为13  -->
<input v-on:keyup.13="submit" />

<!-- 使用全局自定义的按键码别名 -->
<!-- Vue.config.keyCodes['my-enter'] = 13 -->
<input v-on:keyup.my-enter="submit" />

系统修饰符

可以用来设置组合按键,比如 ctrl + A

<!-- 系统修饰符 -->

<!-- Alt + C -->
<input v-on:keyup.alt.67="clear" />

<!-- Ctrl + Click -->
<div v-on:click.ctrl="doSomething">Do something</div>

<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button v-on:click.ctrl="onClick">A</button>

<!-- .exact 修饰符允许你控制由精确的系统修饰符组合触发的事件 -->
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button v-on:click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button v-on:click.exact="onClick">A</button>

属性修饰符

可以对表单输入的值进行一些处理。

<!-- 表单修饰符 -->

<!-- 会将输入的值转成数字 -->
<!-- 如果这个值无法被 parseFloat() 解析,则会返回原始的值 -->
<input v-model.number="form.value" />

<!-- 将v-model的赋值事件修改为了change事件 -->
<!-- 可以避免用户的每个按键输入都会引起值的变化,引起频繁无效的逻辑 -->
<!-- 可以修改model选项的event为change来实现同种效果 -->
<input v-model.lazy="text" />

<!-- 自动去除首位空格 -->
<input v-model.trim="msg" />

<!-- camel会将v-bind的字段转成驼峰 -->
<svg :view-box.camel="viewBox"></svg>

<!-- 通常我们禁止直接在子组件中修改prop的值,而使用$emit触发一个事件来达到修改的效果 -->
<!-- Vue提供了一个.sync语法糖来简写这种场景 -->
<!-- 2.3.0 + -->
<!-- 在父组件使用.sync修饰,在子组件中使用$emit('update:text') 达到修改prop的效果 -->
<x-edit :text.sync="text" />

<!-- 这种写法和上面的效果相同 -->
<x-edit :text="text" @update:text="val => text = val" />

$emit

在组件中使用 $emit 向外传递一个事件

export default {
  mounted () {
    this.$emit('inited', '传递一些额外参数')
  }
}

$on

可以使用 $on 监听当前组件实例的一些事件

export default {
  created () {
    // 监听本组件的emit事件
    this.$on('input', console.info)
  },
  mounted () {
    this.$emit('input', 1)
    this.$emit('input', 2)
    this.$emit('input', 3)
  }
}

$once

$once$on 的区别是 $once 只会执行一次

hook

使用hook来监听组件的生命钩子事件。

监听本组件内部的事件

export default {
  mounted () {
    this.onWinResize()
  },
  methods: {
    onWinResize () {
      function handerWinResize () {}
      window.addEventListener('resize', handerWinResize)
       // 使用$once和hook监听本组件的生命钩子
      this.$once('hook:beforeDestroy', function () {
        window.removeEventListener('resize', handerWinResize)
      })
    }
  }
}

// 如果上面场景不使用hook可能会这么写
export default {
  mounted () {  
    window.addEventListener('resize', this.handerWinResize)
  },
  methods: {
    handerWinResize() {}
  },
  beforeDestroy () {
    window.removeEventListener('resize', this.handerWinResize)
  }
}

监听子组件的生命钩子:


<x-child @hook:mounted="handleChildMounted" />

不使用hook监听子组件的生命钩子可能会这么写:

<!-- 父组件 -->
<x-child @mounted="handleChildMounted" />
// 使用hook就不需要在子组件中手动$emit
export default {
  mounted () {
    this.$emit('mounted')
  }
}

$off

使用 $off 来卸载本组件对内部事件的监听

  • 如果没有提供参数,则移除所有的事件监听器;
  • 如果只提供了事件,则移除该事件所有的监听器;
  • 如果同时提供了事件与回调,则只移除这个回调的监听器。
export default {
  created () {
    this.$on('input', this.handerInput)
  },
  mounted () {
    // 移除该事件的所有监听
    this.$off('input')
    // 移除本组件所有的内部监听
    // this.$off()
    // 根据句柄移除指定事件
    // this.$off('input', this.handleInput)
  },
  methods: {
    handerInput () {}
  }
}

$nextTick

使用 $nextTick 将回调延迟到下次 DOM 更新循环之后执行。常用来安全的操作操作dom,或等待数据监听处理结束等。


// 使用 v-if 控制了一段元素,后修改 v-if 为真
this.show = true
this.$nextTick(() => {
  // 这里在dom更新之后才会触发
})

// 安全的等待ref创建
this.$nextTick(() => {
  this.$refs.xxx && this.$refs.xxx.xxx
})

// 异步执行
this.$nextTick(() => {
  alert(2)
})
alert(1)

$watch

使用 $watch 可以选择性的为数据的watch添加合理的时机。以下代码示例摘自Vue官网

表达式只接受简单的键路径,对于更复杂的表达式,用一个函数取代。

// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
  // 做点什么
})

// 函数
vm.$watch(
  function () {
    // 表达式 `this.a + this.b` 每次得出一个不同的结果时
    // 处理函数都会被调用。
    // 这就像监听一个未被定义的计算属性
    return this.a + this.b
  },
  function (newVal, oldVal) {
    // 做点什么
  }
)

// vm.$watch 返回一个取消观察函数,用来停止触发回调
var unwatch = vm.$watch('a', cb)
// 之后取消观察
unwatch()

$set

如果在实例创建之后添加新的属性到实例上,它不会触发视图更新, 或者当组件的属性是对象或者数组时,直接修改数组的内容可能也不会触发视图更新,使用 $set 确保属性变成响应式的,同时视图会更新。

// 由于vue2是用Object.defineProperty来实现的数据挟持,Vue并不能精准监听到数组等类型的改变
// 手动触发
this.$set(Array, index, newValue)
this.$set(Object, key, newValue)
// 或者尝试改变数据的引用地址来触发监听

$forceUpdate

使用 $forceUpdate 来强制组件重新渲染。

注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

this.$forceUpdate()

如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。 你可能还没有留意到数组或对象的变更检测注意事项,或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。然而,如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过 $forceUpdate 来做这件事。

$refs

$refs 是该组件上用 ref 命名过的直接下级组件引用的数据集合。$refs 只会在组件渲染完成之后生效,并且它们不是响应式的,使用 $refs 是一个不安全的操作,注意调用 $refs 的时机和进行安全判断。

<x-input v-if="edit" ref="input" />
export default {
  data () {
    return {
      edit: false
    }
  },
  mounted () {
    this.edit = true
    // 等待实列渲染
    this.$nextTick(() => {
      // 如果是在v-for中,this.$refs.input会是一个组件引用数组
      // 调用input子组件实例上面的方法
      this.$refs.input && this.$refs.input.focus()
    })
  }
}

render

我们可以不使用template选项而使用render来控制组件的渲染

// 利用render函数实现按钮组超出三个显示更多
// 使用createElement函数描绘节点
// 或者使用jsx语法描绘节点
// 这个例子可以修改为函数式组件
export default {
  render(h) {
    const nodes = this.$slots.default.filter(e => e.tag)
    if (nodes.length <= 3) {
      return h("div", nodes)
    } else {
      const pre = [nodes[0], nodes[1]]
      const suf = nodes.slice(2)
      return (
        <div>
          { h("div", pre) }
          <el-dropdown>
            <span>
              更多操作<i class="el-icon-arrow-down" />
            </span>
            <el-dropdown-menu slot="dropdown">
              {
                suf.map(item => {
                  return <el-dropdown-item>{h("span", [item])}</el-dropdown-item>
                })
              }
            </el-dropdown-menu>
          </el-dropdown>
        </div>
      )
    }
  }
}

keep-alive

使用 keep-alive 可以将创建过的组件在其失活的时候将其缓存起来,下次使用的时候避免再次创建


<!-- 缓存动态组件 -->
<keep-alive>
  <component :is="componentName"></component>
</keep-alive>

<!-- 缓存路由 -->
<keep-alive>
  <router-view v-if="$route.meta && $route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta || !$route.meta.keepAlive"></router-view>

transition

动画效果的篇幅比较多,这里就不描述了emmm😂。 Vue动画

is

使用 is 可以动态选择当前使用哪个组件。

<component :is="componentName" name="name" />

<!-- 当componentName值为x-input时则效果等同如下 -->
<x-input name="name" />

<!-- 当componentName值为x-table时则效果等同如下 -->
<x-table name="name" />

functional

函数式组件只根据props来进行渲染,使用函数式组件相对正常组件来说可以减少很多使用开销。

组件无状态 (没有 data) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使它们渲染的代价更小

使用template写法

<template functional>
  <div>
    {{ props.name }}
  </div>
</template>

使用render写法

Vue.component('my-component', {
  functional: true,
  // Props 是可选的
  props: {
    // ...
  },
  // 为了弥补缺少的实例
  // 提供第二个参数作为上下文
  render: function (createElement, context) {
    // ...
  }
})

new Vue

我们使用的.vue文件创建组件的方式是在打包时编译组件信息,我们也可以在代码运行时动态 new 创建组件。

new Vue 使用 template 选项会获得一个警告,请在运行时创建组件的时候使用 render

vue msg: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.

jsx

Vue 提倡在大多数情况下使用 .vue template 来书写 html 代码。但是在某些特殊场景时使用 render 函数更加有效,render 函数在创建众多节点的时候非常不友好,Vue官方提供了一个Babel插件来支持更加友好的 jsx 写法。

import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
  el: '#demo',
  render: function (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。从 VueBabel 插件的 3.4.0 版本开始,我们会在以 ES2015 语法声明的含有 JSX 的任何方法和 getter 中 (不是函数或箭头函数中) 自动注入 const h = this.$createElement,这样你就可以去掉 (h) 参数了。对于更早版本的插件,如果 h 在当前作用域中不可用,应用会抛错。

Object.defineProperty

Object.defineProperty 方法可以让我们给对象的属性添加 gettersetter 方法和设置对象的一些特殊属性。

应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。

const obj = { name: '王麻子' }

// 对象属性描述符
const descriptor = {
  // 设置描述符为可配置
  configurable: true,
  // 可否被枚举
  enumerable: false,
  // 可否被修改
  writable: false,
  // getter方法
  get: function () {
    // 当某个地方获取对象的该属性值,getter方法会被调用
    return this.name
  },
  // setter方法
  set: function (val) {
    // 为对象的该属性赋值时会触发setter方法
    this.name = val
  }

}

Object.defineProperty(obj, 'name', descriptor)

下面内容和代码实例摘自MDN

如果一个描述符不具有 value、writable、get 和 set 中的任意一个键,那么它将被认为是一个数据描述符。如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。

记住,这些选项不一定是自身属性,也要考虑继承来的属性。为了确认保留这些默认值,在设置之前,可能要冻结 Object.prototype,明确指定所有的选项,或者通过 Object.create(null)__proto__ 属性指向 null

// 使用 __proto__
var obj = {};
var descriptor = Object.create(null); // 没有继承的属性
// 默认没有 enumerable,没有 configurable,没有 writable
descriptor.value = 'static';
Object.defineProperty(obj, 'key', descriptor);

// 显式
Object.defineProperty(obj, "key", {
  enumerable: false,
  configurable: false,
  writable: false,
  value: "static"
});

// 循环使用同一对象
function withValue(value) {
  var d = withValue.d || (
    withValue.d = {
      enumerable: false,
      writable: false,
      configurable: false,
      value: null
    }
  );
  d.value = value;
  return d;
}
// ... 并且 ...
Object.defineProperty(obj, "key", withValue("static"));

// 如果 freeze 可用, 防止后续代码添加或删除对象原型的属性
// (value, get, set, enumerable, writable, configurable)
(Object.freeze||Object)(Object.prototype);

Object.freeze

你可以使用 Object.freeze 来冻结一个对象,使得这个对象不可以被任意扩展和修改。Vue 会跳过已经被 freeze 的对象不会对其添加 gettersetter 方法,对于预料不会改变的数据和禁止改变的数据使用 Object.freeze 可以提高一些性能。

下面内容和代码实例摘自MDN

Object.freeze 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。

冻结对象:

var obj = {
  prop: function() {},
  foo: 'bar'
};

// 新的属性会被添加, 已存在的属性可能
// 会被修改或移除
obj.foo = 'baz';
obj.lumpy = 'woof';
delete obj.prop;

// 作为参数传递的对象与返回的对象都被冻结
// 所以不必保存返回的对象(因为两个对象全等)
var o = Object.freeze(obj);

o === obj; // true
Object.isFrozen(obj); // === true

// 现在任何改变都会失效
obj.foo = 'quux'; // 静默地不做任何事
// 静默地不添加此属性
obj.quaxxor = 'the friendly duck';

// 在严格模式,如此行为将抛出 TypeErrors
function fail(){
  'use strict';
  obj.foo = 'sparky'; // throws a TypeError
  delete obj.quaxxor; // 返回true,因为quaxxor属性从来未被添加
  obj.sparky = 'arf'; // throws a TypeError
}

fail();

// 试图通过 Object.defineProperty 更改属性
// 下面两个语句都会抛出 TypeError.
Object.defineProperty(obj, 'ohai', { value: 17 });
Object.defineProperty(obj, 'foo', { value: 'eit' });

// 也不能更改原型
// 下面两个语句都会抛出 TypeError.
Object.setPrototypeOf(obj, { x: 20 })
obj.__proto__ = { x: 20 }

冻结数据:

let a = [0];
Object.freeze(a); // 现在数组不能被修改了.

a[0]=1; // fails silently
a.push(2); // fails silently

// In strict mode such attempts will throw TypeErrors
function fail() {
  "use strict"
  a[0] = 1;
  a.push(2);
}

fail();
注意假如被冻结的对象的属性值也是一个对象,那么这个值对象是可以修改的。Object.freezee 是浅冻结

实现 deepFreeze

// 深冻结函数.
function deepFreeze(obj) {

  // 取回定义在obj上的属性名
  var propNames = Object.getOwnPropertyNames(obj);

  // 在冻结自身之前冻结属性
  propNames.forEach(function(name) {
    var prop = obj[name];

    // 如果prop是个对象,冻结它
    if (typeof prop == 'object' && prop !== null)
      deepFreeze(prop);
  });

  // 冻结自身(no-op if already frozen)
  return Object.freeze(obj);
}

obj2 = {
  internal: {}
};

deepFreeze(obj2);
obj2.internal.a = 'anotherValue';
obj2.internal.a; // undefined

VUEX

本目录Vuex下的大部分内容都摘自Vuex官网

Vue 中我们通常使用 Vuex 做全局状态管理,它可以帮助我们跨越多级组件传递数据,或者统一我们的一些数据来源,Vuex 中的状态都是响应式的。

禁止直接修改state中的属性,保证每次修改属性都是通过mutation函数,这样我们可以明确地追踪到状态的变化,也能够在vue调试工具中清晰的检查数据的更改。
// 注意你的vue版本,下面这段代码来自vue3的版本
// 定义vuex模块
// /store/index.ts
import { createStore } from 'vuex'

export default createStore({
  state: {},
  mutations: {},
  actions: {},
  modules: {}
})

// 引入vuex模块
// main
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).use(store).mount('#app')

state

Vuex 使用单一状态树,只使用一个对象来包含全部的应用层级状态,它作为一个唯一数据源 (SSOT (opens new window)) 而存在。

// createApp(App).use(store)执行后,会在vue的原型上添加$store属性。
// 打印state
export default {
  created () {
    // 打印整个state状态树
    console.info(this.$store.state)
  }
}

mapState

当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性。

// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'

export default {
  // ...
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

// 当映射的计算属性的名称与 state 的子节点名称相同时,
// 我们也可以给 mapState 传一个字符串数组。
export default {
  computed: mapState([
    // 映射 this.count 为 store.state.count
    'count'
  ])
}

mutation

虽然我们可以直接修改 $store.state 中的属性值,比如 this.$store.state.list = [],但是我们应该约定只在 mutation 方法中对 $store.state 进行修改。

Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会默认接收 state 作为第一个参数,调用 mutation 时传递的参数为回调函数的第二个参数。

`mutations` 中的函数请使用同步函数,混合异步调用会导致你的程序很难调试。
const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    // payload为调用时负载(调用时传递的参数)
    add (state, payload) {
      // 变更状态
      state.count += payload
    }
  }
})

使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然。在需要多人协作的大型项目中,这会很有帮助,但如果你不喜欢,你完全可以不这样做。

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'

// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = new Vuex.Store({
  state: { ... },
  mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
    [SOME_MUTATION] (state) {
      // mutate state
    }
  }
})

mapMutations

辅助函数 mapMutations 可以帮助我们将 mutation 函数映射到组件实例的方法选项中。

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}

action

Mutation 中都是同步操作(没有副作用),我们使用 Action 来处理异步操作(有副作用的逻辑)。

  • Action 可以包含任意异步操作`;
  • Action 通过提交 Mutation 来修改状态,而不直接进行修改。
import api from '@/api'

const store = new Vuex.Store({
  state: {
    list: []
  },
  mutations: {
    SET_LIST (state, list) {
      state.list = list
    }
  },
  actions: {
    queryList: async ({ commit }) => {
      const { list } = await api.queryList()
      commit('SET_LIST', list)
    }
  }
})

mapAction

你在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用。

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

commit和dispatch

我们使用 commit 来调用 Mutation,使用 dispatch 来调用 Action


// 在组件中
this.$store.dispatch('actionFn')
this.$store.commit('mutationFn')

// 在action中
new Vuex.Store({
  actions: {
    queryList: async ({ commit, dispatch }) => {
      dispatch('actionFn')
      commit('mutationFn')
      // 如果调用其他具有namespace的module下面的事件
      dispatch('namespace/actionFn')
      commit('namespace/mutationFn')

      // 以对象形式分发
      dispatch({
        type: 'actionFn',
        // 参数
        payload: 10
      })
    }
  }
}

modules

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块 (module)。每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块——从上至下进行同样方式的分割。

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    },
    actions: {
      // 对于模块内部的 action,局部状态通过 context.state 暴露出来
      // 根节点状态则为 context.rootState
      incrementIfOddOnRootSum ({ state, commit, rootState }) {
        if ((state.count + rootState.count) % 2 === 1) {
          commit('increment')
        }
      }
    }
  },
  // 对于模块内部的 getter,根节点状态会作为第三个参数暴露出来
  getters: {
    doubleCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

namespace

默认情况下,模块内部的 actionmutationgetter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutationaction 作出响应。

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名。

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模块内容(module assets)
      state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 进一步嵌套命名空间
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

getters

有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数。

export default {
  computed: {
    doneTodosCount () {
      return this.$store.state.todos.filter(todo => todo.done).length
    }
  }
}

如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。

Vuex 允许我们在 store 中定义 getter(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

// vue组件内
this.$store.getters.doneTodos

mapGetter

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
    // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ]),
    // 如果你想将一个 getter 属性另取一个名字,使用对象形式
    ...mapGetters({
      // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
      doneCount: 'doneTodosCount'
    })
  }
}

ROUTER

Vue 中会使用 Vue Router 来进行路由管理,我们将组件映射到路由上,达到访问页面展示对应组件内容。

定义路由信息:

// import { createRouter, createWebHistory } from 'vue-router'
import Vue from 'vue'
import VueRouter from 'vue-router';
import { User, UserProfile } from '@/views/user'

Vue.use(VueRouter)

const routers = [
  // 动态匹配路径、支持正则
  // 将 User 组件挂载到上游组件内部的 `<route-view />` 中
  // <route-view /> 就是路由组件的挂载位置,请保证上游组件存在 <route-view />。
  { path: '/user/:id', component: User,
    children: [
      {
        // 当 /user/:id/profile 匹配成功,
        // UserProfile 会被渲染在 User 的 <router-view> 中
        path: 'profile',
        // 上游组件可以定义多个 <route-view name="myName" /> 
        // 多个 route-view 需要使用到 components: { comp1: ..., comp2: ..., default: .. }
        component: UserProfile
      },
      {
        path: 'setting',
        // 为页面具名,采用name跳转可以在路由嵌套关系修改后自动适配跳转
        name: 'UserSetting',
        // 定义元信息
        meta: { title: '设置个人信息', keepAlive: true, icon: 'icon', Auth: true },
        // 如果 props 被设置为 true,route.params 将会被添加到组件prop属性
        // 可以是函数和对象
        prop: true,
        // 如果这个路由需要默认重定向到某个路由
        // 可以是url、返回url的函数、对象
        redirect: '/redirectUrl',
        // 可以脱离路由的嵌套关系起一个特殊的别名
        // 比如当前url是/user/id/profile,具其别名为 /haha,访问/haha等效访问/user/id/profile
        alias: '/haha'
      }
    ]
  }
]

const router = new VueRouter({ 
  routes,
  // 选择实现路由的方式
  mode: 'history',

});

export default router

main 文件中注册 Vue Router

import Vue from 'vue';
import App from './App.vue'
import router from './router'

export default new Vue({
  el: '#app',
  router: router,
  components: { App },
  template: '<App/>'
});

// import { createApp } from 'vue'
// import App from './App.vue'
// import router from './router'

// createApp(App).use(router).mount('#app')

mode

  • hash: 使用 URL hash 值来作路由,支持所有浏览器,包括不支持 HTML5 History Api 的浏览器,默认为 hash 模式,url中会携带“#”,不如history美观;
  • history: 依赖 HTML5 History API 和服务器配置;
  • abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。
注意当 `mode`为 `history` 的项目部署后,会出现访问非根URL路径(“/” 和 “index.html”)时返回404状态码(因为VueRouter的多级路由是在index.html内部进行的,对后台而言在后台只存在index.html一个路径),需要根据部署情况进行修改。

按需加载

配置路由时,如果同步引入了所有的组件代码,当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

结合 Vue异步组件Webpack代码分割功能,轻松实现路由组件的懒加载。

首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):

const Foo = () => Promise.resolve({ /* 组件定义对象 */ })

第二,在 Webpack 2 中,我们可以使用动态import语法来定义代码分块点 (split point):

import('./Foo.vue') // 返回 Promise

结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。

const Foo = () => import('./Foo.vue')
// 还可以指定Foo组件打包后的chunk名,比如group-foo.js
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')

const router = new VueRouter({
  routes: [
    { path: '/foo', component: Foo }
  ]
})

prefetch

在资源文件引用比如 <script> 上使用 rel="prefetch" 可以通知浏览器在空闲的时候加载该资源文件,配合上面的路由懒加载,即可以减小首屏的渲染消耗,改进响应速度,又可以合理的提前加载组件的js文件,加快页面导航。

prefetch 相关的还有一个 preload,前者降低资源的优先级,后者提高资源的优先级。

钩子

  • 导航被触发;
  • 在失活的组件里调用 beforeRouteLeave 守卫;
  • 调用全局的 beforeEach 守卫;
  • 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+);
  • 在路由配置里调用 beforeEnter
  • 解析异步路由组件;
  • 在被激活的组件里调用 beforeRouteEnter
  • 调用全局的 beforeResolve 守卫 (2.5+);
  • 导航被确认;
  • 调用全局的 afterEach 钩子;
  • 触发 DOM 更新;
  • 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

全局钩子

全局前置路由守卫:

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // 如果要访问的是用户页面,但是没有登录,跳转到登录页
  const auth = !!localStorage.getItem('token')
  if (to.name === 'User' && !auth) {
    // store.dispath('logout')
    return next('/login')
  }
  // 保证函数最终都会调用next,结束本个链路节点,使得导航继续执行。
  // 确保代码不会调用多次next
  next()
})

全局后置路由守卫:

// 和 beforeEach 不同,参数不会接收next函数,此时导航已经完成,等待组件渲染
router.afterEach((to, from) => {
  // ...
})

全局解析路由守卫:

2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。

router.beforeResolve((to, from, next) => {})

路由独享守卫

支持给指定路由单独配置 beforeEnter 守卫,使用方法和全局类似。

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

组件路由钩子

export default {
  beforeRouteEnter (to, from, next) {
    next(vm => {
      // 通过 `vm` 访问组件实例
      // 可以在这里进行一些请求
    })
  },
  beforeRouteUpdate (to, from, next) {
    // 2.2+
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
    // 可以在这里进行一些表单未提交提醒
    const answer = window.confirm('有未保存的内容,确定离开吗')
    if (answer) {
      next()
    } else {
      next(false)
    }
  }
}

$route

组件实例上面的 $route 含有当前匹配到的路由信息,比如请求参数等。

export default {
  created () {
    // 注意query和params的区别
    // query取得是地址栏得url参数
    console.info(this.$route.query)
    // 动态路由的:id 在 params中
    console.info(this.$route.params)
  },
  watch: {
    $route () {
      // 监听路由信息得更改
      // 进行一些表单数据刷新
    }
  }
}

$router

在JS中使用 this.$router.push 来进行路由跳转,也可以使用 <router-link> 标签

this.$router.push('/user/setting')
this.$router.push({ name: 'UserSetting' })
this.$router.push({ name: 'UserSetting', params: { id: 123 }})
this.$router.push(`/user/${id}`)

SASS

SASS 是一种基于 CSS 的预处理语言,为 CSS增加了一些编程支持。

install

为项目添加 .npmrc 文件。

sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
registry=https://registry.npm.taobao.org
npm install node-sass sass-loader --save-dev

配置 webpack 配置文件:

// wepback.base.config

{
  module: {
    loaders:[
      { test: /\.(scss|sass)$/, loader: 'style-loader!css-loader!sass-loader'}
    ]
  }
}

scope

style 标签添加 scope 可以使得样式具有当前组件作用域,组件样式不会再污染到全局。

<style lang="scss" scoped>
</style>

在添加了 scoped 的标签下,如果你想修改第三方的组件样式,你可以重起一个没有 scoped 的标签,再在组件的顶级类名下定义第三方组件的样式(避免样式污染全局)。或者你完全可以不使用 scoped 属性,直接 css 全部嵌套在组件的顶级 class 下。

scoped 的标签下,你可以使用 >>> 来调整样式的最终编译,使得样式穿透作用到子组件。Sass 无法正确解析 >>>, 可以使用 >>> 的别名 /deep/::v-deep来达到相同效果。

当组件内部存在有 scoped 属性的 <style>时,Vue在编译组件的时候会给组件内部的dom元素加上组件的 hashdata-v-hash。如:

<div class="root">
  <div class="b-e-m"></div>
</div>

<style scoped lang="scss">
.root .b-e-m {
  background: red;
}
</style>

的最终编译输出为:

<div data-v-dfdfzzzz class="b-e-m"></div>
.root .b-e-m[data-v-dfdfzzzz] {
  background: red;
}

Vue 就是通过这种方式实现的样式隔离,使用 /deep/ 会修改最终的编译输出,data-v-hash 会移动到指定的 /deep/ 位置。

<style lang="scss" scoped>
.root /deep/ .b-e-m {
  background: red;
}
</style>

<!-- 最终输出会如下 -->
<!-- 移动了hash码的位置,使得样式可以对子级生效 -->
<style>
.root[data-v-dfdfzzzz] .b-e-m {
  background: red;
}
</style>

Axios

分享一个我封装的 axios 插件。

Axios封装

GitHub地址