Vue3笔记

Vue——渐进式JavaScript框架

快速上手

通过script标签将Vue3引入你的项目

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<div id="app"></div>

<script src"./index.js"></script>
//这里是index.js文件
const option = {

}

const app = Vue.createApp(option)
app.mount("#app")

这个container可以是任何对象,入门先从 optionAPI 开始入手,下面开始 optionAPI

data

vue管理的数据(或者叫状态),写在option的data函数里,函数里会返回一个对象,
对象键值对里的值可以是任意js类型的值

const app = Vue.createApp({
    data(){
        return {
            name: "ming",
            hobby: {
                learn: 'react'
            }
        }
    }
})

通过vue管理的数据可以用模板语法在html里使用插值语法使用这些数据,插值语法为双大括号

<h1>{{ name }}</h1>
<h2>{{ hobby.learn }}</h2>

html标签的属性中绑定 vue 数据 v-bind

使用vue指令v-bind绑定vue数据到html标签的属性中

<a v-bind:href="link">链接</a>
该指令可以简写成一个冒号,上下等价
<a :href="link">链接</a>
data(){
    return {
        link: 'https://xxx.xxx.xx'
    }
}

html标签的属性名也可以为 data 中的变量,用中括号括住,并且注意中括号里面的js表达式不要出现空格例如 ['place' + 'holder']是会报错的

<input :[attr]="value" />
data(){
    return {
        attr: "placeholder",
        value: "请输入一些字符"
    }
}

动态绑定多个值。小提示:没有参数的 v-bind 会将一个对象的所有属性都作为 attribute 应用到目标元素上。

data() {
  return {
    objectOfAttrs: {
      id: 'container',
      class: 'wrapper'
    }
  }
}
<div v-bind="objectOfAttrs"></div>

列表渲染 v-for

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

data() { 
    return { 
        items: [{ message: 'Foo' }, { message: 'Bar' }] 
    } 
}
<li v-for="item in items"> {{ item.message }} </li>

在 v-for 块中可以完整地访问父作用域内的属性和变量。v-for 也支持使用可选的第二个参数表示当前项的位置索引。在循环中还可以绑定 for 循环出来的值到html标签属性上

data() {
  return {
    todos: [
      {
        content: "把项目做完",
        complete: true,
      },
      {
        content: "去超市购物",
        complete: false,
      },
      {
        content: "看10分钟的书",
        complete: false,
      },
    ],
  };
},

<ul>
    <li v-for="(todo, index) in todos">{{index + 1}}. {{ todo }}</li>
    <li v-for="todo in todos">
      <input type="checkbox" :checked="todo.complete" />{{todo.content}}
    </li>
</ul>

v-for 还可以循环对象,可以像这样拿到对象的 value 和 key,注意先后

<div v-for="(value, key) in object"></div> 
<div v-for="(value, name, index) in object"></div>

v-for 还可以循环数字,用于循环指定次数或者拿序号的时候用

<li v-for="n in 5" :key="n">{{ n }}</li>

v-for 还需要绑定一个指定唯一的 key 值来提升虚拟 DOM 性能,key值一般为后端传过来的唯一值,实在没有可以绑定循环出来的 index

<li v-for="todo in todos" :key="todo.id">{{ todo.content }}</li>

条件渲染 v-if/v-show

条件渲染

const app = Vue.createApp({
  data() {
    return {
      books: ["JavaScript 基础语法详解", "Vue 入门实战", "React 入门到精通"],
      // books: [],
      // books: ["JavaScript 基础语法详解"],
    };
  },
});
app.mount("#app");

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。
v-if 不满足就会走 v-else-if,当所有的 v-else-if 都不满足的时候就走 v-else。if、else-if、else需相邻使用才会生效。当然,v-if 可以单独出现

<p v-if="books.length === 0">目前没有任何书籍</p>
    <h2 v-else-if="books.length === 1">{{ books[0] }}</h2>
<ul v-else>
    <li v-for="book in books">{{ book }}</li>
</ul>

v-show 也可以实现条件渲染,v-show 的原理是 display: none;

<p v-show="books.length === 0">目前没有任何书籍</p>
<ul v-show="books.length > 0">
  <li v-for="book in books">{{ book }}</li>
</ul>

计算属性 computed

可以在 computed 里定义函数,返回值可以直接在插值语法中使用

data() {
  return {
    showAnswer: false,
  };
},
computed: {
  label() {
    return this.showAnswer ? "隐藏答案" : "显示答案";
  },
},
<p>问:Vue 是一个什么样的框架?</p>
<p v-show="showAnswer" class="answer">
  答:Vue 是一套用于构建用户界面的渐进式框架。
</p>
<button @click="showAnswer = !showAnswer">{{ label }}</button>

可写计算属性

计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:

export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName: {
      // getter
      get() {
        return this.firstName + ' ' + this.lastName
      },
      // setter
      set(newValue) {
        // 注意:我们这里使用的是解构赋值语法
        [this.firstName, this.lastName] = newValue.split(' ')
      }
    }
  }
}

注意!官方文档的最佳实践提示:Getter 不应有副作用避免直接修改计算属性值

事件处理 v-on

事件处理
我们可以使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click=“handler” 或 @click=“handler”。
事件处理器 (handler) 的值可以是:

  1. 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
data() { 
    return { 
        count: 0 
    } 
}
<button @click="count++">Add 1</button> 
<p>Count is: {{ count }}</p>
  1. 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
data() {
  return {
    name: 'Vue.js'
  }
},
methods: {
  greet(event) {
    // 方法中的 `this` 指向当前活跃的组件实例
    alert(`Hello ${this.name}!`)
    // `event` 是 DOM 原生事件
    if (event) {
      alert(event.target.tagName)
    }
  }
}
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button>

内联处理器中调用方法以传参

methods: {
  say(message) {
    alert(message)
  }
}
<button @click="say('hello')">Say hello</button>
<button @click="say('bye')">Say bye</button>

在内联事件处理器中访问事件参数

<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>

<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
  Submit
</button>
methods: {
  warn(message, event) {
    // 这里可以访问 DOM 原生事件
    if (event) {
      event.preventDefault()
    }
    alert(message)
  }
}

v-on绑定动态事件(事件的名字是一个vue维护的变量)

<input @[event]="handleChange" />
data(){
    return {
        event: "change",
    }
}

一个 v-on 事件还可以绑定多个方法,绑定多个的话需要写成调用的形式

<button @click="handler1(), handler2('参数')"></button>

一个 v-on 甚至可以绑定多个事件,需写成一个对象

<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>

修饰符

事件修饰符

watch 监听器

watch里写一个data里的属性作为监听值,然后当该值变化时会触发方法或函数

export default {
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)'
    }
  },
  watch: {
    // 每当 question 改变时,这个函数就会执行
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() {
      this.answer = 'Thinking...'
      try {
        const res = await fetch('https://yesno.wtf/api')
        this.answer = (await res.json()).answer
      } catch (error) {
        this.answer = 'Error! Could not reach the API. ' + error
      }
    }
  }
}
<p>
  Ask a yes/no question:
  <input v-model="question" />
</p>
<p>{{ answer }}</p>

其他参数

  1. 深层监听,例如监听对象或数组这种复杂对象:deep
  2. 即时回调的侦听器: immediate
  3. 侦听器回调中能访问被 Vue 更新之后的 DOM: flush
