关于vue的绝大部分知识点都在前端面试—vue部分 | 三叶的博客这篇文章中讲了,这里就说说基础语法

关于vue的绝大部分知识点都在前端面试—vue部分 | 三叶的博客这篇文章中讲了,这里就说说基础语法

Vue2

插值表达式

利用表达式进行插值,渲染数据到页面中

1
2
3
<h3>{{ title }}</h3>
<p>{{nickname.toUppercase()}}</p>
<p>{{age >= 18 ?'成年’:'未成年'}}</p>

注意:不能写到标签内部,表达式涉及到的数据必须存在。

指令

带有v-前缀的特殊标签属性,Vue会根据不同的【指令】,针对标签实现不同的【功能】

v-html

设置元素的innerHTML

v-if/v-show

用来控制元素显示隐藏。

v-show=”表达式”,表达式值true显示, false隐藏

v-if=”表达式”,表达式值true显示, false隐藏

二者的区别可以参考:前端面试—vue部分 | 三叶的博客

v-else/v-else-if

配合v-if使用,v-show没有这个待遇,语法和C语言的if-else语法差不多。

v-on

注册事件。免去了手动捕获元素添加事件监听的工作,只需专心书写回调逻辑,极大地简化了事件监听的代码。

1
2
3
v-on:事件名 = "内联语句"//js代码
v-on:事件名 = "methods中的函数名"
@事件名="" //简写

v-bind

动态的设置标签属性src url title …

1
2
v-bind:属性名 = "表达式"
:属性名="表达式" //简写

动态样式控制

借助v-bind指令可以很方便的实现动态控制样式

  • :class = "数组/对象"

    1
    <div class="box" :class="[类名1,类名2,类名3 ]"></div>//添加到数组中表示这个类名生效
    1
    <div class="box" :class="{类名1:布尔值,类名2:布尔值}"></div>//布尔值为true表示添加这个类
  • :style ="样式对象"

    1
    2
    3
    4
    //属性值可以是表达式
    <div class="box" :style="{CSS属性名1:CSS属性值,CSS属性名2:CSS属性值} "></div>
    //原生行内写法,属性值不能是表达式,是写死的,用分号分隔样式
    <div class="box" style="dispaly:none;width:100px;"></div>

v-for

基于数据循环,多次渲染整个元素,哪个标签需要多次渲染就加到哪个元素身上。

1
2
3
<li v-for="(value, key, index) in obj">
Key: {{ key }}, Value: {{ value }}, Index: {{ index }}
</li>//只有一个变量就是value,两个第二个就是key,三个第三个就是index

关于给标签添加key的作用,还有v-ifv-for优先级问题以及能否一起使用的问题,参考前端面试—vue部分 | 三叶的博客

为什么 v-for指令中的参数比如(value,key),能被该标签内的其他属性使用,还能在标签体内使用?

模板作用域:Vue 的模板语法中,标签的属性和内容共享同一个作用域

参数注入v-for 中定义的参数(如 valuekey)会被自动注入到当前标签及其子节点的作用域中。这意味着,只要是在这个作用域内的代码(标签属性、子元素、插值表达式等),都可以访问这些参数。

v-model

给表单元素使用,双向数据绑定,可以快速获取或设置表单元素内容。

1
<input type="text" v-model="a">

本质上是一个语法糖:

1
<input type="text" :value="a" @input="(e)=>{a = e.target.value}">

应用于其他表单元素:

  • checkbox:绑定一个布尔值变量,true表示选中

    1
    <input type="checkbox" :checked="a" @input="(e)=>{a = e.target.checked}">
  • radio

    给同一组radio绑定相同的变量,点击哪个就会把哪个的value赋值给变量

    1
    2
    <input v-model="gender" type="radio" name="gender" value="1">
    <input v-model="gender" type="radio" name="gender" value="2">

sync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 父组件 -->
<template>
<ChildComponent :message.sync="parentMessage"/>
</template>

<!-- 子组件 -->
<script>
export default {
// ...
props:['message']
methods: {
updateMessage() {
this.$emit('update:message', newValue);
}
}
}
</script>

sync修饰符,通常与v-bind指令一起使用,用来简化子组件向父组件通信的代码,就是不需要手动在组件上添加自定义事件,也不需要书写对应的回调逻辑

而是使用提供的默认事件,就能实现子组件向父组件传递数据,从而实现简化父子组件通信,

简单的来说,在vue2中,v-bind加上sync修饰符仿佛就实现了组件自己v-model指令

因为在vue2中直接给组件使用v-model,就意味着只能传入value属性,监听的事件只能是input,而借助sync,就能指定多个传入子组件的属性和对应的事件。所以说syncv-bind就是用来解决vue2中,v-model应用在组件上功能不足的问题。

在 Vue 3 中,.sync 修饰符已经被移除,推荐的做法是使用自定义事件(就是手动给组件标签添加事件监听并传入事件回调)v-model 来达到类似的效果。

vue 3 对 v-model 进行了增强,使其更加灵活,允许在一个组件上使用多个 v-model 绑定,并且可以自定义绑定的propevent 名称。

默认情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*父组件*/
<template>
<ChildComponent v-model="message" />
/*等价于: <ChildComponent :modelValue="message" @update:modelValue="(val)=>{message=val}" />*/
/*如果在vue2中就等价于*/
/* <ChildComponent :value="message" @input="(val)=>{message=val}" />*/
/*val是子组件传递过来的值*/
</template>

<script>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
components: { ChildComponent },
setup() {
const message = ref('Hello from parent');
return { message };
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*子组件内*/
<template>
/*回调函数直接书写行内代码,其中$event指向原生事件对象*/
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
/*等价于<input :value="modelValue" @input="(e)=>{$emit('update:modelValue',e.target.value)}" />*/
</template>

<script>
export default {
props: {
modelValue: String // 默认情况下,v-model 在组件内使用的 prop 名称为 modelValue
}
}
</script>
  • $emitthis.$emit的简写,this指向当前组件实例,在模板中可以省略

  • $event是在模板中事件处理器中使用的特殊变量,用于引用原生的事件对象

    而在原生dom中,可以直接使用event代替原生事件对象

    1
    <a href="/about" onclick="event.preventDefault()" target="_blank">about</a>
  • <input @input="logValue($event)"><input @input="logValue">这两种写法效果都是一样的

  • 要注意的是,给组件添加事件监听和给dom元素添加事件监听,都传入一个回调函数,但不同的是,Vue 组件中的事件是自定义事件,不是浏览器原生的 DOM 事件,没有与之相关的 event(事件) 对象,对于组件的自定义事件的回调函数,传入的第一个参数其实是子组件通过emit传递过来的值,后者传入的则是事件对象event

Vue 3 支持在一个组件上使用多个 v-model 绑定。这可以通过指定自定义的 prop 和 event 名称来实现

在vue2中的:message.sync等价于vue3中的v-model:message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*父组件*/
<template>
<ChildComponent v-model:title="pageTitle" v-model:description="pageDescription" />
</template>

<script>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
components: { ChildComponent },
setup() {
const pageTitle = ref('Default Title');
const pageDescription = ref('Default Description');
return { pageTitle, pageDescription };
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*子组件*/
<template>
<div>
/*因为我们要做的不是直接修改title的值,所以不能使用v-model*/
<input :value="title" @input="$emit('update:title', $event.target.value)">
<textarea :value="description" @input="$emit('update:description', $event.target.value)"></textarea>
</div>
</template>

<script>
export default {
props: {
title: String,
description: String
}
}
</script>

指令修饰符

参考前端面试—vue部分 | 三叶的博客

计算属性computed

  • 基于现有的数据,计算出来的新属性。依赖的数据变化,自动重新计算。

  • 使用起来和普通数据一样: {{计算属性名}}

简写:

1
2
3
4
5
6
computed: {
计算属性名(){
//基于现有数据,编写求值逻辑
return 结果
}
}

完整写法:

1
2
3
4
5
6
7
8
9
10
11
computed: {
计算属性名:{
get() {
//一段代码逻辑(计算逻辑)
return 结果
},
set(修改的值){
//一段代码逻辑(修改逻辑)
}
}
}
  • 计算属性一般只用来展示,赋值。如果尝试直接修改计算属性,并不会生效,因为计算属性的值只与其相关的数据有关,但是会把传入的值传递到set函数,set函数拿到这个值可以做一些操作。

  • 依赖的数据必须是响应式的(即 data 或其他计算属性),不然依赖的数据改变了计算属性也无法发觉,就不会即时更新。

  • 当计算属性依赖的任何数据发生变化时,Vue 会标记计算属性为,并在下次访问时重新计算其值。计算属性采用惰性求值策略(被访问的时候再求值),并具有缓存机制(如果依赖的数据未改变,直接使用缓存,而不需要重新计算),只有当依赖的数据发生变化,即被标记为,并被访问的时候,才会重新计算其值。

  • 同时计算属性也是响应式的,当计算属性的值改变,也会通知计算属性的依赖更新。

  • 因为计算属性和data的用法是一样的,都属于响应式数据,它们收集依赖的方式也是一样的,都是被访问的时候,通过getter方法收集。

  • 计算属性在首次计算其值时,通过访问响应式数据,触发响应式数据的 getter 方法,成功收集计算属性为它们的依赖。

监听器watch

用来监视data计算属性中数据的变化。

简写:

1
2
3
4
5
6
7
8
9
10
watch:{
//检测words数据变化
words(newValue,oldvalue) {
console.log('变化了',newValue,oldvalue)
}
//检测obj中的words数据变化
"obj.words"(newValue,oldvalue){
console.log('变化了',newValue,oldvalue)
}
}//和计算属性配置一样,都是直接放函数

完整写法:

1
2
3
4
5
6
7
8
9
watch: {
数据属性名:{
immediate: true//初始化立刻执行一次handler方法
deep: true,//深度监视
handler(newValue,oldValue){
console.log(newValue)
}
}
}

deep: true,开启对复杂类型深度监视后,可以监听一整个对象,可以监听这个对象中的全部属性,否则监听的只是对象的地址变化。

通过vue实例的$watch方法也能添加监听

1
2
vue.$watch('监听的数据',{//配置对象})//完整写法
vue.$watch('监听的数据',function(newValue,oldValue){}) //简写

watch监听的数据必须是响应式的,然而在vue2中数据默认都是响应式的。

watch可以理解为用来自定义扩充某个响应式数据的setter方法体。

watch和computed的区别与联系

  • watch,computed和视图,这三者都依赖响应式数据,是响应式数据的三大订阅者,而响应式数据又包括data中的数据和计算属性。

  • 在watch里可以写异步操作,但是在computed内部不适合写异步操作,比如无法异步返回值。

    1
    2
    3
    4
    5
    6
    7
    {
    computed:{
    sum(){
    setTimeout(()=>{return this.price*this.num},1000)
    }
    }
    }
  • computed能做的,用watch也能实现,就是更为麻烦;但是watch能做的,computed不一定能实现,比如延迟几秒更新数据。

method和computed的区别与联系

我们讨论的是有返回值的方法computed的区别于联系

  • 当某个数据属性的值发生变化时,Vue 会自动检测到这个变化,并重新计算依赖于这个数据属性的任何方法,对于计算属性则是标记为’脏’
  • 计算属性本质就是一个有返回值的方法
  • 计算属性存在缓存机制,无论被使用多少次,只要依赖的数据未改变,计算属性函数只会被调用一次;而方法被使用多少次就会被调用多少次
  • method在模板内使用需要显式的调用,而computed直接当成data的使用。

MVVM

model-view-viewmodel

model

层负责管理应用程序的业务逻辑和数据,不包含任何的ui逻辑,类似小程序中的逻辑层

view

View 负责显示数据给用户;是应用程序直接与用户直接交互的部分,类似小程序中的视图层

viewmodel

ViewModel 作为 Model 和 View 之间的桥梁,负责将数据转换成view可以使用的格式。在vue中,这个桥梁(vm)就是vue实例。

Vue3

setup()

  • vue3中的一个新的配置项,值为一个函数,组合式api都写在这里面。

  • 执行时机在beforeCreate()之前;而beforeCreate的执行时机又再data函数被调用前。

  • setup中不能使用this,this的值是undefined

  • setup不能是async函数,因为async函数的返回值会被Promise.resolve包装

在组合式api中的写法

组合式api中,setup()以下面的方式书写,格式类似生命周期函数。

setup中准备的数据函数 需要在setup最后return后,才能在模板中应用 。

配置了setup,还能配置datamethods,同时在methods里面也能读取到setup中的配置,当data中和setup中存在数据冲突,setup中的数据优先级更高,但是还是建议vue2的配置和vue3的配置不要混用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
export default{
setup(){
const message = 'hello Vue3'
const logMessage = () =>{console.log(message)}
return {message,logMessage}
}
data(){
return {name:'tom'}
}
methods:{
test(){
console.log(this.message)
console.log(this.logMessage)//都可以访问到
}
}
beforeCreate() {
console.log('beforeCreate函数')
}
}
</script>

其实还能返回一个渲染函数,不过用的很少。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
import {h} from 'vue'//在vue2中我们也接触过的渲染函数。
export default{
setup(){
const message = 'hello Vue3'
const logMessage = () =>{console.log(message)}
return () => h('你好啊')
}
beforeCreate() {
console.log('beforeCreate函数')
}
}
</script>

然后这个组件就会使用这个函数来渲染,而忽略模板结构

语法糖写法

1
2
3
4
5
//独占一个script标签,不需要return
<script setup>
const message = 'this is a message'
const logMessage = ()=>{console.log(message)}
</script>

setup的参数

setup的参数在混合选项式api的时候,也就当不使用setup语法糖开发的时候是有意义的,使用语法糖开发的时候参数都没了。

setup(props,context)

props

是第一个参数,值为对象,包含组件外部传入组件的,且在组件内部接收的值,也就是在props属性中接受的值。

这个参数的作用就在于,让通过选项式api中的props属性接收的值,能够在setup函数内部使用。

这些属性是响应式的,因此当父组件更新 props 时,子组件中的 props 也会自动更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template)
<h1>一个人的信息</h1>
<h2>姓名:{{person.name}}</h2>
<h2>年龄:{{person.age}}</h2>
</template>
<script>
import { reactive } from 'vue
export default {
name: 'Demo'
props:['msg','school'],
setup(props){
console.log('---setup---', props)//输出Proxy{msg:'你好啊',school:'南昌大学'}
//数据
let person = reactive({
name:'张三'
age:18
})
//返回一个对象(常用)
return {person}
}
}
</script>

context

第二个参数,上下文对象,包含多个属性

  • attrs:值为对象,包含组件外部传入组件的,但在组件内部未被接收的值,因为这些值会被当成组件的属性,所以存储context.attrs中,类似vue2中的this.$attrs(用来获取组件实例的属性)

  • emit:值为函数,相当于this.$emit,用来触发自定义事件

    要注意的是,在vue3中,给组件添加的事件监听,默认都是原生事件,如果需要指定为自定义事件,需要在子组件中通过emits属性声明,或者使用组合式api中的defineEmits中指定(返回emit函数)

    1
    context.emit('hello',666)
  • slots:收到的插槽内容,类似vue2中的this.$slots,可以访问到父组件通过插槽传递的所有内容。this.$slots 是一个对象,其键名对应于插槽的名字(对于默认插槽,键名为 default),值则是包含一组 VNode 的数组。

简单的来说,context参数弥补了在setup函数内部因为不能使用this,而缺失的部分功能,比如this.$emit可以被替换为context.emitthis.$slot可以被替换为context.slots

reactive和ref

vue3中数据默认不是响应式的,需要手动添加响应式。

reactive

  • 接受对象类型数据的参数,并返回一个响应式的对象,就是Proxy类型的对象。
  • 传入一个源对象,经过proxy操作返回一个代理对象,修改代理对象会映射到源对象。
1
2
3
4
5
<script setup>
import { reactive } from 'vue'
//执行函数传入参数变量接收
const state = reactive(对象类型数据)
</script>

源码分析:

修改代理对象会映射到源对象这一点,在传入的第一个参数:配置对象上就能看出,操作对象一直是target。

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
function reactive(target) {
// 如果目标已经是响应式的,则直接返回
if (isReactive(target)) {
return target;
}
//关于proxy的介绍,可参考本博客中前端面试-vue一文
return new Proxy(target, {
get(target, key, receiver) {
//收集依赖
track(target, key);
const result = Reflect.get(target, key, receiver);
//这说明proxy类型的响应式数据是在系统被取出的时候,才递归的添加响应式的,这种方式叫做懒递归或者按需递归添加响应式
return isObject(result) ? reactive(result) : result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
//通知依赖更新
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
const hadKey = hasOwn(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey) {
//通知依赖更新
trigger(target, key);
}
return result;
}
});
}

ref

  • 接受简单类型或者复杂类型数据,返回一个响应式对象,就是RefImpl类型的对象,如果传入的值是对象,那ref.value的类型就是proxy

  • ref在内部其实会使用reactive(如果传入的是一个对象),总的来说基于proxy实现。

  • RefImpl实例结构分析:

    下面三个属性是并列关系

    • value:不可枚举属性,访问实际是在调用get函数,返回_value中的数据。

    • _value:访问value返回的实际数据

    • _rawValue:源数据,传入ref的原始数据raw的中文意思就是”未经加工的,原始的”;当原始数据是简单类型,_rawValue等于_value,当传入的是对象,_rawValue的值始终是对象,_value的值则是proxy对象,修改_value会映射到_rawValue上。

至于value属性对应的getter,setter,存在于[[prototype]]对象中。

大致源码如下:

1
2
3
function ref(val){
return new RefImpl(val)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class RefImpl {
constructor(value, __v_isShallow) {
//表示是否为浅层响应式,默认为false,默认不是浅层响应式,默认是深层响应式
//如果为true,表示执行浅层响应式,不会递归地将嵌套的对象或数组转换为响应式对象
this.__v_isShallow = __v_isShallow;

//这个属性通常用于依赖收集,在 Vue 的响应式系统中,它帮助追踪哪些地方使用了这个 ref,
//从而在数据变化时能够通知到这些地方进行更新。
//初始值设为 void 0 即 undefined,意味着在初始化时还未开始依赖收集。
this.dep = void 0;
//明确标识该实例是一个 ref 对象,这对于 Vue 内部识别和处理非常关键
this.__v_isRef = true;


//存储原始值(未被包装成响应式的值)。
//如果__v_isShallow为true,则直接使用传入的value
//否则调用toRaw(value),以确保即使是已包装的响应式对象,也能获取到其原始值。
this._rawValue = __v_isShallow ? value : toRaw(value);
//在这行代码可以看出ref和reactive密不可分的关系
//toReactive方法内部很可能会判断value是不是对象,如果是就会调用reactive()包装。由此也可以看出ref和reactive之间的联系
this._value = __v_isShallow ? value : toReactive(value);//默认取第二个值
}
//没想到吧,在类里面还能这样定义方法
get value() {
trackRefValue(this);
//返回的是_value,也就是说其实value取的值就是_value
return this._value;
}
set value(newVal) {
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
newVal = useDirectValue ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
const oldVal = this._rawValue;
//如果给ref.value赋予一个新的对象,则修改_rawValue为这个对象的同时,重新创建一个响应式对象,然后赋值给_value
this._rawValue = newVal;
this._value = useDirectValue ? newVal : toReactive(newVal);
triggerRefValue(this, 4, newVal, oldVal);
}
}
}

我们可以观察到,构造函数中并没有初始化value,但是refImpl实例中出现了,我自己测试了一下,发现并没有借助defineProperty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//自定义一个refImpl类
class refImpl {
constructor(value) {
this._value = value
}
get value() {
return this._value
}
set value(val) {
this._value = val
return 0
}
}
const p = new refImpl(123)

最后打印p的结构如图:

观察到,value属性出现了,这就说明,是通过在原型上挂载get value()set value(val)定义了value属性。

注意

  • 当我们直接给.value赋予一个新的对象,vue会帮我们 重新创建一个响应式对象

    1
    2
    3
    4
    this._rawValue = toRaw(newVal);//如果传入的本身是响应式对象,则获取它的原始值
    //重新创建一个响应式对象,如果传入的本身是响应式对象,那toReactive方法如何处理呢?Vue 会直接使用它,不会重新代理
    //无论赋予value的是ref,reactive包装的对象,还是普通对象,最终this._value的值都是proxy对象
    this._value = useDirectValue ? newVal : toReactive(newVal);
  • 脚本中访问ref数据,需要通过.value,而在template中, .value不需要加(帮我们扒了一层),就好比在vue2的模板内,可以省略this,比如this.$store在模板内直接写为$store,其实data中的数据能直接在模板中使用,也是省略了this。

toRef()和toRefs()

响应式丢失

下面举个例子来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<button @click="txt.num++">num++</button>
<button @click="console.log(++age)">age++</button>
<div>{{ obj }}</div>
<div>{{ txt }}</div>
<div>{{ age }}</div>
</template>

<script setup>
import { reactive, toRef, ref } from 'vue'
const obj = ref({ txt: { num: 1 }, age: 12 })

const txt = obj.value.txt//触发getter
console.log(txt)//输出Proxy(Object) {num: 1}
const age = obj.value.age
console.log(age)//输出12
</script>

我们点击num++,发现视图中的objtxt都被更新了,输出txt发现是个Proxy类型的对象,说明响应式并没有丢失。

我们点击age++,发现视图中的age并没有随之改变,输出age,发现是简单数据类型,说明响应式丢失了。

因此我们可以得出,vue3是递归给对象添加响应式的(其实是需要的时候再递归添加),解构对象并不是所有属性的响应式都会丢失。

虽然我们对refreactive已经比较了解了,为了保险起见,我们将ref替换为reactive再测试一下,果然,现象完全一样。

其实在vue2中,也存在着响应式丢失的现象,我们知道vue2是响应式实现是基于Object.defineProperty方法的,当vue2给一个对象添加响应式,如果这个对象中还包含对象,则也会递归添加响应式。下面是一个例子来说明这一点:

1
2
3
4
5
6
7
8
9
10
11
export default {
name: 'App',
data(){
return {
obj:{a:'tom', child: {b:'cindy'}}
}
},
created(){
console.log(this.obj.a,this.obj.child)
}
}

上述代码会输出'tom'和一个响应式对象child:

toRef()

toRef() 函数可以将一个响应式对象(ref或者reactive包装的对象)的某个属性转换成一个ObjectRefImpl(注意是返回一个新的ObjectRefImpl对象,并不会修改任何对象),从而解决对象解构响应式丢失的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { reactive, toRef, ref } from 'vue';

const a = reactive({
count: 0
});
const b = ref({
price: 20
});

const countRef = toRef(a, 'count');
//const priceRef = toRef(b.value, 'price');这里一定要加value

// 修改countRef 同时会更新a.count
countRef.value++;
console.log(a.count); // 输出: 1
// 修改 a.count 同时会更新countRef
a.count++;
console.log(countRef.value); // 输出: 2

a.countcountRef的共用一份源数据。输出countRef

其中_object参数toRef传入的第一个参数,_key就是toRef传入的第二个参数,当我们访问value的时候,返回的就是_object[_key],当我们修改value的时候,其实就是在修改_object[_key],而_object本身就是一个代理对象。

1
2
3
4
5
6
7
get value() {
const val = this._object[this._key];
return val === void 0 ? this._defaultValue : val;
}
set value(newVal) {
this._object[this._key] = newVal;
}

toRefs()

toRefs() 函数可以将整个响应式对象的所有属性都转换为refs 的集合。这对于解构响应式对象特别有用,因为直接解构会导致某些属性丢失响应性。直接解构就等同于取值然后赋值给新的变量,我们取值取的数据来源就是源对象,而源对象中的部分属性值不具有响应式的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { reactive, toRefs } from 'vue';

const state = reactive({
name: 'Vue',
version: 3
});
//返回一个新的对象,不修改源对象
const stateAsRefs = toRefs(state);
console.log(stateAsRefs)
// 解构赋值,同时保持响应性
const { name, version } = stateAsRefs;

// 修改 name 或 version 同时会更新 state 中对应的属性
name.value = 'Vue.js';
console.log(state.name); // 输出: Vue.js

version.value = 3.0;
console.log(state.version); // 输出: 3.0

可以看出stateAsRefs对象就是一个普通的对象,不过它的每个属性的值都变成了ObjectRefImpl类型的数据。

computed

1
2
3
4
import  { computed } from 'vue'
const a = computed(()=>{
return计算返回的结果
})

注意:

  • vue3中的computed属性默认是响应式的
  • 避免直接修改计算属性的值,计算属性应该是只读的,特殊情况可以配置get,set
  • 计算属性中不应该有副作用,不建议在计算属性中写dom操作和异步请求

与vue2计算属性区别:

  • vue2使用computed不需要导入,是一个配置属性;vue3中的computed是一个函数,需要按需导入。
  • vue2中批量添加计算属性更方便,在vue3中每次获得一个计算属性都要传入一个回调函数,调用一次computed函数

watch

vue3中使用watch监听ref包装的数据,是存在一定问题的。

watch能正常监听通过ref(基本类型数据)得到的对象的改变,比如ref(0);但是监听不到ref(引用类型数据)得到的对象的改变。

比如ref({ age: 0 }),虽然这个问题可以通过开启深度监听解决(在第三个参数传入{ deep: true }),或者监听ref.value(返回一个reactive包装的对象),但是存在新旧数据相同的问题。

watch监听reactive类型的数据默认是深度监听,没有太大的问题,但也存在新旧数据相同的问题。

如果不关心新旧数据的问题,监听reactive包装的数据可以认为是没有问题的,但是监听ref包装的引用数据,就会出现监听不到的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import  { watch } from 'vue'
//语法--监听一个ref对象
watch(ref对象,(newValue, oldValue) =>{ ... })//第一个数据是要监听的数据
//语法--监听多个ref对象
watch(
//通过数组叠加多个监听对象
[count,name],
//newVal和oldVal是数组
(newVal,oldVal)=> {
console.log( 'count或者name变化了',newVal,oldVal)
}
)
//精确监听reactive对象的某个属性
//传入一个回调函数,非常不人性
watch(() =>userInfo.value.age, (newValue,oldValue)=>{ console.log(newValue,oldValue) } )

与vue2中watch的区别

  • vue2中watch是属性,vue3中是需要导入的函数
  • 在vue2的watch中的函数名就是监听的对象,执行的操作是函数体
  • vue2中添加多个监听对象只需要都写在watch:{}中就行,vue3中可以放在数组中整体监听

watchEffect

  • watch 的套路是:既要指明监视的属性,也要指明事件的回调。
  • watchEffect 的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
  • watchEffect有点像computed
    • computed 注重的是计算出来的值(回调函数的返回值),所以必须要写返回值。
    • watchEffect 更注重的是过程(回调函数的函数体),所以不用写返回值。
1
2
3
4
5
6
// watchEffect 所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
watchEffect(() => {
const x1 = sum.value
const x2 = person.age
console.log('watchEffect配置的回调执行了')
})

生命周期函数

在vue3中,既支持选项式生命周期函数,也支持组合式生命周期函数,但是要注意的是,在vue3中即便能使用选项式风格的生命周期函数,也与vue2中的不完全相同。

选项式

  1. beforeCreate:实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
  2. created:实例创建完成后被调用。此时已完成数据观测 (data observer),属性和方法的运算,watch/event 事件回调。但是尚未挂载,$el 属性目前不可见。
  3. beforeMount:在挂载开始之前被调用,相关的 render 函数首次被调用。
  4. mounted:实例挂载到 DOM 后调用,这时 el 被新创建的 vm.$el 替换,并挂载到实例上。注意,不能保证它在整个组件树完全渲染完成时才调用。
  5. beforeUpdate:在数据更新导致虚拟 DOM 重新渲染和打补丁之前调用。你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  6. updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。
  7. beforeUnmount(在 Vue 2 中称为 beforeDestroy):在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
  8. unmounted(在 Vue 2 中称为 destroyed):卸载组件后调用。调用此钩子时,组件实例的所有指令都被解绑,所有事件监听器都被移除,所有子组件实例也都会被销毁。

简单的来说,vue3的选项式生命周期函数与vue2的生命周期函数的区别仅仅在于最后两个。

组合式

在 Vue 3 的组合式 API 中,你可以使用 onXXX 形式的函数,来注册生命周期钩子。这些函数可以直接在 setup() 函数或 <script setup> 中使用。

  • onBeforeMount
  • onMounted
  • onBeforeUpdate
  • onUpdated
  • onBeforeUnmount
  • onUnmounted

Vue 3 中,setup 函数涵盖了 beforeCreatecreated 钩子的功能,因此不再需要这两个钩子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { onMounted, onUpdated, onUnmounted } from 'vue';
export default {
setup() {
// 这里相当于 beforeCreate 和 created 的合并作用域
console.log('Setup function: Initialization logic here.');

onMounted(() => {
console.log('Component is mounted!');
});

onUpdated(() => {
console.log('Component has been updated!');
});

onUnmounted(() => {
console.log('Component has been unmounted.');
});

return {};
}
};

组件通信

props与emit

父传子

父组件给子组件,添加属性的方式传值。

1
2
3
<template>
<sonComVue :message="message" />
</template>

在子组件,通过props接收,借助于编译器宏

1
2
3
4
//使用编译器宏不需要导入
const props = defineProps({
message: String,
})//defineProps返回一个对象,对象内部的数据可以直接在模板内使用,在script标签中则需要通过props(返回的对象)访问

与vue2的区别

  • 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //在vue2中
    export default {
    props: {
    title: String,
    count: {
    type: Number,
    default: 0,
    validator: (v) => v >= 0
    }
    }
    }
    //在vue3中
    <script setup>
    import { defineProps } from 'vue';
    const props = defineProps({
    title: String,
    count: {
    type: Number,
    default: 0,
    validator: (v) => v >= 0
    }
    });
    </script>
  • 响应式处理

    在vue2中,Props 数据通过 Object.defineProperty 实现响应式;在vue3中基于 Proxy 的响应式系统,自动处理 Props 的响应式绑定,支持动态属性监听和更高效的数组更新。

子传父

父组件中给子组件标签通过@绑定事件

1
2
3
4
<template>
<!--绑定自定义事件-->
<sonComVue :message="message" @get-message="getMessage" />
</template>

子组件内部通过emit方法触发事件

1
2
3
4
5
6
7
8
9
<script setup>
const props = defineProps({
message: String,
})
const emit = defineEmits(['get-message'])//传入一个数组,指定哪些事件是自定事件,并返回emit方法
const sendMsg = () => {
emit('get-message', 'this is son msg')
}
</script>

在vue3中,父子组件通信,其实用v-model也能实现,详细介绍参考前文v-model的部分

1
2
3
<template>
<sonComVue v-model:message="message"/>
</template>
1
2
3
4
5
6
7
8
9
<script setup>
const props = defineProps({
message: String,
})
const emit = defineEmits(['update:message'])//指定哪些事件是自定事件,并返回emit方法
const sendMsg = () => {
emit('update:message', 'this is son msg')
}
</script>

与vue2的区别

  • 无法直接通过this调用$emit函数,需要通过编译器宏获得emit函数。
  • 在vue2中可以通过<component @click.native></component>,表示添加的点击事件是原生事件,而不是自定义事件;在vue3中这种写法被移除了,给组件添加的事件默认是原生的。

provide与reject

顶层通过provide提供数据,底层通过inject接收数据,数据传递是单向的

provide

1
2
3
4
5
6
7
8
9
10
import { provide, ref } from 'vue';
export default {
setup() {
const user = ref('John Doe');
provide('user', user); // 提供一个响应式的 user,并把它命名为user
return {
// 返回其他需要暴露给模板的内容
};
}
}

inject

1
2
3
4
5
6
7
8
9
10
import { inject } from 'vue';
export default {
setup() {
//返回值就是祖先组件提供的值,如果没有找到 'user',则使用默认值
const user = inject('user', 'Default User'); //
return {
user
};
}
}

响应式

当使用 provide 提供的数据是响应式的(例如通过 refreactive 创建),那么在数据更新时,所有通过 inject 接收到该数据的组件都会自动更新。这是因为 provide/inject 维护的是对原始数据的引用,我们认为这些组件依赖这些注入的数据。

与vue2的区别与联系

  • vue2中可以通过inject:[]来批量接收数据,vue3中只能一个一个接收。

  • vue2中可以在provide(){ return{//传递的数据,格式,键:值}}中批量传递数据,vue3中只能一个一个的传递数据

  • 和vue3中相同的是,在vue2中通过provide传递的如果是响应式数据,当父组件修改这个数据的时候,注入了此响应式数据的子组件也会更新。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    <template>
    <div>
    num:{{num}}age:{{age}}
    mom:{{ parents.mom }}
    <button @click="add('num')">num++</button>//点击,只有父组件视图改变
    <button @click="add('age')">age++</button>//点击,父子组件视图都改变
    <button @click="add('mom')">mom++</button>//点击,父子组件视图都改变
    //props
    //即便父组件通过props传递给子组件的是基本数据类型(this.age),如果子组件展示了这个数据
    //在父组件中修改这个数据,子组件也会触发视图更新
    //因为传递这个数据的时候,其实就确定了哪个组件依赖它,所以能成功收集依赖
    //provide
    //而通过provide传递的数据,没有明确的依赖关系,因为父组件provide了数据,也可能没有任何子组件inject
    //但是如果provide的是一个响应式对象,传递的是引用,父子组件共享的是同一份数据
    //在父组件中修改数据,子组件中的此数据也会改变(因为是同一份数据)
    //然后因为是响应式数据,子组件中在访问这个响应式数据的时候,触发getter,就会被这个响应式数据收集为依赖。
    //当父组件修改此数据,触发setter,就会通知依赖更新,这个依赖就包括子组件
    <HelloWorld :age="age"></HelloWorld>
    </div>
    </template>

    <script>
    import HelloWorld from '@/components/HelloWorld.vue';
    export default {
    data(){
    return {
    num:1,
    age:1,
    parents:{mom:43}
    }
    },
    provide(){
    return {
    num:this.num,//传递的只是一个值
    parents:this.parents//传递的是一个对象,一个响应式对象
    }
    },
    components:{
    HelloWorld
    },
    methods:{
    add(str){
    if(str=='num'){
    this.num++
    }else if(str=='age'){
    this.age++
    }else{
    this.parents.mom++
    }
    }
    }
    }
    </script>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <template>
    <div class="hello">
    num:{{num}}age:{{age}}
    parent:{{ parents.mom }}
    </div>
    </template>

    <script>
    export default {
    name: 'HelloWorld',
    props: {
    age: Number
    },
    inject:['num',"parents"]
    }
    </script>

模板引用

步骤

  • 调用ref函数生成一个ref对象:const inp = ref(null)
  • 通过ref标识,进行绑定<input ref="inp" type="text">
  • 通过ref对象.value即可访问到绑定的元素

与vue2中ref/$ref的区别与联系

  • vue2中通过给子元素添加ref属性并任意赋值命名,然后通过this.$ref.属性名就能获取到绑定的dom或者组件实例
  • 总的来说,都是给dom元素或者组件添加ref属性,然后赋一个值;在vue2中这个值是一个独一无二的名字,在vue3中这个值就是一个RefImpl类型的数据;然后都要拿到dom元素或者组件实例,在vue2中是通过this.$ref.属性名拿到,在vue3中是通过RefImpl.value拿到
  • 作用范围都是当前组件内

defineExpose()

默认情况下在<script setup>语法糖下,组件内部的属性和方法是不开放给父组件访问的,即便能够拿到组件实例也访问不了。

可以通过defineExpose编译宏指定哪些属性和方法允许访问

1
2
3
4
5
6
7
<script setup>
const count = 999
const sayHi = ( )=>{
console.log('打招呼')
}
defineExpose({count,sayHi})//暴露给父组件
</script>

而在vue2中,只要获取到了组件对象,就能访问里面的属性(data)和方法(methods)。

事件修饰符

  • stop:阻止事件冒泡,等在传入的回调函数中添加 event.stopPropagation()

    1
    2
    3
    4
    5
    6
    <button @click.stop="handleClick">点击不会冒泡</button>
    //等效于
    const handleClickWithStop = (event) => {
    event.stopPropagation(); // 手动阻止冒泡
    // 其他业务逻辑
    };
  • prevent:阻止默认行为,等同于在传入的回调函数中添加 event.preventDefault()

    1
    <form @submit.prevent="handleSubmit">提交表单不会刷新页面</form>
  • capture:使用事件捕获模式(默认是冒泡模式)

    1
    <div @click.capture="parentClick">父级先触发</div>
  • self:仅当事件从元素本身(而非子元素)触发时执行

    1
    <div @click.self="onlySelfClick">点击子元素不触发</div>
  • once:事件只触发一次,之后自动移除对该事件的监听,避免因长期持有未使用的监听函数导致内存泄漏、

    1
    <button @click.once="oneTimeAction">仅首次点击有效</button>

    其实在原生dom事件中,实现这个效果也是非常简单的,只需要在第三个参数传入{ once: true },手动通过removeEventListener还是比较消耗精力的,不过灵活度更大。

    1
    element.addEventListener('click', handler, { once: true });
  • passive:提升滚动性能,不与 prevent 同时使用

    1
    <div @scroll.passive="onScroll">滚动更流畅</div>

    当监听 touchstarttouchmovewheel(滚动)等高频事件时,浏览器的默认行为是:等待事件处理函数执行完毕

    再决定是否执行默认行为(如滚动页面),如果事件处理函数中存在耗时操作(如复杂计算),会导致 滚动卡顿,因为浏览器必须等待函数执行完毕才能滚动页面(默认行为)。

    passive 修饰符的作用,是通过将事件监听器标记为 被动模式(Passive),本质是向浏览器承诺:
    ​“此事件处理函数不会调用 event.preventDefault()”​,从而允许浏览器 ​立即触发默认行为,无需等待函数执行。

    Vue 3 的 .passive 修饰符对应原生 addEventListener{ passive: true } 配置:

    1
    2
    // Vue 编译后的等效代码
    element.addEventListener('scroll', handler, { passive: true });

    .passive 向浏览器承诺 不会阻止默认行为,而 .prevent 的作用是 主动阻止默认行为,二者语义冲突,所以不能同时使用。

Vue-Router

在vue2,vue3项目,使用的路由插件都是vue-router,就是语法有所不同。

修改地址栏路径时,切换显示匹配的组件

单页面应用

所有功能在一个html页面上实现,基于前端路由实现

优点:按需更新性能高,开发效率高,用户体验好

缺点:学习成本,首屏加载慢(如果不使用代码分割,加载首屏还会将其他页面的资源一同加载并处理,然后才开始首屏渲染),还不利于SEO。

路由导航

使用vue-router提供的全局组件router-link,替代a标签实现跳转,必须配置to属性指定路径(不需要加#)。本质还是a标签。

1
<router-link to="/路径值"</router-link>

能高亮,默认就会提供高亮类名,可以直接设置高亮样式,不需要手动添加类名,高亮类名包括:

  • router-link-active(模糊匹配,常用)

    to="/my"可以匹配/my/b,意思是当前页面的前端路由是/my/b,这个高亮类名就会生效。

  • router-link-exact-active(精确匹配):就必须完全一样,这个高亮类名才会生效。

编程式导航

1
this.$router.push()
  • 使用path跳转

    • push字符串

      1
      2
      3
      this.$router.push('路由路径')
      this.$router.push('/路径?参数名1=参数值1&参数2=参数值2')//传入查询参数
      this.$router.push('/路径/参数值')//动态路由传参
    • push对象

      1
      2
      3
      4
      5
      6
      this.$router.push({path: '路由路径'})
      this.$router.push({
      path: '/路径',
      query: {参数名1:'参数值1',参数名2:'参数值2'}
      })//传入查询参数
      this.$router.push({path:'/路径', params:{参数名:参数值}})
  • 使用name跳转

    适合路径过长的路由,给path路径取名,用name替代path,好处是不用写过长的路径,缺点是只能通过push对象的方式跳转,传参,因为没有path无法拼接成合法的url。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const routes = [
    {
    path: '/',
    name: 'Home', // 给首页路由命名为 'Home'
    component: Home,
    },
    {
    path: '/about',
    name: 'About', // 给关于页面路由命名为 'About'
    component: About,
    },
    // 其他路由...
    ];
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    this.$router.push({
    name:"路由名字'
    })
    this.$router.push({
    name:"路由名字',
    query: {参数名1:'参数值1',参数名2:'参数值2'}
    })
    this.$router.push({
    name:"路由名字'
    params: {参数名1:'参数值1',参数名2:'参数值2'}
    })

参数接受

  • this.$route.query:接收查询参数

  • this.$route.params:接收动态参数

    动态路由传参的前提是组件配置了动态参数

    1
    2
    3
    4
    5
    6
    7
    const routes = [
    {
    path: '/about/:a',
    component: About,
    }
    // 其他路由...
    ];

    然后跳转传参:to="/about/3",3就会被赋值给a,然后在About组件中,通过this.$route.params.a访问。

    如果跳转不穿参:to="/about",就会报错,如果希望可传参,可不传,则在动态参数后加上?

    1
    2
    3
    4
    5
    6
    7
    const routes = [
    {
    path: '/about/:a?',
    component: About,
    }
    // 其他路由...
    ];

    动态路由的其他意义:让不同的路由对应相同的组件。

嵌套路由

children 属性用于定义嵌套路由。每个子路由的 path 应该相对于其父路由的路径来理解。

1
2
3
4
5
6
7
8
{
path: "/", component: Layout,
children: [
{ path: 'article', component: Article },
{ path: 'collect', component: Collect },
{ path: 'like', component: Like },
]
}

重定向redirect

1
2
3
4
5
6
7
8
9
10
const routes = [
{
path: '/',
component: layout,
//children 属性用于定义嵌套路由
children:[{ path: 'home', component: home }],
redirect: '/home'
}
// 其他路由...
];

路由出口router-view

router-view是vue-router提供的一个全局的组件,是一个可以被替换掉的动态组件

导航守卫

next()

  • 无参数:直接调用 next() 表示允许导航继续进行,效果和 next(true) 是完全等价的,都表示允许导航继续。
  • 传递路径或命名路由next('/somePath')next({ name: 'SomeRoute' }) 用于重定向到另一个位置。
  • 传递 falsenext(false)阻止导航继续进行。
  • 传递错误对象next(error) 触发路由错误处理逻辑。

局部守卫

  • 组件内部

    • beforeRouteEnter

      1
      2
      3
      4
      5
      6
      7
      8
      9
      export default {
      beforeRouteEnter(to, from, next) {
      // 在渲染该组件的对应路由被confirm 前调用
      // 不能获取组件实例 `this`,因为当守卫执行前,组件实例还没被创建
      next(vm => {
      // 通过 `vm` 访问组件实例
      })
      }
      }
    • beforeRouteLeave

      这个守卫用来阻止用户离开当前路由。比如,你可以用它来提示用户是否有未保存的更改

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      export default {
      beforeRouteLeave(to, from, next) {
      const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
      if (answer) {
      next()//确认离开
      } else {
      next(false)//取消离开
      }
      }
      }
    • beforeRouteUpdate

      这个守卫在当前路由改变,但是该组件被复用时调用。举例来说,对于一个带有动态路由参数的路径 /foo/:id,当你从 /foo/1 导航到 /foo/2 时,由于会使用同一个组件实例,所以 beforeRouteUpdate 守卫会在这种情况下被调用。

      但是如果这个组件实例被缓存了,从别的组件切换到这个组件也不会触发这个钩子。只能借助activated或者beforeRouteEnter

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      export default {
      beforeRouteUpdate(to, from, next) {
      // 路由改变时重新获取数据
      this.fetchData()
      next()
      },
      methods: {
      fetchData() {
      //获取数据的逻辑
      }
      }
      }

      可以观察到组件内的路由守卫都以beforeRoute为前缀,而全局前置守卫和路由独享守卫都只以before为前缀

  • 路由独享守卫beforeEnter

    这个路由守卫是在配置路由的时候书写的,也是路由对象的一个属性,和path,component等是同一级别。

    1
    2
    3
    4
    5
    6
    7
    8
    { 
    path: '/home',
    component: home,
    beforeEnter:(to,from,next)=>{
    console.log(to,from,next)
    next()
    }
    },//这里的to显然是home组件的路由对象,即this.$route

全局守卫

  • beforeEach

    beforeEach全局前置守卫,在每次导航时都会触发,无论是从一个路由跳转到另一个路由,还是首次进入应用。所有的路由在真正被访问到之前(解析渲染对应组件页面前),都会先经过全局前置守卫,只有全局前置守卫放行了,才会到达对应的页面,或者说才会开始渲染对应的组件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //next()表示放行,next("url")表示拦截到url
    router.beforeEach((to, from, next) => {
    //前两个是对象(和$route一样 是一个反应式的对象(路由对象)),后一个是函数
    //如果要访问的网站to对象的路径:path 不是'/pay', '/myorder'
    if (!paths.includes(to.path)) {
    next()//放行
    } else {
    if (store.getters.token) {
    next()//如果登录了,放行
    } else {
    next('/login') //拦截到 登录页面
    }
    }
    })

$route和$router的区别

一个是路由,表示当前页面的路由对象;一个是路由器,记录了所有页面的对应的路由路径。

在组件内可以通过this.$route或得当前组件对应的路径。

404页面

{ path: '*', component: NotFound }路由的匹配顺序是声明顺序,通常写在最后,匹配不到组件就匹配这个组件 。

当我们使用的路由是history路由的时候,这个页面非常有用,因为后端为了防止使用history路由的页面,将前端路由发送到后端导致响应404,所以每当匹配不到资源的时候,后端都返回index.html,这样浏览器就会重新解析html,渲染页面,将路由控制权交给前端路由。但是这样也有缺点,就是如果用户请求的路径,既不对应后端资源也不对应前端页面,这种情况,确实需要响应404,我们就需要一个404页面来提示用户。

区别

在vue2中

1
2
3
4
5
6
7
8
//router/index.js
import VueRouter from 'vue-router '
const router = new VueRouter({
mode: 'history ' ,
routes: [],
base:'/api/'//设置基础路径
})
export default router

在vue3中

1
2
3
4
5
6
7
8
9
10
//router/index.js
import { createRouter,createwebHistory } from 'vue-router'
const router = createRouter({
history: createwebHistory(import.meta.env.BASE_URL)//指定路由模式的同时指定基础路径,
routes: [],
scrollBehavior: () => {
return { top: 0 }
}
})
export default router

import.meta.env.BASE_URL:路由基地址,导入的是vite的环境变量,可以通过修改vite.config.js文件的base属性来改变基地址,不能把import.meta.env.BASE_URL直接替换成'./'这种字面量。

scrollBehavior 是 Vue Router 3.5.0 版本引入的一个功能,允许你定义一个滚动行为的函数,用于在导航时控制页面的滚动位置。

它和routes这个常见的配置项属于同级别的属性。

scrollBehavior 函数接收三个参数:

  1. to:即将进入的路由对象。
  2. from:即将离开的路由对象。
  3. savedPosition:仅当 popstate 导航(如用户点击浏览器的前进/后退按钮)时可用。这是一个包含滚动位置的对象(如果有保存的话),比如{top:20}。savedPosition记录的是用户点击前进或后退按钮时,的页面滚动位置
1
2
3
4
5
6
7
8
9
10
const router = createRouter({
// ...
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
},
})

这段代码会在用户点击浏览器的前进/后退按钮时恢复之前保存的滚动位置;如果没有保存的位置,则默认滚动到顶部

创造实例方式

  • 一个是使用构造函数创建router实例,一个是通过函数创造
  • 使用的库都是vue-router
  • 创造实例的时候传入的都是配置项

路由模式

  • vue2中控制路由用mode属性,history标识历史模式,hash是默认模式
  • vue3中控制路由模式用history属性,createwebHistory表示历史模式,createwebHashHistory表示哈希模式。

获取router/route对象方式

  • vue2在组件中可以通过this.$router/$route获取,在js文件中则通过直接导入router的方式实现路由跳转。

  • 在vue3中,在普通js文件中,也是通过直接导入router的方式实现路由跳转,在组件中(默认使用setup语法糖),通过useRoute, useRouter这两个api分别获取当前页面路由对象路由器对象,因为没有this,所以不能再使用vue2中的语法。

    1
    2
    3
    4
    //在js
    import router from '@/router';
    // 执行导航操作
    router.push('/some-path');
    1
    2
    3
    4
    5
    6
    <script setup>
    //这些函数依赖于Vue的响应式系统和组件上下文,只能在组件内使用,在普通js文件中无法使用。
    import { useRoute, useRouter } from 'vue-router'
    const route = useRoute()
    const router = useRouter()
    </script>
  • 要注意的是,在组件内使用useRoute, useRouter,不要在函数内部需要的时候再使用,否则api调用返回的值是undefined

  • 无论是在vue2还是在vue3中,在template中都能通过$route访问当前页面路由对象。

导航守卫

在 Vue 3 中,如果你使用的是 Vue Router 4,在某些情况下,你可以直接返回值(return或者return true)来代替调用 next(),这使得代码更加简洁,但是也要注意这样会终止导航守卫函数。

1
2
3
4
5
6
7
8
9
10
router.beforeEach((to, from) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isAuthenticated()) {
return '/login'; // 返回字符串作为重定向的目标路径,等效于next('/login')
}
}
if (shouldBlockNavigation(to)) {
return false; // 取消导航,等效于next(false)
}
});

Vuex

在vue2开发过程中使用的状态管理工具。

场景

  • 某个状态在很多个组件来使用(个人信息)
  • 多个组件共同维护一份数据比如(购物车)
  • 数据传递存在困难用vuex就完事了

优势

  • 共同维护同一份数据
  • 响应式变化,响应式变化基于vue的响应式
  • 操作简洁

注册

创建vuecli项目时勾选vuex或者手动添加。

  • 安装vuex
  • 在src新建文件store,新建文件index.js
  • 在index文件中初始化插件:Vue.use(Vuex);创造空仓库,配置仓库。
  • 导出store对象,最后在创建根实例的时候传入这个store对象,它最终会被注入到每个组件实例中,也就是说this.$store是组件实例自己的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from 'vue'
import Vuex from 'vuex'
import user from '@/store/modules/user'
import cart from '@/store/modules/cart'
//注册插件
Vue.use(Vuex)
//Store显然是一个构造器
export default new Vuex.Store({
state: {
index: 0
},
mutations: {
setIndex: (state, index) => {
state.index = index
}
},
modules: {
user, cart
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vue from 'vue'
//App是一个被vue-loader解析后的js对象
import App from './App.vue'
//导入router对象,会先把router文件夹里的index.js里的代码运行
import router from './router'
// 导入store对象
import store from './store'
Vue.config.productionTip = false
const app = new Vue({
router,
store,
//h是createElement函数,用来创建虚拟结点(VNode),返回这个 VNode,告诉 Vue使用 App 组件作为根组件
render: h => h(App)
}).$mount('#app')

四大属性

state

提供唯一的公共数据源

无论组件内外,都是从store对象出发,来拿到state,不过获取store对象的方式不同。

组件内的可以通过this.$store拿到,在模板内可以直接使用$store(模板内默认去除this),后面的3大属性同理。

组件外则通过import导入:import store from './store',直接拿到store实例。

state中的数据的生命周期

初始化:当 Vuex store实例被创建时,定义在 store 中的 state 也会一同被初始化。这意味着一旦应用启动,并创建了 Vuex store 实例,state 就会被初始化并准备好使用。

页面刷新:默认情况下,Vuex 的 state 在页面刷新时,会重新初始化。这是因为 state 存储在内存中,页面刷新会导致内存中的数据丢失。如果你需要在页面刷新后保持 state 数据,可以使用一些持久化存储的方法,如 localStorage 或者 sessionStorage,初始化的时候再从中取数据。

应用运行期间:在应用运行的过程中,state 可以通过 commit mutations 来直接改变,或通过 dispatch actions 后再 commit mutations 来间接改变。这是 Vuex 管理状态变化的主要方式,确保状态的变化是可追踪和预测的。

路由变化:当用户在应用的不同路由之间导航时,除非明确改变了 state 或者触发了相关的 mutations/actions,否则 state 不会因为路由变化而自动改变

组件销毁与重建:Vue 组件在其生命周期内,可能会被多次销毁和重建(例如,在动态路由或条件渲染下)。但是,只要 Vuex store 没有被销毁,state 将保持不变,不会因单个组件的销毁和重建受到影响。

挂载位置

模块中的state,最终会挂载在根仓库的state中,**无论模块是否开启命名空间(开启命名空间是防止命名冲突)**,但要注意的是根仓库中的属性直接通过this.$store.state就能拿到,比如this.$store.state.index,而模块中的属性需要通过this.$store.state.module才能拿到,路径更长,比如购物车模块中的购物车列表,通过this.$store.state.cart.cartList才能拿到。

例子如下:

1
2
3
4
5
6
7
8
export default new Vuex.Store({
state: {
index: 0//根仓库自己的属性
},
modules: {
user, cart
}
})
1
2
3
4
5
6
7
8
9
10
//modules/cart.js
const state = function(){
return {
cartList: []
}
}
export default {
namespaced: true,//开启命名空间
state
}
1
2
3
4
5
6
7
8
9
//modules/user.js
export default {
namespace:false,//不开启命名空间
state: {
//页面刚加载完成,或者刷新,从本地存储中获取个人信息,更新info
// 一旦 info 被初始化,后续对 info 的访问都是直接从 Vuex store 中获取已存储的值,不会再调用 getInfo 函数。
info: getInfo()
},
}

可以看到user模块即便没有开启命名空间,其state中的数据也还是不会直接挂载在this.$store.state下,而是挂载在this.$store.state.user下。

mutations

里面是一些修改/维护state中的数据的函数,只能通过调用这里的函数来修改数据。

参数

第一个参数是state,用来访问state中的数据,也同时说明了mutations只能同步修改数据

1
2
3
4
5
mutations: {
setIndex: (state, index) => {
state.index = index
}
}

我们在使用mutations中的方法的时候忽略第一个参数,我们传入的参数就是index,比如this.$store.commit('setIndex',1)

挂载位置

是否开启命名空间,也不影响mutations的挂载位置。但是会影响挂载时候的属性名,举个例子:

1
2
3
4
5
6
7
8
9
10
export default new Vuex.Store({
mutations: {
setIndex: (state, index) => {
state.index = index
}
},
modules: {
user, cart
}
})
1
2
3
4
5
6
7
8
9
10
11
//modules/cart.js
const mutations = {
setCartList(state, list) {},
toggleCheck(state, goods_id) {},
toggleAllCheck(state,flag) {},
changeCount(state, obj) {}
}
export default {
namespaced: true,//开启命名空间
mutations
}
1
2
3
4
5
6
7
//modules/user.js
export default {
namespace:false, 未开启命名空间
mutations: {
setUserInfo(state, info) {}
}
}

如图所示,cart模块开启了命名空间,所以属性名加上了cart/前缀,而user模块未开启,就不会加上前缀,如同根仓库中的mutations,但是无论开不开启命名空间,它们模块的mutations中的方法都是直接挂载在this.$store._mutations下的

然后我们使用commit调用mutations中的方法,就有:

1
this.$store.commit(属性名,参数)

比如:

1
2
this.$store.commit('cart/changeCount',3)
this.$store.commit('setIndex',1)

commit是Store实例的自己的属性,值是函数

getter

相当于计算属性,第一个参数也一般是state,第二个参数可以是getters(用来拿到getters中的属性)

挂载位置

是否开启命名空间,也不影响getter的挂载位置。但是会影响挂载时候的属性名,举个例子:

1
2
3
4
5
6
7
8
9
10
export default new Vuex.Store({
getters: {
token(state) {
return state.user.info.token
}
},
modules: {
user, cart
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//modules/cart.js
const getters = {
isAllChecked(state) {
return state.cartList.every(item => item.checked)
},
numOfAll(state) {
},
numOfChecked(state) {
},
sumPrice(state) {
},
selectedGoods(state) {
}
}
export default {
namespaced: true,//开启命名空间
getters
}
1
2
3
4
5
6
7
8
9
//modules/user.js
export default {
namespaced: false,
getters:{
name(){
return 'tom'
}
}
}

如图所示,cart模块开启了命名空间,所以属性名加上了cart/前缀,而user模块未开启,就不会加上前缀,如同根仓库中的getter。

这一挂载规则就和mutations的挂载规则相同,因为getters本质也是函数啊。

然后我们访问的时候就有:

1
this.$store.getters[属性名]

比如:

1
2
3
this.$store.getters['token']
this.$store.getters.name
this.$store.getters['cart/numOfAll']

actions

里面是一些异步操作/函数

参数

第一个参数是context(上下文),可以通过context.commit()调用mutations里的方法,来修改state,通过context.dispatch()调用actions里的方法;可以通过context.state拿到数据但是不能直接修改数据,总之就是雨露均沾。

挂载位置

原理和mutations,getters一样(三者本质都是函数啊),是否开启命名空间,也不影响actions的挂载位置,都挂载在Store._actions属性中。

然后调用的时候就有:

1
this.$store.dispatch('属性名',参数)//类似mutations

dispatchcommit一样是Store实例的自己的属性,值是函数。

辅助函数

在我们了解了各个属性在Store实例中的挂载位置后,我们拿到Store实例后,就知道如何访问,使用这四个属性了。

其实vuex还提供了辅助函数来简化操作。

四个属性,分别对应四个辅助函数,mapState,mapMutations,mapActions,mapGetters

辅助函数内部本质其实也是使用this.$store来执行各种操作的,所以只能在组件中使用。

它们的使用方法是类似的。

mapState

mapState返回的是一个对象,这个对象可以有多个属性,属性的值类型都是函数,都是计算属性,内部使用了this.$store来获取state中的数据。

1
2
3
4
5
6
7
8
import { mapState } from 'vuex'
export default{
computed: {
...mapState('cart',['cartList']),
...mapState(['index']),
...mapState('user',['info'])//报错,因为user未开启命名空间
}
}

如果使用辅助函数使用了传递了2个参数的形式,如图,则第一参数是模块名,要求必须开启命名空间

mapMutations

1
2
3
4
5
6
7
8
9
10
11
import { mapMutations } from 'vuex'
export default{
created(){
this.setUserInfo({name:'tom'}),
this.changeCount()
},
methods:{
...mapMutations('cart',['changeCount','setCartList']),
...mapMutations(['setIndex','setUserInfo'])
}
}

mapGetters

1
2
3
4
5
6
7
import { mapGetters } from 'vuex'
export default{
computed:{
...mapGetters('cart',['isAllChecked','numOfAll']),
...mapGetters(['name','token'])
}
}

Pinia

vue的最新状态管理工具,vuex的替代品。

优点

  • 和Vue3新语法统一,提供符合组合式风格的API

  • 提供更加简单的API(去掉了mutation,合并到actions)

  • 去掉了modules的概念,替换为一个一个的同级别的store,他们都由pinia管理,创建好的pinia实例最终会在app上注册,也就是vue实例上(app.use(pinia)

  • 配合TypeScript更加友好,提供可靠的类型推断。

注册

1
2
3
4
5
6
7
//main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App. vue'
const app = createApp(App) //传入App.vue创建根实例
app.use(createPinia()) //注册pinia插件
app.mount('#app') //视图的挂载

组合式风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//cart.js
import { defineStore } from 'pinia'
//defineStore返回值的类型是一个函数,导出的就是这个函数
//调用这个函数返回的是一个对象
export const useCounterStore = defineStore('counter', () =>{
const count = ref(100)//state
const addCount =() => {count.value++}//actions
const subCount =() =>{count.value--}//actions
const double = computed(() => count.value * 2)//getters
const msg = ref('hello pinia')//state
return {
count,double,addCount,subCount,msg//返回
}
})//所以说在仓库里写组合式api风格的代码就行

特点:

  • 定义的变量就是state属性
  • 定义的计算属性。即computed,就是getters
  • 定义的函数,即function(),就是actions,支持异步操作,也支持同步操作

选项式风格

个人还是一味的使用组合式了….

1
2
import { defineStore } from 'pinia'
defineStore(仓库的唯一标识,{//配置对象} )

持久化插件

无论是vuex还是pinia,仓库中的数据是保存在内存中的,如果不使用持久化存储,当页面刷新或者关闭时,数据就会丢失。这是因为这些状态是存储在 JavaScript 运行时的内存中的,一旦页面卸载(比如刷新或关闭),这些数据就会被销毁。

使用步骤

1
npm i pinia-plugin-persistedstate
1
2
3
4
5
import { createPinia } from 'pinia'
//导入插件
import persist from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(persist)//注册插件

再指定要持久化的store,对于组合式store,需要在defineStore函数里再传入一个参数(第三个参数),{persist:true}。

对于选项式store,直接添加属性persist:true即可

1
2
export const category = defineStore('category',() => {},{persist:true})
export const category = defineStore('category',{persist:true,state:{}})

原理

  • 简化了localStorage的相关操作,会自动使用JSON. stringify/JSON.parse进行序列化/反序列化。值为函数和undefined的属性无法被序列化,Symbol属性和值为Symbol的属性无法被序列化。
  • 存储到localStorage的键名默认是仓库唯一标识
  • 默认把仓库整个state做持久化,可以指定具体哪些数据做持久化。

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 假设在stores/counter.js中定义了一个仓库
// stores/counter.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
//选项式风格
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++;
}
}
});
1
2
3
4
5
6
7
8
9
10
//在组件中使用
<script setup>
import { useCounterStore } from '@/stores/counter';
import { computed } from 'vue';
const counterStore = useCounterStore();
//我们可以发现无论是state还是getters还是actions,都可以直接通过store实例访问,非常方便啊
const count = computed(() => counterStore.count);
const doubleCount = computed(() => counterStore.doubleCount),
const increment = () => counterStore.increment()
</script>

router的useRoute和useRouter只能在组件中使用,在想要在js文件中使用,就必须引入router对象。

无论在是组件还是js文件中,都只能通过引入并调用useXXXStore的方式获得store对象,然后才能访问其中的状态,调用其中提供的方法。不过,在js 文件(非 Vue 组件)中使用 Pinia store 需要一些额外的步骤,因为你没有 Vue 组件的上下文(比如 setup() 函数)。需要确保 Pinia 已经被安装并且可以访问到,所以最好不要写到js文件的全局作用域中。

与vuex的区别

  • vue2中要使用vuex中的哪个数据,调用哪个方法,都是单独获取,而Vue3是一个一个store整体导入使用。
  • 在vue2的组件中,通过this.$store就能访问到store对象,而在js文件中就需要手动导入。

启用命名空间

在 Pinia 中,无需手动启用命名空间特性即可避免 Store 之间的命名冲突,这是其设计哲学的一部分。以下是具体实现原理和实践方法:

Pinia 通过 唯一 Store ID 自动实现命名空间隔离。每个 Store 在定义时必须指定一个全局唯一的 ID(字符串),该 ID 会作为所有状态的命名空间前缀。

1
2
3
4
5
6
7
8
9
// 定义 user 模块
export const useUserStore = defineStore('user', {
state: () => ({ name: 'Alice' })
});

// 定义 product 模块
export const useProductStore = defineStore('product', {
state: () => ({ price: 100 })
});

若两个 Store 使用相同 ID(如 defineStore('user', ...) 重复),构建时会直接报错。

Vue-cli

是什么

vue使用webpack进行模块化开发,但是webpack的配置工作是一个很繁琐的过程,所以出现了vue-cli这个脚手架工具,它可以帮助我们快速创建一个开发vue项目的标准化webpack配置。

快速开始

1
2
3
4
npm i @vue/cli -g//全局安装,安装一次就行,之后就可以在任意目录执行以下指令
vue --version//查看Vue版本
vue create project-name(项目名不能用中文)//创建项目架子
npm run serve //启动项目,查看package.json可知

项目结构

  • node_modules:存储项目的所有 npm 依赖包。

  • public:存放静态资源,如 index.html 和 favicon.ico,这些文件不会被 webpack 处理,而是会直接被打包进最终文件。

  • src:源代码的主要目录

    • assets:用于存放静态资源文件,如图片、字体等,会被 webpack 处理

    • components :存放 .vue 单文件组件

    • views:存放页面

    • App.vue:根组件,整个应用的入口点。

    • main.js:应用程序的入口脚本,通常在这里创建 Vue 实例(根实例)。

      1
      2
      3
      4
      5
      new Vue({
      router,
      store,
      render: h => h(App)
      }).$mount('#app')
  • package.json:项目元数据和依赖关系列表,以及 npm 脚本(script)。

  • vue.config.js:Vue CLI 的可选配置文件,用于自定义构建设置

  • babel.config.js:Babel 的配置文件,用于转换 ES6+ 代码。它可以帮助你将现代 JavaScript 特性(ES6+)转换为向后兼容的 JavaScript 版本。

json-server

是一个轻量级的 Node.js 模块,可以让你快速地启动一个具有 REST API 的 JSON 文件数据库服务器。它提供了一个简单的、可立即使用的后端。

1
npm install -g json-server

在项目根目录下创建一个文件,比如 db.json,并填充一些数据

1
2
3
4
5
6
7
8
9
{
"posts": [
{ "id": 1, "title": "Hello World" },
{ "id": 2, "title": "Second Post" }
],
"comments": [
{ "id": 1, "body": "Some comment", "postId": 1 }
]
}

在db.json文件所在目录下执行json-server --watch db.json,这会启动一个默认监听在http://localhost:3000的服务器

使用各种 HTTP 请求工具(如 Postman 或 cURL)或前端框架中的 AJAX 请求来与之交互,比如可以发送一个 GET 请求到http://localhost:3000/posts来获取所有的帖子

eslint,eslint扩展,prettier

eslint扩展

ESLint 扩展的作用是在编码的时候,就提示代码存在的问题,通常依赖于项目中的本地安装的 ESLint。这意味着它会在项目的node_modules目录中寻找 ESLint。如果您没有在项目中安装 ESLint,VSCode 的 ESLint 扩展可能会无法正常工作。

eslint和prettier

我们在创建vue3项目的时候,就能根据vue-create脚手架的提示,快速的下载好eslint和prettier所需的包

1
2
3
4
5
6
7
8
9
"devDependencies": {
"@rushstack/eslint-patch": "^1.8.0",
"@vue/eslint-config-prettier": "^9.0.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"prettier": "^3.2.5",
"vite": "^5.3.1",
"@vitejs/plugin-vue": "^5.0.5"
}

然后目录下还会多出.eslintrc.cjs.prettierrc.json文件,.eslintrc.cjs是ESLint 的配置文件,主要用于代码质量检查和部分风格约束(如变量未声明、未使用的导入等);.prettierrc.json是Prettier 的配置文件,专注于代码格式化(如缩进、引号、换行符等),不涉及代码逻辑问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
//.eslintrc.cjs
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}
1
2
3
4
5
6
7
8
9
//.prettierrc.json
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

同时项目根目录下的.vscode文件中也会多出一个setting.json文件(相比于不使用eslint和prettier的项目),其实也不需要我们过于操心这个文件,一般保持默认配置就好。

1
2
3
4
5
6
7
8
9
10
//setting.json
{
//在保存时手动触发 所有可自动修复的代码问题
"editor.codeActionsOnSave": {
"source.fixAll": "explicit" //"explicit"表示需通过快捷键或弹窗确认修复项
},
"editor.formatOnSave": true,//保存文件时自动格式化代码
//指定 Prettier 为默认格式化工具,需安装 Prettier 插件并配置 .prettierrc 文件(或直接在 rules 中定义规则)
"editor.defaultFormatter": "esbenp.prettier-vscode"
}

保存文件时,第一步,Prettier 自动格式化代码(formatOnSave),第二步,用户手动触发代码修复(codeActionsOnSave:explicit)

此时,我们编码的时候,vscode并不会提示我们代码不符合prettier规范,因为我们没安装prettier扩展

但是我们安装了eslint扩展啊,只要把prettier的配置代码集成到eslint的配置代码中就好了,这样编写代码的时候,也会有prettier规范提示。

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
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
},
//新增了一个rules配置项
rules: {
'prettier/prettier': [
'warn',
{
singleQuote: true, // 单引号
semi: false, // 无分号
printWidth: 80, // 每行宽度至多80字符
trailingComma: 'none', // 不加对象|数组最后逗号
endOfLine: 'auto' // 换行符号不限制(win mac 不一致)
}
],
//这里千万不能写false写成'off'或者0
'vue/multi-word-component-names': 0,
'vue/no-setup-props-destructure': ['off'], // 关闭 props 解构的校验
//添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
'no-undef': 'error'
}
}

如果发现修改.eslintrc.cjs保存后没有效果,则可以尝试重启vscode,重新加载这个配置文件。

husky

是什么

husky能够帮助我们轻松的设置git hooks。

Husky 将 Git 原生钩子(如 pre-commit)的配置,从 .git/hooks 迁移到项目根目录的 .husky 文件夹中,通过脚本化的方式统一管理,避免钩子文件被 Git 忽略或覆盖的问题。

husky通常与lint-staged配套使用,lint-staged 是一个针对 Git 暂存区(Staged Files)运行代码检查(Linters)和格式化工具的工具。它通过仅对即将提交的代码,进行增量检查,避免全量扫描,从而显著提升校验效率。例如,在提交前仅对修改过的 .js 文件运行 ESLint,而不是整个项目。

lint-staged 本身不依赖 ESLint,但通常与 ESLint 结合使用。其核心功能是按需调用不同的代码检查工具,具体支持的 Linter 包括:ESLint,Prettier,Stylelint,TSLint(TypeScript,已逐渐被 ESLint 替代)

使用步骤

安装:

1
npm install husky lint-staged --save-dev

初始化:要注意的是,再初始化之前,需要先确保git仓库存在,Husky 的核心功能是管理 Git 钩子(Git Hooks),而 Git 钩子本身是 Git 仓库的组成部分。只有当项目是 Git 仓库时,Husky 才能通过修改 Git 配置(如 core.hookspath)将钩子脚本的存储路径从 .git/hooks 重定向到 .husky 目录。

1
npx husky init  # v9+版本需执行此命令生成.husky目录[7,9](@ref)

我们执行这个初始化命令后,发现根目录下多出来一个名为.husky的文件夹,.husky/_ 目录下通常包含 husky.sh 等脚本文件,用于统一管理 Git 钩子的执行环境,_ 目录内容由 Husky 自动维护,手动修改可能导致钩子功能异常,简单的来说,不需要我们管理,操心。

配置钩子脚本:pre-commit文件中书写:

1
npx lint-staged

然后再我们提交代码之前,这个命令就会被执行

与代码检查工具集成:package.json 中配置 lint-staged,这样我们才知道执行npx lint-staged,具体做了什么

1
2
3
4
5
6
{
"lint-staged": {
"*.{js,vue,ts}": ["eslint --fix", "prettier --write"],
"*.{css,scss}": "prettier --write"
}
}

eslint --fix:使用 ESLint 自动修复可修复的代码问题(如语法错误、风格问题),这行命令在下载好eslint后是可以直接执行的

prettier --write:使用 Prettier 对文件进行格式化(如缩进、引号、换行符等),这行命令在下载好prettier后是可以直接执行的

vetur

Vetur插件是一个为Vue.js项目提供支持的Visual Studio Code(VS Code)插件,适用于vue2

功能

  • 语法高亮:Vetur插件为Vue.js文件提供了语法高亮,让你的代码更易于阅读和理解
  • 智能补全:当你输入Vue组件或属性时,它会自动提示可用选项,并提供文档信息
  • 代码导航:Vetur插件可以帮助你更轻松地导航和理解Vue.js项目的结构。你可以点击组件名称或引用,快速跳转到相关的文件。
  • 语法检查:Vetur插件集成了ESLint和TSLint,可以帮助你在编码时捕获潜在的错误和不规范的代码风格。

问题

Vetur的语法检查会认为vue3的一些新特性,比如可以有两个根元素,v-model:xxx=' xxx'为语法错误。

解决办法:打开设置,搜索 Vetur › Validation: Template ,关闭语法检查,本质上修改扩展设置。

组件样式渗透

scoped只会给当前组件内的标签元素,添加唯一的data-v-hash属性,并给当前组件内的所有选择器加上属性选择

父组件通过插槽传递给子组件的内容(即父组件模板中直接编写的插槽内容),会被父组件的scoped样式处理,添加父组件的data-v-hash属性;而子组件内部模板中的内容(包括默认插槽或具名插槽),则由子组件自身的scoped处理,添加子组件的data-v-hash属性。

1
2
3
4
5
6
<!-- 父组件模板 -->
<template>
<ChildComponent>
<div class="slot-content">来自父组件的插槽内容</div>
</ChildComponent>
</template>

编译后,父组件的<div class="slot-content">会被添加父组件的data-v-xxx属性,如:<div class="slot-content" data-v-xxx>。若子组件自身模板中包含插槽(如<slot>),且存在默认结构,则这部分内容由子组件的scoped处理,添加子组件的data-v-yyy属性。

默认情况下,父组件的scoped,样式不会影响子组件内部的元素(包括slot),因为子组件可能有自己的data-v-yyy属性。

如果在当前组件内使用了scoped的情况下,想要修改子组件内的样式,就可以使用样式渗透,下面举个项目中的例子

1
2
3
4
5
6
7
8
9
10
<div class="editor">
<!--通过ref拿到quillEditor组件实例 -->
<quill-editor
ref="editorRef"
v-model:content="formModel.content"
theme="snow"
content-type="html"
>
</quill-editor>
</div>
1
2
3
4
5
6
7
.editor {
width: 100%;
//组件样式渗透
:deep(.ql-editor) {
min-height: 200px;
}
}

显然,quill-editor,这个富文本组件中存在类名为ql-editor的标签,但是因为我们在组件中开启了scoped,所以不能直接使用:

1
2
3
4
5
6
7
.editor {
width: 100%;
//组件样式渗透
.ql-editor{
min-height: 200px;
}
}

来修改quill-editor组件中的元素,因为编译后的样式会变为:

1
2
3
.editor[data-v-8a25066b] .ql-editor[data-v-8a25066b] {
min-height: 200px;
}

但是这个富文本组件中,类名为ql-editor的标签并没有添加任何属性,所以,这个样式不会生效。

但是如果我们使用了样式渗透,编译后的样式代码就会变为:

1
2
3
.editor[data-v-8a25066b] .ql-editor {
min-height: 200px;
}

显然,这样是能够生效的。由此我们可以看出,使用样式渗透,就是为了解决因为组件内开启了scoped,导致无法直接修改子组件内元素样式的问题。

然而,样式渗透的语法分别是如何的呢?

原生 CSS:>>>
直接作用于子组件内部元素,但部分预处理器可能不支持:

1
2
3
4
/* 父组件样式 */
.parent >>> .child-inner {
color: red;
}

编译后:.parent[data-v-xxx] .child-inner
​适用场景:原生 CSS 项目

预处理器兼容语法:/deep/
通用性更强,支持 SASS/LESS 等:

1
2
3
.parent /deep/ .child-inner {
background: blue;
}

编译后:.parent[data-v-xxx] .child-inner
​注意:在 Vue3 中已废弃

Vue2 专用语法:::v-deep
/deep/ 等价,但语义更明确:

1
2
3
.parent::v-deep .child-inner {
border: 1px solid #ccc;
}

我们观察到::v-deep是附加在.parent后面的,这表明了.parent就是父组件中的元素,而 .child-inner 是子组件中的目标元素

编译后:.parent[data-v-xxx] .child-inner
适用场景:Vue2 项目升级过渡期

Vue3 样式穿透语法

Vue3 统一使用 CSS 伪类 :deep()废弃了 >>>/deep/

1
2
3
4
5
6
/* 修改子组件内部的 .child-inner 样式 */
.parent {
:deep(.child-inner) {
font-size: 16px;
}
}

编译后:.parent[data-v-xxx] .child-inner
优势:

  • 语法标准化,符合 CSS 规范
  • 支持嵌套和动态选择器

富文本编辑器VueQuill

官网地址:https://vueup.github.io/vue-quill/

是什么

VueQuill 是基于 Quill.js 的 Vue 3 专用富文本编辑器组件,它将 Quill.js 的功能封装为 Vue 的响应式组件,提供与 Vue 生态无缝集成的开发体验。Quill.js 本身以轻量、模块化和高扩展性著称,而 VueQuill 在此基础上进一步优化了配置方式和 API 设计。

特点如下:

  • 支持 v-model 双向绑定,直接通过 content 属性管理富文本数据
  • 响应式更新机制与 Vue 的生命周期完美契合,便于状态管理

示例:

1
2
3
<template>
<QuillEditor v-model:content="htmlContent" :options="editorOptions" />
</template>
  • 可通过 options.modules.toolbar 配置工具栏按钮(如加粗、列表、图片上传等)
  • 支持按需加载功能模块(如代码高亮、表格插入),减少打包体积

示例:

1
2
3
4
5
6
7
8
9
10
const editorOptions = {
modules: {
toolbar: [
["bold", "italic"],
[{ list: "ordered" }, { list: "bullet" }],
["image", "link"]
]
},
theme: "snow" // 支持 'snow'(默认)和 'bubble' 主题
};

使用步骤

  • 安装依赖:

    1
    npm install @vueup/vue-quill quill --save
  • 全局或局部引入组件:

    全局注册:

    1
    2
    3
    import { QuillEditor } from '@vueup/vue-quill';//引入的是一个组件
    import '@vueup/vue-quill/dist/vue-quill.snow.css';
    app.component('QuillEditor', QuillEditor);

    局部引入:

    1
    2
    3
    4
    <script setup>
    import '@vueup/vue-quill/dist/vue-quill.snow.css';
    import { QuillEditor } from '@vueup/vue-quill';
    </script>
  • 在模板中使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <div class="editor">
    <!-- 通过ref拿到quillEditor组件实例 -->
    <quill-editor
    ref="editorRef"
    v-model:content="formModel.content"
    theme="snow"
    content-type="html"
    >
    </quill-editor>
    </div>
  • 常用api: editorRef.value.setHTML(''),清空编辑器的内容