理解闭包

变量的作用域

要理解闭包,首先必须理解Javascript特殊的变量作用域。

变量的作用域无非就是两种:全局变量和局部变量。

Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

1
2
3
4
5
6
7
  var n=999;

  function f1(){
    alert(n);
  }

  f1(); // 999

另一方面,在函数外部自然无法读取函数内的局部变量。

1
2
3
4
5
  function f1(){
    var n=999;
  }

  alert(n); // error

这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

1
2
3
4
5
6
7
  function f1(){
    n=999;
  }

  f1();

  alert(n); // 999

如何从外部读取局部变量?

出于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。

那就是在函数的内部,再定义一个函数。

1
2
3
4
5
6
7
8
9
  function f1(){

    var n=999;

    function f2(){
      alert(n); // 999
    }

  }

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  function f1(){

    var n=999;

    function f2(){
      alert(n);
    }

    return f2;

  }

  var result=f1();

  result(); // 999

专业的讲,一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

我的理解是,闭包就是能够读取其他函数内部变量的函数。

上一节代码中的f2函数,就是闭包。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”定义在一个函数内部的函数”。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的用途

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

怎么来理解这句话呢?请看下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  function f1(){

    var n=999;

    nAdd=function(){n+=1}

    function f2(){
      alert(n);
    }

    return f2;

  }

  var result=f1();

  result(); // 999

  nAdd();

  result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是”nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

最近运行Springboot项目是,报了一个端口号被占用的错

1
2
3
4
5
6
7
Description:

Web server failed to start. Port 10000 was already in use.

Action:

Identify and stop the process that's listening on port 10000 or configure this application to listen on another port.

报错原因:端口被占用。

解决办法

1.使用cmd命令查看端口号占用情况,例如查看端口10000,可以看出进程号为5372;

1
netstat -ano | findstr 10000

2.关闭该进程

方法一: 使用命令关闭

命令:

1
taskkill /F /pid 5372 

如果是非管理员用户可能报拒绝访问的错误

重新以管理员的身份打开cmd窗口,重新运行以上的关闭进程命令

方法二:使用任务管理器关闭:

菜单栏 -> 右键 - > 任务管理器 -> 详细信息,根据PID排序找到PID为5372的进程,选择后点击结束任务。

发现问题

优于长时间没进Linux连接mysql,今天进入Linux连接mysql,使用docker ps,mysql的状态一直是Restarting (1) x seconds ago,mysql一直在重启无法连接

查看日志

1
docker logs mysql

发现是内存不足

1
2
3
......
2021-06-14T11:41:26.576589Z 0 [ERROR] InnoDB: Error number 28 means 'No space left on device'
......

使用 df -h 查看 发现是 /var/docker/overlay2里面的东西占了很大的空间

检查内存占用情况,发现vagrant占用36G,一路找下去,发现到application data目录无线循环,上网搜索发现了问题所在。

ls查看发现里的东西很多和我本机C盘里的文件一模一样 AppData之类的 为什么这些东西会在虚拟机里 虚拟机同步了C盘里的内容 百度发现 vagrant提供了将本机

目录挂载到虚拟机目录下的功能,默认是将vagrant配置文件所在目录挂载到虚拟机/vagrant目录下。

也就是说 我的VagrantFile 是 C/user/username/VagrantFile 那么所有和VagrantFile同级的 C/user/username/里面的内容会全部同步到vagrant中 导致虚拟机内存爆满

解决办法

  1. 在和Vagrantfile同级目录(C:\Users\Administrator\)创建一个自己的文件夹,我的叫VagrantSyncFolder

  2. 然后打开目录到C:\Users\Administrator.vagrant.d\boxes\centos-VAGRANTSLASH-7\2004.01\virtualbox

  3. 打开Vagrantfile,

    config.vm.synced_folder “.”, “/vagrant”, type: “rsync”

    修改为

    config.vm.synced_folder “./vagrantCache”, “/vagrant”, type: “rsync”

  4. 修改完这个之后,vagrant reload发现还是不行,猜测可能只有vagrant成功重启之后修改的这个映射才能生效,但是已经占用100%不能成功重启,没办法,只能删东

    西了

    查看了一下vagrant目录里文件的大小

    1
    du -sh *

    其中AppData很大

    试着删了一下3D Objects文件里的一些东西,发现文件里的文件并没有影响,ok,直接将AppData删了,释放了很大的空间,然后exit退出

    vagrant reload重启,大功告成!

其实这种情况,可能在使用vagrant up命令的时候就会报Rsync 错误,可以参考这篇文章,其问题和上方的一样

Windows 10 上的 CentOS 7 Vagrant 框出现 Rsync 错误

介绍

2020年9月18日Vue.js 3.0“海贼王”正式发布。该框架的这个新主要版本提供了改进的性能、更小的包大小、更好的 TypeScript 集成、用于处理大规模用例的新 API,以及该框架未来长期迭代的坚实基础。

特点:

  • 随着时间的推移,它演变成我们所说的“渐进式框架”:一个可以逐渐学习和采用的框架,同时随着用户处理越来越多的要求的场景提供持续的支持。
  • Vue 3.0 核心仍然可以通过一个简单的<script>标签使用,但它的内部已经从头开始重写为一组解耦模块。新架构提供了更好的可维护性,并允许最终用户通过 tree-shaking 将运行时大小减少一半。
  • 2.x 基于对象的 API 在 Vue 3 中基本保持不变。但是,3.0 还引入了Composition API——一组新的 API,旨在解决 Vue 在大规模应用程序中使用的痛点。
  • 性能改进
  • Vue 3 的代码库是用 TypeScript 编写的,具有自动生成、测试和捆绑的类型定义,因此它们始终是最新的

快速上手

使用 vue-cli 创建Vue3.0工程

先查看@vue/cli版本,确保@vue/cli版本在4.5.0以上

1
vue --version

如果版本过低,就需要安装或者升级你的@vue/cli

1
npm install -g @vue/cli

运行以下命令来创建一个新项目:

1
vue create hello-world

启动项目

1
2
cd hello-world
npm run serve

常用 Composition API

setup

使用 (datacomputedmethodswatch) 组件选项来组织逻辑通常都很有效。然而,当我们的组件开始变得更大时,逻辑关注点的列表也会增长。尤其对于那些一开始没有编写这些组件的人来说,这会导致组件难以阅读和理解。

这是一个大型组件的示例,其中逻辑关注点按颜色进行分组。

这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。

如果能够将同一个逻辑关注点相关代码收集在一起会更好。

setup的设计是为了使用组合式api,使相关逻辑的代码集中在一起.

setup 组件选项

新的 setup 选项在组件创建之前执行,一旦 props 被解析,就将作为组合式 API 的入口。

setup 中你应该避免使用 this,因为它不会找到组件实例。setup 的调用发生在 data property、computed property 或 methods 被解析之前,所以它们无法在 setup 中被获取。

setup 选项是一个接收 propscontext 的函数,此外,我们将 setup 返回的所有内容都暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。

1
2
3
4
5
6
7
8
9
10
11
12
export default {
name: 'HelloWorld',
props: {
msg: String
},
setup(prop,context) {
console.log("prop:",prop)
console.log("context:",context)

return {} // 这里返回的任何内容都可以用于组件的其余部分
}
}

使用 setup 函数时,它将接收两个参数:

  1. props
  2. context

Props

setup 函数中的第一个参数是 props。正如在一个标准组件中所期望的那样,setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。

1
2
3
4
5
6
7
8
9
10
// MyBook.vue

export default {
props: {
title: String
},
setup(props) {
console.log(props.title)
}
}

但是,因为 props 是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。

如果需要解构 prop,可以在 setup 函数中使用 toRefs 函数来完成此操作:

1
2
3
4
5
6
7
8
9
// MyBook.vue

import { toRefs } from 'vue'

setup(props) {
const { title } = toRefs(props)

console.log(title.value)
}

如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:

1
2
3
4
5
6
// MyBook.vue
import { toRef } from 'vue'
setup(props) {
const title = toRef(props, 'title')
console.log(title.value)
}

props在浏览器的控制台输出如下图:

Context

传递给 setup 函数的第二个参数是 contextcontext 是一个普通的 JavaScript 对象,它暴露组件的三个 property:

的三个 property:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MyBook.vue

export default {
setup(props, context) {
// Attribute (非响应式对象)
console.log(context.attrs)

// 插槽 (非响应式对象)
console.log(context.slots)

// 触发事件 (方法)
console.log(context.emit)
}
}

context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。

1
2
3
4
5
6
// MyBook.vue
export default {
setup(props, { attrs, slots, emit }) {
...
}
}

attrsslots 是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.xslots.x 的方式引用 property。

context在浏览器的控制台输出如下图:

setup注意点:

  1. 尽量不要与Vue2.x配置混用
    • Vue2.x配置(data、methos、computed…)中可以访问到setup中的属性、方法。
    • 但在setup中不能访问到Vue2.x配置(data、methos、computed…)。
    • 如果有重名, setup优先。
  2. setup不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)
  3. setup() 内部,this 不是该活跃实例的引用

执行 setup 时,组件实例尚未被创建。因此,你只能访问以下 property:

  • props
  • attrs
  • slots
  • emit

换句话说,你将无法访问以下组件选项:

  • data
  • computed
  • methods

ref

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value

示例:

1
2
3
4
5
6
7
8
9
10
11
setup() {
const count = ref(0)
function countAdd() {
count.value++
}

return {
count,
countAdd
}
}

如果将对象分配为 ref 值,则通过 reactive 方法使该对象具有高度的响应式。

使用数据

1
2
<h2>count: {{count}}</h2>
<button @click="countAdd">count++</button>
  • 接收的数据可以是:基本类型、也可以是对象类型。
  • 基本类型的数据:响应式依然是靠Object.defineProperty()getset完成的。
  • 对象类型的数据:内部 使用了Vue3.0中的一个新函数—— reactive函数。

reactive函数

返回对象的响应式副本,Proxy的实例对象

1
const obj = reactive({ count: 0 })

reactive 将解包所有深层的 refs,同时维持 ref 的响应性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const count = ref(1)
const obj = reactive({ count })

// ref 会被解包
console.log(obj.count === count.value) // true

// 它会更新 `obj.count`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2

// 它也会更新 `count` ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3

Vue3.0中的响应式原理

vue2.x的响应式

  • 实现原理:

    • 对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)。

    • 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。

      1
      2
      3
      4
      Object.defineProperty(data, 'count', {
      get () {},
      set () {}
      })
  • 存在问题:

    • 新增属性、删除属性, 界面不会更新。
    • 直接通过下标修改数组, 界面不会自动更新。

Vue3.0的响应式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Proxy(data, {
// 拦截读取属性值
get (target, prop) {
return Reflect.get(target, prop)
},
// 拦截设置属性值或添加新属性
set (target, prop, value) {
return Reflect.set(target, prop, value)
},
// 拦截删除属性
deleteProperty (target, prop) {
return Reflect.deleteProperty(target, prop)
}
})

proxy.name = 'tom'

计算属性和侦听器

计算属性

有时我们需要依赖于其他状态的状态——在 Vue 中,这是用组件计算属性处理的,以直接创建计算值,我们可以使用 computed 方法:它接受 getter 函数并为 getter 返回的值返回一个不可变的响应式 ref 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setup() {
const person = reactive({
firstName: '',
lastName: ''
})

person.fullName = computed(() => {
return person.firstName + ' ' + person.lastName
})

return {
person
}
}

使用数据

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="hello">
<h1>请输入姓名</h1>
<h3>firstName</h3>
<input type="text" v-model="person.firstName" />
<h3>lastName</h3>
<input type="text" v-model="person.lastName" />
<h3>fullName</h3>
<h3>{{ person.fullName }}</h3>
</div>
</template>

效果

或者,它可以使用一个带有 getset 函数的对象来创建一个可写的 ref 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
setup() {
const person = reactive({
firstName: "",
lastName: "",
});

person.fullName = computed({
get() {
return person.firstName + " " + person.lastName;
},
set(newValue) {
const names = newValue.split(" ");
person.firstName = names[0];
person.lastName = names[names.length - 1];
},
});

return {
person,
};
},