watch: {
someObject: {
  handler(newValue, oldValue) {
    // 注意:在嵌套的变更中,
    // 只要没有替换对象本身,
    // 那么这里的 `newValue` 和 `oldValue` 相同
  },
  deep: true,
  //watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。
  // 强制立即执行回调
  immediate: true
  //当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项:
  flush: 'post'
}

this.$watch() 和 停止监听

我们也可以使用组件实例的 $watch() 方法来命令式地创建一个侦听器:

export default {
  created() {   
    const unwatch =this.$watch('question', (newQuestion) => {
      // ...
    })
  }
  
  ...
  // ...当该侦听器不再需要时 
  unwatch()
}

methods 和 computed 和 watch 区别

methods 和 watch 更在意过程,computed 更在意结果,
computed 计算属性会进行缓存,只有 computed 里依赖的data变化的时候才会重新触发,对比 methods 方法性能更高。
methods 如果放在模板中当变量用,每次触发响应式都会更新。
watch 不会直接返回结果。

数据双向绑定/表单输入绑定 v-model

v-model
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:例如这样

<input
  :value="text"
  @input="event => text = event.target.value">

v-model 指令帮我们简化了这一步骤:

<input v-model="text">
  • 文本类型的 和 元素会绑定 value property 并侦听 input 事件;
  • 和 会绑定 checked property 并侦听 change 事件;
  • 会绑定 value property 并侦听 change 事件。

多行文本时注意显示时加上 white-space: pre-line;

<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="add multiple lines"></textarea>

v-model 作用与组件中

父组件中

<CustomInput v-model="searchText"/>
等于转换成
<CustomInput
  :modelValue="searchText"
  @update:modelValue="newValue => searchText = newValue"
/>

要想让 v-model 绑定在组件上工作起来,相当于给子组件注册了一个名为 “@upadate:modelValue” 的事件,并且传递了一个 名为 “modelValue” 的 props

在子组件中
<!-- CustomInput.vue -->
<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

上面的写法可以等价与下面的写法,用一个 computed 属性内进行 getter 和 setter,可以进一步进行复杂的操作,封装组件用得上

<!-- CustomInput.vue -->
<template>
  <input v-model="value" />
</template>

<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
</script>

我们知道了 v-model 相当于传递一个 名为 modelValue 的值作为 props 传入,还可以往 v-model 传递一个参数以修改这个 modelValue 的名字,这个参数可以使用短横杠起名法,起别名后甚至可以绑定多个v-model

父组件中
<UserName
  v-model:first-name="first"
  v-model:lastName="last"
/>
子组件中
<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

<script>
export default {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName']
}
</script>

v-html

对于富文本编辑器以及markdown编辑器我们通常有预览的需求。
一定要在可信的内容上使用v-html,永远不要在用户提交的内容上使用,否则会遭受xss攻击。

<div id="app">
  <div>
    <article v-html="content" />
    <!-- <article>{{ content }}</article> -->
  </div>
</div>
const app = Vue.createApp({
  data() {
    return {
      content: `<p>这是一段<span style="color: hsl(0, 80%, 70%);">HTML</span>代码</p>`,
    };
  },
});
app.mount("#app");

v-once

仅渲染元素和组件一次,并跳过之后的更新。

<div>
  <p v-once>list 初始长度: {{ list.length }}</p>
  <p>list 新长度: {{ list.length }}</p>
  <button @click="list.push(list.length + 1)">添加元素</button>
</div>
data() {
  return {
    list: [1, 2, 3],
  };
},

v-cloak

当使用直接在 DOM 中书写的模板时,可能会出现一种叫做“未编译模板闪现”的情况:
用户可能先看到的是还没编译完成的双大括号标签,直到挂载的组件将它们替换为实际渲染的内容。
v-cloak 会保留在所绑定的元素上,直到相关组件实例被挂载后才移除。
配合像 [v-cloak] { display: none } 这样的 CSS 规则,它可以在组件编译完毕前隐藏原始模板。

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

template标签

当我们想要使用内置指令而不在 DOM 中渲染元素时, 标签可以作为占位符使用。

对 的特殊处理只有在它与以下任一指令一起使用时才会被触发:

  • v-if、v-else-if 或 v-else
  • v-for
  • v-slot

如果这些指令都不存在,那么它将被渲染成一个原生的 元素。带有 v-for 的 也可以有一个 key 属性。所有其他的属性和指令都将被丢弃,因为没有相应的元素,它们就没有意义。单文件组件使用顶层的 标签来包裹整个模板。这种用法与上面描述的 使用方式是有区别的。该顶层标签不是模板本身的一部分,不支持指令等模板语法。

通过vue实例访问和修改属性

在 optionAPI 中,我们可以通过访问 this 拿到 vue 实例的属性

data() {
  return {
    msg: "你好!",
    name: "张三",
  };
},
computed: {
  greetings() {
    return `${this.msg} ${this.name}`;
  },
},

在与传统项目结合vue开发中,在实例 .mount 之后也会返回 vue 实例

const app = Vue.createApp({

});
const vm = app.mount("#app");
console.log(vm);        //可以看到 vue 实例

生命周期钩子

在这里插入图片描述

  • beforeCreate() 在调用 app.mount() 创建实例之后,createApp() 中的配置生效之前
  • create() 是在 beforeCreate() 之后,createApp() 中的配置项生效之后调用,这个时候计算属性、方法、watcher 监听器已经配置好了。
  • beforeMount() 是在create() 之后应用还没挂载到 app.mount() 指定的 html 元素上,在挂在之前会调用beforeMount,这个时候应用还没在页面上渲染出来
  • mounted() 会在应用挂载到 app.mount() 指定的 html 元素之后执行,这个时候应用已经在页面上渲染出来了。
  • beforeUpdate() 当应用中的 html 模板重新 render 时,比如 data 中的属性发生了变化,那么会在render之前执行 beforeUpdate()
  • updated() 是在数据更新之后,html 重新 render 完成之后调用。
  • beforeUnmount() 会在应用卸载前执行,调用 app.unmount() 会卸载组件,那么在正式卸载之前,先执行 beforeUnmount() ,这个时候 vue 应用还是正常的,通常做一些清理收尾操作,例如关闭定时器,因为应用还存在,还能找到在组件里打开的定时器。
  • unmounted() 会在应用正式卸载之后调用,这个时候够狠应用有关的事件监听,或者指令绑定都已经卸载了

vue 组件

vue 组件基础

全局注册

全局注册组件,可以调用 Vue.createApp 返回的对象的 component 方法创建。

const app = Vue.createApp({});
app.component('some-component', {
    template: `
        <div></div>
    `,
    data(){},
    methods:{},
})

如果使用单文件组件,你可以注册被导入的 .vue 文件,该方法也可以被链式调用

import MyComponent from './App.vue'

app.component('MyComponent', MyComponent)
     .component('ComponentA', ComponentA) 
     .component('ComponentB', ComponentB) 
     .component('ComponentC', ComponentC)

局部注册

局部注册是在配置对象里写一个 components 属性,值为一个对象

<script>
import ComponentA from './ComponentA.vue'

export default {
  components: {
    ComponentA: ComponentA
  }
}
</script>

<template>
  <ComponentA />
</template>

上面的例子中可以使用 ES2015 的缩写语法

export default {
  components: {
    ComponentA
  }
  // ...
}

使用组件

在 template 里像这样使用

<script>
import ButtonCounter from './ButtonCounter.vue'

export default {
  components: {
    ButtonCounter
  }
}
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>

组件名格式

在单文件组件和内联字符串模板中,我们都推荐使用PascalCase大驼峰命名法。但是,PascalCase 的标签名在 DOM 模板中是不可用的(比如原生html文件),如果想使用详情参见 DOM 模板解析注意事项
为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent 为名注册的组件,在模板中可以通过 或 引用。

vue脚手架

通过npm创建一个 vue 应用,运行 npm init vue@latest ,这里我们选项 vue-router 和 pinia 为 yes 创建一个vue 应用,并看一下文件结构

|-- 文件根目录
|-- .gitignore
|-- index.html
|-- package.json
|-- README.md
|-- vite.config.js
|-- .vscode
| |-- extensions.json
|-- public 存放一些不在vue里使用到的静态文件
| |-- favicon.ico
|-- src src文件目录,为主要编写代码的地方
|-- App.vue 入口文件组件
|-- main.js 主入口文件,用于给html文件引用
|-- assets 静态资源目录,图片,音频,视频等,可以通过 import 作为模块化导入,也可以通过比如 img 标签的 src 引入
| |-- base.css
| |-- logo.svg
| |-- main.css
|-- components 组件目录,编写的组件都会放在这个文件里面
| |-- HelloWorld.vue
| |-- TheWelcome.vue
| |-- WelcomeItem.vue
| |-- icons
| |-- IconCommunity.vue
| |-- IconDocumentation.vue
| |-- IconEcosystem.vue
| |-- IconSupport.vue
| |-- IconTooling.vue
|-- router
| |-- index.js
|-- stores
| |-- counter.js
|-- views
|-- AboutView.vue
|-- HomeView.vue

组件 props

我们在使用组件的时候,可以往组件身上传一些 attributes,也可以说传一些 props ,在react组件里万物皆 props,例如

<MessageItem foo="张三"  />

一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attributes (关于透传 attributes,我们会在专门的章节中讨论)。

export default {
  props: ['foo'],
  created() {
    // props 会暴露到 `this` 上
    console.log(this.foo)
  }
}

除了使用字符串数组来声明 prop 外,还可以使用对象的形式,
对于以对象形式声明中的每个属性,key 是 prop 的名称,而值则是该 prop 预期类型的构造函数。

export default {
  props: {
    title: String,
    likes: Number
  }
}

在vue中使用组件,我们一般会把父组件的数据作为 props 传给子组件,所以我们还可以绑定动态的props值

<template>
    <!-- 根据一个变量的值动态传入 -->
    <BlogPost :title="post.title" />

    <!-- 根据一个更复杂表达式的值动态传入 -->
    <BlogPost :title="post.title + ' by ' + post.author.name" />
</template>

<script>
export default {
  data(){
    return {
        post: {
            title:  xxx,
            author: {name: xxx}
        }
    }
  }
}
</script>

动态绑定的 props ,在父组件中如果修改了 props 的值,那么也会触发子组件的响应式视图更新,而且在子组件中也能用 watch 去监听 props 的变化

export default {
  props: ['foo'],
  watch: {
    foo(newValue,oldValue){
        console.log(newValue,oldValue);
    }
  }
}

透传 attributes / 透传 props

传递到子组件的 props 如果没有被子组件声明接收,那么这个属性会被 template 的最外层的标签接收,例如

父组件中
<SomeComponent nonExistProps="someValue" />
子组件
<template>
    <div nonExistProp="someValue"></div>
<template>
<script>
    export default{
        //无props定义
    }
</script>

如果最外层元素还是一个组件,还会继续传给下一个子组件,例如

子组件
<template>
    <AnyComponent></AnyComponent>
<template>
子组件的子组件
<template>
    <div></div>
<template>
<script>
    export default{
        props: ['nonExistProp'],
        created() {
        // props 会暴露到 `this` 上
            console.log(this.nonExistProp)
        } 
    }
</script>

或者我们可以访问 $attrs 访问到透传的属性

<span>Fallthrough attribute: {{ $attrs }}</span>

<script>
export default{
    created() {
        console.log(this.$attrs.nonExistProp)
    } 
}
</script>

透传 attributes 我们通常用于:

  1. 传递一些 html 的原生属性给最外层元素,例如 class、id等,注意传给最外层元素后如最外层元素存在同名属性则会合并。见官方文档 Attributes 继承

如果我们不需要透传,可以禁用Attributes继承

export default{    
    inheritAttrs: false,
}

单向数据流

接触组件和 props 开始,我们开始接触组件间的数据传递,也就是组件通信,那么我们需要遵守单向数据流。如果你尝试在子组件中修改父组件传递过来的 props ,那么将会发出警告

export default {
  props: ['foo'],
  created() {
    // ❌ 警告!prop 是只读的!
    this.foo = 'bar'
  }
}

如果子组件需要用到父组件数据,不需要做修改而是在原来的数值上做单纯的拓展,可以使用 computed 属性做二次拓展

export default {
  props: ['size'],
  computed: {
    // 该 prop 变更时计算属性也会自动更新
    normalizedSize() {
      return this.size.trim().toLowerCase()
    }
  }
}

如果子组件需要用到父组件数据,且自身需要修改,那么需要在自身备份一份再使用,点击标题查看官方文档举例即可。在自身data中备份 props 的值

export default {
  props: ['initialCounter'],
  data() {
    return {
      // 计数器只是将 this.initialCounter 作为初始值
      // 像下面这样做就使 prop 和后续更新无关了
      counter: this.initialCounter
    }
  },
  methods: {
    changeCount(){
        this.counter ++ ;
    }
  }
}

如果想修改父组件的数据,那么需要遵守 数据向下,事件向上 的原则,类似 react 里,父组件传值过来,把修改值的方法也通过 props 传过来,在子组件中触发父组件传来的方法即可。但是在 vue 中,如果父组件没有传递方法,子组件还是调用了,那么会报错。
这个时候,就需要用到 vue 特有的触发自定义事件:$emit

事件

子组件修改不了父组件的数据,但可以触发父组件的方法,在 vue 里我们称之为触发事件,通过 $emit 来触发,$emit 接收两个参数:参数1为要触发的事件名,参数2为传给事件的参数

父组件中注册 someEvent 事件
<MyComponent @someEvent="callback" /><MyComponent @some-event.once="callback" />

也可以这样接收参数
<MyButton @someEvent="(n) => count += n" />

<script>
export default {
    methods: {
        callback(value){
            console.log(value);     //输出1
        }
    }
}
</script>
子组件中
<button @click="$emit('someEvent',1)">click me</button>
<!-- 或者通过 this.$emit 触发 -->
<button @click="submit">click me</button>

<script>
export default {
  emits: ['inFocus', 'someEvent'],     //良好的习惯:显式声明一下触发的事件
  methods: {
    submit() {
    //或者通过 this.$emit 触发
      this.$emit('someEvent', 1)
    }
  }
}
</script>

slot 插槽

组件传递html结构,vue除了可以使用类似 react 的 jsx 以外,官方有另一个更符合 vue 概念的 slot

<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>
<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>
最终渲染出来是这样的
<button class="fancy-btn">Click me!</button>

来自官方的图解
在这里插入图片描述

插槽内容可以是任意合法的模板内容,不局限于文本。例如我们可以传入多个元素,甚至是组件:

<FancyButton>
  <span style="color:red">Click me!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

slot 的默认内容:

如果我们想在父组件没有提供任何插槽内容时在 内渲染“Submit”,只需要将“Submit”写在 标签之间来作为默认内容,如果父组件没有传递任何 slot 内容,那么默认内容将会生效,否则则替换。

<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>

具名插槽v-slot

上面的插槽只能放置一块内容,如果我们需要在不同的位置插入不同的内容,则需要具名插槽,没有提供 name 的 出口会隐式地命名为“default”

子组件内
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
父组件使用时
<BaseLayout>
  <template v-slot:header>
    <!-- header 插槽的内容放这里 -->
  </template>
  <template v-slot:default>
    <!-- 没有提供 name 的 <slot> 出口会隐式地命名为“default”。这里将会插入main的内容 -->
  </template>
</BaseLayout>

v-slot还有一个简写为一个井号 #

<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

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

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

插槽名也可以指定为一个动态值

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

具名插槽官方图解
在这里插入图片描述

插槽渲染作用域

父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。下面的代码只能访问到父组件的message

父组件中
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

作用域插槽v-slot

在上面的渲染作用域中我们讨论到,插槽的内容无法访问到子组件的状态。
然而在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。这个时候我们就需要用到作用域插槽:
写法为往 slot 标签上传递 attributes

子组件中
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>
这样我们就能在父组件中取到一个 slotProps 对象,里面包含所有子组件的 attributes 组成的键值对
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

或者我们使用结构语法取值
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

作用域插槽官方图解
在这里插入图片描述

当然作用域插槽也可以具名,变成具名作用域插槽,注意具名作用域插槽通常搭配 template 标签使用,看一下 elementUI 里的 table 组件用得非常多

父组件中
<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>
子组件中

<slot name="header" message="hello"></slot>

注意插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps 的结果是 { message: 'hello' }。

依赖注入 provide/inject

我们在进行组件通信的时候,使用props存在只能一层一层传的问题,provide 和 inject 可以帮助我们解决这一问题。
在这里插入图片描述

在 optionAPI 中为这样写,有两种形式,一种是普通对象形式,一种是函数形式,函数形式能访问this对象而对象形式不能,但是特别注意:provide不会保持响应性。

export default {
  data() {
    return {
      message: 'hello!'
    }
  },
  provide() {
    // 使用函数的形式,可以访问到 `this`
    return {
      message: this.message
    }
  }
}

全局 provide:往 createApp 执行时返回的实例,调用这个实例的 provide 函数

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

inject:在我们使用 provide 之后在组件内就可以使用 inject 接收,类似 props 接收一样,接收到的 provide 可以在 this 上访问到,这就意味着可以直接在 template 中使用:

export default {
  inject: ['message'],  
  data() { 
    return { 
        // 基于注入值的初始数据 
        fullMessage: this.message 
    } 
  }
  created() {
    console.log(this.message) // injected value
  }
}

inject 起别名和设置默认值:如果 inject 进来的 provide 存在与组件本身的数据同名的情况,就可以使用别名的形式,将 inject 写成一个对象,对象形式还可以有默认值的配置项

export default {
  inject: {
    localMessage: {     /* 本地属性名 */
      from: 'message',       /* 注入来源名 */ 
      default: 'default value',     /* 默认值 */ 
    },
    user: { 
    // 对于非基础类型数据,如果创建开销比较大,或是需要确保每个组件实例 // 需要独立数据的,请使用工厂函数 
    default: () => ({ name: 'John' })
    }
  }
}

optionAPI 中想使 inject 保持响应性可以借助 computed 函数,这种是 compositionAPI 和 optionAPI 搭配使用

import { computed } from 'vue'

export default {
  data() {
    return {
      message: 'hello!'
    }
  },
  provide() {
    return {
      // 显式提供一个计算属性
      message: computed(() => this.message)
    }
  }
}

template ref

这里介绍的是optionAPI 模板标签里的 ref ,用于获取原生 dom 元素或者 组件实例,官方解释为”用于注册模板引用。“
引用将被注册在组件的 this.$refs 对象里:

<!-- 存储为 this.$refs.p -->
<p ref="p">hello</p>

<script>
export default {
    created(){
        console.log(this.$refs.p)
    }
}
</script>

如果用于组件,引用将是子组件的实例。或者 ref 可以接收一个函数值,用于对存储引用位置的完全控制:
关于 ref 注册时机的重要说明:因为 ref 本身是作为渲染函数的结果来创建的,必须等待组件挂载后才能对它进行访问。
访问到实例对象之后可以进行父组件对子组件的属性修改,但是万不得已不要使用,这会破坏数据的流向

<ChildComponent ref="childComponentRef" /><ChildComponent :ref="(el) => child = el" />

<script>
export default {
    mounted(){
        console.log(this.$refs.childComponentRef)
    }
}
</script>

但是要特别注意:使用了 <script setup> 的组件是默认私有的一个父组件无法访问到一个使用了

子组件中

<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
  a,
  b
})
</script>

template is 动态组件

标签上还可以存在 is 属性,通常搭配 <component> 标签实现动态组件,
is 可以绑定为原生标签(3.1开始支持),属性值为要渲染的标签名,例如:我们现在根据父组件传过来的值渲染出 h1 到 h6 不同的原生标签

父组件中
<TextHeading level="1">一级标题</TextHeading>
<TextHeading level="2">二级标题</TextHeading>
<TextHeading level="3">三级标题</TextHeading>
<TextHeading level="4">四级标题</TextHeading>
<TextHeading level="5">五级标题</TextHeading>
<TextHeading level="6">六级标题</TextHeading>
子组件中
<template>
  <Component :is="heading"><slot></slot></Component>
</template>

<script>
export default {
  props: ["level"],
  computed: {
    heading() {
      return `h${this.level}`;
    },
  },
};
</script>

is 还可以绑定一个组件名作为值以渲染不同的组件,具体可以看官方实例:

<!-- currentTab 改变时组件也改变 -->
<component :is="currentTab"></component>

<script>
export default {
  components: {
    Home,
    Posts,
    Archive
  },
  data() {
    return {
      currentTab: 'Home',
    }
  }
}
</script>

当使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 <KeepAlive> 组件强制被切换掉的组件仍然保持“存活”的状态。

KeepAlive

是一个内置组件,可以将组件内包裹的内容缓存下来,一般用于表单组件,或者 tabs 切换时,不销毁组件。

<!-- 非活跃的组件将会被缓存! -->
<KeepAlive>
  <component :is="activeComponent" />
</KeepAlive>

KeepAlive 还可以接收 max 属性,属性值为最大缓存数

<KeepAlive :max="10">
  <component :is="activeComponent" />
</KeepAlive>

我们可以通过 include 和 exclude 属性来控制缓存特定的组件。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组。它会根据组件的 name 选项进行匹配,所以组件如果想要条件性地被 KeepAlive 缓存,就必须显式声明一个 name 选项。KeepAlive还可以缓存路由组件,使用 include 会匹配声明路由时的name属性

<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
  <component :is="view" />
</KeepAlive>

<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
  <component :is="view" />
</KeepAlive>

<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
  <component :is="view" />
</KeepAlive>

<!-- 路由组件 -->
<keep-alive include="componentXXX">
    <router-view to="xxx"></router-view>
</keep-alive>

经过 KeepAlive 组件包裹的组件会具有新的生命周期 activated 和 deactivated

异步组件