使用数据

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="hello">
<h1>请输入姓名</h1>
<h3>fullName</h3>
<input type="text" v-model="person.fullName" />
<h3>firstName</h3>
<h3>{{ person.firstName }}</h3>
<h3>lastName</h3>
<h3>{{ person.lastName }}</h3>
</div>
</template>

效果


Watch

watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。

侦听单个数据源

侦听器数据源可以是返回值的 getter 函数,也可以直接是 ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)

// 直接侦听ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})

侦听多个数据源

侦听器还可以使用数组同时侦听多个源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const firstName = ref("zhang");
const lastName = ref("san");

watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues);
});

function changeFirstName() {
firstName.value = "Steve"; // logs: ["Steve", "san"] ["zhang", "san"]
}

function changeLastName() {
lastName.value = "Jobs"; // logs: ["zhang", "Jobs"] ["zhang", "san"]
}

尽管如此,如果你在同一个方法里同时改变这些被侦听的来源,侦听器仍只会执行一次:

1
2
3
4
5
function changeFullName() {
firstName.value = "Steve";
lastName.value = "Jobs";
// 打印 ["Steve", "Jobs"] ["zhang", "san"]
}

注意多个同步更改只会触发一次侦听器。

两个小“坑”:

  • 监视reactive定义的响应式数据时:oldValue无法正确获取、默认已经强制开启了深度监视(deep配置失效)。
  • 监视reactive定义的响应式数据中某个属性时:deep配置有效。

watchEffect

  • watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。

  • watchEffect有点像computed:

    • 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。

    • 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。

      1
      2
      3
      4
      5
      6
      //watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
      watchEffect(()=>{
      const x1 = sum.value
      const x2 = person.age
      console.log('watchEffect配置的回调执行了')
      })

生命周期

下表包含如何在 setup () 内部调用生命周期钩子:

选项式 API Hook inside setup
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
activated onActivated
deactivated onDeactivated

因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

toRef

可以用来为源响应式对象上的某个 property 新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。

1
2
3
4
5
6
7
8
9
10
11
12
const state = reactive({
foo: 1,
bar: 2
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

toRefs

将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref

当从组合式函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行分解/扩散:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2
})

// 操作 state 的逻辑

// 返回时转换为ref
return toRefs(state)
}

export default {
setup() {
// 可以在不失去响应性的情况下解构
const { foo, bar } = useFeatureX()

return {
foo,
bar
}
}
}

toRefs 只会为源对象中包含的 property 生成 ref。如果要为特定的 property 创建 ref,则应当使用 toRef


其他 Composition API

shallowReactive 与 shallowRef

  • shallowReactive:创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const state = shallowReactive({
    foo: 1,
    nested: {
    bar: 2
    }
    })

    // 改变 state 本身的性质是响应式的
    state.foo++
    // ...但是不转换嵌套对象
    isReactive(state.nested) // false
    state.nested.bar++ // 非响应式
  • shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。

    1
    2
    3
    4
    5
    const foo = shallowRef({})
    // 改变 ref 的值是响应式的
    foo.value = {}
    // 但是这个值不会被转换。
    isReactive(foo.value) // false
  • 什么时候使用?

    • 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
    • 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef

readonly 与 shallowReadonly

  • readonly: 接受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const original = reactive({ count: 0 })

    const copy = readonly(original)

    watchEffect(() => {
    // 用于响应性追踪
    console.log(copy.count)
    })

    // 变更 original 会触发依赖于副本的侦听器
    original.count++

    // 变更副本将失败并导致警告
    copy.count++ // 警告!
  • shallowReadonly:创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const state = shallowReadonly({
    foo: 1,
    nested: {
    bar: 2
    }
    })

    // 改变 state 本身的 property 将失败
    state.foo++
    // ...但适用于嵌套对象
    isReadonly(state.nested) // false
    state.nested.bar++ // 适用
  • 应用场景: 不希望数据被修改时。

toRaw 与 markRaw

  • toRaw:

    • 作用:将一个由reactive生成的响应式对象转为普通对象
    • 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
  • markRaw:

    • 作用:标记一个对象,使其永远不会再成为响应式对象。返回对象本身。

      1
      2
      3
      4
      5
      6
      const foo = markRaw({})
      console.log(isReactive(reactive(foo))) // false

      // 嵌套在其他响应式对象中时也可以使用
      const bar = reactive({ foo })
      console.log(isReactive(bar.foo)) // false

      有些值不应该是响应式的,例如复杂的第三方类实例或 Vue 组件对象

      当渲染具有不可变数据源的大列表时,跳过 proxy 转换可以提高性能。

customRef

创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 tracktrigger 函数作为参数,并且应该返回一个带有 getset 的对象。

自定义customRef配合自定义hook函数使用。

什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装。类似于vue2.x中的mixin。自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。

在项目中新建hooks文件夹,在hooks文件夹下新建useDebounce.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { customRef } from "vue"

export default function(value, delay = 1000) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}

使用自定义 ref 通过 v-model 实现 debounce 的示例:

1
2
<input v-model="text" />
<h3>{{text}}</h3>
1
2
3
4
5
6
7
8
9
import useDebouncedRef from '../hooks/useDebounce'

export default {
setup() {
return {
text: useDebouncedRef('hello')
}
}
}

provide 与 inject

通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。

对于这种情况,我们可以使用一对 provideinject。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。

用法:

父组件中提供:

1
2
3
4
5
6
setup(){
......
let car = reactive({name:'奔驰',price:'40万'})
provide('car',car)
......
}

子组件中使用:

1
2
3
4
5
6
setup(props,context){
......
const car = inject('car')
return {car}
......
}

响应式数据的判断

  • isRef: 检查一个值是否为一个 ref 对象
  • isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
  • isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
  • isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

新的组件

Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下渲染了 HTML,而不必求助于全局状态或将其拆分为两个组件。

让我们修改 modal-button 以使用 <teleport>,并告诉 Vue “Teleport 这个 HTML 该‘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
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>

<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})

其他

全局API的转移

  • Vue 2.x 有许多全局 API 和配置。

    • 例如:注册全局组件、注册全局指令等。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      //注册全局组件
      Vue.component('MyButton', {
      data: () => ({
      count: 0
      }),
      template: '<button @click="count++">Clicked {{ count }} times.</button>'
      })

      //注册全局指令
      Vue.directive('focus', {
      inserted: el => el.focus()
      }
  • Vue3.0中对这些API做出了调整:

    • 将全局的API,即:Vue.xxx调整到应用实例(app)上

      2.x 全局 API(Vue 3.x 实例 API (app)
      Vue.config.xxxx app.config.xxxx
      Vue.config.productionTip 移除
      Vue.component app.component
      Vue.directive app.directive
      Vue.mixin app.mixin
      Vue.use app.use
      Vue.prototype app.config.globalProperties

其他改变

  • data选项应始终被声明为一个函数。

  • 过度类名的更改:

    • Vue2.x写法

      1
      2
      3
      4
      5
      6
      7
      8
      .v-enter,
      .v-leave-to {
      opacity: 0;
      }
      .v-leave,
      .v-enter-to {
      opacity: 1;
      }
    • Vue3.x写法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      .v-enter-from,
      .v-leave-to {
      opacity: 0;
      }

      .v-leave-from,
      .v-enter-to {
      opacity: 1;
      }
  • 移除keyCode作为 v-on 的修饰符,同时也不再支持config.keyCodes

  • 移除v-on.native修饰符

    • 父组件中绑定事件

      1
      2
      3
      4
      <my-component
      v-on:close="handleComponentEvent"
      v-on:click="handleNativeClickEvent"
      />
    • 子组件中声明自定义事件

      1
      2
      3
      4
      5
      <script>
      export default {
      emits: ['close']
      }
      </script>
  • 移除过滤器(filter)

    过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。

简介

Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:

  • 嵌套的路由/视图表
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 基于 Vue.js 过渡系统的视图过渡效果
  • 细粒度的导航控制
  • 带有自动激活的 CSS class 的链接
  • HTML5 历史模式或 hash 模式,在 IE9 中自动降级
  • 自定义的滚动条行为

安装

NPM

1
npm install vue-router

使用实例

引入插件并使用

main.js中引入插件并使用

1
2
3
import VueRouter from 'vue-router'

Vue.use(VueRouter)

编写router配置项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import VueRouter from 'vue-router'
import Chinese from "../pages/Chinese.vue"
import English from "../pages/English.vue"

export default new VueRouter({
routes: [
{
path: '/chinese',
component: Chinese
},
{
path: '/english',
component: English
}
]
});

使用router

main.js中引入router并使用

1
2
3
4
5
6
import router from './router'

new Vue({
render: h => h(App),
router: router
}).$mount('#app')

实现路由页面切换

(active-class可配置高亮样式)

1
2
<router-link class="list-group-item" active-class="active" to="/chinese">中文</router-link>
<router-link class="list-group-item" active-class="active" to="/english">英文</router-link>

指定组件渲染的位置

1
2
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>

几个注意点

  1. 路由组件通常存放在pages文件夹,一般组件通常存放在components文件夹。
  2. 通过切换,“隐藏”了的路由组件,默认是被销毁掉的,需要的时候再去挂载。
  3. 每个组件都有自己的$route属性,里面存储着自己的路由信息。
  4. 整个应用只有一个router,可以通过组件的$router属性获取到。

嵌套路由

实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件。

配置路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default new VueRouter({
routes: [
{
path: '/chinese',
component: Chinese
},
{
path: '/english',
component: English,
//通过children配置子级路由
children: [
{
path: 'book',
component: Book
},
{
path: 'author',
component: Author
}
]
}
]
});

路由跳转:

1
2
3
4
5
6
<li>
<router-link class="list-group-item" active-class="active" to="/english/book">Book</router-link>
</li>
<li>
<router-link class="list-group-item" active-class="active" to="/english/author">Author</router-link>
</li>

命名路由

有时候,通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些跳转的时候。你可以在创建 Router 实例的时候,在 routes 配置中给某个路由设置名称。

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
export default new VueRouter({
routes: [
{
name: 'chineseRouter',
path: '/chinese',
component: Chinese
},
{
path: '/english',
component: English,
children: [
{
name: 'bookRouter',
path: 'book',
component: Book
},
{
name: 'authorRouter',
path: 'author',
component: Author
}
]
}
]
});

路由跳转:

1
2
3
4
<!--简化前,需要写完整的路径 -->
<router-link to="/english/book">Book</router-link>
<!--简化后,直接通过名字跳转 -->
<router-link :to="{name : 'bookRouter'}">Book</router-link>

路由的query参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 跳转路由并携带query参数,to的字符串写法 -->
<router-link :to="`/home/message/detail?id=${m.id}&title=${m.title}`">{{m.title}}</router-link>&nbsp;&nbsp;

<!-- 跳转路由并携带query参数,to的对象写法 -->
<router-link :to="{
path: '/english/author/info',
query: {
id: author.id,
name: author.name,
info: author.info
}
}">
{{ author.name }}
</router-link>&nbsp;&nbsp;

接收参数:

1
2
3
4
5
<ul>
<li>编号: {{$route.query.id}}</li>
<li>作家: {{$route.query.name}}</li>
<li>简介: {{$route.query.info}}</li>
</ul>

路由的param参数

vue-router 的路由路径中使用“动态路径参数”(dynamic segment)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
children: [
{
name: 'bookRouter',
path: 'book',
component: Book
},
{
name: 'authorRouter',
path: 'author',
component: Author,
children: [
{
name: 'authorInfoRouter',
path: 'info/:id/:name/:info',//使用占位符声明接收params参数
component: AuthorInfo
}
]
}
]

路由跳转:

1
2
3
4
5
6
7
8
9
10
<router-link :to="{
name: 'authorInfoRouter',
params: {
id: author.id,
name: author.name,
info: author.info
}
}">
{{ author.name }}
</router-link>&nbsp;&nbsp;