vue 提供了一个 defineAsyncComponent 实现组件异步加载,参数是一个函数,函数返回的 Promise 的 resolve 的结果将会异步加载

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`

es6 的 import 语法也会返回一个 Promise,搭配使用看起来会更简洁

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

异步组件可以全局注册

app.component('MyComponent', defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
))

也可以局部注册

<script>
import { defineAsyncComponent } from 'vue'

export default {
  components: {
    AdminPage: defineAsyncComponent(() =>
      import('./components/AdminPageComponent.vue')
    )
  }
}
</script>

<template>
  <AdminPage />
</template>

compositionAPI 组合式API

组合式API可以让相同功能逻辑的代码放在一个地方,使用方法为在选项式API里声明一个 setup 函数或者声明一个 <script setup> 标签。

setup 函数

vue2.7 也支持 setup 函数和 script setup ,但一般情况下为了改动 2.7 版本以下的项目,通常搭配 setup 函数使用:

要在组件模板中使用响应式状态,需要在 setup() 函数中定义并返回。我们也可以在同一个作用域下定义更新响应式状态的函数,并将他们作为方法与状态一起暴露出去:

<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    
    function increment() { count.value++ }

    // 返回值会暴露给模板和其他的选项式 API 钩子
    return {
      count,
      increment
    }
  },

  mounted() {
    console.log(this.count) // 0
  }
}
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

setup函数还接收两个参数,一个是props,另一个是 context ,context 里面包含 emit,slot 等

setup 函数中的 props

在没有使用

export default {
  props: ['foo'],
  setup(props) {
    // setup() 接收 props 作为第一个参数
    console.log(props.foo)
  }
}

请注意如果你解构了 props 对象,解构出的变量将会丢失响应性。因此我们推荐通过 props.xxx 的形式来使用其中的 props。如果你确实需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs() 和 toRef() 这两个工具函数:

import { toRefs, toRef } from 'vue'

export default {
  setup(props) {
    // 将 `props` 转为一个其中全是 ref 的对象,然后解构
    const { title } = toRefs(props)
    // `title` 是一个追踪着 `props.title` 的 ref
    console.log(title.value)

    // 或者,将 `props` 的单个属性转为一个 ref
    const title = toRef(props, 'title')
  }
}

在 script setup 中,可以使用 defineProps 宏来获取 props,如果需要读取,访问 defineProps 的返回值即可,在模板中也是直接使用

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>

<template>
<p>{{foo}}</p>
</template>

setup ref()

compositionAPI 中定义响应式数据,我们可以使用 ref() 函数,ref 接收一个 js 变量作为参数,使其变为响应式的。
在 Vue 中,状态(ref、reactive、computed)都是默认深层响应式的。这意味着即使在更改深层次的对象或数组,你的改动也能被检测到。

import { ref } from "vue"

const num = ref(0)      //数字
const str = ref('字符串')      //字符串
const arr = ref([0, 1, 2])      //数组
const obj = ref({a: 1, b: 2})       //对象,此时 a 和 b 也具有响应性

在 script 中想读写 ref 定义的响应式数据,需要使用其返回数据的 .value ,详情见 setup 函数举的例子。
setup 中如果想要获取 template ref,创建一个 ref 变量,并在 template 中使用 ref 属性绑定即可

<script setup>
import { ref, onMounted } from 'vue'

const p = ref()

onMounted(()=>{
    console.log(p.value)        //onMounted 后才能访问到
})
</script>

<template>
  <p ref="p">hello</p>
</template>

但是要特别注意:使用了 <script setup> 的组件是默认私有的一个父组件无法访问到一个使用了

子组件中

<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
  a,
  b
})
</script>

setup reactive()

一般情况下我们定义 ref 就能满足响应式数据的需求。reactive 通常用来绑定表单数据。调用 reactive函数,传入一个数组或对象,使其内部变为响应式数据。

import { reactive } from "vue"

const arr = reactive([0, 1, 2])      //数组
const obj = reactive({a: 1, b: 2})       //对象,此时 a 和 b 也具有响应性

reactive有许多局限性:比如reactive 仅对复杂类型数据有效
,又比如我们如果定义了一个 reactive 数据我们如果改变返回的值,就会丢失其响应性。
同时这也意味着当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性:

let state = reactive({ count: 0 })

// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state = reactive({ count: 1 })
//比如我们获取一个后端数据,使其重新赋值,就会发生上面的情况

// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++

// count 也和 state.count 失去了响应性连接
let { count } = state
// 不会影响原始的 state
count++

// 该函数接收一个普通数字,并且
// 将无法跟踪 state.count 的变化
callSomeFunction(state.count)

所以通常情况下,我们一般使用 ref 定义响应式数据,ref 传入一个复杂类型数据,它的 .value 实际上也是相当于调用 reactive 。

setup computed()

setup中定义 computed 数据,使用 vue 提供的 computed 方法,该方法接收一个无参回调函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露computed返回值。

import { computed } from "vue"

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误

也可以写成一个可写计算属性,详情点击看文档

setup watch()

在组合式 API 中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数:

  1. watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象(ref复杂类型.value或reactive)、一个 getter 函数、或多个数据源组成的数组:该 getter 函数通常用于 get 一下 ref 的 .value 和 reactive 的对象属性值。如果getter函数用于 reactive(或ref复杂类型.value) 本身,则需要开启 deep:true
  2. 第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。(下面代码没有体现,文档也没有体现,可以看下面watchEffect的清理回调示例)
import { ref, watch } from 'vue'

const x = ref(0)
const obj = reactive({ count: 0 })

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`count is: ${count}`)
})

// 单个 ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter 函数
watch(
  () => x.value + obj.count,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// 多个来源组成的数组
watch([x, () => obj.count], ([newX, newCount]) => {
  console.log(`x is ${newX} and count is ${newCount}`)
})
  1. 第三个可选的参数是一个对象,支持使 watch 变成回调立即执行一次、深度监听和访问更新后DOM。参数分别为:
const obj = reactive({ count: 0 })

watch(
    obj,        //()=>JSON.parse(JSON.stringify(obj))
    (newValue,oldValue) => {   
      // 注意:`newValue` 此处和 `oldValue` 是相等的 // 因为它们是同一个对象!如果想获取不同的值,需要对第一个参数转为 getter 函数并进行深度克隆,见上面注释
      console.log(newValue,oldValue)
    },
    {
        immediate: true,        //立即执行监听
        deep: true,     //深度监听,通常用于参数是 getter 函数的情况,因为 ref 和 reactive 默认是深度监听
        flush: 'post',      //侦听器回调中能访问被 Vue 更新之后的 DOM
    }
)

如果 watch 的是父组件 props 传过来的内容,则机制极其复杂,往后看 “ref 和 reactive 在 props 中传递时响应性问题,以及 watch 问题”章节

setup watchEffect()

watchEffect 也是监听数据时执行一些副作用行为,但与 watch 不同的是,它不需要指定监听的对象,而是自动监听其依赖,在依赖更新时自动执行 watchEffect 函数(有点像computed)。并且回调函数会立即执行一次。

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> 声明时就会立即执行一次,输出 0

count.value++
// -> 输出 1

同时第一个参数回调函数中,也不存在 newValue 和 oldValue 了,但还是有清理回调

watchEffect(async (onCleanup) => {      //形参为清理回调
  const { response, cancel } = doAsyncWork(id.value)
  // `cancel` 会在 `id` 更改时调用
  // 以便取消之前
  // 未完成的请求
  onCleanup(cancel)
  data.value = await response
})

如果想停止 watchEffect ,只需要用一个变量接住 watchEffect 的返回值,并执行即可

const watEffReturn = watchEffect(() => {})

// 当不再需要此侦听器时:
watEffReturn()

ref 和 reactive 在 props 中传递时响应性问题,以及 watch 问题

  • 注意 ref 的复杂类型数据通过 props 传递后,子组件拿到的不是 RefImpl 而是一个 proxy。
  • ref复杂类型 和 reactive 的数据通过 props 传到子组件中也会保持其内容的响应性(比如数组项修改、对象值修改)。
  • 但是如果 ref复杂类型 和 reactive 的数据本身发生修改(比如重新赋予一个新对象或者新数组),那么仅仅保留页面 template 的响应式,无法在子组件中直接 watch 到这个数据本身发生改变,但可以使用 getter 函数 watch 到。
父组件中
<ChildComponent :objRef="objRef" :arr="arr"/>
<button @click="arr.pop()">1</button> arr内容发生改变,子组件能直接watch到,父组件需开启 deep:true,子组件用 getter也需 deeptrue
<button @click="objRef.x=2">2</button> objRef内容发生改变,子组件能直接watch到,父组件需开启 deep:true,子组件用 getter也需 deeptrue
<button @click="arr=[1,2]">3</button>arr本身发生改变子组件无法直接watch到父组件可以,子组件使用 getter 函数即可
<button @click="objRef={ x: 2 }">4</button> no!这种子组件无法直接watch到

<script setup>
import { ref, watch } from 'vue';

const objRef = ref({ x: 1 })
const arr = ref([1, 2, 3])

//下面的代码仅作与子组件对比使用,实际原因看 setup watch 章节
watch(
  arr,      //或objRef
  () => {
    console.log(111);       //按钮3,4能直接触发,按钮1,2需开启 deep:true触发
  },
)

//一般不会用 getter 取复杂数据本身,仅作了解,可读性极差
watch(     
  ()=>arr,      //或 arr.value 或objRef
  () => {
    console.log(112);          //必须开启 deep:true,否则都不会触发,开启后1,2,3,4都能触发
  },
  {
    deep:true
  }
)
</script>
子组件中
<script setup>
const props = defineProps(['objRef', 'arr'])
console.log(props.objRef)     //proxy类型

watch(
  [props.arr, props.objRef],
  () => {
    console.log(221);       //按钮1,2能触发,也就是说子组件不用开启 deep 就能监测父组件值内容的修改
  },
);
watch(
  [()=>props.arr, ()=>props.objRef],
  () => {
    console.log(222);       //按钮3,4能触发,如果开启{deep:true}的话1,2也能触发
  },
);
</script>

如果进行 props 解构赋值,那么解构出来的数据依然有响应性,但只能通过 getter 函数监测到,且只能监测数据内容的修改且必须开启deep,解构之后数据本身发生修改无法监测。
如果是 script setup 中模板,解构出来之后就没有 template 的响应性了,但如果是 optionAPI,因为 props 配置项的原因,template还是有响应性的

const {arr} = props

watch(
  ()=>arr,
  () => {
    console.log(222);       //解构之后只有按钮1,2能触发,且必须 deep
  },
  {deep:true}
);

如果传递的是普通数据类型的 ref ,那么该 ref 传到子组件后也不具有响应性。
像上面说的情况还想要保持响应性,需使用 toRefs 或 toRef

setup methods

在 setup 中的 methods 就相当于普通的 JavaScript 函数,如果现在模板中使用,如果使用的不是 script setup 记得把方法返回出去给模板使用

<!-- 使用特殊的 $event 变量 -->
<button @click="handleClick('Form cannot be submitted yet.', $event)">
  Submit
</button>

<!-- 使用内联箭头函数 -->
<button @click="(event) => handleRemove('Form cannot be submitted yet.', event)">
  Submit
</button>
setup(){
    function handleClick(){
        xxx
    }
    const handleRemove = () => {
        xxx
    }
    return {handleClick, handleRemove}
}

setup emit

如果你显式地使用了 setup 函数而不是

export default {
  emits: ['inFocus', 'submit'],
  setup(props, ctx) {
    ctx.emit('submit')
  }
}

//与 setup() 上下文对象中的其他属性一样,emit 可以安全地被解构:
export default {
  emits: ['inFocus', 'submit'],
  setup(props, { emit }) {
    emit('submit')
  }
}

在 template 中触发自定义事件跟以前一样,也是 通过 $emit

<button @click="$emit('increaseBy', 1)">
  Increase by 1
</button>

父组件中内联监听自定义事件接收参数或者触发时执行自定义函数接收参数也是一样的

<MyButton @increase-by="(n) => count += n" />
<MyButton @increase-by="increaseCount" />

<script>
function increaseCount(n) {
  count.value += n
}
</script>

<script setup> 中我们就没有 this.$emit 了,但是我们能通过 defineEmits 宏来显式声明以及 emit ,但是注意:defineEmits() 宏不能在子函数中使用。如上所示,它必须直接放置在

<script setup>
const emit = defineEmits(['inFocus', 'submit'])

function buttonClick() {
  emit('submit')
}
</script>

defineEmits还支持对象形式对事件传参进行校验

<script setup>
const emit = defineEmits({
  submit(payload) {
    // 通过返回值为 `true` 还是为 `false` 来判断
    // 验证是否通过
  }
})
</script>

setup 生命周期

setup中使用生命周期是一个方法,里面接收一个回调函数作为参数,回调函数会在该声明周期中调用,与 optionAPI 不同的是前面加个 on ,且需要在 vue 中引入

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  console.log(`the component is now mounted.`)
})
</script>

当调用 onMounted 时,Vue 会自动将回调函数注册到当前正被初始化的组件实例上。这意味着这些钩子应当在组件初始化时被同步注册。例如,请不要这样做:

setTimeout(() => {
  onMounted(() => {
    // 异步注册时当前组件实例已丢失
    // 这将不会正常工作
  })
}, 100)


//注意这并不意味着对 onMounted 的调用必须放在 setup() 或 <script setup> 内的词法上下文中。onMounted() 也可以在一个外部函数中调用,只要调用栈是同步的,且最终起源自 setup() 就可以

setup provide inject

setup中使用 provide 是一个函数,第一个参数为传递的值的名字,第二个参数为要传递的值。
如果传递的值是响应性的,那么后代组件inject的也是响应性的,如果传递的是一个 reactive 的属性值,想要保持响应性,则需搭配 toRef 使用

<script setup>
import { ref, provide } from 'vue'

provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

const count = ref(0)
provide('key', count)

const movie = ref({title:'111'})
provide("title", toRef(movie.value, "title"))
</script>

我们还可以进行全局provide

import { createApp } from 'vue'

const app = createApp({})

app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

provide的数据也要保持数据流向单一性,通常搭配 readOnly 使用

<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)
provide('read-only-count', readonly(count))
</script>

inject 的时候也是一个函数,第一个参数为要接收的 provide 的名字,第二个参数为默认值

<script setup>
import { inject } from 'vue'

// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')
</script>

有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数:

<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation() {
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>
<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>

<template>
  <button @click="updateLocation">{{ location }}</button>
</template>

setup 透传Attributes

在 setup 中想要访问透传 attributes ,可以使用 setup 函数的 context 里的 attrs 中使用

export default {
  setup(props, ctx) {
    // 透传 attribute 被暴露为 ctx.attrs
    console.log(ctx.attrs)
  }
}

如果是 使用 script setup 可以使用 useAttrs 访问

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

在 template 还是一样的使用 $attrs 访问

<span>Fallthrough attribute: {{ $attrs }}</span>
<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">click me</button>
  小提示:没有参数的 v-bind 会将一个对象的所有属性都作为 attribute 应用到目标元素上。
</div>

注意 attrs 本身是具有响应性的,使用 watch 依然可以监听到,但解构后和 props 一样就监听不到了

vue3 逻辑复用最佳实践——Composables组合式函数

一个 Composables 就相当于一个 setup 函数,每一个 Composables 都是一个以 use 开头命名的 文件,该文件内容是一个函数,该函数返回一些方法及状态可供其他组件调用。

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
  // 被组合式函数封装和管理的状态
  const x = ref(0)
  const y = ref(0)

  // 组合式函数可以随时更改其状态。
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // 一个组合式函数也可以挂靠在所属组件的生命周期上
  // 来启动和卸载副作用
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // 通过返回值暴露所管理的状态
  return { x, y }
}

上面这个就是官网提供的一个鼠标位置的一个组合式函数,在使用的时候类似 react 的 hook 一样使用即可。
每一个调用 useMouse() 的组件实例会创建其独有的 x、y 状态拷贝,因此他们不会互相影响。如果你想要在组件之间共享状态,可以使用状态管理库

<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

官方文档还有其他约定和最佳实践:比如以 use 开头、处理输入的参数、为什么用ref而不是reactive、记得清理副作用、同步调用等。
composable 一个 fetch 请求

async function useRequest(url = "", option = { method: "GET" }) {
    const baseURL = 'http://127.0.0.1:7777/api/';
    const res = await fetch(baseURL + url, {
        method: option.method || 'GET',
        headers: {
            //全局请求头
            "Content-Type": "application/json",
            Authorization: "Bearer SOMEJWTTOKEN",
            ...option.headers
        },
        body: option.data ? JSON.stringify(option.data) : undefined,
    });
    //400状态处理
    if (res.status >= 400) {
        const error = await res.json();
        const e = new Error(res.statusText);
        e.error = error;
        e.status = res.status;
        throw e;
    }
    const result = await res.json();
    return result
}
export default useRequest;
import useRequest from '@/composables/useRequest'

//使用时如果想携带 params 参数,需搭配URLSearchParams
const getData = async () => {
    const res = await useRequest(`/xxx?{new URLSearchParams({
        name: 'ming',
    })}`)
}

const postData = async () => {
    const res = await useRequest(`/xxx`,{
        method: 'POST',
        data: {
            age: 18,
        }
    })
}

Teleport 组件传送

有时我们可能会遇到这样的场景:一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。这类场景最常见的例子就是全屏的模态框。
teleport 组件接收一个 to 属性,属性值为要挂载到的标签上,比如 body 上

<button @click="open = true">Open Modal</button>

<Teleport to="body">
  <div v-if="open" class="modal">
    <p>Hello from the modal!</p>
    <button @click="open = false">Close</button>
  </div>
</Teleport>

多个 组件可以将其内容挂载在同一个目标元素上,而顺序就是简单的顺次追加,后挂载的将排在目标元素下更后面的位置上。

<Teleport to="#modals">
  <div>A</div>
</Teleport>
<Teleport to="#modals">
  <div>B</div>
</Teleport>

渲染结果为:
<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

自定义指令

指定i指令会直接操作 dom 实例,非必要情况不要使用,想要拓展 html 一般还是使用组件的形式。
全局注册自定义指令:

const app = createApp({})

// 使 v-focus 在所有组件中都可用
app.directive('focus', {
  /* ...钩子函数 */
})
<input v-focus />

里面的钩子函数是一系列指令钩子,这些指令钩子会在合适的时候调用,并且每个钩子都有自己的参数,具体点击链接查看即可,这里不做拓展。

对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:

app.directive('color', (el, binding) => {
  // 这会在 `mounted` 和 `updated` 时都调用
  el.style.color = binding.value
})

当然自定义指令也可以在组件上使用,但官方不推荐

mixin

在 Vue 2 中,mixins 是创建可重用组件逻辑的主要方式。尽管在 Vue 3 中保留了 mixins 支持,但对于组件间的逻辑复用,Composition API 是现在更推荐的方式。
如果你的 mixin 包含了一个 created 钩子,而组件自身也有一个,那么这两个函数都会被调用。Mixin 钩子的调用顺序与提供它们的选项顺序相同,且会在组件自身的钩子前被调用。但是组件自身的属性会覆盖mixin的属性
比如我们封装一个分页mixin,接收父组件传过来的 totalPage 和 defaultCurrentPage

const PaginationMixin = {
  props: ["totalPage", "defaultCurrentPage"],
  data() {
    return {
      currentPage: this.defaultCurrentPage,
    };
  },
  methods: {
    changePage(page) {
      this.currentPage = page;
    },
  },
};
export default PaginationMixin;
使用时
<script>
import PaginationMixin from "../mixins/PaginationMixin";
export default {
  mixins: [PaginationMixin],
};
</script>

渲染函数

在绝大多数情况下,Vue 推荐使用模板语法来创建应用。然而在某些使用场景下,我们真的需要用到 JavaScript 完全的编程能力。这时渲染函数就派上用场了。
Vue 提供了一个 h() 函数用于创建 vnodes,这个函数接收三个参数,参数一为要渲染的标签名,参数二为属性名和值组成的键值对对象,参数三为渲染函数渲染出来的子内容:

import { h } from 'vue'

const vnode = h(
  'div', // type
  { id: 'foo', class: 'bar' }, // props
  
  /* children,子内容可以是子渲染函数组成的数组,可以是字符串 */
  []
)

在 setup 函数中,context上下文还包含一个 slots,父组件通过 slot 传递进来的内容会变成子组件 slots 中的一个函数,执行即可访问

setup(props, { slots }) {
  // console.log(slots);
  console.log(slots.header);
  console.log(slots.default);
  return () => [
    h("header", {}, slots.header()),
    h("main", {}, slots.default()),
  ];
},

其他进阶用法点击标题查看官方文档

组件错误处理

createApp 调用后返回的实例可以调用他身上的 config.errorHandler 捕获错误信息

import { createApp } from 'vue'
const app = createApp(...)
app.config.errorHandler = (err, instance, info) => {
  // 向追踪服务报告错误
  console.log(err);     //错误信息
  console.log(instance);        //vue实例
  console.log(info);        //错误位置
}

vue的错误机制为子组件如果发生报错,会一层层往上冒泡传递错误,最终传递到 App.vue,如果App.vue 也不处理,那么控制台会报红,我们不想让他一层层往上冒,可以在子组件中使用 errorCaptured 生命钩子捕获,返回一个 false 将会停止向上传播

errorCaptured(err, instance, info){

}
//当然也有 compositionAPI 的
const onErrorCaptured((err, instance, info)=>{
    
})

状态管理

我们可以利用 vue 的响应式系统创建一个小 store

// store.js
import { reactive } from 'vue'

export const store = reactive({
  count: 0,
  increment() {
    this.count++
  }
})
<template>
  <button @click="store.increment()">
    From B: {{ store.count }}
  </button>
</template>

<script setup>
import { store } from './store.js'
</script>

还可以利用 composable 创建拥有使用者独立局部状态的小 store

import { ref } from 'vue'

// 全局状态,创建在模块作用域下
const globalCount = ref(1)

export function useCount() {
  // 局部状态,每个组件都会创建
  const localCount = ref(1)

  return {
    globalCount,
    localCount
  }
}

状态管理库 Pinia

如果仅仅是小项目,单纯使用 composable 已经可以解决大部分的状态共享问题。
但是使用 pinia 可以进行获得例如:开发者工具浏览、热更新、状态永久化插件等,我们执行 npm install pinia 后就可以进行 vue 使用插件 app.use的操作了

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

创建一个 store

创建 store ,我们使用 pinia 里的 defineStore 函数
第一个参数为 store 的名字(用于区分以及开发者工具观察),第二个参数为该 store 的内容,为一个函数形式,下面是 vite 创建的 pinia 文件

import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore(
    'counter' /** store名字 */ ,  
    /** store内容 */ 
    () => {
      const count = ref(0)
      const doubleCount = computed(() => count.value * 2)
      function increment() {
        count.value++
      }
      return { count, doubleCount, increment }
    }
)

使用 store

在组件中像使用 composable 一样使用 store

<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
counter.count++
// 自动补全! ✨
counter.$patch({ count: counter.count + 1 })
// 或使用 action 代替
counter.increment()
</script>
<template>
  <!-- 直接从 store 中访问 state -->
  <div>Current Count: {{ counter.count }}</div>
</template>

修改 store 中的状态

  1. 直接修改:像上面的例子所示, pinia 支持直接在组件中修改状态
  2. 通过 $patch 同时修改多个状态,类似于 react 的 setState
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
counter.$patch({
  count: store.count + 1,
  age: 120,
  name: 'DIO',
})
  1. 通过 action 修改

action 和 异步action

pinia 支持组件中直接修改 store 中的状态,如果想定义复用的修改状态逻辑,可以使用 action,pinia 中定义 action 是定义一个个 function,action 中可以直接定义异步操作

import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore(
    'counter',  
    () => {
      const count = ref(0)
      async function increment(value) {
        setTimeout(() => {
            count.value = count.value + value
        }, 1000)
      }
      return { count, increment }
    }
)

getter/computed

pinia 中的 getter 是在 store 里定义 computed 函数,见 “创建store”章节中的例子

多个 store 以及互相通信

pinia 中定义多个 store 不用像 vuex 一样统一集中在一个 store 上进行模块化,
想要使用哪个 store 就引入哪个 store 文件即可,一个 store 中也可以引入另一个 store 文件进行互相通信

pinia 插件

pinia 插件就是一个函数,函数的返回值将会附加到所有的 store 上

import { createPinia } from 'pinia'

// 创建的每个 store 中都会添加一个名为 `secret` 的属性。
// 在安装此插件后,插件可以保存在不同的文件中
function SecretPiniaPlugin() {
  return { secret: 'the cake is a lie' }
}

const pinia = createPinia()
// 将该插件交给 Pinia
pinia.use(SecretPiniaPlugin)

// 在另一个文件中
const store = useStore()
store.secret // 'the cake is a lie'

该函数可以接收 pinia 的 context 参数

export function myPiniaPlugin(context) {
  context.pinia // 用 `createPinia()` 创建的 pinia。 
  context.app // 用 `createApp()` 创建的当前应用(仅 Vue 3)。
  context.store // 该插件想扩展的 store
  context.options // 定义传给 `defineStore()` 的 store 的可选对象。
  // ...
}

OptionAPI 的 pinia

上面的例子都是用 setup 里的 composationAPI 写的 store,当然我们还可以写成和 vuex 差不多的 optionAPI 类型的 store

const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

const useUserStore = defineStore('user', {
  // ...
})

在组件中也可以使用 mapState,mapActions 来访问 store

import { mapStores, mapState, mapActions } from 'pinia'
export default defineComponent({
  computed: {
    // 其他计算属性
    // ...
    // 允许访问 this.counterStore 和 this.userStore
    ...mapStores(useCounterStore, useUserStore)
    // 允许读取 this.count 和 this.double
    ...mapState(useCounterStore, ['count', 'double']),
  },
  methods: {
    // 允许读取 this.increment()
    ...mapActions(useCounterStore, ['increment']),
  },
})

vue-router

运行命令 npm install vue-router@4 安装 vue-router 后,
使用 createRouter 创建一个 router,和 createWebHistory 使用 history 模式。
通常,router 是一个位于 router 目录下的一个 js 文件,文件会导出一个 router

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (About.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import('../views/AboutView.vue')
    }
  ]
})
export default router

然后在 main.js 中引入并 use 即可

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

const app = createApp(App)
app.use(router)
app.mount('#app')

use之后使用 router-link 可以实现路由跳转,使用 router-view 可以看到路由出口

<p>
<!--使用 router-link 组件进行导航 -->
<!--通过传递 `to` 来指定链接 -->
<!--`使用一个 router-link 来创建链接。这使得 Vue Router 可以在不重新加载页面的情况下更改 URL-->
<router-link to="/">Go to Home</router-link>
<router-link to="/about">Go to About</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>

createRouter

createRouter 返回一个 router 以供 app.use 使用,该方法的基本用法如上,其他配置项查看 文档 RouterOptions
例如全局配置一些 route 里的配置项,例如 sensitive 和 strict。全局配置活跃的 router-link 样式 linkActiveClass 和 linkExactActiveClass。

最常用的配置是 scrollBehavior ,也就是全局配置路由跳转后的滚动条行为,例如路由跳转后返回页面顶部。

const router = createRouter({
  scrollBehavior(to, from, savedPosition) {
    if( savedPosition ){
        return savedPosition        //浏览器点击前进后退时上一个页面的位置
    }else{
        return { 
            top: 0,     // 始终滚动到顶部
            behavior: 'smooth',     //平滑滚动
        }
    }
  },
})

routes

在 createRouter 里使用的那个 routes 数组,每一个数组项都是一个对象,对象可以有许多属性:

  • path:用于匹配 url,匹配了就会将 component 的值渲染到路由出口处
  • component:用于指定该 path 匹配后渲染的组件;也可以是一个对象,用于命名视图;也可以是一个函数,用于路由懒加载。
  • name:给该路由规则命名,用于路由跳转对象形式用name来做跳转,keep-alive也用到
  • alias:路由别名,如果匹配到别名也将渲染该对象。值可以是一个字符串或者是一个数组,如有参数看文档
  • sensitive 和 strict:值为 true 则路由匹配区分大小写且能匹配带有或不带有尾部斜线
  • redirect:路由重定向,值可以是字符串,也可以是也可以是一个命名的路由,甚至是一个方法,动态返回重定向目标
  • children:子路由,用于路由嵌套,值与父路由对象一样
  • meta: 路由元数据,通常用来给给路由守卫使用,useRoute 返回的 route 身上的 meta 是所有路由的 meta 的总和,在 matched 里面的数组里每一个对象的 meta 是该路由单独设置的 meta router.beforeEnter(to)=>{console.log(to.meta,to.matched)}
  • props:开启后将 params 参数以 props 传播,可以是布尔值类型、对象类型、函数类型,具体看下面的“props参数篇”
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
      alias: ['/user'],
      sensitive: true,
      strict: true,
      children: [
          {
            path: 'profile',
            component: UserProfile,
            meta: { private: true },
          },
          {
            path: 'posts',
            component: () => import('../views/UserPosts.vue'),
          },
      ],
    },
    {
        path: '/post',
        redirect: {
            name: 'home',
        }
    },
    {
        path: '/search/:searchText',
        redirect: to => { 
            // 方法接收目标路由作为参数 
            // return 重定向的字符串路径/路径对象 
            return { path: '/search', query: { q: to.params.searchText } } 
        },
        props: true,
    },
  ]