特别注意:路由携带params参数时,若使用to的对象写法,则不能使用path配置项,必须使用name配置!

接受参数:

当匹配到一个路由时,参数值会被设置到 this.$route.params

1
2
3
4
5
<ul>
<li>编号: {{$route.params.id}}</li>
<li>作家: {{$route.params.name}}</li>
<li>简介: {{$route.params.info}}</li>
</ul>

路由的props配置

在上面的例子中,我们读取参数需要不停重复的写 this.$route.params 前缀,很繁琐,使用props配置可以解决

布尔模式

如果 props 被设置为 trueroute.params 将会被设置为组件属性。

props为true,就会把路由组件收到的所有params参数,以props的形式传给AuthorInfo组件

1
2
3
4
5
6
7
8
children: [
{
name: 'authorInfoRouter',
path: 'info/:id/:name/:info',
component: AuthorInfo,
props: true
}
]

使用props 配置项接受参数:

1
2
3
4
5
6
7
8
9
10
export default {
components: {},
props: ['id','name','info'],
data() {
//这里存放数据
return {

}
}
},

使用参数

1
2
3
4
5
<ul>
<li>编号: {{id}}</li>
<li>作家: {{name}}</li>
<li>简介: {{info}}</li>
</ul>

函数模式

你可以创建一个函数返回 props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
children: [
{
name: 'authorInfoRouter',
path: 'info',
component: AuthorInfo,
props($route) {
return {
id: $route.query.id,
name: $route.query.name,
info: $route.query.info
}
}
}
]

props函数的结构赋值形式:

1
2
3
4
5
6
7
props({query: {id, name, info}}) {
return {
id: id,
name: name,
info: info
}
}

<router-link>的replace属性

  1. 作用:控制路由跳转时操作浏览器历史记录的模式
  2. 浏览器的历史记录有两种写入方式:分别为pushreplacepush是追加历史记录(相当于向一个栈中添加元素),replace是替换当前记录(相当于替换掉栈顶元素)。路由跳转时候默认为push
  3. 如何开启replace模式:<router-link replace>News</router-link>

编程式的导航

除了使用 <router-link> 创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法push,通过编写代码来实现。

声明式 编程式
<router-link :to="..."> router.push(...)

注意:在 Vue 实例内部,你可以通过 $router 访问路由实例。因此你可以调用 this.$router.push

router.push(location, onComplete?, onAbort?)

可选的在 router.pushrouter.replace 中提供 onCompleteonAbort 回调作为第二个和第三个参数。这些回调将会在导航成功完成 (在所有的异步钩子被解析之后) 或终止 (导航到相同的路由、或在当前导航完成之前导航到另一个不同的路由) 的时候进行相应的调用。

想要导航到不同的 URL,则使用 router.push 方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 字符串
router.push('home')

// 对象
router.push({ path: 'home' })

// 命名的路由
router.push({
name: 'user',
params: {
userId: '123'
}
})

// 带查询参数,变成 /register?plan=private
router.push({
path: 'register',
query: {
plan: 'private'
}
})

注意:如果提供了 pathparams 会被忽略,使用params就不用path,而是用name

router.replace(location, onComplete?, onAbort?)

router.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录,相当于替换掉栈顶元素。

声明式 编程式
<router-link :to="..." replace> router.replace(...)
1
2
3
4
5
6
router.replace({ 
path: 'register',
query: {
plan: 'private'
}
})

forwardbackgo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
this.$router.forward() //前进
this.$router.back() //后退

// 在浏览器记录中前进一步,等同于 history.forward()
router.go(1)

// 后退一步记录,等同于 history.back()
router.go(-1)

// 前进 3 步记录
router.go(3)

// 如果 history 记录不够用,那就默默地失败呗
router.go(-100)

keep-alive

  • Props

    • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
    • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
    • max - 数字。最多可以缓存多少组件实例。
  • 用法

    <keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

    当组件在 <keep-alive> 内被切换,它的 activateddeactivated 这两个生命周期钩子函数将会被对应执行。

主要用于保留组件状态或避免重新渲染。

includeexclude prop 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组(组件名)来表示:

1
2
3
<keep-alive include="News"> 
<router-view></router-view>
</keep-alive>

路由相关的两个生命周期钩子

activated

  • 类型Function

  • 详细

    被 keep-alive 缓存的组件激活时调用。

  • 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default {
//import 引入的组件需要注入到对象中才能使用
name: "English",
components: {},
props: {},
data() {
//这里存放数据
return {};
},
//计算属性类似于data 概念
computed: {},
activated() {}, // 被 keep-alive 缓存的组件激活时调用。
deactivated() {} // 被 keep-alive 缓存的组件停用时调用。
};

deactivated

  • 类型Function

  • 详细

    被 keep-alive 缓存的组件停用时调用。


路由守卫

作用:对路由进行权限控制

全局前置守卫

你可以使用 router.beforeEach 注册一个全局前置守卫:

1
2
3
4
5
const router = new VueRouter({ ... })

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

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中

每个守卫方法接收三个参数:

  • to: Route: 即将要进入的目标路由对象
  • from: Route: 当前导航正要离开的路由
  • next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
    • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
    • next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
    • next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: truename: 'home' 之类的选项以及任何用在 router-linkto prop 或 router.push中的选项。
    • next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError()注册过的回调。

确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错

1
2
3
4
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})

全局后置钩子

你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:

1
2
3
router.afterEach((to, from) => {
// ...
})

路由独享的守卫

你可以在路由配置上直接定义 beforeEnter 守卫:

1
2
3
4
5
6
7
8
9
10
11
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})

这些守卫与全局前置守卫的方法参数是一样的

组件内的守卫

最后,你可以在路由组件内直接定义以下路由导航守卫:

  • beforeRouteEnter
  • beforeRouteUpdate (2.2 新增)
  • beforeRouteLeave
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
//import 引入的组件需要注入到对象中才能使用
name: "English",
components: {},
props: {},
data() {
//这里存放数据
return {};
},
beforeRouteEnter(to, from, next) {
// 通过路由规则,进入该组件时被调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 通过路由规则,离开该组件时被调用
// 可以访问组件实例 `this`
}

beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。

不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。

1
2
3
4
5
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}

注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdatebeforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。

1
2
3
4
5
6
7
8
beforeRouteLeave (to, from, next) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}

路由元信息

定义路由的时候可以配置 meta 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})

通过to.meta.requiresAuth访问

什么是Vuex?

Vuex是专门在Vue 中实现集中式状态(数据)管理的一个Vue 插件,对vue 应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。

当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这就是 Vuex 背后的基本思想.

什么情况下我应该使用 Vuex?

Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。

如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是:

Flux 架构就像眼镜:您自会知道什么时候需要它。


使用

安装vuex

NPM

1
npm install vuex 

使用Store

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的**状态 (state)**。Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用

  3. 创建文件:src/store/index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    import Vue from 'vue'
    import Vuex from 'vuex'

    Vue.use(Vuex)

    const store = new Vuex.Store({
    actions: {

    },
    state: {
    count: 0
    },
    mutations: {
    increment(state) {
    state.count++
    }
    }
    })
  4. main.js中创建vm时传入store配置项

    为了在 Vue 组件中访问 this.$store property,你需要为 Vue 实例提供创建好的 store。Vuex 提供了一个从根组件向所有子组件,以 store 选项的方式“注入”该 store 的机制:

    1
    2
    3
    4
    new Vue({
    el: '#app',
    store: store,
    })

    如果使用 ES6,你也可以以 ES6 对象的 property 简写 (用在对象某个 property 的 key 和被传入的变量同名时):

    1
    2
    3
    4
    new Vue({
    el: '#app',
    store
    })
  5. 在子组件HelloWorld中绑定一个方法,用来改变store中的数据

    1
    2
    3
    4
    <div class="container">
    <h1>当前的值是:{{$store.state.count}}</h1>
    <button @click="increase">+1</button>
    </div>

    在组件中使用 this.$store.dispatch('xxx') 分发 action

    1
    2
    3
    4
    5
    methods: {
    increase() {
    this.$store.dispatch('increment',1)
    }
    }
  6. 完善src/store/index.js中的方法

    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 Vue from 'vue'
    import Vuex from 'vuex'

    Vue.use(Vuex)

    const store = new Vuex.Store({
    actions: {
    // Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。
    // 参数value值是通过dispatch方法传过来的参数
    increment(context, value) {
    context.commit("increment",value)
    }
    },
    state: {
    count: 0
    },
    mutations: {
    increment(state,value) {
    state.count += value
    }
    }
    })

    export default store
  1. 效果

    点击按钮前:

    点击按钮5下后


Getter

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

Getter 接受 state 作为其第一个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const store = new Vuex.Store({
actions: {
increase(context, value) {
context.commit("increase",value)
}
},
state: {
dollar: 0,
rate: 7
},
getters: {
rmb(state){
return state.dollar * state.rate
}
},
mutations: {
increase(state,value) {
state.dollar += value
}
}
})
export default store

Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:

1
$store.getters.rmb 

mapGetters 辅助函数

先引入mapGetters

1
import { mapGetters } from 'vuex'

数组写法

1
2
3
4
5
6
7
8
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}

如果你想将一个 getter 属性另取一个名字,使用对象形式:

1
2
3
4
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})

mapState 辅助函数

先引入mapState

1
import { mapState } from 'vuex'

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

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。

1
2
3
4
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])

对象写法

1
2
3
4
computed: mapState([
// 映射 this.count 为 store.state.count
count:'count'
])

mapActions 辅助函数

先导入mapActions

1
import { mapActions } from 'vuex'

数组写法:

1
2
3
4
5
6
7
8
9
10
11
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')`
})
}

对象写法:

1
2
3
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})

mapMutations 辅助函数

先导入mapActions

1
import { mapMutations } from 'vuex'

数组写法:

1
2
3
4
5
6
7
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

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

对象写法

1
2
3
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})

Module模块化

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const moduleA = {
namespaced:true,//开启命名空间
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}

const moduleB = {
namespaced:true,//开启命名空间
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}

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

在组件中对数据进行读取:

读取state数据:

1
2
3
4
//第一个参数空间名称字符串,第二个参数是空间名称字符串的中的参数
computed: {
...mapState('currency',['dollar','rate'])
}

读取getters数据:

1
2
3
4
//第一个参数空间名称字符串
computed: {
...mapGetters('currency',['rmb'])
}

组件中调用dispatch

1
2
3
4
5
6
7
methods: {
//方式一:自己直接dispatch
this.$store.dispatch('currency/increase',1),
//方式二:借助mapGetters读取:
...mapActions('currency',['increase'])
},

组件中调用commit

1
2
3
4
5
6
7
8
methods: {
addOnePerson() {
//方式一:自己直接dispatch
this.$store.commit('person/addOnePerson',newPerson)
},
//方式二:借助mapGetters读取:
...mapActions('person',['addOnePerson'])
},

Vue-CLI的使用

介绍

vue脚手架的作用是用来自动一键生成vue+webpack的项目模版,包括依赖库,免去你手动安装各种插件,寻找各种cdn并一个个引入的麻烦。

vue脚手架指的是vue-cli,它是一个专门为单页面应用快速搭建繁杂的脚手架,它可以轻松的创建新的应用程序而且可用于自动生成vue和webpack的项目模板。

vue-cli是有Vue提供的一个官方cli,专门为单页面应用快速搭建繁杂的脚手架。它是用于自动生成vue.js+webpack的项目模板,是为现代前端工作流提供了 batteries-included 的构建设置。只需要几分钟的时间就可以运行起来并带有热重载,保存时 lint 校验,以及生产环境可用的构建版本

安装

Node 版本要求

Vue CLI 4.x 需要 Node.js v8.9 或更高版本 (推荐 v10 以上)。你可以使用 nnvmnvm-windows 在同一台电脑中管理多个 Node 版本。

可以使用下列任一命令安装这个新的包:

1
2
3
npm install -g @vue/cli
# OR
yarn global add @vue/cli

安装之后,你就可以在命令行中访问 vue 命令。你可以通过简单运行 vue,看看是否展示出了一份所有可用命令的帮助信息,来验证它是否安装成功。

你还可以用这个命令来检查其版本是否正确:

1
vue --version

升级

如需升级全局的 Vue CLI 包,请运行:

1
2
3
4
npm update -g @vue/cli

# 或者
yarn global upgrade --latest @vue/cli

创建项目

切换到你要创建项目的目录,然后运行以下命令来创建一个新项目:

1
vue create hello-world

你会被提示选取一个 preset。你可以选默认的包含了基本的 Babel + ESLint 设置的 Vue2或者Vue3,也可以选“手动选择特性”来选取需要的特性。

使用图形化界面

你也可以通过 vue ui 命令以图形化界面创建和管理项目:

1
vue ui

上述命令会打开一个浏览器窗口,并以图形化界面将你引导至项目创建的流程。

启动项目

1
npm run serve
1
如出现下载缓慢请配置npm 淘宝镜像:npm config set registry https://registry.npm.taobao.org
1
Vue 脚手架隐藏了所有webpack 相关的配置,若想查看具体的webpakc 配置,请执行:vue inspect > output.js

vue.config.js

vue.config.js 是一个可选的配置文件,如果项目的 (和 package.json 同级的) 根目录中存在这个文件,那么它会被 @vue/cli-service 自动加载。你也可以使用 package.json 中的 vue 字段,但是注意这种写法需要你严格遵照 JSON 的格式来写。

这个文件应该导出一个包含了选项的对象:

1
2
3
4
5
6
7
8
// vue.config.js

/**
* @type {import('@vue/cli-service').ProjectOptions}
*/
module.exports = {
// 选项...
}

模板项目的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├── node_modules 
├── public
│ ├── favicon.ico: 页签图标
│ └── index.html: 主页面
├── src
│ ├── assets: 存放静态资源
│ │ └── logo.png
│ │── component: 存放组件
│ │ └── HelloWorld.vue
│ │── App.vue: 汇总所有组件
│ │── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件

ref属性

ref 被用来给元素或子组件注册引用信息。

  • 引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素(id的替代者);
  • 如果用在子组件上,引用就指向组件实例VueComponent
1
2
3
4
5
<!-- `vm.$refs.p` will be the DOM node -->
<p ref="p">hello</p>

<!-- `vm.$refs.child` will be the child component instance -->
<child-component ref="child"></child-component>

v-for 用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组。

获取:this.$refs.xxx


props

props 可以是数组或对象,用于接收来自父组件的数据。

三种读取方式

  • 示例

    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
    // 简单语法
    Vue.component('props-demo-simple', {
    props: ['size', 'myMessage']
    })

    // 指定名称和类型
    Vue.component('props-demo-simple', {
    props: {
    title: String,
    likes: Number,
    isPublished: Boolean,
    commentIds: Array,
    author: Object,
    callback: Function,
    contactsPromise: Promise // or any other constructor
    }
    })

    // 对象语法,提供验证
    Vue.component('props-demo-advanced', {
    props: {
    // 检测类型
    height: Number,
    // 检测类型 + 其他验证
    age: {
    type: Number,
    default: 0, // 默认值
    required: true,
    validator: function (value) {
    return value >= 0
    }
    }
    }
    })
1
备注:props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。

mixin

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。

当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

1
2
3
4
// 定义一个混入对象
var mixin = {
created: function () { console.log(1) }
}

混入对象的局部使用

1
2
3
4
5
6
7
8
// 定义一个使用混入对象的组件
var vm = new Vue({
created: function () { console.log(2) },
mixins: [mixin]
})

// => 1
// => 2

如果你的混入包含一个 created 钩子,而创建组件本身也有一个,那么两个函数都会被调用。

Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。

全局使用

全局混入:Vue.mixin(xxx)

全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。插件作者可以使用混入,向组件注入自定义的行为。不推荐在应用代码中使用


插件

插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:

  1. 添加全局方法或者 property。如:vue-custom-element
  2. 添加全局资源:指令/过滤器/过渡等。如 vue-touch
  3. 通过全局混入来添加一些组件选项。如 vue-router
  4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router

开发插件

Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 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
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}

// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})

// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})

// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}

使用插件

通过全局方法 Vue.use() 使用插件。它需要在你调用 new Vue() 启动应用之前完成:

1
2
3
4
5
6
// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)

new Vue({
// ...组件选项
})

scoped样式

  1. 作用:让样式在局部生效,防止冲突。

  2. 写法:

    1
    2
    3
    4
    5
    6
    7
    <style scoped>
    @media (min-width: 250px) {
    .list-container:hover {
    background: orange;
    }
    }
    </style>

这个可选 scoped attribute 会自动添加一个唯一的 attribute (比如 data-v-21e5b78) 为组件内 CSS 指定作用域,编译的时候 .list-container:hover 会被编译成类似 .list-container[data-v-21e5b78]:hover


组件的自定义事件

组件的自定义事件是一种组件间通信方式,适用于:子组件给父组件传递数据。

不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。

不同于组件和 prop,事件名不会被用作一个 JavaScript 变量名或 property 名,所以就没有理由使用 camelCase 或 PascalCase 了。并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到。

因此,我们推荐你始终使用 kebab-case 的事件名

步骤:

先在父组件中给子组件TodoFooter绑定事件:

1
<TodoFooter @custom-event="doSomething"></TodoFooter>

并在methods中定义doSomething回调方法:

1
2
3
doSomething(msg) {
console.log(msg)
}

在子组件中绑定一个按钮来触发自定义事件:

1
<button @click="triggerCustomEvent">触发自定义事件</button>

触发自定义事件:

1
2
3
triggerCustomEvent() {
this.$emit("custom-event","doSomething...")
}

上面绑定自定义事件还有第二种方式:

1
2
3
4
5
<TodoFooter ref="todofooter"></TodoFooter>
......
mounted() {
this.$refs.todofooter.$on("custom-event",this.doSomething)
},

解绑自定义事件:

vm.$off( [event, callback] )

  • 参数

    • {string | Array<string>} event (只在 2.2.2+ 支持数组)
    • {Function} [callback]
  • 用法

    移除自定义事件监听器。

    • 如果没有提供参数,则移除所有的事件监听器;
    • 如果只提供了事件,则移除该事件所有的监听器;
    • 如果同时提供了事件与回调,则只移除这个回调的监听器。
1
this.$off("custom-event")

将原生事件绑定到组件

你可能有很多次想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用 v-on.native 修饰符:

1
<TodoFooter @click.native="show"></TodoFooter>

全局事件总线

vue组件中的数据传递最最常见的就是父子组件之间的传递。父传子通过props向下传递数据给子组件;子传父通过$emit发送事件,并携带数据给父组件。而有时两个组件之间毫无关系,或者他们之间的结构复杂,如何传递数据呢?这时就要用到 vue 中的事件总线 EventBus的概念。

使用 EventBus

  • 第一种方式: 可以在 main.js中,初始化 EventBus
1
2
3
4
5
6
new Vue({
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this
}
}).$mount('#app')

X组件想接收数据,则在X组件中给$bus绑定自定义事件,事件的回调留在A组件自身。

1
2
3
4
5
6
7
8
methods(){
demo(data){
}
}
......
mounted() {
this.$bus.$on('xxxx',this.demo)
}

Y组件想要给X组件传递数据,则在Y组件中发送事件

1
this.$bus.$emit('xxxx',param)

在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件

语法:this.$EventBus.$off(要移除监听的事件名)


$nextTick

vm.$nextTick( [callback] )

  • 参数

    • {Function} [callback]
  • 用法

    将回调延迟到下次 DOM 更新循环之后执行。

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    new Vue({
    // ...
    methods: {
    // ...
    example: function () {
    // 修改数据
    this.message = 'changed'
    // DOM 还没有更新
    this.$nextTick(function () {
    // DOM 现在更新了
    // `this` 绑定到当前实例
    this.doSomethingElse()
    })
    }
    }
    })

过渡&动画

单元素/组件的过渡

Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡

  • 条件渲染 (使用 v-if)
  • 条件展示 (使用 v-show)
  • 动态组件
  • 组件根节点

这里是一个典型的例子:

1
2
3
4
5
6
7
8
<div id="demo">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<p v-if="show">hello</p>
</transition>
</div>
1
2
3
4
5
6
new Vue({
el: '#demo',
data: {
show: true
}
})
1
2
3
4
5
6
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}

在进入/离开的过渡中,会有 6 个 class 切换。

  1. v-enter:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
  2. v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
  3. v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成之后移除。
  4. v-leave:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
  5. v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
  6. v-leave-to:定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除。

对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 <transition>,则 v- 是这些类名的默认前缀。如果你使用了 <transition name="my-transition">,那么 v-enter 会替换为 my-transition-enter

v-enter-activev-leave-active 可以控制进入/离开过渡的不同的缓和曲线。

CSS 过渡

常用的过渡都是使用 CSS 过渡。

下面是一个简单例子:

1
2
3
4
5
6
7
8
<div id="example-1">
<button @click="show = !show">
Toggle render
</button>
<transition name="slide-fade">
<p v-if="show">hello</p>
</transition>
</div>
1
2
3
4
5
6
new Vue({
el: '#example-1',
data: {
show: true
}
})
1
2
3
4
5
6
7
8
9
10
11
12
/* 可以设置不同的进入和离开动画 */
/* 设置持续时间和动画函数 */
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to {
transform: translateX(10px);
opacity: 0;
}

第三方 CSS 动画库

我们可以通过以下 attribute 来自定义过渡类名:

  • enter-class
  • enter-active-class
  • enter-to-class
  • leave-class
  • leave-active-class
  • leave-to-class

他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css 结合使用十分有用。

Install with npm:

1
$ npm install animate.css

Import it into your file:

1
import "animate.css"
1
2
3
4
5
6
7
8
9
10
<div id="demo">
<button @click="show = !show">Toggle render</button>
<transition
name="animate__animated animate__bounce"
enter-active-class="animate__bounce"
leave-active-class="animate__backOutDown"
>
<h1 v-if="show">An animated element</h1>
</transition>
</div>

那么怎么同时渲染整个列表,比如使用 v-for?在这种场景中,使用 <transition-group> 组件。在我们深入例子之前,先了解关于这个组件的几个特点:

  • 不同于 <transition>,它会以一个真实元素呈现:默认为一个 <span>。你也可以通过 tag attribute 更换为其他元素。
  • 过渡模式不可用,因为我们不再相互切换特有的元素。
  • 内部元素总是需要提供唯一的 key attribute 值。
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。

Vue-CLI配置代理

如果你的前端应用和后端 API 服务器没有运行在同一个主机上,你需要在开发环境下将 API 请求代理到 API 服务器。这个问题可以通过 vue.config.js 中的 devServer.proxy 选项来配置。

resolve CORS policy Access-Control-Allow-Origin

  • devServer.proxy 可以是一个指向开发环境 API 服务器的字符串:
1
2
3
4
5
module.exports = {
devServer: {
proxy: 'http://localhost:4000'
}
}

这会告诉开发服务器将任何未知请求 (没有匹配到静态文件的请求) 代理到http://localhost:4000

说明:

  1. 优点:配置简单,请求资源时直接发给前端(8080)即可。
  2. 缺点:不能配置多个代理,不能灵活的控制请求是否走代理。
  3. 工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源)

配置多个代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
devServer: {
proxy: {
'/api1': {// 匹配所有以 '/api1'开头的请求路径
target: 'http://localhost:5000',// 代理目标的基础路径
changeOrigin: true,
pathRewrite: {'^/api1': ''}
},
'/api2': {// 匹配所有以 '/api2'开头的请求路径
target: 'http://localhost:5001',// 代理目标的基础路径
changeOrigin: true,
pathRewrite: {'^/api2': ''}
}
}
}
}
/*
changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:8080
changeOrigin默认值为true
*/

说明:

  1. 优点:可以配置多个代理,且可以灵活的控制请求是否走代理。
  2. 缺点:配置略微繁琐,请求资源时必须加前缀。

插槽(slot)

Slot 通俗的理解就是“占坑”,在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置)
并且可以作为承载分发内容的出口