router-link

router-link 的值,可以是一个字符串,也可以是一个描述地址的对象,可以见下面编程式导航对描述地址对象的展开说明

<router-link :to="{ 
    name: 'about',
    params: { id: 50 }
}">Go to about</router-link>

可以开启 replace 模式,默认是 push

<router-link to="/about" replace>Go to about</router-link>

router-link 在 url 匹配中时,会默认添加活跃样式,原理为默认在 route-link 逐渐添加一个名为 .router-link-active.router-link-exact-active 的 class,前者是泛匹配,后者是精确严格匹配,此时我们只需要设置该 class 的样式,即可修改 router-link 活跃时的样式

.router-link-active{
    color: xxx;
}
.router-link-exact-active{
    color: xxx;
}

可以通过 activeClass 和 linkExactActiveClass 这两个 Attribute 来设置活跃时的样式名,前者是泛匹配,后者是精确严格匹配

<router-link to="/about" linkExactActiveClass="customRouterLinkActiveClass">Go to about</router-link>

<style>
.customRouterLinkActiveClass{
    background-color: pink;
}
</style>

编程式路由导航

需要使用 useRouter() 的返回值访问整个 router 对象,使用 useRoute() 的返回值访问当前 route。
调用 router.push 或 router.replace 可以实现路由跳转

import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()

function pushWithQuery(query) {
  router.push({
    name: 'search',
    query: {
      xxx
    },
  })
}

push方法的参数可以是一个字符串路径,或者一个描述地址的对象。
对象的参数为:

  • path:要跳转的路径地址
  • name:要跳转到的已命名路由目标
  • params 和 query :动态路由参数和携带的query参数
  • hash:带hash跳转
// 字符串路径
router.push('/users/eduardo')

// 带有路径的对象
router.push({ path: '/users/eduardo' })

// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })

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

// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })

注意:如果提供了 path,params 会被忽略

const username = 'eduardo'
// 我们可以手动建立 url,但我们必须自己处理编码
router.push(`/user/${username}`) // -> /user/eduardo
// 同样
router.push({ path: `/user/${username}` }) // -> /user/eduardo
// 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
router.push({ name: 'user', params: { username } }) // -> /user/eduardo
// `params` 不能与 `path` 一起使用
router.push({ path: '/user', params: { username } }) // -> /user

router 还有 go 方法,接收正负数字代表前进多少步或后退多少步,还有 back 和 forward

params 参数 / 路径参数 / 动态路由

路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 $route.params 的形式暴露出来。

const routes = [
  // 动态字段以冒号开始
  { 
      path: '/users/:id', 
      component: User 
  },
]

现在像 /users/johnny 和 /users/jolyne 这样的 URL 都会映射到同一个路由。
下面的 $route.params.id 和 route.params.id 都会根据 path 的不同获取到 johnny 或 jolyne