因为在2.6.0中,具名插槽作用域插槽 引入了一个新的统一的语法 (即v-slot 指令)。它取代了 slotslot-scope

插槽内容

Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 <slot> 元素作为承载分发内容的出口。

它允许你像这样合成组件:

1
2
3
<HelloWorld>
Hello World
</HelloWorld>

然后你在 HelloWorld 组件中可能会写为:

1
2
3
<div class="hello">
<slot></slot>
</div>

当组件渲染的时候,<slot></slot> 将会被替换为“Hello World”。插槽内可以包含任何模板代码,包括 HTML。

如果HelloWorld 组件 的 template没有包含一个 <slot> 元素,则该组件起始标签<HelloWorld>和结束标签 </HelloWorld>之间的任何内容都会被抛弃。

具名插槽

对于这样的情况,<slot> 元素有一个特殊的 attribute:name

1
2
3
4
5
6
7
8
9
10
11
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>

一个不带 name<slot> 出口会带有隐含的名字“default”。

在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:

1
2
3
4
5
6
7
8
9
10
11
12
<HelloWorld>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>

<p>A paragraph for the main content.</p>
<p>And another one.</p>

<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</HelloWorld>

现在 <template>元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有v-solt<template> 中的内容都会被视为默认插槽的内容。

然而,如果你希望更明确一些,仍然可以在一个 <template> 中包裹默认插槽的内容:<template>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<HelloWorld>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>

<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>

<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</HelloWorld>

任何一种写法都会渲染出:

1
2
3
4
5
6
7
8
9
10
11
12
<div class="container">
<header>
<h1>Here might be a page title</h1>
</header>
<main>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</main>
<footer>
<p>Here's some contact info</p>
</footer>
</div>
1
注意 v-slot 只能添加在 <template> 上

具名插槽的缩写

v-onv-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header

1
2
3
4
5
6
7
8
9
10
11
12
<HelloWorld>
<template #header>
<h1>Here might be a page title</h1>
</template>

<p>A paragraph for the main content.</p>
<p>And another one.</p>

<template #footer>
<p>Here's some contact info</p>
</template>
</HelloWorld>

作用域插槽

有时让插槽内容能够访问子组件中才有的数据是很有用的。例如,父组件访问子组件中才有的数据

在子组件HelloWorld中先定义数据

1
2
3
4
5
6
7
8
data() {
return {
user: {
firstName : '张',
lastName : '三'
}
}
}

为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 <slot> 元素的一个 attribute 绑定上去:

1
2
3
<span>
<slot :user="user"></slot>
</span>

在父组件中使用子组件的数据:

1
2
3
4
5
<HelloWorld>
<template slot-scope="{user}">
<h1>法外狂徒{{ user.firstName }}{{ user.lastName }}</h1>
</template>
</HelloWorld>

效果如下:


Vue.js 是什么

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架

Vue的特点

  1. 采用组件化模式,提高代码的复用率、且让代码更好维护。
  2. 声明式编码,让编码人员无需直接操作DOM,提高开发效率。
  3. 使用虚拟DOM+优秀DIff算法,尽量复用DOM结点。

模板语法

模板的理解

html 中包含了一些JS 语法代码,语法分为两种,分别为:

  1. 插值语法(双大括号表达式)
  2. 指令(以v-开头)

插值语法

功能: 用于解析标签体内容

语法: ,xxxx 会作为js 表达式解析

指令语法

功能: 解析标签属性、解析标签体内容、绑定事件

举例:v-bind:href = ‘xxxx’ ,xxxx 会作为js 表达式被解析


数据绑定

单向数据绑定

语法:v-bind:href =”xxx” 或简写为:href

特点:数据只能从data 流向页面

1
v- 前缀作为一种视觉提示,用来识别模板中 Vue 特定的 attribute。当你在使用 Vue.js 为现有标签添加动态行为 (dynamic behavior) 时,v- 前缀很有帮助,然而,对于一些频繁用到的指令来说,就会感到使用繁琐。同时,在构建由 Vue 管理所有模板的单页面应用程序 (SPA - single page application) 时,v- 前缀也变得没那么重要了。因此,Vue 为 v-bind 和 v-on 这两个最常用的指令,提供了特定简写:

v-bind 缩写

1
2
3
4
5
6
7
8
<!-- 完整语法 -->
<a v-bind:href="url">...</a>

<!-- 缩写 -->
<a :href="url">...</a>

<!-- 动态参数的缩写 (2.6.0+) -->
<a :[key]="url"> ... </a>

v-on 缩写

1
2
3
4
5
6
7
8
<!-- 完整语法 -->
<a v-on:click="doSomething">...</a>

<!-- 缩写 -->
<a @click="doSomething">...</a>

<!-- 动态参数的缩写 (2.6.0+) -->
<a @[event]="doSomething"> ... </a>

双向数据绑定

语法:v-mode:value=”xxx” 或简写为v-model=”xxx”

特点:数据不仅能从data 流向页面,还能从页面流向data

1
2
1、双向绑定一般都应用在表单类元素上(如:input、select等)
2、v-mode:value 可以简写为v-model,因为v-model默认收集的就是value值

el的两种写法

第一种写法

1
2
3
4
5
6
new Vue({
el: "#root",
data() {
name: "Vue"
},
})

第二种写法

1
v.$mount("#root");

data的两种写法

第一种写法:对象式

1
2
3
4
5
6
const v = new Vue({
el: "#root",
data: {
name: "Vue"
},
})

第二种写法:函数式

1
2
3
4
5
6
7
8
const v = new Vue({
el: "#root",
data() {
return {
name: "Vue"
}
},
})
1
以后学习到组件时,data必须写成函数式

MVVM模型

MVVMModel–view–viewmodel)是一种软件架构模式。

MVVM模式不同于MVC,在MVVM模式中,将ViewModel层绑定到View层后,它基本不使用点击事件,而是使用命令(Command)来控制。数据的显示也是不同于MVC,而是使用Binding来绑定相关数据。

值得一提的是,MVVM通常会使用属性更改通知,即数据驱动而不是事件驱动。在WPF中当数据发生改变时,会通过接口INotifyPropertyChanged通知到对应的组件绑定的数据,实现同步数据刷新。

M:模型(Model) :对应data 中的数据

V:视图(View) :模板

VM:视图模型(ViewModel) : Vue 实例对象

每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例开始的:

1
2
3
var vm = new Vue({
// 选项
})

虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示 Vue 实例。

1
vm身上的所有属性及Vue原型上的所有属性,在vue模板中都可以直接使用

事件处理

事件的基本使用:

  • 使用v-on:xxx 或@xxx 绑定事件,其中xxx是事件名
  • 事件的回调需要配置在methods对象中,最终会在vm上
  • methods中配置的函数不要用箭头函数!否则this就不是vm了
  • methods中配置的函数,都是被Vue所管理的函数,this指向是vm或组件实例化对象
  • @click=”demo”和@click=”demo($event)”效果一致,但后者可以传参。

事件修饰符

在事件处理程序中调用 event.preventDefault()event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。

为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。

  • .stop:阻止事件冒泡(常用)
  • .prevent:阻止默认事件(常用)
  • .capture:使用事件的捕获模式
  • .self:只有event.target是当前操作的元素时才触发事件
  • .once:事件只能触发一次(常用)
  • .passive:事件的默认行为立即执行,无需等待事件回调执行完毕
1
2
3
4
5
6
7
8
9
10
11
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>

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

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

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

键盘事件

在监听键盘事件时,我们经常需要检查详细的按键。Vue 允许为 v-on 在监听键盘事件时添加按键修饰符:

1
2
<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input v-on:keyup.enter="submit">

你可以直接将 KeyboardEvent.key暴露的任意有效按键名转换为 kebab-case 来作为修饰符。

1
<input v-on:keyup.page-down="onPageDown">

在上述示例中,处理函数只会在 $event.key 等于 PageDown 时被调用。

Vue 提供了绝大多数常用的按键码的别名:

  • .enter
  • .tab(必须配合keydown使用)
  • .delete (捕获“删除”和“退格”键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

系统修饰键

  • .ctrl
  • .alt
  • .shift
  • .meta
1
注意:在 Mac 系统键盘上,meta 对应 command 键 (⌘)。在 Windows 系统键盘 meta 对应 Windows 徽标键 (⊞)。在 Sun 操作系统键盘上,meta 对应实心宝石键 (◆)。在其他特定键盘上,尤其在 MITLisp 机器的键盘、以及其后继产品,比如 Knight 键盘、space-cadet 键盘,meta 被标记为“META”。在 Symbolics 键盘上,meta 被标记为“META”或者“Meta”。
  1. 配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才触发。
  2. 配合keydown使用:正常触发

计算属性

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如:

1
2
3
<div id="example">
{{ message.split('').reverse().join('') }}
</div>

在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 message 的翻转字符串。当你想要在模板中的多处包含此翻转字符串时,就会更加难以处理。

所以,对于任何复杂逻辑,你都应当使用计算属性

计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// ...

现在再运行 vm.fullName = 'John Doe' 时,setter 会被调用,vm.firstNamevm.lastName 也会相应地被更新。

  1. 定义:要用的属性不存在要通过已有的属性计算得到。
  2. 原理:底层借助了Object.defineproperty方法提供的getter和setter。
  3. get函数什么时候执行?
    • 初次读取时会调用一次
    • 当依赖的数据发生变化时会再次调用
  4. 优势:与methods实现相比,内部有缓存机制,效率更高,调试更方便
  5. 备注:
    • 计算属性最终会出现在vm上,直接读取使用即可。
    • 如果计算属性要被修改,那必须写set函数去响应修改,且set中要引起计算时依赖的数据发生变化

简写形式:当计算属性只有getter时

1
2
3
4
5
6
7
8
9
10
11
12
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
})

等价于

1
2
3
4
5
6
7
8
9
10
11
12
13
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
}
}
})

侦听属性

先看一个小例子

1
2
3
4
<div id="root">
<h2>定个小目标,赚他{{amount}}个亿</h2>
<button @click="amount++">提高小目标</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const vm = new Vue({
el: "#root",
data: {
amount: 1
},
watch: {
amount:{
immediate: true, //初始化时让handler调用一下
// 当amount发生改变时调用handler
handler(newValue, oldValue){
console.log(newValue, oldValue)
}
}
}
})

侦听属性简写形式如下:

1
2
3
4
5
6
7
8
9
10
11
const vm = new Vue({
el: "#root",
data: {
amount: 1
},
watch: {
amount(newValue, oldValue){
console.log(newValue, oldValue)
}
}
})

除了 watch 选项之外,您还可以使用命令式的 vm.$watch API

1
2
3
4
5
6
7
vm.$watch('amount',{
// 当amount发生改变时调用handler
immediate: true,
handler(newValue, oldValue){
console.log(newValue, oldValue)
}
})

深度侦听

为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。

1
2
3
4
5
6
<div id="root">
<h3>a的值是:{{numbers.a}}</h3>
<button @click="numbers.a++">a+1</button>
<h3>b的值是:{{numbers.b}}</h3>
<button @click="numbers.b++">a+1</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const vm = new Vue({
el: "#root",
data: {
numbers: {
a: 1,
b: 1
}
},
watch: {
numbers:{
deep: true, //当deep为true时,a或b的值改变下面的handler会执行,否则不会执行
handler(){
console.log("numbers改变了")
}
}
}
})

计算属性 vs 侦听属性

computed和watch之间的区别

  1. computed能完成的功能,watch都可以完成。watch能完成的功能,computed不一定能完成,例如异步操作。
  2. 虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
1
2
1、所有被Vue管理的函数最好写成普通函数,这样this的指向才是vm或组件实例对象
2、所有不背Vue管理的函数(定时器的回调函数、ajax的回调函数等),最好写成箭头函数,这样this的指向才是vm或组件实例对象

Class与Style的绑定

Class的绑定

操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute,所以我们可以用 v-bind 处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind 用于 classstyle 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。

字符串语法

1
<div v-bind:class="danger"></div>

数组语法

我们可以把一个数组传给 v-bind:class,以应用一个 class 列表:

1
<div v-bind:class="[activeClass, errorClass]"></div>
1
2
3
4
data: {
activeClass: 'active',
errorClass: 'text-danger'
}

渲染为:

1
<div class="active text-danger"></div>

对象语法

我们可以传给 v-bind:class 一个对象,以动态地切换 class:

1
<div v-bind:class="{ active: isActive }"></div>

上面的语法表示 active 这个 class 存在与否将取决于数据 property isActivetruthiness


Style的绑定

v-bind:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名:

1
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
1
2
3
4
data: {
activeColor: 'red',
fontSize: 30
}

直接绑定到一个样式对象通常更好,这会让模板更清晰:

1
<div v-bind:style="styleObject"></div>
1
2
3
4
5
6
data: {
styleObject: {
color: 'red',
fontSize: '13px'
}
}

v-bind:style 的数组语法可以将多个样式对象应用到同一个元素上:

1
<div v-bind:style="[baseStyles, overridingStyles]"></div>

条件渲染

v-show

用于根据条件展示元素的选项是 v-show 指令。用法大致一样:

1
<h1 v-show="ok">Hello!</h1>

不同的是带有 v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS property display

1
注意,v-show 不支持 <template> 元素,也不支持 v-else。

v-if

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染。

1
<h1 v-if="awesome">Vue is awesome!</h1>

也可以用 v-else 添加一个“else 块”:

1
2
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>

v-if v-else-if v-else

v-else-if,顾名思义,充当 v-if 的“else-if 块”,可以连续使用:

1
2
3
4
5
6
7
8
9
10
11
12
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>

<template>元素上使用-v-if-条件渲染分组

因为 v-if 是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template> 元素当做不可见的包裹元素,并在上面使用 v-if。最终的渲染结果将不包含 <template> 元素。

1
2
3
4
5
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>

v-if vs v-show

v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。

v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

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

key 管理可复用的元素

Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。这么做除了使 Vue 变得非常快之外,还有其它一些好处。例如,如果你允许用户在不同的登录方式之间切换:

1
2
3
4
5
6
7
8
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address">
</template>

那么在上面的代码中切换 loginType 将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,<input> 不会被替换掉——仅仅是替换了它的 placeholder

所以 Vue 为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的 key attribute 即可:

1
2
3
4
5
6
7
8
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address" key="email-input">
</template>

现在,每次切换时,输入框都将被重新渲染。


列表渲染

v-for遍历 元素

我们可以用 v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名

1
2
3
4
5
<ul id="example-1">
<li v-for="item in items" :key="item.message">
{{ item.message }}
</li>
</ul>
1
2
3
4
5
6
7
8
9
var example1 = new Vue({
el: '#example-1',
data: {
items: [
{ message: 'Foo' },
{ message: 'Bar' }
]
}
})

结果:

  • Foo
  • Bar

你也可以用 of 替代 in 作为分隔符,因为它更接近 JavaScript 迭代器的语法:

1
<div v-for="item of items"></div>

v-for 块中,我们可以访问所有父作用域的 property。v-for 还支持一个可选的第二个参数,即当前项的索引。

1
2
3
4
5
<ul id="example-2">
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
</ul>
1
为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 `key` attribute:
1
2
3
<div v-for="item in items" v-bind:key="item.id">
<!-- 内容 -->
</div>

建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。

因为它是 Vue 识别节点的一个通用机制,key 并不仅与 v-for 特别关联。

1
不要使用对象或数组之类的非基本类型值作为 v-for 的 key。请用字符串或数值类型的值。

v-for遍历 对象

你也可以用 v-for 来遍历一个对象的 property。

1
2
3
4
5
<ul id="v-for-object" class="demo">
<li v-for="value in object">
{{ value }}
</li>
</ul>
1
2
3
4
5
6
7
8
9
10
new Vue({
el: '#v-for-object',
data: {
object: {
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
}
}
})

结果:

  • How to do lists in Vue
  • Jane Doe
  • 2016-04-10

你也可以提供第二个的参数为 property 名称 (也就是键名):

1
2
3
<div v-for="(value, name) in object">
{{ name }}: {{ value }}
</div>

title: How to do lists in Vue

author: Jane Doe

publishedAt: 2016-04-10

还可以用第三个参数作为索引:

1
2
3
<div v-for="(value, name, index) in object">
{{ index }}. {{ name }}: {{ value }}
</div>
  1. title: How to do lists in Vue

  2. author: Jane Doe

  3. publishedAt: 2016-04-10


Key的原理

key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

对比规则:
(1).旧虚拟DOM中找到了与新虚拟DOM相同的key:
①.若虚拟DOM中内容没变, 直接使用之前的真实DOM!
②.若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。

(2).旧虚拟DOM中未找到与新虚拟DOM相同的key,创建新的真实DOM,随后渲染到到页面。

用index作为key可能会引发的问题:
1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作: 会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。

  1. 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
  2. 开发中如何选择key?:
  3. 最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
  4. 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。

显示过滤后的结果

有时,我们想要显示一个数组经过过滤或排序后的版本,而不实际变更或重置原始数据。在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组。

例如:

1
<li v-for="n in evenNumbers">{{ n }}</li>
1
2
3
4
5
6
7
8
9
10
data: {
numbers: [ 1, 2, 3, 4, 5 ]
},
computed: {
evenNumbers: function () {
return this.numbers.filter(function (number) {
return number % 2 === 0
})
}
}

显示排序后的结果

1
2
3
4
5
6
7
8
9
10
11
computed: {
evenNumbers() {
const arr this.numbers.filter(function (number) {
return number % 2 === 0
})
// 升序排列
return arr.sort((prv,next)=> {
return next - prv
})
}
}

Vue监视数据的原理

Vue监视数据的原理:

  1. vue会监视data中所有层次的数据。

如何监测对象中的数据?
通过setter方法实现监视,且要在new Vue时就传入要监测的数据。
(1).对象中后追加的属性,Vue默认不做响应式处理
(2).如需给后添加的属性做响应式,请使用如下API:

1
Vue.set(target,propertyName/index,value)

1
vm.$set(target,propertyName/index,value)
  • 参数
    • {Object | Array} target
    • {string | number} propertyName/index
    • {any} value
1
注意对象不能是 Vue 实例即vm,或者 Vue 实例的根数据对象vm.data。

3、如何监测数组中的数据?
通过包裹数组更新元素的方法实现,本质就是做了两件事:
(1).调用原生对应的方法对数组进行更新。
(2).重新解析模板,进而更新页面

4、在Vue修改数组中的某个元素一定要用如下方法:
1).使用这些API:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

2).Vue.set() 或 vm.$set()

1
特别注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象 添加属性!!!

表单输入绑定

文本

  • text 和 textarea 元素使用 value property ,v-mode收集的是value

单选框

  • ,则v-model收集的是value值,且要给标签配置value值。
1
2
3
4
5
6
7
8
9
<div id="example-4">
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<br>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
<br>
<span>Picked: {{ picked }}</span>
</div>

复选框

若:
1.没有配置input的value属性,那么收集的就是checked(勾选 or 未勾选,是布尔值)
2.配置input的value属性:
(1) v-model的初始值是非数组,那么收集的就是checked(勾选 or 未勾选,是布尔值)
(2) v-model的初始值是数组,那么收集的的就是value组成的数组

修饰符

.lazy

在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 (除了上述输入法组合文字时)。你可以添加 lazy 修饰符,从而转为在 失去焦点 事件_之后_进行同步:

1
2
<!-- 在input框失去焦点时更新 -->
<input v-model.lazy="msg">

.number

如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符:

1
<input v-model.number="age" type="number">

这通常很有用,因为即使在 type="number" 时,HTML 输入元素的值也总会返回字符串。如果这个值无法被 parseFloat() 解析,则会返回原始的值。

.trim

如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符:

1
<input v-model.trim="msg">

过滤器

Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:

1
2
3
4
5
<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

你可以在一个组件的选项中定义本地的过滤器:

1
2
3
4
5
6
7
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}

或者在创建 Vue 实例之前全局定义过滤器

1
2
3
4
5
6
7
8
9
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({
// ...
})

当全局过滤器和局部过滤器重名时,会采用局部过滤器。

过滤器传参

过滤器是 JavaScript 函数,因此可以接收参数:

1
{{ message | filterA('arg1', arg2) }}

这里,filterA 被定义为接收三个参数的过滤器函数。其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数。

过滤器可以串联:

1
{{ message | filterA | filterB }}

在这个例子中,filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。

1
过滤器不改变原有的数据,是产生新的数据

其他指令

v-text

  • 预期string

  • 更新元素的 textContent。如果要更新部分的 textContent,需要使用 {{ Mustache }} 插值。

  • 示例

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

v-html

  • 预期string

  • 详细

    更新元素的 innerHTML

  • 示例

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

v-cloak

  • 不需要表达式

  • 用法

    这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。

  • 示例

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

    未解析的元素不会显示,直到编译结束,可以解决插值闪烁问题。

v-once

  • 不需要表达式

  • 详细

    只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!-- 单个元素 -->
    <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-pre

  • 不需要表达式

  • 用法

    跳过这个元素和它的子元素的编译过程。

    可以利用它跳过没有使用指令语法、没有使用插值语法的结点。跳过大量没有指令的节点会加快编译。

  • 示例

    1
    <span v-pre>{{ this will not be compiled }}</span>

自定义指令

有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。举个聚焦输入框的例子,如下:

当页面加载时,该元素将获得焦点。事实上,只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态。现在让我们用指令来实现这个功能:

1
2
3
4
5
6
7
8
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})

如果想注册局部指令,组件中也接受一个 directives 的选项,

对象形式:

1
2
3
4
5
6
7
8
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}

自定义指令也可以写成函数形式,但是无法在钩子函数中做自定义的操作

1
2
3
4
5
6
7
directives: {
// 1.当指令与元素成功绑定时会调用
// 2.指令所在的模板被重新解析时也会调用
focus(element, binding){
element.value = binding.value
}
}

然后你可以在模板中任何元素上使用新的 v-focus property,如下:

1
<input v-focus>

钩子函数

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

钩子函数参数

指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM。

  • binding

    :一个对象,包含以下 property:

    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
1
2
自定义指定定义时不加v-,但是使用时要加v-
指令名如果是多个单词,要使用kebab-case命名方式,不要用camelCase命名

Vue生命周期函数

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,下图展示了实例的生命周期。

所有的生命周期钩子自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())。这是因为箭头函数绑定了父上下文,因此 this 与你期待的 Vue 实例不同,this.fetchTodos 的行为未定义。

beforeCreate

在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。

created

在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el property 目前尚不可用。

beforeMount

在挂载开始之前被调用:相关的 render 函数首次被调用。

该钩子在服务器端渲染期间不被调用。

mounted(重要)

实例被挂载后调用,这时 el 被新创建的 vm.$el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el 也在文档内。

在此钩子函数中可以发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】。

注意 mounted 不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted 内部使用 vm.$nextTick:

1
2
3
4
5
6
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the
// entire view has been rendered
})
}

beforeUpdate

数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。

该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务端进行。

updated

由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。

当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或 watcher 取而代之。

注意 updated 不会保证所有的子组件也都一起被重绘。如果你希望等到整个视图都重绘完毕,可以在 updated 里使用 vm.$nextTick

beforeDestroy(重要)

实例销毁之前调用。在这一步,实例仍然完全可用。

在此钩子函数中,可以清楚定时器、解绑自定义事件、取消订阅消息等【收尾工作】

1
一般不会在beforeDestroy操作数据,因为即便操作数据也不会再触发更新流程了

destroyed

实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。


Vue组件化编程

非单文件组件

通常一个应用会以一棵嵌套的组件树的形式来组织:

例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。

为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册局部注册

这里有一个 Vue.component 全局注册的示例:

1
2
3
4
5
6
7
8
9
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

局部注册的示例:

1
2
3
4
5
6
7
8
const counter = Vue.extend({
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
1
2
3
4
5
6
new Vue({
el: "#root",
components: {
'button-counter':counter
}
})

组件是可复用的 Vue 实例,且带有一个名字:在这个例子中是 <button-counter>。我们可以在一个通过 new Vue 创建的 Vue 根实例中,把这个组件作为自定义元素来使用:

1
2
3
<div id="components-demo">
<button-counter></button-counter>
</div>

你可以将组件进行任意次数的复用:

1
2
3
4
5
<div id="components-demo">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>

使用Vue.extend(options)创建,其中optionsnew Vue(options)时传入的那个options几乎一样,但也有点区别;区别如下:

1
Vue.extend(options)中el不要写,为什么? ——— 最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器。
1
一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝,如果 Vue 没有这条规则,点击一个按钮就可能会像如下代码一样影响到其它所有实例:

几个注意点:

  1. 关于组件名:

    • 一个单词组成:

      第一种写法(首字母小写):school
      第二种写法(首字母大写):School

    • 多个单词组成:

      使用 kebab-case

      1
      Vue.component('my-component-name', { /* ... */ })

      当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my-component-name>

      使用 PascalCase

      1
      Vue.component('MyComponentName', { /* ... */ })

      当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 <my-component-name><MyComponentName> 都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。

  2. 关于组件标签:

    第一种写法:
    第二种写法: (备注:不用使用脚手架时,会导致后续组件不能渲染。)

  3. 一个简写方式:
    const school = Vue.extend(options) 可简写为:const school = options

1
2
组件名尽可能回避HTML中已有的元素名称,例如:h2、H2都不行。
可以使用name配置项指定组件在开发者工具中呈现的名字

VueComponent

  1. button-counter组件本质是一个名为VueComponent的构造函数,且不是程序员定义的,是Vue.extend生成的。
  2. 我们只需要写,Vue解析时会帮我们创建school组件的实例对象,即Vue帮我们执行的:new VueComponent(options)。
  3. 每次调用Vue.extend,返回的都是一个全新的VueComponent!
  4. 关于this指向:
    • .new Vue(options)配置中:data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【Vue实例对象】
    • data函数、methods中的函数、watch中的函数、computed中的函数 它们的this均是【VueComponent实例对象】
1
2
一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype
为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。

单文件组件

在很多 Vue 项目中,我们使用 Vue.component 来定义全局组件,紧接着用 new Vue({ el: '#container '}) 在每个页面内指定一个容器元素。

这种方式在很多中小规模的项目中运作的很好,在这些项目里 JavaScript 只被用来加强特定的视图。但当在更复杂的项目中,或者你的前端完全由 JavaScript 驱动的时候,下面这些缺点将变得非常明显:

  • 全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复
  • 字符串模板 (String templates) 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的 \
  • 不支持 CSS (No CSS support) 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
  • 没有构建步骤 (No build step) 限制只能使用 HTML 和 ES5 JavaScript,而不能使用预处理器,如 Pug (formerly Jade) 和 Babel

文件扩展名为 .vuesingle-file components (单文件组件) 为以上所有问题提供了解决方法,并且还可以使用 webpack 或 Browserify 等构建工具。

这是一个文件名为 Hello.vue 的简单实例:

一个.vue 文件的组成(3 个部分)

  1. 模板页面

    1
    2
    3
    <template>
    页面模板
    </template>
  2. JS 模块对象

    1
    2
    3
    4
    5
    6
    7
    8
    <script>
    export default {
    data() {return {}},
    methods: {},
    computed: {},
    components: {}
    }
    </script>
  3. 样式

    1
    2
    3
    <style>
    样式定义
    </style>

简介

网关作为流量的入口,常用功能包括路由转发、权限校验、限流控制等。而springcloud gateway作为SpringCloud 官方推出的第二代网关框架,取代了Zuul 网关。

网关提供API 全托管服务,丰富的API 管理功能,辅助企业管理大规模的API,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略、防刷、流量、监控日志等功能。
Spring Cloud Gateway 旨在提供一种简单而有效的方式来对API 进行路由,并为他们提供切面,例如:安全性,监控/指标和弹性等。

Spring Cloud Gateway 特点:

  • 基于Spring5,支持响应式编程和SpringBoot2.0
  • 支持使用任何请求属性进行路由匹配
  • 特定于路由的断言和过滤器
  • 集成Hystrix 进行断路保护
  • 集成服务发现功能
  • 易于编写Predicates 和Filters
  • 支持请求速率限制
  • 支持路径重写

核心概念

  • Route: 路由是网关最基础的部分,路由信息有一个ID、一个目的URL、一组断言和一组Filter 组成。如果断言路由为真,则说明请求的URL 和配置匹配
  • Predicate: 断言。Java8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是Spring5.0 框架中的ServerWebExchange。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自于http request 中的任何信息,比如请求头和参数等。
  • Filter: 过滤器。一个标准的Spring webFilter。Spring cloud gateway 中的filter 分为两种类型的Filter,分别是Gateway Filter 和Global Filter。过滤器Filter 将会对请求和响应进行修改处理
  • 通俗的讲,断言Predicate是判断当前请求是否向下继续传递,而过滤器Filter可以对请求和响应进行修复和处理

工作原理

​ 下图提供了 Spring Cloud Gateway 工作原理的高级概述:

​ 客户端发送请求给网关,弯管HandlerMapping 判断是否请求满足某个路由,满足就发给网关的WebHandler。这个WebHandler 将请求交给一个过滤器链,请求到达目标服务之前,会执行所有过滤器的pre 方法。请求到达目标服务处理之后再依次执行所有过滤器的post 方法。

在没有端口的路由中定义的 URI 分别获得 HTTP 和 HTTPS URI 的默认端口值 80 和 443。


使用

引入gateway

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
predicates:
- Query=baz
filters:
- AddRequestParameter=foo, bar

各字段含义如下:

  • id:我们自定义的路由 ID,保持唯一
  • uri:目标服务地址
  • predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
  • filters:过滤规则。

各种Predicates 同时存在于同一个路由时,请求必须同时满足所有的条件才被这个路由匹配。
一个请求满足多个路由的谓词条件时,请求只会被首个成功匹配的路由转发


Predicate 断言条件

Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。

在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。网上有一张图总结了 Spring Cloud 内置的几种 Predicate 的实现。

通过请求时间匹配

After Route Predicate Factory

The following example configures an after route predicate:After datetime ZonedDateTime

Example 1. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- After=2017-01-20T17:42:00

此路由匹配 2017 年 1 月 20 日 17:42Mountain Time (Denver) 之后提出的任何请求。

Before Route Predicate Factory

The following example configures a before route predicate:Before datetime ZonedDateTime datetime

Example 2. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: before_route
uri: https://example.org
predicates:
- Before=2017-01-20T17:42:47.789-07:00[America/Denver]

此路由匹配 2017 年 1 月 20 日 17:42 Mountain Time (Denver)之前提出的任何请求。

Between Route Predicate Factory

The following example configures a between route predicate:Between datetime1 datetime2 ZonedDateTime datetime1 datetime2 datetime2 datetime1

Example 3. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: between_route
uri: https://example.org
predicates:
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]

此路由匹配 任何在2017 年 1 月 20 日 17:42 Mountain Time (Denver) 之后并在2017 年 1 月 20 日 17:42 Mountain Time (Denver) 之前的请求。


通过Cookie匹配

此路由谓词工厂接受两个参数,cookie 和 Java 正则表达式。 此谓词匹配具有给定名称且其值与正则表达式匹配的 cookie。

The following example configures a cookie route predicate factory:Cookie name regexp

Example 4. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: https://example.org
predicates:
- Cookie=chocolate, ch.p

此路由匹配 请求的Cookie中有“chocolate”且其值等于“ch.p”


通过Header匹配

Header Route Predicate Factory

Header 路由谓词工厂接受两个参数,Header名称和一个 regexp(这是一个 Java 正则表达式)。 此谓词与具有给定名称的Header标头匹配,并且其值与正则表达式匹配。 以下示例配置标头路由谓词:

Example 5. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \d+

如果请求具有名为 X-Request-Id 的标头,其值与 \d+正则表达式匹配(即,它具有一个或多个数字的值),则此路由匹配。


通过Host匹配

Host Route Predicate Factory

Host路由谓词工厂采用一个参数:host名模式列表。 该模式是 Ant 风格的模式,带有 . 作为分隔符。 此谓词匹配与模式匹配的 Host 标头。 以下示例配置主机路由谓词:

Example 6. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: host_route
uri: https://example.org
predicates:
- Host=**.somehost.org,**.anotherhost.org

还支持 URI 模板变量(例如 {sub}.myhost.org)。

如果请求具有值为 www.somehost.org 或 beta.somehost.org 或 www.anotherhost.org 的 Host 标头,则此路由匹配。

此谓词提取 URI 模板变量(例如 sub,在前面的示例中定义)作为名称和值的映射,并将其放置在ServerWebExchange.getAttributes()中,并使用 ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE中定义的键。 然后这些值可供GatewayFilter工厂使用


通过请求方式匹配

Method Route Predicate Factory

方法路由谓词工厂采用一个方法参数,它是一个或多个参数:要匹配的 HTTP 方法。 以下示例配置方法路由谓词:

Example 7. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: method_route
uri: https://example.org
predicates:
- Method=GET,POST

如果请求方法是 GET 或 POST,则路由匹配。


通过请求路径匹配

Path Route Predicate Factory

Path Route Predicate Factory 接受两个参数:一个 Spring PathMatcher 模式列表和一个名为 matchTrailingSlash 的可选标志(默认为 true)。 以下示例配置路径路由谓词:

Example 8. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: path_route
uri: https://example.org
predicates:
- Path=/red/{segment},/blue/{segment}

如果请求路径是例如:/red/1/red/1//red/blue/blue/green,则此路由匹配。

如果matchTrailingSlash(匹配反斜杠)设置为false,则不会匹配请求路径/red/1/

此谓词提取 URI 模板变量(例如在前面的示例中定义的段)作为名称和值的映射,并将其放置在 ServerWebExchange.getAttributes()中,键是在 ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE中定义的。

然后这些值可供 GatewayFilter 工厂使用 可以使用实用方法(称为 get)来更轻松地访问这些变量。 以下示例显示了如何使用 get 方法:

1
2
3
Map<String, String> uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange);

String segment = uriVariables.get("segment");

通过查询匹配

Query Route Predicate Factory

查询路由谓词工厂有两个参数:一个必需的参数和一个可选的正则表达式(这是一个 Java 正则表达式)。 以下示例配置查询路由谓词:

Example 9. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: query_route
uri: https://example.org
predicates:
- Query=green

如果请求包含参数green,则路由匹配。

application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: query_route
uri: https://example.org
predicates:
- Query=red, gree.

如果请求包含参数red,并且其对应的值满足正则表达式gree.,则路由匹配。所以 green 和 greet 会匹配。


通过远程ip地址匹配

RemoteAddr Route Predicate Factory

RemoteAddr 路由谓词工厂采用源列表(最小大小 1),这些源是 CIDR 表示法(IPv4 或 IPv6)字符串,例如 192.168.0.1/16(其中 192.168.0.1 是 IP 地址,16 是子网掩码 )。 以下示例配置RemoteAddr路由谓词:

Example 10. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: https://example.org
predicates:
- RemoteAddr=192.168.1.1/24

如果请求的远程地址是例如 192.168.1.10,则此路由匹配。


过滤器规则(Filter)

路由过滤器允许以某种方式修改传入的 HTTP 请求或传出的 HTTP 响应。 路由过滤器的范围是特定的路由。 Spring Cloud Gateway 包括许多内置的GatewayFilter工厂。

AddRequestHeade GatewayFilter Factory

The AddRequestHeader GatewayFilter factory takes a name and value parameter. The following example configures an AddRequestHeader GatewayFilter:

Example 13. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blue

会给所有匹配的请求头的请求集合添加上X-Request-red:blue header 。

AddRequestParameter GatewayFilter Factory

The AddRequestParameter GatewayFilter Factory takes a name and value parameter. The following example configures an AddRequestParameter GatewayFilter:

Example 15. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
filters:
- AddRequestParameter=red, blue

会给所有匹配的请求添加上 red=blue 的请求参数。

The AddResponseHeader GatewayFilter Factory

The AddResponseHeader GatewayFilter Factory takes a name and value parameter. The following example configures an AddResponseHeader GatewayFilter:

Example 17. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: add_response_header_route
uri: https://example.org
filters:
- AddResponseHeader=X-Response-Red, Blue

会给所有的匹配的请求添加上 X-Response-Foo:Bar 的响应头。

PrefixPath GatewayFilter Factory

The PrefixPath GatewayFilter factory takes a single prefix parameter. The following example configures a PrefixPath GatewayFilter:

Example 28. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- PrefixPath=/mypath

这会为所有匹配请求的路径加上/mypath 前缀。 因此,对 /hello 的请求将被发送到 /mypath/hello。

PreserveHostHeader GatewayFilter Factory

PreserveHostHeader GatewayFilter 工厂没有参数。 此过滤器设置路由过滤器检查的请求属性,以确定是否应发送原始主机标头,而不是由 HTTP 客户端确定的主机标头。 以下示例配置 PreserveHostHeader GatewayFilter:

Example 29. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: preserve_host_route
uri: https://example.org
filters:
- PreserveHostHeader

RedirectTo GatewayFilter Factory

RedirectTo GatewayFilter 工厂接受两个参数, statusurlstatus 参数应该是一个 300 系列的重定向 HTTP 代码,比如 301。url参数应该是一个有效的 URL。 这是 Location 标头的值。 对于相对重定向,您应该使用 uri: no://op 作为路由定义的 uri。 以下清单配置了一个 RedirectTo GatewayFilter:

Example 35. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- RedirectTo=302, https://acme.org

这将发送带有 Location:https://acme.org 标头的状态 302 以执行重定向。

RemoveRequestHeader GatewayFilter Factory

RemoveRequestHeader GatewayFilter 工厂只有一个 name参数。 它是要删除的header的名称。 以下清单配置了 RemoveRequestHeader GatewayFilter:

Example 36. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: removerequestheader_route
uri: https://example.org
filters:
- RemoveRequestHeader=X-Request-Foo

请求在向下转发之前会删除 header X-Request-Foo

RemoveResponseHeader GatewayFilter Factory

RemoveResponseHeader GatewayFilter工厂只有一个 name参数。 它是要删除的header的名称。 以下清单配置了 RemoveResponseHeader GatewayFilter:

The following listing configures a RemoveResponseHeader GatewayFilter:

Example 37. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: removeresponseheader_route
uri: https://example.org
filters:
- RemoveResponseHeader=X-Response-Foo

这将在响应返回到网关客户端之前从响应中删除 X-Response-Foo 标头。

要删除任何类型的敏感标头,您应该为您可能想要这样做的任何路由配置此过滤器。 此外,您可以使用 spring.cloud.gateway.default-filters 配置一次此过滤器,并将其应用于所有路由。

RemoveRequestParameter GatewayFilter Factory

RemoveRequestParameter GatewayFilter 只有一个name参数。 它是要删除的查询参数的名称。 以下示例配置 RemoveRequestParameter GatewayFilter:

Example 38. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: removerequestparameter_route
uri: https://example.org
filters:
- RemoveRequestParameter=red

请求在向下转发之前会删除red参数。

RewritePath GatewayFilter Factory

RewritePath GatewayFilter 工厂带有两个参数,一个是正则表达式的路径参数,一个是替换参数。这使用 Java 正则表达式来灵活地重写请求路径。 以下清单配置了 RewritePath GatewayFilter:

Example 39. application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: https://example.org
predicates:
- Path=/red/**
filters:
- RewritePath=/red/?(?<segment>.*), /$\{segment}

对于 /red/blue 的请求路径,这会在发出下游请求之前将路径设置为 /blue。 请注意,由于 YAML 规范,$ 应替换为 $\。

SetPath GatewayFilter Factory

SetPath GatewayFilter 工厂采用路径模板参数。 它提供了一种通过允许路径的模板化段来操作请求路径的简单方法。 这使用了 Spring Framework 中的 URI 模板。 允许多个匹配段。 以下示例配置 SetPath GatewayFilter:

Example 43. application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: setpath_route
uri: https://example.org
predicates:
- Path=/red/{segment}
filters:
- SetPath=/{segment}

对于 /red/blue 的请求路径,这会在发出下游请求之前将路径设置为 /blue。

SetStatus GatewayFilter Factory

SetStatus GatewayFilter 工厂只有一个参数 status。 它必须是有效的 Spring HttpStatus。 它可能是整数值 404 或枚举的字符串表示形式:NOT_FOUND。 以下清单配置了 SetStatus GatewayFilter:

Example 48. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: setstatusstring_route
uri: https://example.org
filters:
- SetStatus=BAD_REQUEST
- id: setstatusint_route
uri: https://example.org
filters:
- SetStatus=401

无论哪种情况,响应的 HTTP 状态都设置为 401。

StripPrefix GatewayFilter Factory

StripPrefix GatewayFilter 工厂只有一个参数,partsparts部数指在将请求发送到下游之前要从请求中剥离的路径中的部分数。 以下清单配置了 StripPrefix GatewayFilter:

Example 50. application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: nameRoot
uri: https://nameservice
predicates:
- Path=/name/**
filters:
- StripPrefix=2

当通过网关向 /name/blue/red 发出请求时,对 nameservice/name/blue/red 发出的请求看起来像 nameservice/red。


Global Filters(全局过滤器)

GlobalFilter 接口与 GatewayFilter 具有相同的签名。 这些是有条件地应用于所有路由的特殊过滤器。

组合全局过滤器和网关过滤器排序

当请求与路由匹配时,过滤 Web 处理程序会将 GlobalFilter 的所有实例和 GatewayFilter 的所有特定于路由的实例添加到过滤器链中。 这个组合过滤器链由 org.springframework.core.Ordered 接口排序,您可以通过实现 getOrder() 方法设置该接口。

由于 Spring Cloud Gateway 区分过滤器逻辑执行的“pre”和“post”阶段(参见 How it Works),具有最高优先级的过滤器是“pre”阶段的第一个,“post”阶段的最后一个—— 阶段。

以下例子配置了过滤器链:

Example 55. ExampleConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
public GlobalFilter customFilter() {
return new CustomGlobalFilter();
}

public class CustomGlobalFilter implements GlobalFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("custom global filter");
return chain.filter(exchange);
}

@Override
public int getOrder() {
return -1;
}
}

ReactiveLoadBalancerClientFilter

ReactiveLoadBalancerClientFilter 在名为 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 的交换属性中查找 URI。 如果 URL 具有 lb 方案(例如 lb://myservice),则它使用 Spring Cloud ReactorLoadBalancer 将名称(在此示例中为 myservice)解析为实际主机和端口,并替换同一属性中的 URI。 未修改的原始 URL 将附加到 ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR 属性中的列表中。 过滤器还会查看 ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR 属性以查看它是否等于 lb。如果是,则应用相同的规则。 以下清单配置了一个 ReactiveLoadBalancerClientFilter:

Example 56. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: myRoute
uri: lb://service
predicates:
- Path=/service/**
1
默认情况下,当 ReactorLoadBalancer 找不到服务实例时,会返回 503。 您可以通过设置 spring.cloud.gateway.loadbalancer.use404=true 将网关配置为返回 404。
1
从 ReactiveLoadBalancerClientFilter 返回的 ServiceInstance 的 isSecure 值会覆盖向网关发出的请求中指定的方案。 例如,如果请求通过 HTTPS 进入网关,但 ServiceInstance 指示它不安全,则通过 HTTP 发出下游请求。 相反的情况也可以适用。 但是,如果在网关配置中为路由指定了 GATEWAY_SCHEME_PREFIX_ATTR,则前缀将被剥离,并且来自路由 URL 的结果方案将覆盖 ServiceInstance 配置。

Netty Routing Filter

如果位于 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 交换属性中的 URL 具有 http 或 https 方案,则 Netty 路由过滤器运行。 它使用 Netty HttpClient 发出下游代理请求。 响应放在 ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR 交换属性中,以供稍后过滤器使用。 (还有一个实验性的 WebClientHttpRoutingFilter 执行相同的功能但不需要 Netty。)

Netty Write Response Filter

如果 ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR 交换属性中存在 Netty HttpClientResponse,则 NettyWriteResponseFilter 运行。 它在所有其他过滤器完成后运行,并将代理响应写回网关客户端响应。 (还有一个实验性的 WebClientWriteResponseFilter 可以执行相同的功能,但不需要 Netty。)

RouteToRequestUrl Filter

如果 ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR 交换属性中存在 Route 对象,则 RouteToRequestUrlFilter 运行。 它基于请求 URI 创建一个新的 URI,但使用 Route 对象的 URI 属性进行更新。 新 URI 放置在 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 交换属性中。

如果 URI 具有方案前缀,例如 lb:ws://serviceid,则 lb 方案将从 URI 中剥离并放置在 ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR 中,以便稍后在过滤器链中使用。

Websocket Routing Filter

如果位于 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 交换属性中的 URL 具有 ws 或 wss 方案,则 websocket 路由过滤器运行。 它使用 Spring WebSocket 基础结构向下游转发 websocket 请求。

您可以通过在 URI 前加上 lb 来对 websockets 进行负载平衡,例如 lb:ws://serviceid。

1
如果你使用 SockJS 作为普通 HTTP 的后备,你应该配置一个普通的 HTTP 路由以及 websocket 路由。

The following listing configures a websocket routing filter:

Example 57. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
# SockJS route
- id: websocket_sockjs_route
uri: http://localhost:3001
predicates:
- Path=/websocket/info/**
# Normal Websocket route
- id: websocket_route
uri: ws://localhost:3001
predicates:
- Path=/websocket/**

Gateway Metrics Filter

要启用网关指标,请将 spring-boot-starter-actuator 添加为项目依赖项。 然后,默认情况下,只要属性 spring.cloud.gateway.metrics.enabled 未设置为 false,网关指标过滤器就会运行。 此过滤器添加了一个名为 gateway.requests 的计时器指标,并带有以下标签:

routeId:路由ID。

routeUri:API 路由到的 URI。

结果:按 HttpStatus.Series 分类的结果。

status:返回给客户端的请求的 HTTP 状态。

httpStatusCode:返回给客户端的请求的 HTTP 状态。

httpMethod:用于请求的 HTTP 方法。

然后可以从 /actuator/metrics/gateway.requests 抓取这些指标,并且可以轻松地与 Prometheus 集成以创建 Grafana 仪表板。

1
要启用 prometheus 端点,请将 micrometer-registry-prometheus 添加为项目依赖项。

简介

Feign是一个声明式的HTTP客户端,它的目的就是让远程调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。

Feign整合Ribbon(负载均衡)和Hystrix(服务熔断),可以让我们不再需要显示地使用这两个组件。

SpringCloudFeign在NetflixFeign的基础上扩展了对SpringMVC注解的支持,在其实现下,我们只需要创建一个接口并用注解的方式配置它,即可完成对服务提供方的接口绑定。简化了SpringCloudRibbon自行封装服务调用客户端的开发量。


使用

假设会员服务想要调用优惠卷服务,想要远程调用别的服务的步骤如下:

引入open-feign的依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

编写一个接口告诉SpringCloud这个接口需要调用远程服务

先修改CouponController,添加以下controller方法:

1
2
3
4
5
6
@RequestMapping("/member/list")
public R memberCoupons(){
CouponEntity couponEntity = new CouponEntity();
couponEntity.setCouponName("discount 20%");
return R.ok().put("coupons",Arrays.asList(couponEntity));
}

新建CouponFeignService接口

@FeignClient("gulimall-coupon")注解告诉SpringCloud这是一个远程客户端,要调用远程的gulimall-coupon服务

声明接口的每一个方法都是调用那个远程服务的那个请求

上面的路径和方法是根据远程服务中的路径和方法所得

使用@EnableFeignClients注解开启远程调用功能

测试访问

访问http://localhost:8000/member/member/coupons

0%