<template>
<div>{{ $route.params.id }}</div>
</template>

<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.id)
</script>

一个路由中可以有多个路径参数,嵌套路由也可以获取得到路径参数

route.js声明实际pathroute.params
/users/:username/users/eduardo{ username: ‘eduardo’ }
/users/:username/posts/:postId/users/eduardo/posts/123{ username: ‘eduardo’, postId: ‘123’ }

还可以只在 route.js 中声明一个路径参数,但是匹配到多段:使用正则声明( *(0 个或多个)和 +(1 个或多个)? (0 个或1个)

const routes = [
  // /:chapters ->  匹配 /one, /one/two, /one/two/three, 等
  { path: '/:chapters+' },
  // /:chapters -> 匹配 /, /one, /one/two, /one/two/three, 等
  { path: '/:chapters*' },
]

这样子拿到的params参数是一个数组,而不是一个字符串。在使用命名路由时也需要你传递一个数组

// 给定 { path: '/:chapters*', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// 产生 /
router.resolve({ name: 'chapters', params: { chapters: ['a', 'b'] } }).href
// 产生 /a/b

// 给定 { path: '/:chapters+', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// 抛出错误,因为 `chapters` 为空

还可以使用右括号正则来限定 params 参数的格式,例如下面的例子,title只会拿到数字字母横杠格式的 params

/posts/:title([a-zA-Z0-9-]+)

使用正则可以写出捕获所有路由的动态路由,可以用来做 404 路由

const routes = [
  // 将匹配所有内容并将其放在 `$route.params.pathMatch` 下
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
  // 将匹配以 `/user-` 开头的所有内容,并将其放在 `$route.params.afterUser` 下
  { path: '/user-:afterUser(.*)', component: UserGeneric },
]

匹配的优先级问题,可以使用官方工具 path ranker tool

query 参数

query 参数,是路径上以问号?开头,与号&分割开的参数,例如

/post?category=fontend&limit=10

或者路由导航对象式导航携带的query参数

router.push({ path: '/register', query: { plan: 'private' } })

我们可以通过模板的 $route.query 和 route.query 拿到参数

<template>
<div> {{ $route.query }} </div>
</template>

<script>
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.query)
</script>

props 参数

一些公用组件如果想使用路由 params 参数,需要绑定 vue-router 提供的一些方法,这限制了组件的灵活性。我们可以通过路由配置 props 来解决。

const routes = [{ 
    path: '/user/:id', 
    component: User, 
    props: true 
}]

值如果是布尔值,表示开启 params 参数以 props 方式传到组件。如果使用的是命名视图,需要为每个命名视图开启 props 模式

const routes = [
  {
    path: '/user/:id',
    components: { default: User, sidebar: Sidebar },
    props: { default: true, sidebar: false }
  }
]

值还可以是对象形式,用于通过路由传递静态 props

const routes = [
  {
    path: '/promotion/from-newsletter',
    component: Promotion,
    props: { newsletterPopup: false }
  }
]

值还可以是一个函数,函数可以读取到 route,用于静态props与路由参数相结合。返回值就是传递的 props

const routes = [
  {
    path: '/search',
    component: SearchUser,
    props: route => ({ query: route.query.q })
  }
]

嵌套路由

如果路由出口的组件中还有 router-view ,那么路由配置中的 children 会继续渲染

这是 User 组件
<template>
<div class="user">
    <h2>User {{ $route.params.id }}</h2>
    <router-view></router-view>
</div>
</template>

routes 里的 children 匹配成功将会在 user 的 router-view 中渲染

const routes = [
  {
    path: '/user/:id',
    component: User,
    children: [
      {
        // 当 /user/:id/profile 匹配成功
        // UserProfile 将被渲染到 User 的 <router-view> 内部
        path: 'profile',
        component: UserProfile,
      },
      {
        // 当 /user/:id/posts 匹配成功
        // UserPosts 将被渲染到 User 的 <router-view> 内部
        path: 'posts',
        component: UserPosts,
      },
    ],
  },
]

路由组件不刷新以及 route的响应式

上面 params 参数的例子,当用户从 /users/johnny 导航到 /users/jolyne 时,相同的组件实例将被重复使用。这意味着组件的生命周期钩子不会被调用。

想要在 params 参数修改时进行一些副作用,可以使用 watch。
route 对象是一个响应式对象,所以它的任何属性都可以被监听,但你应该避免监听整个 route 对象。在大多数情况下,你应该直接监听你期望改变的参数。

import { useRoute } from 'vue-router'
import { ref, watch } from 'vue'

const route = useRoute()
const userData = ref()

// 当参数更改时获取用户信息
watch(
  () => route.params.id,
  async newId => {
    userData.value = await fetchUser(newId)
  }
)

当然我们还可以通过 beforeRouteUpdate 路由守卫钩子对路由变化作出响应

async beforeRouteUpdate(to, from) {
    // 对路由变化做出响应...
    this.userData = await fetchUser(to.params.id)
}

命名视图

使用命名视图可以使得一个组件内同时使用多个路由出口

<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>

这样子使用,需要 createRouter 里的 routes 的 component 写为对象形式

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: '/',
      components: {
        default: Home,
        // LeftSidebar: LeftSidebar 的缩写
        LeftSidebar,
        // 它们与 `<router-view>` 上的 `name` 属性匹配
        RightSidebar,
      },
    },
  ],
})

命名视图还可以在嵌套视图中使用

动态路由

使用 addRoute 可以动态添加路由,参数同 routes 里的数组每一项的对象一样。
删除路由可以使用 removeRoute 方法,或者调用 addRoute 的返回值

import { useRouter } from 'vue-router'
const router = useRouter()
const removeblogs = router.addRoute({
    path: '/blogs',
    name: 'blogs',
    component: BlogListPage
})

removeblogs()
//或者
router.removeRoute('blogs')

使用 router.getRoutes() 可以查看现在的路由列表

import { useRouter } from 'vue-router'
const router = useRouter(
router.getRoutes()

路由守卫

在学习路由守卫时,先大致看一下所有路由守卫的执行顺序:

  1. 导航被触发。
  2. 在失活的组件里调用 onbeforeRouteLeave 守卫。
  3. 调用全局的 router.beforeEach() 守卫。
  4. 在重用的组件里调用 onbeforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
    7. 在被激活的组件里调用 beforeRouteEnter
  7. 调用全局的 router.beforeResolve() 守卫(2.5+)。
  8. 导航被确认。
  9. 调用全局的 router.afterEach() 钩子。
  10. 触发 DOM 更新。
    12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

其中3个 router. 开头的全局守卫,1个路由独享守卫(带下划的那个),3个组件内守卫
执行顺序为:
onbeforeRouteLeave
beforeEach
onbeforeRouteUpdate
beforeEnter
beforeRouteEnter
beforeResolve
afterEach

全局守卫

全局守卫会在所有路由跳转间触发,写法是往 createRouter 方法返回的 router 上挂载全局守卫方法

const router = createRouter({ ... })

router.beforeEach((to, from) => {
  // ...
  // 返回 false 以取消导航
  return false
})
全局前置守卫
router.beforeEach((to, from) => {
  // ...
  // 返回值决定要做的事
  return XXX
})

全局前置守卫接收一个回调函数作为参数,该回调函数的返回值决定路由守卫要做的事。
返回值可以是:

  1. false:取消当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
  2. 一个字符串路由地址或一个描述地址的对象。
router.beforeEach((to, from) => {
  // ...
  return false;
  //或者
  return '/login';
  //或者
  return { name: 'Login', params: { id:188 } }
})

该回调函数接收两个参数,分别是 to(即将要进入的目标) 和 from(当前导航正要离开的路由),这个两个参数长得和 useRoute 的返回值是一样的,然后我们可以使用这两个参数配合 return 来让我们处理路由跳转

router.beforeEach((to, from) => {
  console.log(to)
  console.log(from)
  
  //如果跳转的是需要登录的页面
  if(to.path.startsWith('/manage')){
      if (
         // 检查用户是否已登录
         !isAuthenticated &&
         // ❗️ 避免无限重定向
         to.name !== 'Login'
       ) {
         // 将用户重定向到登录页面
         return { name: 'Login' }
       }
   }
})
全局解析守卫

全局解析守卫会在 导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用。
官方的例子为:如果该路径的meta上写着需要 requiresCamera,则调用 askForCameraPermission 接口。router.beforeResolve 是获取数据或执行任何其他操作的理想位置。

router.beforeResolve(async to => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission()
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... 处理错误,然后取消导航
        return false
      } else {
        // 意料之外的错误,取消导航并把错误传给全局处理器
        throw error
      }
    }
  }
})
全局后置钩子

全局后置钩子对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。
例如在 url 实际变化后,对 document.title 作出修改

router.afterEach((to, from) => {
  document.title(to.name)
})

路由独享守卫

beforeEnter 守卫 只在进入路由时触发,不会在 params、query 或 hash 改变时触发。它们只有在 从一个不同的 路由导航时,才会被触发。

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]

参数还可以是多个回调组成的数组,可以查看官方用独享守卫重置全局守卫的案例

组件内守卫

compositionAPI 只有两个组件内守卫,params 参数变更时和组件离开时

import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'
  
const userData = ref()
// 与 beforeRouteUpdate 相同,无法访问 `this`
onBeforeRouteUpdate(async (to, from) => {
  //仅当 id 更改时才获取用户,例如仅 query 或 hash 值已更改
  if (to.params.id !== from.params.id) {
    userData.value = await fetchUser(to.params.id)
  }
})

// 与 beforeRouteLeave 相同,无法访问 `this`
onBeforeRouteLeave((to, from) => {
  const answer = window.confirm(
    'Do you really want to leave? you have unsaved changes!'
  )
  // 取消导航并停留在同一页面上
  if (!answer) return false
})

beforeRouterEnter 需要写在 defineOption 里面

defineOptions({
  name: '***',
  beforeRouteEnter(_to, _from, next) {
    next((vm) => {
      const instance = vm as IInstance
      instance.getData() // 刷新列表数据(不缓存)
    })
  }
})

// 获取表格数据(示例方法)
const listData = ref([])
const getData = async () => {
  listData.value = []
}

// * beforeRouteEnter中要用到的方法,需要暴露出来
defineExpose({ getData })

vue-router 的 optionAPI

  • optionAPI 依然可以通过访问 this 上的 this.$routerthis.$route
  • 想要 watch 到 route 参数的变化,也可以通过
watch: {
    "$route.params": funtion(){
        //副作用...
    }
}
  • 组件内路由守卫,有多出一个 beforeRouteEnter
  beforeRouteEnter(to, from) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 `this` !
    // 因为当守卫执行时,组件实例还没被创建!
  },
  beforeRouteUpdate(to, from) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
    // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from) {
    // 在导航离开渲染该组件的对应路由时调用
    // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
  },

拓展 router-link 封装 router-link 组件

router-link 组件会通过 slot 向外部透露以下五个属性

  1. isActive:是否为当前活跃导航,布尔值
  2. isExcactActive:是否为精确匹配当前活跃导航
  3. href:转化后的超链接字符串
  4. route:路由对象
  5. navigate:一个函数,调用就能触发导航,比如给 slot 元素的 click 调用
<script setup>
import { RouterLink } from "vue-router";
const handleView = (value) => {
  console.log(value, RouterLink.props);
};
</script>
<template>
  <router-link custom v-slot="slotProps" v-bind="$props" to="/">
    <a @click="handleView(slotProps)">点我试试</a>
  </router-link>
</template>

给 router-link 套上 custom 属性以及使用以上5个 slot 属性。并且给 router-link 标签绑定 $props 用于给组件内逻辑使用。
即可拓展封装一个自己的 router-link 组件。
在 compositionAPI 中还可以使用 useLink 来拿到 router-link 暴露出来的属性

<script setup>
import { useLink, RouterLink } from "vue-router"
const { navigate } = useLink(RouterLink.props)
</script>

<template>
  <router-link custom v-slot="slotProps" v-bind="$props" to="/">
    <a @click="handleView(slotProps)">点我试试</a>
  </router-link>
</template>

路由过渡动效

router-view 组件会通过 slot 向外部透露两个属性,一个是 component 即该 router-view 渲染的组件,一个是 route 即目前 route 对象。
通过拿取这个 component 配合 Transition 动效组件和 component 动态组件实现路由组件的转场动画,通过搭配 meta 可以实现不同的动效

<router-view v-slot="{ Component, route }">
  <transition name="fade">
    <component :is="Component" />
  </transition>
</router-view>

路由错误处理

路由导航错误有三种形式:

  1. aborted:导航被中断,例如导航首位阻止了路由的跳转
  2. cancelled:导航取消,例如用户多次点击导致路由跳转过程中,又有了新的路由跳转
  3. duplicated:导航重复,当前页面已是要跳转的路由

this.$router.push('/') 方法是一个异步方法,内部会返回一个 promise,一个响应对象,可以使用 isNavigationFailure 判断这个 promise 是不是一个错误,通过 NavigationFailureType 可以判断它是哪一种错误

import { useRouter, isNavigationFailure } from 'vue-router'
const router = useRouter()
const failure = router.push('/')
console.log(failure)        // Error:xxxxx
console.log(isNavigationFailure(failure))       //true
console.log(isNavigationFailure(failure, NavigationFailureType.aborted))        //判断是不是 aborted 类型的,布尔值
console.log(failure.to, failure.from)       //类似 route 的对象,可以根据这个对象里的值进行一些逻辑操作

重定向也是一种跳转状态,但不算是错误,
可以通过 router.currentRoute.value 获取到重定向的路由对象,里面有个 redirectedFrom 属性可以看到是从哪里重定向过来的

import { useRouter } from 'vue-router'
const router = useRouter()
console.log(router.currentRoute.value)

CSS

官方文档单文件组件CSS功能
正常绑定的css都是静态的,我们经常需要用到变化的css,我们就可以通过变化html标签的style属性和class属性来操作css变化

绑定style样式

注意:使用style绑定样式,key值要用小驼峰写法

  1. 对象写
<div class="basic" :style="styleObj" @click="changeMood"></div>
<script>
    const Counter = {
        data(){
            return {
                styleObj:{
                    backgroundColor:'green',
                    fontSize:'40px'
                }
            }
        }
    }
app = Vue.createApp(Counter).mount('#app')
</script>
  1. 数组写法
    :style="[a,b]"其中a、b是样式对象

  2. 绑定计算属性,例如一些操作使得样式会变化

data(){
    return {
        fontSize: 16;
    }
}
computed:{
    pStyle(){
        return {
            color: 'white',
            fontSize: this.fontSize + 'px'
        }
    }
}

此时style绑定的是这个计算属性

<p :style="pStyle"></p>

绑定class样式

使用 v-bind 绑定的 class 会跟静态的 class 共存而不会覆盖

  1. 字符串写法:适用于:绑定单一css类名,且类名会动态变化
<div class="basic" :class="mood" @click="changeMood"></div>
<script>
    const Counter = {
        data(){
            return {
                mood: 'style1'
            }
        }
        methods:{
            changeMood(){
                this.mood = 'style2'
            }
        }
    }
app = Vue.createApp(Counter).mount('#app')
</script>
  1. 数组写法,适用于:要绑定的样式个数不确定、名字也不确定。因为可以使用数组的方法删除或添加
<div class="basic" :class="classArr" @click="changeMood"></div>
<script>
    const Counter = {
        data(){
            return {
                classArr:['style1','style2','style3']
            }
        }
        methods:{
            changeMood(){
                this.classArr.shift()
            }
        }
    }
app = Vue.createApp(Counter).mount('#app')
</script>
  1. 对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用
<div class="basic" :class="classObj" @click="changeMood"></div>
<script>
    const Counter = {
        data(){
            return {
                classObj:{
                    style1: true,
                    style2: true
                }
            }
        }
        methods:{
            changeMood(){
                this.classObj.style1 = false
            }
        }
    }
app = Vue.createApp(Counter).mount('#app')
</script>
  1. 数组 + 对象:两者可以混合使用
<div class="basic" :class="[textClass, stateClass, {hide: isHidden}]" ></div>
<script>
    const Counter = {
        data(){
            return {
                textClass: "text-blue",
                stateClass: "danger",
                isHidden: false
            }
        }
    }
app = Vue.createApp(Counter).mount('#app')
</script>

组件作用域 CSS scope

<style scoped>
.example {
  color: red;
}
</style>

<template>
  <div class="example">hi</div>
</template>

可以混合使用局部与全局样式

<style>
/* 全局样式 */
</style>

<style scoped>
/* 局部样式 */
</style>

样式穿透:处于 scoped 样式中的选择器如果想要影响到子组件的其他元素,可以使用 :deep() 这个伪类:

<style scoped>
.a :deep(.b) {
  /* ... */
}
</style>

样式扩散:处于 scoped 中的样式想要作用到全局,可以使用可以使用 :global 伪类,全局选择器

<style scoped>
:global(.red) {
  color: red;
}
</style>

插槽选择器:默认情况下,作用域样式不会影响到 渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。使用 :slotted 伪类以明确地将插槽内容作为选择器的目标:(这个仅作了解即可,很少用到,因为我们一般不知道父组件会传什么内容到 slot 里)

<style scoped>
:slotted(div) {
  color: red;
}
</style>

CSS 中的 v-bind()

在 vue3 中我们可以在 style 标签里绑定响应式数据了

<template>
  <div class="text">hello</div>
</template>

<script>
export default {
  data() {
    return {
      color: 'red'
    }
  }
}
</script>

<style>
.text {
  color: v-bind(color);
}
</style>

这个语法同样也适用于

<script setup>
const theme = {
  color: 'red'
}
</script>

<template>
  <p>hello</p>
</template>

<style scoped>
p {
  color: v-bind('theme.color');
}
</style>

实际的值会被编译成哈希化的 CSS 自定义属性,因此 CSS 本身仍然是静态的。自定义属性会通过内联样式的方式应用到组件的根元素上,并且在源值变更的时候响应式地更新。

css modules

在 vue 中也支持导入一个 css 文件将其内容作用于整个 vue 文件,不同于 react 的是,不能单独指定哪一个类名生效

imort './style';

export default {
    ...
}

取而代之的是:一个

<template>
  <p :class="$style.red">This should be red</p>
</template>

<style module>
.red {
  color: red;
}
</style>

less 和 scss

在 style 标签中加入 lang 属性即可使用 less 和 scss 语法

<style lang="scss">
  $primary-color: #333;
  body {
    color: $primary-color;
  }
</style>

vue 动画技巧

比如利用 v-bind 来控制 animation 的速度

<div class="box"></div>
<input type="text" v-model="duration" />

<script setup>
import { ref } from "vue";
const duration = ref(10);
</script>

<style>
.box {
  width: 100px;
  height: 100px;
  background: blue;
  animation: rotate v-bind(duration + "s") linear infinite;
}
</style>

官网还有个利用 ref 修改 style 属性,并利用 transition 实现动画的例子,点击标题查看。

手动通过 class 等进行动画设置,只能设置存在页面上的 dom 节点,对于那些需要出现和隐藏的元素,隐藏时的动画不会出现,比如 v-if="false"v-show="false" 时,离场添加的动画或 class 都不生效,这个时候就需要用到 组件了

组件

Transition 组件会在包裹在里面的组件进入或卸载的时候往内组件添加一些特定的类名以实现进场离场动画
使用 Transition 组件需要在元素外面包裹

<button @click="show = !show">Toggle</button>
<Transition>
  <p v-if="show">hello</p>
</Transition>

然后在 style 标签里书写特定名字的 class,在这些 class 里面编写要执行的动画

.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}

一般我们会在 xxx-from 和 xxx-to 中编写初始样式和结束样式,在 active 中编写 transition 或 animation
这个标签的动画只有在下面 4 种中情况触发

  1. 由 v-if 所触发的切换
  2. 由 v-show 所触发的切换
  3. 由特殊元素 切换的动态组件
  4. 改变特殊的 key 属性

Transition 还支持设置一个 name 属性以自定义类名
Transiton 支持设置 mode 属性以处理多个元素动画执行顺序的问题,比如上一个元素没有完全离开下一个就进入了
Transition 搭配第三方动画库执行动画
Transition 搭配 JavaScript 钩子完成更复杂的动画

TransitionGroup

是一个内置组件,用于对 v-for 列表中的元素或组件的插入、移除和顺序改变添加动画效果。

<TransitionGroup name="list" tag="ul">
  <li v-for="item in items" :key="item">
    {{ item }}
  </li>
</TransitionGroup>
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值