Vue全家桶系~2.Vue3基础(更新)

Vue全家桶

先贴一下Vue3的官方文档:https://cn.vuejs.org/guide/introduction.html

官方API文档:https://cn.vuejs.org/api/

1.前言:新旧时代交替

1.1.开发变化

1.网络模型的变化

  1. 以前网页大多是b/s,服务端代码混合在页面里;

  2. 现在是c/s,前后端分离,通过js api(类似ajax的方式)获取json数据,把数据绑定在页面上渲染。

2.文件类型变化

  1. 以前是.html文件,开发也是html,运行也是html。

  2. 现在是.vue文件,开发是vue,经过编译后,运行时已经变成了js文件。 现代前端开发,很少直接使用HTML,基本都是开发、编译、运行

3.外部文件引用方式变化

  1. 以前通过script srclink href引入外部的js和css;

  2. 现在是es6的写法,import引入外部的js模块(注意不是文件)或css

4.开发方式变化

  1. 以前是Ajax获取数据,然后DOM操作
  2. 现在是Vue的MVVM模式(程序员不用操作DOM了,只关心逻辑和数据即可)

我们延续这种演变进行vue3的学习吧,先从传统html文件混合开始,再到单vue文件选项APIOption API,最后再到vu3新的组合APIComposition API


1.2.环境配置

1.2.1.VSCode

官网下载地址:https://code.visualstudio.com/

1.必装插件

VSCode官方插件Vue Language Features (Volar)

TypeScript支持:TypeScript Vue Plugin (Volar)

Vue快速开发Vue VSCode Snippets(输入缩写快速生成代码段)

错误高亮提示Error Lens

Git管理插件

2.扩展插件

这个插件主要就是高亮TODO: 部分

JavaScript快速开发:输入缩写快速生成js的代码段

1.2.2.NodeJS

NodeJS下载:https://nodejs.org (一般都是下载LTS版本)

npm镜像:https://npmmirror.com/

NodeJS安装后设置下npm的国内镜像:(cnpm 需要全局安装,它是你为数不多的需要全局安装的命令行之一)

安装好后,以后只要是npm xxx的命令,都可以使用cnpm xxx

1.cnpm window

打开Git Bash,输入:npm i cnpm -g --registry=https://registry.npmmirror.com

PS:如果失败,可以先安装npminstall,然后通过它来安装cnpm

# 安装npminstall
npm i npminstall -g --registry=https://registry.npmmirror.com
# 通过npminstall安装cnpm
npminstall -c -g cnpm
# 升级cnpm
cnpm i cnpm -g
# 查看版本号:
cnpm -v

2.cnpm linux

不使用sudo的root权限安装,如果该命令npm i cnpm -g --registry=https://registry.npmmirror.com不行在尝试:

# 看看 npm 全局目录的路径
npm prefix -g

# 把 npm 的全局目录换个位置
mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
npm bin -g

# 把 ~/.npm-global/bin 加到 PATH
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

# 全新安装,请勿使用 sudo
npm i cnpm -g --registry=https://registry.npmmirror.com

# 升级新版本
cnpm i cnpm -g

# 检查版本号
cnpm -v

如果还不行可以尝试把npm全局目录权限改成当前用户

# 把 npm 的全局目录权限 owner 改为当前用户
sudo chown -R `whoami` `npm prefix -g`
sudo chown -R `whoami` ~/.npminstall_tarball

安装完毕如果找不到cnpm,看看是不是没加环境变量里面:

which cnpm
echo $PATH

1.2.3.Vue DevTools ★

开发过程中再安装一个vuejs的开发者工具:如果是edge浏览器直接打开应用市场搜索并安装即可

官网https://devtools.vuejs.org/guide/installation.html 源码https://github.com/vuejs/devtools

配置下:

之后F12后浏览器会有个vue选项,来方便我们开发和观察

2.第一个程序

先以我们熟悉的脚本引入写个demo:

<div id="app"></div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
    const app = Vue.createApp({
        template: '<h2>hello vue3</h2>'
    })
    app.mount('#app'); // 挂载到div#app上
</script>

3.VSCode配置默认终端

默认不是git bash,而是win的PowersShell,我们改下,这样可以方便使用cnpmshell:windows

4.VsCode代码片段(含光标位置)

在线配置:https://snippet-generator.app,开源备份地址:https://github.com/lotapp/snippet-generator

4.1.HTML混合开发阶段

比如这段代码每个页面都要输入,那就可以设置一个代码片段:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app">
        
    </div>
    <!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
    <script src="../assets/vue3.js"></script>
    <script>
        const app = Vue.createApp({
            
        });
        app.mount('#app');
    </script>
</body>

</html>

把内容复制到左边文本框,设置下快捷输入的命令,以及简单描述下

代码复制到这个地方:首选项 > 配置用户代码片段 > 文件选html

粘贴到这个里面,最外面的两个大括号保留({}

如果网站挂了也可以自己手动改写代码片段,示例我贴下:prefix:编辑器快捷命令,body:模板内容,description:描述

{
	"create vue app": {
		"prefix": "createvue",
		"body": [
			"<!DOCTYPE html>",
			"<html>",
			"",
			"<head>",
			"    <meta charset=\"UTF-8\">",
			"    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">",
			"    <title>Document</title>",
			"</head>",
			"",
			"<body>",
			"    <div id=\"app\">",
			"        ",
			"    </div>",
			"    <!-- <script src=\"https://unpkg.com/vue@3/dist/vue.global.js\"></script> -->",
			"    <script src=\"../assets/vue3.js\"></script>",
			"    <script>",
			"        const app = Vue.createApp({",
			"            $0",
			"        });",
			"        app.mount('#app');",
			"    </script>",
			"</body>",
			"",
			"</html>"
		],
		"description": "create vue app"
	}
}

以后新建html的时候,输入createvue,就可以快速生成代码了

要指定生成后的光标可以设置下$0,代码生成后鼠标就自动停在那块

4.2.Vue单文件-选项API

vue文件也配置下:

{
	"vue file init": {
		"prefix": "vue3",
		"body": [
			"<template>",
			"    <div>",
			"        $1",
			"    </div>",
			"</template>",
			"",
			"<script>",
			"$2",
			"export default {",
			"    $3",
			"}",
			"</script>",
			"",
			"<style scoped>",
			"$0",
			"</style>"
		],
		"description": "vue file init"
	}
}

4.3.Vue单文件-组合API

{
	"vue file init": {
		"prefix": "vue3",
		"body": [
			"<script setup>",
			"$1",
			"</script>",
			"",
			"<template>",
			"    <div>",
			"        $2",
			"    </div>",
			"</template>",
			"",
			"<style scoped>",
			"$0",
			"</style>"
		],
		"description": "vue file init"
	}
}

其实类似这种vue的代码段,Vue VSCode Snippets中封装的很多,可以在开发中逐步熟悉:vxxx基本上就看到了

2.Vue3语法基础(引入)★

这块上一篇介绍Vue基础的时候基本上都说了,这边快速说下:文本插值、列表、函数、v-model......

文档代码:https://gitee.com/lotapp/BaseCode/tree/master/javascript/3.Vue/3vue3base

2.1.文本插值 {{xxx}}

变量定义在data之中,通过{{title}}显示在页面中,当变量改变时页面中会实时渲染(可以通过控制台app.title=xxx修改查看)

const app = Vue.createApp({
    data() {
        return {
            title: '你好,vue3' // 定义并赋值
        }
    },
    template: `<h2>{{title}}</h2>`
})
app.mount('#app')

PS:template里面的内容,相当于是写在<div id="app">中(如果div#app里面有东西,template里面也有东西,渲染出来的会是template内容)

<div id="app">
    <h2>{{title}}</h2>
</div>

官方文档:https://cn.vuejs.org/guide/essentials/template-syntax.html

隐藏渲染前的元素:v-cloak

有些JS加载或者Ajax操作比较耗时,页面会显示{{xxx}}

这种不太友好,Vue提供了一种v-cloak的方式来隐藏渲染前的dom,我们通过设置style里面的[v-cloak]来控制渲染前的样式

<!-- dom元素中添加v-cloak -->
<div v-cloak>{{title}}</div>

然后样式里面设置一下即可

[v-cloak] {
    display: none;
}

完整案例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        [v-cloak] {
            display: none;
        }
    </style>
</head>
<body>
    <div id="app">
        <!-- dom元素中添加v-cloak -->
        <div v-cloak>{{title}}</div>
    </div>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    title: 'demo'
                }
            }
        });
        app.mount('#app');
    </script>
</body>
</html>

尚未加载成功前不显示{{title}}而是被隐藏掉了。渲染成功后正常显示(v-cloak被vue移除掉了)

官方API:https://cn.vuejs.org/api/built-in-directives.html#v-cloak

2.2.属性绑定 v-bind

官方文档:https://cn.vuejs.org/api/built-in-directives.html#v-bind

API文档:https://cn.vuejs.org/api/built-in-directives.html#v-bind

设置dom的属性(eg:src、href、class),可以通过v-bind:xxx="XX"来设置,简写为:xxx="XX"

PS:还可以向另一个组件传递props

1.简单属性案例

<div id="app">
    <h2 :title="msg">鼠标放我身上看看</h2>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app=Vue.createApp({
        data() {
            return {
                msg: '我是一个属性值'
            }
        },
    })
    app.mount('#app')
</script>

效果如下:

扩展说明:属性值不能使用之前的{{xxx}}来绑定,需要使用特定的v-bind

错误写法:<button title="{{msg}}">{{msg}}</button>

微调下就生效了:<button :title="msg">{{msg}}</button>

2.样式绑定 :class★

在dom中写上:class="{'类名':bool}",bool可以是一个变量,也可以是一个bool结果的表达式

解析下代码:

  1. :class="{active:selectedStudent==name}给li设置了一个类active,这个类显示不显示就看selectedStudent变量是否和当前name相同
  2. @click="selectedStudent=name"当我单击li的时候,把当前name赋值给selectedStudent
    1. 这就意味着,我只要单击li,li的active类就生效了
<div id="app">
    <ul>
        <li v-for="name in students" :key="name" :class="{active:selectedStudent==name}"
            @click="selectedStudent=name">
            {{ name }}
        </li>
    </ul>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                selectedStudent: '',
                students: ['张三', '李四', '王二']
            }
        },
    })
    app.mount('#app') // 挂载
</script>

效果也是一样的:

3.多样式绑定

案例简单贴下::class="{'btn-bgcolor':bool,'active':bool}

dom自带的class:class可以同时使用,最后会把:class追加到class里面

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .btn-bgcolor {
            border: none;
            background-color: rgb(14, 230, 190);
        }
        .btn {
            padding: 10px 10px;
        }
        .active {
            color: white;
        }
    </style>
</head>
<body>
    <div id="app">
        <button class="btn" :class="{'btn-bgcolor':true,'active':isActive}" @click="change">我是一个按钮</button>
    </div>
    <!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
    <script src="../assets/vue3.js"></script>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    isActive: false
                }
            },
            methods: {
                change() {
                    this.isActive = !this.isActive;
                }
            },
        });
        app.mount('#app');
    </script>
</body>
</html>

效果:打开后是这样的,然后每次点击字体颜色在黑白之间切换

样式逻辑比较复杂的话也可以通过函数来实现上面的效果:就:class这边变换下

<button class="btn" :class="getClass()" @click="change">我是一个按钮</button>

然后method里面新增个getClass的方法,其他都一样

getClass() {
    return { 'btn-bgcolor': true, 'active': this.isActive }
}

之前的样式绑定都是属于对象绑定{},也可以数组绑定,eg:

<div :class="[{ 'active': isActive }, errorClass]"></div>

更多参考官方文档:https://cn.vuejs.org/guide/essentials/class-and-style.html

4.动态绑定属性值

语法::[xx]="xxx",比如:<img :[name]="value">,然后通过name控制属性名,value控制属性值

这边举一个常用案例v-bind="xxx":把学生信息以属性的方式绑定到span中:

<div id="app">
    <ul>
        <li v-for="student in students" :key="student.id">
            <!-- <span v-bind="student">{{student.name}}</span> -->
            <span :="student">{{student.name}}</span>
        </li>
    </ul>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                students: [
                    { id: 1, name: '张三', age: 22, gender: '男' },
                    { id: 2, name: '李四', age: 29, gender: '女' },
                    { id: 3, name: '王二', age: 32, gender: '男' }
                ]
            }
        },
    });
    app.mount('#app');
</script>

效果:

2.3.列表渲染 v-for

通过v-for进行循环遍历students数组中的内容

<div id="app">
    <ul>
        <li v-for="name in students" :key="name">
            {{ name }}
        </li>
    </ul>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                students: ['张三', '李四', '王二']
            }
        },
    })
    app.mount('#app')
</script>

效果:

官方文档:https://cn.vuejs.org/guide/essentials/list.html

官方API:https://cn.vuejs.org/api/built-in-directives.html#v-for

2.4.条件渲染 v-if

有个students数组,当里面没有数据的时候需要显示一下提示语,有数据则正常显示

<div id="app">
    <template v-if="students.length==0">
        <h3>暂时没有看到学生</h3>
    </template>
    <template v-else>
        <ul>
            <li v-for="name in students" :key="name">
                {{ name }}
            </li>
        </ul>
    </template>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                students: ['小明', '小张', '王二', 'goudan']
            }
        },
    });
    app.mount('#app');
</script>

当students数组里面没有数据时,显示v-if里面的内容,否则就显示v-else里面的内容(template不会显示)

条件渲染:https://cn.vuejs.org/guide/essentials/conditional.html

v-show和v-if最大的不同主要就是:v-show是通过css控制显示与否,不管显示不显示都会有dom,且不支持v-else、template

v-if只要不满足条件直接不渲染,如果一个元素频繁的显示和隐藏可以考虑v-show

官方API文档:https://cn.vuejs.org/api/built-in-directives.html#v-if

2.5.事件处理 v-on

通过method来定义函数,dom通过v-on:事件名="调用函数" 来调用对应的方法,简写:@事件名="调用函数"

1.简单案例

来个案例:通过两个按钮来控制count数值的加和减

<div id="app">
    <h3>计数:{{count}}</h3>
    <button @click="addCount">++</button>&nbsp;
    <button @click="subCount">--</button>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                count: 0
            }
        },
        methods: {
            addCount() {
                this.count++;
            },
            subCount() {
                this.count--;
            }
        },
    })
    app.mount('#app')
</script>

2.事件传参案例

如果@click后面方法没有参数,默认就是传event回来,被调用的函数可以是无参,也可以接收这个event

PS:通过 event.target 获取的是 <li> 元素本身,而不是student对象

<div id="app">
    <ul>
        <li v-for="student in students" :key="student.id" :title="student.name" @click="show">
            {{student.id}}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
        </li>
    </ul>
</div>
<script src=" ../assets/vue3.js">
</script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                students: [
                    { id: 1, name: '张三', age: 22, gender: '男' },
                    { id: 2, name: '李四', age: 29, gender: '女' },
                    { id: 3, name: '王二', age: 32, gender: '男' }
                ]
            }
        },
        methods: {
            show(event) {
                //如果@click后面方法没有参数,默认就是传event回来
                console.log(event.target.title); // event.target获取的是li元素本身,不是student哦~
            }
        },
    });
    app.mount('#app');
</script>

效果:单击就显示li的title

如果是多个参数,还需要event,可以通过$event传过来

PS:为什么event要加个$ ==> 你不加他到底是字符串,还是data中的变量呢?vue就不知道了

案例代码:

<div id="app">
    <ul>
        <li v-for="student in students" @click="show(student,$event)" :title="student.name" :key="student.id">
            {{student.id}}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
        </li>
    </ul>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                students: [
                    { id: 1, name: '张三', age: 22, gender: '男' },
                    { id: 2, name: '李四', age: 29, gender: '女' },
                    { id: 3, name: '王二', age: 32, gender: '男' }
                ]
            }
        },
        methods: {
            show(student, event) {
                console.log(student.id, student.name, student.age, student.gender);
                console.log(event.target.title);
            }
        },
    });
    app.mount('#app');
</script>

效果:

3.函数为什么不用箭头函数★

为什么不用箭头函数?==> 函数中的this不一样

<script>
    const app = Vue.createApp({
        methods: {
            test1: function () {
                console.log(this);
            },
            test2() { // es6写法,本质和test1的一样
                console.log(this);
            },
            test3: () => {
                console.log(this); // 这种虽然方便,但是this不一样了
            }
        },
    })
    app.mount('#app')
</script>

看下输出示意图:前面两个this都是app对象,而箭头函数里面的this就变成windows了

官方文档:https://cn.vuejs.org/guide/essentials/event-handling.html

https://cn.vuejs.org/api/built-in-directives.html#v-on

事件修饰符:https://cn.vuejs.org/guide/essentials/event-handling.html#event-modifiers

2.6.双向绑定 v-model

1.简单小案例

可以在表单 <input> <textarea><select> 元素上使用v-model来创建双向数据绑定

<div id="app">
    <input v-model="name" type="text" />
    <div>{{ name }}</div>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                name: '小明',
            }
        },
    })
    app.mount('#app')
</script>

文本框内容一改变,文字就改变

再看个综合的案例:列表显示学生信息 + 通过文本框输入内容,回车后添加到列表中

<div id="app">
    <input v-model="name" type="text" @keyup.enter="addStudent" />
    <div v-if="students.length==0">
        <h3>暂时没有学生</h3>
    </div>
    <ul v-else>
        <li v-for="name in students" :key="name">
            {{ name }}
        </li>
    </ul>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                name: '',
                students: ['张三', '李四', '王二']
            }
        },
        methods: {
            addStudent() {
                // 把文本框内容加到数组中
                this.students.push(this.name);
                this.name = '';// 清空文本框
            }
        },
    })
    app.mount('#app')
</script>

2.修饰符案例

  • .lazy:监听 change 事件而不是 input
    • 默认是刚输入就改变model,现在是回车、提交、事件改变后才修改model
  • .number: 将输入的合法字符串转为数字
  • .trim:移除输入内容两端空格
    <div id="app">
        <input type="text" v-model.lazy="name">
        <div>{{name}}</div>
    </div>
    <script src="../assets/vue3.js"></script>
    <script>
        const app = Vue.createApp({
            data() {
                return {
                    name: '小明'
                }
            },
        });
        app.mount('#app');
    </script>

三个合起来的案例:

<div id="app">
    <input v-model.lazy="name" type="text">
    <input v-model.number="age" type="text" />
    <input v-model.trim="rmark" type="text" />
    <div>{{name}}-{{age}}-{{rmark}}</div>
</div>
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                name: '小明',
                age: 22,
                rmark: '暂无'
            }
        },
    });
    app.mount('#app');
</script>

更多可以查看文档:https://cn.vuejs.org/guide/essentials/forms.html

https://cn.vuejs.org/api/built-in-directives.html#v-model

2.7.性能优化 v-memo(new)

这个可以理解为一种缓存,只有设定的内容修改后才会出现渲染列表 ==> 用于性能优化(有时候数据一样,再重新渲染 大列表 就太浪费了

下面做个测试:v-memo="[name,age,gender]当name、age、gender改变时会出现渲染。rmark更新后不会重新渲染列表

<div id="app">
    <!-- 三个属性有一个改变都重新渲染 -->
    <div v-memo="[name,age,gender]">
        name:{{name}},age:{{age}},gender:{{gender}},rmark:{{rmark}}
    </div>
    <div>
        <button @click="updateName">修改name</button>
        <button @click="updateRMark">修改rmark</button>
        <button @click="updateInfo">修改所有</button>
    </div>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                name: '张三',
                age: 23,
                gender: '男',
                rmark: '暂无'
            }
        },
        methods: {
            updateName() {
                this.name = '李四';
            },
            updateRMark() {
                // 修改不在v-memo列表中的内容,页面不会出现渲染
                this.rmark = '修改';
            },
            updateInfo() {
                this.name = '测试';
                this.age = 44;
                this.gender = '中';
                this.rmark = '没有';
            }
        },
    });
    app.mount('#app');
</script>

官方文档:https://cn.vuejs.org/api/built-in-directives.html#v-memo

2.8.计算属性 computed

有复杂计算的时候直接在双括号{{}}里面写复杂表达式,是不合适的,可以通过computed来处理

<div id="app">
    <p v-for="i in 3" :key="i">
        {{ name }}-{{genderStr}}-{{dataTime}}
    </p>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                name: '张三',
                gender: 1,
                time: 1670944665
            }
        },
        computed: {
            genderStr() {
                console.log('gender')
                return this.gender == 1 ? '男' : '女';
            },
            dataTime() {
                console.log('dataTime')
                const date = new Date(this.time)
                return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
            }
        },
    });
    app.mount('#app');
</script>

computed比method多了个缓存:

官方文档:https://cn.vuejs.org/guide/essentials/computed.html

2.9.侦听器 watch

watch监听数据的案例:title修改可以直接监听到,而student只修改name则监听不到了(student中三个属性全部被修改才能监听到

<div id="app">
    <h2>{{title}}</h2>
    <ul>
        <li v-for="item in student" :key="item">
            {{ item }}
        </li>
    </ul>
    <div><button @click="changeInfo">修改内容</button></div>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                title: '欢迎光临~',
                student: { id: 1, name: '张三', age: 32 },
            }
        },
        methods: {
            changeInfo() {
                this.title = '网站已经被黑!';
                this.student.name = '德行';
                this.student.age = 32;
            }
        },
        watch: {
            title(newValue, oldValue) {
                console.log(`【${oldValue}】被修改为:【${newValue}】`)
            },
            student(newValue, oldValue) {
                console.log(newValue);
            }
        },
    });
    app.mount('#app');
</script>

如果想student对象一变化就侦听到,可以使用deep: true(慎重使用,数据太多太深会影响性能)如果一开始就触发监听就使用immediate: true

如果只是想检测student里面某个值的改变可以这样写:

官方文档:https://cn.vuejs.org/guide/essentials/watchers.html

2.10.综合案例

模拟一个购物车,有这些要求:

  1. 如果购物车没有数据要提示下
    1. v-if and v-else
  2. 通过加减可以设置书籍的数量,当数量<=1的时候,减按钮不能点
    1. :disabled="disable=item.count<2"
  3. 点击移除书籍可以删掉该图书
    1. 先获取当前book的index(注意不是id)v-for="(item,index) in books"
    2. 然后通过array.splice(index, 1);删掉对应对象
  4. 底部有个价格的汇总
    1. 通过computed,遍历books,价格*数量并累加

样式

<style>
    .border {
        border-collapse: collapse;
    }

    td {
        border: 1px solid #aaa;
        padding: 10px;
    }
</style>

HTML

<div id="app">
    <table class="border">
        <tr v-for="(item,index) in books" :key="index">
            <td>{{ item.id }}</td>
            <td>{{ item.name }}</td>
            <td>{{ item.price }}</td>
            <td>
                <button :disabled="disable=item.count<2" @click="subtract(item)">-</button>
                {{ item.count }}
                <button @click="add(item)">+</button>
            </td>
            <td><button @click="del(index)">移除书籍</button></td>
        </tr>
    </table>
    <h3 v-if="books.length>0">结算金额:{{totalCompute}}</h3>
    <h3 v-else>购物车中暂时没有书籍数据!</h3>
</div>

Script:

const app = Vue.createApp({
    data() {
        return {
            books: [
                { id: 1, name: 'Net安全', price: 44.5, count: 1 },
                { id: 2, name: 'Web安全', price: 64, count: 1 },
                { id: 3, name: 'Net基础', price: 38, count: 1 },
                { id: 4, name: 'Vue学习', price: 50.1, count: 1 },
            ]
        }
    },
    methods: {
        subtract(item) {
            item.count--;
        },
        add(item) {
            // this.books[id - 1].count++;
            item.count++;
        },
        del(index) {
            this.books.splice(index, 1); //删除数组序号为index的对象
        }
    },
    computed: {
        totalCompute() {
            // let total = 0;
            // // 遍历一下books,并把每一项价格统计下
            // this.books.forEach(book => {
            //     total += book.price * book.count;
            // });
            // return total;
     		return this.books.reduce((total, book) => total + book.count * book.price, 0);
        }
    },
});
app.mount('#app');

效果


3.Vue3组件入门(过渡)

目前这一块以选项APIOptions API)为主,后面会逐步过渡到组合APIComposition API

官方文档:https://cn.vuejs.org/guide/essentials/component-basics.html

深入组件:https://cn.vuejs.org/guide/components/registration.html

3.1.组件注册

  1. 全局组件:在任何其他的组件中都可以使用的组件
    1. app.component()
  2. 局部组件:局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用
    1. components:{}

1.app根组件

先看下全局组件的案例:我们平时创建的app对象,其实就是一个全局组件:

<div id="app">
    {{title}}
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({
        data() {
            return {
                title: 'this is component test'
            }
        },
    });
    app.mount('#app');
</script>

我们换个写法和这个也一样的效果:

<div id="app">
    {{title}}
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    // vueApp对象{}
    const vueApp = {
        data() {
            return {
                title: 'this is component test'
            }
        },
    };
    const app = Vue.createApp(vueApp);
    app.mount('#app');
</script>

效果:

2.全局组件

全局组件:每个组件都可以在组件内部调用全局组件

PS:组件本身是可以有自己的代码逻辑的,比如data、computed、methods等等

我们自定义一个全局组件:app.component('组件名',{})

组件名最好是xxx-xxx的格式,Vue文件中也可以是AbcDemo的这种格式

const app = Vue.createApp({});
// 创建自定义的全局组件
app.component('my-title', {
    data() {
        return {
            title: 'this is component test'
        }
    },
    template: `<h2>{{title}}</h2>`
})
app.mount('#app');

HTML部分:

<div id="app">
    <my-title></my-title>
</div>

效果:template不出现在dom中

换种写法效果和这个一样(组件里面的template没有智能提示,放页面中就有了,这种写法后面会用

<div id="app">
    <my-title></my-title>
</div>

<!-- my-title组件的template-->
<template id="title">
    <h2>{{title}}</h2>
</template>

<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
    const app = Vue.createApp({});
    // 创建自定义的全局组件
    const myTitle = {
        data() {
            return {
                title: 'this is component test'
            }
        },
        template: '#title'
    };
    app.component('my-title', myTitle)
    app.mount('#app');
</script>

显示效果是一样的,template默认是不会显示出来的,但是dom里面是有的


3.局部组件

开发中基本上都是局部组件,还是上面的案例,我们用局部组件注册的方式改写下:

组件名一般都是xxx-xxx的格式,Vue文件中也可以是AbcDemo的这种大驼峰的命名格式

<div id="app">
    <my-title></my-title>
</div>
<script src="../assets/vue3.js"></script>
<script>
    // 根组件
    const App = {
        components: {
            // 局部组件名:对象内容
            'my-title': {
                data() {
                    return {
                        title: 'this is vue component test'
                    }
                },
                template: '<h2>{{title}}</h2>',
            },
        },
    };
    Vue.createApp(App).mount('#app'); //创建并挂载
</script>

展示效果:

进一步分离实现相同效果:

<div id="app">
    <my-title></my-title>
</div>

<template id="title">
    <h2>{{title}}</h2>
</template>

<script src="../assets/vue3.js"></script>
<script>
    // 自定义组件名
    const MyTitle = ('my-title', {
        data() {
            return {
                title: 'this is vue component test'
            }
        },
        template: '#title',
    });
    // 根组件
    const App = {
        components: {
            MyTitle, // 局部组件
        },
    };
    Vue.createApp(App).mount('#app'); //创建并挂载
</script>

展示:

3.2.单文件组件

文档代码:https://gitee.com/lotapp/BaseCode/tree/master/javascript/3.Vue/4vue3component

.vue单文件里面就三块内容:<template>放HTML、<script>放JS、<style>放CSS

PS:xxx.vue浏览器是不认识的,最后是通过webpack、vite这类打包工具进行打包,最后生成了类似我们上面写的代码风格的html

单个Vue组件中可以有更多的支持

  1. 代码高亮
  2. ES6、CommonJS的模块化能力;
  3. 组件作用域的CSS
  4. 预处理器来构建组件
    1. eg:TypeScriptBabelLessSass

说那么多,怎么去使用呢?

  1. 通过Vue CLI创建项目,所有配置都默认配置好了,我们在里面直接使用Vue文件开发即可
  2. 通过webpack、vite这类打包工具进行打包处理

1.脚手架创建项目

vue官方现在推荐使用:cnpm create vue@latest来创建。这个打包默认使用的是vite了

PS:vue-cli默认使用的是webpack,现在已经不更新了

cd vue3-demo切换到新建的目录中,然后输入cnpm install来安装一下依赖。然后运行项目cnpm run dev

PS:vscode终端中运行npm run dev(PowerShell中运行cnpm有点问题)

命令行输入:运行npm run dev编译npm run build预览npm run preview,IDE运行:点下调试选对应选项即可

我们也可以使用图形化操作,bash里面输入:vue ui,然后会自动打开一个UI页面,在里面新建或者选择项目也行

build的话会在原项目下面创建一个dist的发布文件夹,我们写的代码都打包成这里面的文件了,直接部署到服务器即可

2.目录说明

vscode打开后大致这样的感觉,我们平时在src中开发即可,index.html是主页面

main.js是主函数,里面会加载main.css以及挂载vue对象。这个导入进来的createApp方法,相当于是以前写的vue.createApp

PS:从App.vue中导入进来的App对象,相当于之前写的const App={xxx}

每一个xxx.vue都相当于一个独立的组件,里面就三个元素:<template><srcipt><style>


3.根组件案例(含详细说明)

以前我们要创建一个vue组件都是在html中写,比如:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        h2 {
            color: red;
        }
    </style>
</head>
<body>
    <div id="app">
        <h2>{{title}}</h2>
    </div>
    <script src="../../assets/vue3.js"></script>
    <script>
        const App = {
            data() {
                return {
                    title: 'this is vue component test'
                }
            },
        };
        Vue.createApp(App).mount('#app');
    </script>
</body>
</html>

运行后是这样的:


现在用Vue文件开发略有不同,我这边详细说下:

我们看下main.js:导入了vue.createApp方法、也从App.vue中导入了App对象{}

这里的#appindex.html中写了

贴一下App.vue的根组件:如果晕头转向请自己练一遍,然后就清楚了

脚手架创建后可以把不相关的vue文件删掉,只保留App.vue(把main.cssbase.css中的样式先删掉)

<template>
  <h2>{{ title }}</h2>
</template>

<script>
// 有import导入就有export导出
export default {
  data() {
    return {
      title: 'this is vue component test'
    }
  },
}
</script>

<style>
h2 {
  color: red;
}
</style>

运行项目可以手动点vscode里面的调试运行,也可以Ctrl + J打开vscode终端,命令行输入npm run dev

如果不需要调试:开发过程中也可以在项目目录下单独开一个bash,输入npm run dev,然后不关闭它,你修改它也会同步修改的

运行效果:


4.全局组件-vue文件版

这种方式平时用的不多,简单说下:

MyTitle.vue:

<script>
// 有import导入就有export导出
export default {
  data() {
    return {
      title: 'this is vue component test'
    }
  },
}
</script>

<template>
  <h2>{{ title }}</h2>
</template>

<style>
h2 {
  color: red;
}
</style>

App.vue:

<template>
  <MyTitle></MyTitle>
</template>

main.js

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import MyTitle from './components/MyTitle.vue'

const app = createApp(App)
app.component('MyTitle', MyTitle)
app.mount('#app')

效果:

5.局部组件-vue文件版

平时基本上都是这种局部组件的方式开发,还是上面的案例,贴下代码:

PS:以后我就直接贴组件.vueApp.vue,这边把相关文件都贴下

1MyTitle.vue代码:

<script>
// 有import导入就有export导出
export default {
  data() {
    return {
      title: 'this is vue component test'
    }
  },
}
</script>

<template>
  <h2>{{ title }}</h2>
</template>

<style>
h2 {
  color: red;
}
</style>

2App.vue代码:

<script>
// 1先拿到组件-导入MyTitle
import MyTitle from './components/MyTitle.vue';

export default {
  components: {
    MyTitle, // 2写下使用到的局部组件MyTitle
  },
}
</script>

<template>
  <!-- 3使用自定义组件 -->
  <MyTitle></MyTitle>
</template>

<style></style>

3main.js内容不变:

import './assets/main.css'

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

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

4index.html内容不变:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

项目运行后结果:

6.样式的作用域

之前代码其实有个问题,比如代码和局部组件一样的情况下,我App.vue里面也有个<h2>,而h2的样式其实是在MyTitle组件中设置的,但也影响到了App.vue

解决也很简单,就是在样式里面设置一个作用域:<style scoped>

这样MyTitle.vue中设置的样式就不会影响到其他组件了(其实本质就是给他设置了一个属性选择器

7.组件嵌套

父组件用了几个子组件就导入几个,不用管子组件里面套了多少层,来个组件树的案例:

App.vue:对于App.vue来说,我的子组件就这四个

AppHeader.vue

AppFlooter.vue

AppMain.vue:就一个子组件AppList

AppSideBar.vue:也只有一个子组件

AppList.vue:里面一个子组件,重复2次

AppItem.vue,来个数据展示

运行后效果:

3.3.组件通信

父子组件间的通信比较简单,父传子是通过props传递,子传父是通过$emit回传

1.父组件发信息给子组件-props★

父组件给子组件传递信息的本质就是:在HTML标签上打上自定义属性,eg:<span name="张三" age="22" gender="男">

然后子组件通过props的配置,eg:props:['name','age','gender'],知道去获取哪些自定义属性,并得到他们的值:

来个案例:StudentInfo.vue

<template>
    {{ id }}.{{ name }}-{{ age }}-{{ gender }}
</template>

<script>
export default {
    // props的作用就是接收父组件传递过来的属性
    props: ['id', 'name', 'age', 'gender']
}
</script>

<style scoped></style>

传递本质就是这种:{{ xxx }}除了在data里面找数据,也会去props里面查找

<template>
	<!-- {{ xxx }}除了在data里面找数据,也会去props里面查找 -->
    <StudentInfo id="1" name="小明" age="22" gender="男"></StudentInfo>
</template>
<script>
// 1.父组件传递信息给子组件
import StudentInfo from './components/StudentInfo.vue'
export default {
    components: {
        StudentInfo,
    },
}
</script>

但vue中有自定义属性,可以更方便处理,我们改写成灵活的写法:

<template>
    <StudentInfo :id="id" :name="name" :age="age" :gender="gender"></StudentInfo>
    <!-- <AppSon :students="objs"></AppSon> -->
</template>

<script>
// 1.父组件传递信息给子组件
import StudentInfo from './components/StudentInfo.vue'
export default {
    components: {
        StudentInfo,
    },
    data() {
        return {
            id: 1,
            name: '小明',
            age: 22,
            gender: '男'
        }
    },
}
</script>

效果:1.小明-22-男


props除了传递列表,还可以传递对象,并在这个对象里面定义数据类型、默认值、是否必传(require默认是false)等,来个案例:

PS:props中的type类型可以是:String、Number、Boolean、Array、Object、Date、Function、Symbol

AppSon.vue

<template>
    <ul>
        <li v-for="student in students" :key="student.id">
            {{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
        </li>
    </ul>
</template>

<script>

export default {
    props: {
        students: {
            type: Array,
            default: []
        },
    },
}
</script>

<style scoped></style>

App.vue

<template>
    <AppSon :students="objs"></AppSon>
</template>

<script>
import AppSon from './components/AppSon.vue';

export default {
    components: {
        AppSon,
    },
    data() {
        return {
            objs: [
                { id: 1, name: '张三', age: 22, gender: '男' },
                { id: 2, name: '李四', age: 29, gender: '女' },
                { id: 3, name: '王二', age: 32, gender: '男' }
            ]
        }
    },
}
</script>

效果:

2.子组件发信息给父组件-$emit★

子组件给父组件传递信息,其实是通过某个事件方法执行了:$emit(自定义事件,传递的值),当自定义事件触发后就会调用父方法进行数值的处理

为了方便理解,我事件名都弄不一样的:

说下流程,用户文本框里面输入内容,回车后触发子组件定义的method;这个method里面执行了$emit并把文本框清空;接着自定义的@addStudent就被触发了,从而调用父组件的方法parentMethod对数据进行进一步处理

AppSon1.vue:

<template>
    <div>
        <input v-model="name" type="text" @keyup.enter="sonMethod" />
    </div>
</template>

<script>
export default {
    data() {
        return {
            name: ''
        }
    },
    emits: ['addStudent'], // 为了代码提示和协同开发
    methods: {
        sonMethod() {
            console.log('into appson1.vue sonMethod');
            this.$emit('addStudent', this.name); // 把name值传递给组件自定义事件addStudent
            this.name = '';
        }
    },
}
</script>

<style scoped></style>

App.vue:

<template>
    <!-- 3.子组件传递信息给父组件 -->
    <span v-for="student in students" :key="student">
        {{ student }}-
    </span>
    <AppSon1 @addStudent="parentMethod"></AppSon1>
</template>

<script>
// 3.子组件传递信息给父组件
import AppSon1 from './components/AppSon1.vue';
export default {
    components: {
        AppSon1
    },
    data() {
        return {
            students: [],
        }
    },
    methods: {
        // 自定义addStudent触发后,会调用对应的parentMethod方法
        parentMethod(name) {
            console.log('into app.vue parentMethod');
            this.students.push(name);
        }
    },
}
</script>

输入小明回车,再输入小华回车,就现在这个效果:

扩展补充下:对于协同开发来说,你可能是开发的父组件,但是子组件是另一个人开发的,emit在逻辑里面,需要找半天,很麻烦。vue3提供了一种emits的参数支持,以后在定义自定义事件的时候把emits列表里面也写下,这样协同开发比较方便,而且vscode也会有对应的提示

比如,我在emits里面定义了这几个自定义事件:emits: ['addStudent','delStudent','updateStudent']

emits也可以是对象,可以对传给父组件的值进行验证,平时列表就够了,扩展可看官网:https://cn.vuejs.org/api/options-state.html#emits

那么我在vscode开发的时候,父组件在写事件的时候就会有提示了

再来个navbar的案例:TabBar.vue

<template>
    <div id="nav">
        <div :id="key" v-for="(value, key, index) in contents" @click="getContent(value)" :key="index">
            <span :class="{ active: currentIndex == index }" @click="currentIndex = index">
                {{ key }}
            </span>
        </div>
    </div>
</template>

<script>
export default {
    data() {
        return {
            currentIndex: -1,
            contents: {
                'left': '我是Left的内容',
                'center': '我是Center的内容',
                'right': '我是Right的内容'
            }
        }
    },
    emits: ['content'],
    methods: {
        getContent(value) {
            this.$emit('content', value)
        }
    },
}
</script>

<style scoped>
.active {
    font-weight: bold;
    padding: 10px;
    border-bottom: solid 2px red;
}

#nav {
    display: flex;
    background-color: rgb(255, 251, 255);
}

#left {
    flex: 1;
    text-align: center;
    padding: 10px;
    /* background-color: rgb(245, 201, 230); */
}

#center {
    flex: 3;
    text-align: center;
    padding: 10px;
    /* background-color: rgb(246, 246, 161); */
}

#right {
    flex: 1;
    text-align: center;
    padding: 10px;
    /* background-color: rgb(87, 243, 178); */
}
</style>

App.vue

<template>
    <div>
        <TabBar @content="getContent"></TabBar>
        <div id="bar-item">{{ content }}</div>
    </div>
</template>

<script>
import TabBar from './components/TabBar.vue';
export default {
    components: {
        TabBar,
    },
    data() {
        return {
            content: ''
        }
    },
    methods: {
        getContent(value) {
            this.content = value;
        }
    },
}
</script>

<style scoped>
#bar-item {
    text-align: center;
    padding: 20px;
    background-color: rgb(249 248 248);
}
</style>

效果:


3.插槽案例☆

插槽通俗讲就是:子组件预留一个坑位,父组件后期来填坑

PS:如果插槽没有放元素,则显示默认内容。如果放了内容,插槽原先的内容会被忽略掉

3.1.默认插槽

每个插槽都有一个具体的名字,当子组件里面就一个插槽的时候,我们不用写(default),vue自动帮我们处理了

SlotSimple.vue

<template>
    <div>
        <h2>slot案例:</h2>
        <slot>
            我是默认的内容
        </slot>
    </div>
</template>

<script>

export default {
    
}
</script>

<style scoped>

</style>

App.vue

<template>
    <div>
        <SlotSimple></SlotSimple>
        <SlotSimple>
            <button>slot one btn</button>
        </SlotSimple>
    </div>
</template>

<script>
import SlotSimple from './components/SlotSimple.vue';
export default {
    components: {
        SlotSimple,
    },
}

</script>

<style scoped></style>

效果:上面的父组件没填坑,就会显示子组件里默认的内容,下面的父组件放了一个按钮,则插槽内容显示自定义的button

3.2.具名插槽☆

子组件里面写下<solt name=xxx>,父组件在插槽内容外包裹一个<template>,eg:<template v-slot=xxx> or <template #xxx>

SlotName.vue:

<template>
    <div>
        <h2>slot案例:</h2>
        <slot name="default">
            我是默认的slot
        </slot>
        <slot name="center">
            我是默认的Center
        </slot>
        <slot name="right">
            我是默认的Right
        </slot>
    </div>
</template>

App.vue<template v-slot:center><template #center>效果一样

<template>
    <div>
        <SlotName>
            <template #default>
                <h3>default</h3>
            </template>
            <template v-slot:center>
                <h3>center</h3>
            </template>
            <template #right>
                <h3>right</h3>
            </template>
        </SlotName>
    </div>
</template>

<script>
import SlotName from './components/SlotName.vue';

export default {
    components: {
        SlotName,
    },
}
</script>

效果:

3.3.动态插槽名

父组件里面的插槽名也可以使用:动态插槽名,这个用的不多就不写案例了,子组件和具名一样定义,就是父组件这边v-slot:[变量名]

  1. 子组件正常定义具名插槽:<solt name=xxx>
  2. 父组件支持动态插槽名的控制:<template v-slot:[变量名]>

3.4.作用域插槽☆

说作用域插槽的时候先看一个编译作用域的概念:

比方说我现在有一个子组件TabBar.vue,这个子组件里面的data数据,父组件App.vue是不能直接访问的(ps:能相互访问还搞啥通信)

反过来也一样,子组件也不能直接访问父组件里面的data(父组件可以直接访问父组件的数据)

现在要说的是:子组件里定义了一个slot插槽,开始内容是在父组件里面书写的,父组件有数据还好,如果父组件没有插槽里面的数据呢?可以直接传过去吗? ==> 可以的,vue提供了一种便捷方法

为什么要从solt中传递参数给父组件? ==> 受编译作用域限制,拿不到子组件的变量,只能通过pros拿到

作用域插槽的本质就是:把子组件的数据传递给父组件的插槽使用。看个案例:

SlotProps.vue:把值绑定到属性上,template的name和上面一样,是唯一的

<template>
    <div>
        <template v-for="(v, k, i) in contents" :key="i">
            <slot :name="k" :k="k" :v="v" :i="i"></slot>
        </template>
    </div>
</template>

<script>

export default {
    data() {
        return {
            contents: {
                'left': '我是Left的内容',
                'center': '我是Center的内容',
                'right': '我是Right的内容'
            }
        }
    },

}
</script>

App.vuev-slot:ID名称="props" ==> 简写:#ID名称="props"

<template>
    <div>
        <SlotProps>
            <template v-slot:left="props">
                <p>{{ props.i }} - {{ props.k }} - {{ props.v }}</p>
            </template>
            <template v-slot:center="props">
                <p>{{ props.i }} - {{ props.k }} - {{ props.v }}</p>
            </template>
            <template #right="props">
                <p>{{ props.i }} - {{ props.k }} - {{ props.v }}</p>
            </template>
        </SlotProps>
    </div>
</template>

<script>
import SlotProps from "./components/SlotProps.vue";
export default {
    components: {
        SlotProps,
    },
}
</script>

效果

4.非父子组件通信

如果深层次嵌套的组件想要传递数据,靠props一层层传递会非常麻烦,vue提供了Provide(父组件提供数据)和Inject(子孙组件进行注入)和全局事件总线来实现非父子组件之间的通信

PS:真实开发中一般都是用诸如:Pinia官方推荐)、Vuex(已不更新)之类的状态管理库

4.1.依赖注入案例

来个案例,点击按钮可以修改信息,这样可以验证一下数据是否一致

先说下下树结构:我们现在不想层层传递数据,想直接把app.vue的数据传给appItem.vue

PS:依赖注入是针对父级组件与子孙级组件之间的数据传递(隔了很多层,我们不想一层层传递数据,就可以使用依赖注入)

AppItem.vueinject是一个列表

<template>
    <div>
        <h2>{{ title }}</h2>
        {{ student.name }}-{{ student.age }}-{{ student.gender }}
    </div>
</template>

<script>
export default {
    inject: ['title', 'student'] // 注入title、student
}
</script>

App.vueprovide是方法的形式(不是方法this就获取不到data里面的数据了)

<template>
    <div>
        <AppMain></AppMain>
        <button @click="changeStudent">改数值</button>
    </div>
</template>
<script>
import AppMain from './components/AppMain.vue';

export default {
    data() {
        return {
            title: '这是一个依赖注入案例',
            student: {
                name: '小明',
                age: 22,
                gender: '男'
            }
        }
    },
    components: {
        AppMain,
    },
    // 要用provide方法,不然this就不对了,获取不到data信息了
    provide() {
        return {
            title: this.title,
            student: this.student
        }
    },
    methods: {
        changeStudent() {
            this.student.name = '长孙无敌';
            this.student.age = 33;
            this.title = '网站已经被黑';
        }
    },
}
</script>

打开后是这样的,看起来貌似没有问题:

但是点击修改数值后发现:title没有被修改,student类型反而被修改了

PS:引用类型这种没有问题,简单类型你传递title: this.title相当于传递了这个:title:'这是一个依赖注入案例'所以要处理下

我们导入响应式API中的computed函数,微微修改下App.vue

<template>
    <div>
        <AppMain></AppMain>
        <button @click="changeStudent">改数值</button>
    </div>
</template>

<script>
// 导入computed函数
import { computed } from 'vue';
    
import AppMain from './components/AppMain.vue';

export default {
    data() {
        return {
            title: '这是一个依赖注入案例',
            student: {
                name: '小明',
                age: 22,
                gender: '男'
            }
        }
    },
    components: {
        AppMain,
    },
    // 要用provide方法,不然this就不对了,获取不到data信息了
    provide() {
        return {
            // 变化点在这,title不是直接复制一个简单的字段,而是计算属性
            title: computed(() => {
                return this.title;
            }),
            student: this.student
        }
    },
    methods: {
        changeStudent() {
            this.student.name = '长孙无敌';
            this.student.age = 33;
            this.title = '网站已经被黑';
        }
    },
}
</script>

再点一下就发现标题也修改了

更多可以查看文档:https://cn.vuejs.org/guide/components/provide-inject.html

4.2.事件总线案例

依赖注入必须是有子孙关系的组件通信,如果来个不相关的组件通信呢?==> 事件总线

PS:这个有点类似后端开发中MQ的订阅与发布

我们来个树结构:现在AppHeader要和AppItem进行通信

Vue2里面是有事件总线,vue3从实例中移除了$on、$off、$once方法,如果要使用事件总线可以使用官方推荐的库:mitttiny-emitter

首先安装一下mitt:cnpm i mitt,然后我们来封装一个事件总线的js库(eventBus.js):

import mitt from 'mitt';
const eventBus = new mitt();
export default eventBus;

AppHeader作为我们数据的发送方:(发布消息

<template>
    <div>
        <button @click="sendMessage">AppHeader send message</button>
    </div>
</template>

<script>
import eventBus from '../util/eventBus'

export default {
    data() {
        return {
            title: '这是一个事件总线的订阅发布案例',
            student: {
                name: '小华',
                age: 32,
                gender: '男'
            }
        }
    },
    methods: {
        sendMessage() {
            console.log('AppHeader.vue sendMessage')
            // 发送事件,传递消息
            eventBus.emit('sendData', {
                title: this.title,
                student: this.student
            });
        }
    },
}
</script>

AppItem作为信息接收方:(订阅消息unmounted一定也写下,组件销毁后事件就没必要继续订阅了

<template>
    <div v-if="title != '' && student != []">
        <h2>{{ title }}</h2>
        {{ student.name }}-{{ student.age }}-{{ student.gender }}
    </div>
</template>

<script>
import eventBus from '../util/eventBus';

export default {
    data() {
        return {
            title: '',
            student: {}
        }
    },
    methods: {
        getMessage(data) {
            console.log('AppItem.vue getMessage');
            this.student = data.student;
            this.title = data.title;
        }
    },
    created() {
        console.log('AppItem.vue created');
        eventBus.on('sendData', this.getMessage); // 订阅事件sendData,触发后调用getMessage
    },
    unmounted() {
        console.log('AppItem.vue mounted');
        eventBus.off('sendData', this.getMessage); // 组件销毁的时候取消事件订阅
    },
}
</script>

运行后的效果:

当我们触发事件发布时:信息正常被发送并接收了

3.4.生命周期★

每个组件都会经历:创建挂载更新卸载等系列的过程,我们可以在对应阶段做一些操作(eg:create阶段可以异步加载数据)

Vue提供了一些生命周期的回调函数(钩子函数)https://cn.vuejs.org/api/composition-api-lifecycle.html

1.简单说明

  1. beforecreate // 执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
    1. eg:App.vue中有个<AppItem>,vue自己初始化完毕,准备去创建<AppItem>实例,但是还没创建
  2. created★ // 组件实例(js对象)创建完毕,各种数据可以使用,常用于异步数据获取、事件监听、this.$watch()
  3. eg:<AppItem>组件实例已经创建了,但是里面<template>还没编译
  4. beforeMounted // 未执行渲染、更新,dom未创建
    1. eg:这时候<AppItem><template>内容已经有了,但是还没挂载到App.vue上(还没挂载到虚拟DOM)
  5. mounted ★// 初始化(挂载)结束,dom已创建,可用于获取访问数据和dom元素
    1. eg:已经挂载到虚拟DOM上,并且生成了真实的DOM(用户可以看到HTML元素了)
  6. beforeupdate // 更新前,可用于获取更新前各种状态
    1. 当我们数据发生改变的时候,会通过DIFF算法,重新渲染和更新DOM
  7. updated // 更新后,所有状态已是最新
    1. 在数据更新完毕,重新进入mounted 挂载之前,会进入updated
  8. beforeUnmount // 销毁前,可用于一些定时器或订阅的取消(组件还在
    1. PS:当组件显示与否通过v-if控制时,不显示的时候就会把组件销毁,在销毁前进入这个函数
  9. unmounted★ // 组件已销毁,可用于一些定时器或订阅的取消(组件不在了
    1. 已经移除掉组件的虚拟DOM了,组件实例会被销毁掉

更多参考官方文档:https://cn.vuejs.org/guide/essentials/lifecycle.html

生命周期钩子:https://cn.vuejs.org/api/composition-api-lifecycle.html

2.案例演示

App.vue:

<template>
    <div>
        <div>
            <button @click="b = !b">是否显示组件</button>
        </div>
        <template v-if="b">
            <AppItem :count="count">
                <button @click="addCount">count++</button>
            </AppItem>
        </template>
    </div>
</template>

<script>
import AppItem from './components/AppItem.vue';

export default {
    components: { AppItem },
    data() {
        return {
            b: false,
            count: 0
        }
    },
    methods: {
        addCount() {
            this.count++;
        }
    }
}
</script>

AppItem.vue

<template>
    <div>
        {{ count }}&nbsp;<slot></slot>
    </div>
</template>

<script>
export default {
    props: ['count'],
    beforeCreate() {
        console.log('AppItem beforeCreate');
    },
    created() {
        console.log('AppItem created');
    },
    beforeMount() {
        console.log('AppItem beforeMount');
    },
    mounted() {
        console.log('AppItem mounted');
    },
    beforeUpdate() {
        console.log('AppItem beforeUpdate');
    },
    updated() {
        console.log('AppItem updated');
    },
    beforeUnmount() {
        console.log('AppItem BeforeUnmount');
    },
    unmounted() {
        console.log('AppItem Unmounted');
    },
}
</script>

运行后因为v-if=false,所以AppItem没有加载:

我们现在点一下按钮:AppItem组件被创建并挂载了

我们点3下count++按钮就会触发三次数据修改函数(AppItem组件销毁前基本上都beforeUpdate、updated中来回反复)

我们再点下是否显示组件的按钮,这时候v-if又变成false了,AppItem组件就被卸载了

3.5.组件ref引用☆

$refs案例

$refs场景:有些时候组件中想要直接获取到元素对象、子组件实例(比如获取元素的宽高)【很少用到】

Vue不推荐进行原生DOM操作 ==> 给元素或者组件绑定一个ref的attribute属性

来个例子:把dom元素里面加个属性:ref="名字",然后就可以通过:this.$refs.名称获取到dom

AppItem.vue

<template>
    <div>
        <input v-model="title" type="text" ref="title" />
        <button ref="btn" @click="getRefs">提交</button>
    </div>
</template>

<script>

export default {
    data() {
        return {
            title: 'title'
        }
    },
    methods: {
        getRefs() {
            console.log(this.$refs.title);
            console.log(this.$refs.btn);
        }
    },
}
</script>

效果:提交的时候就可以获取到input的dom和button的dom对象

如果ref绑定在组件上,可以获取组件的实例,通过这个实例可以调用里面的方法、字段,也可以获取里面的dom($el

获取组件DOMthis.$refs.item.$el获取组件数据this.$refs.item.xxx调用组件方法this.$refs.item.xxx();

AppItem.vue还是上面的代码

<template>
    <div>
        <input v-model="title" type="text" ref="title" />
        <button ref="btn" @click="getRefs">提交</button>
    </div>
</template>

<script>

export default {
    data() {
        return {
            title: 'title'
        }
    },
    methods: {
        getRefs() {
            console.log(this.$refs.title);
            console.log(this.$refs.btn);
        },
    },
}
</script>

App.vue修改了下:

<template>
    <div>
        <button @click="callItem">ref调用AppItem</button>
        <AppItem ref="item"></AppItem>
    </div>
</template>

<script>
import AppItem from './components/AppItem.vue';

export default {
    components: { AppItem },
    methods: {
        callItem() {
            console.log(this.$refs.item.$el);
            console.log(this.$refs.item.title);
            this.$refs.item.getRefs();
        }
    },
}
</script>

效果:

扩展:\(parent、\)root

this.$parent ==> 获取父组件实例this.$root ==> 获取根组件实例(Vue3中已经删掉$children

PS:这个用的不多,和上面ref获取组件差不多,eg:this.$parent.$el

3.6.动态组件

1.切换组件的传统开发

讲动态组件之前先来个案例:现在有三个按钮,按钮名对应着同名的组件,我们点击按钮的时候下面显示对应的组件内容+按钮字变红

PS:我们平时如果要实现多组件切换,基本上都是通过:v-if、v-else-if 、v-else来实现:

AppLeft、AppCenter、AppRight代码差不多,就H2这边名字不一样:

<template>
    <div>
        <h3>AppCenter</h3>
        <ul>
            <li v-for="item in students" :key="item.id">
                {{ item.id }}-{{ item.name }}-{{ item.Age }}-{{ item.gender }}
            </li>
        </ul>
    </div>
</template>

<script>

export default {
    props: ['students']
}
</script>

<style scoped></style>

App.vue

<template>
    <div>
        <button :class="{ active: currName == name }" v-for="name in  names " :key="name" @click="changeTab(name)">
            {{ name }}
        </button>
        <template v-if="currName == 'AppLeft'">
            <AppLeft :students="students"></AppLeft>
        </template>
        <template v-else-if="currName == 'AppCenter'">
            <AppCenter :students="students"></AppCenter>
        </template>
        <template v-else>
            <AppRight :students="students"></AppRight>
        </template>
    </div>
</template>

<script>
import AppCenter from './components/AppCenter.vue';
import AppLeft from './components/AppLeft.vue';
import AppRight from './components/AppRight.vue';

export default {
    data() {
        return {
            names: ['AppLeft', 'AppCenter', 'AppRight'], // 组件名称
            currName: 'AppLeft', // 相当于索引
            students: [ // 传递给子组件的数据
                { id: 1, name: '小明', age: 22, gender: '男' },
                { id: 2, name: '小华', age: 33, gender: '女' },
                { id: 1, name: '小花', age: 28, gender: '男' },
            ]
        };
    },
    methods: {
        changeTab(name) {
            this.currName = name; // 把当前tab名赋予currName,这样active类就生效了
        }
    },
    components: { AppLeft, AppCenter, AppRight } // 子组件局部注册
}
</script>

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

运行效果:点击谁就显示谁

2.切换组件-Vue动态组件

这种if else的方式判断数据少还好,数据多了能写死,vue提供了一种动态组件的方式

语法:<component :is="tabs[currentTab]"></component>

被传给 :is 的值可以是:1.被注册的组件名(全局、局部)2.导入的组件对象

不用改任何代码,就是把一系列的判断改成了:<component :is="currName" :students="students"></component>

App.vue:is="组件名":student="students"是父组件传给子组件的数据

<template>
    <div>
        <button :class="{ active: currName == name }" v-for="name in names" @click="changeTab(name)" :key="name">
            {{ name }}
        </button>
        <component :is="currName" :students="students"></component>
    </div>
</template>

<script>
import AppCenter from './components/AppCenter.vue';
import AppLeft from './components/AppLeft.vue';
import AppRight from './components/AppRight.vue';


export default {
    data() {
        return {
            names: ['AppLeft', 'AppCenter', 'AppRight'],
            currName: 'AppLeft',
            students: [
                { id: 1, name: '小明', age: 22, gender: '男' },
                { id: 2, name: '小华', age: 33, gender: '女' },
                { id: 1, name: '小花', age: 28, gender: '男' },
            ]
        };
    },
    methods: {
        changeTab(name) {
            this.currName = name;
        }
    },
    components: { AppLeft, AppCenter, AppRight }
}
</script>

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

运行效果一样:点击谁就显示谁

3.7.keep-alive☆

1.组件切换后被销毁

keep-alive相当于是一个组件的状态缓存,我们先看个例子,看完案例就知道为什么引入keep-alive

AppLeft、AppCenter、AppRight内容差不多,就名称不一样:我们写两个生命周期的函数来监听

<template>
    <div>
        <h3>Appxxx:{{ count }}</h3>
        <button @click="count++">count++</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            count: 0
        }
    },
    created() {
        console.log('Appxxx created');
    },
    unmounted() {
        console.log('Appxxx unmounted');
    }
}
</script>

<style scoped></style>

App.vue:用的还是动态组件<component :is="组件名"></component>

<template>
    <div>
        <button :class="{ active: currName == name }" v-for="name in names" @click="changeTab(name)" :key="name">
            {{ name }}
        </button>
        <component :is="currName"></component>
    </div>
</template>
<script>
import AppLeft from './components/AppLeft.vue';
import AppRight from './components/AppRight.vue';
import AppCenter from './components/AppCenter.vue';

export default {
    data() {
        return {
            names: ['AppLeft', 'AppCenter', 'AppRight'],
            currName: 'AppLeft'
        };
    },
    methods: {
        changeTab(name) {
            this.currName = name; // 当前选项卡名称赋值给currName
        }
    },
    components: { AppLeft, AppCenter, AppRight }
}
</script>

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

打开后AppLeft组件已经被创建,我们点3下按钮,AppLeft中的count值为3

切换到AppCenter组件:控制台提示AppLeft已经被卸载了

我们重新切换会AppLeft,发现count=3已经没了,又变成初始的0了。。。

2.keep-alive来缓存

试想一下,我们如果是实际使用中写个复杂表单,写大半了,不小心切换了下标签,内容直接没了,是什么心情?==> keep-live即可解决

上面演示的代码不用修改,就是在原有基础上增加一个<KeepAlive>即可

效果:组件不会被卸载,而且里面的值切换后还在(不会重置,而是被缓存了)

切换过来内容还是在的,且组件都没有被销毁

如果只想要某个选项缓存,其他不缓存,可以使用include - string | RegExp | Array(名字匹配就会被缓存)

  1. 字符串:分割要用,eg:include="名称1,名称2"
  2. 正则include前面要加个:eg::include="/名称1|名称2/"
  3. 数组include前面要加个:eg::include="[名称1,名称2]"

AppVue:动态组件外面包裹一下即可:<KeepAlive include="left,center">动态组件</KeepAlive>

这个名称直接写组件名是没用的,而是在组件里面定义的name:(name与name直接就,间隔,别额外加个空格什么的)

还有几个属性:

  1. exclude - string | RegExp | Array:名字匹配的不缓存,其他都缓存(也是字符串、正则、数组)
  2. max - number | string:最多可以缓存多少组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁

3.缓存组件生命周期

对于缓存的组件来说,再次进入时,我们是不会执行created或者mounted等生命周期函数的,KeepAlive提供了两个钩子:activateddeactivated

在center里面加上两个钩子(其他还是上面的案例,代码不变)

效果

3.8.异步组件

异步组件一般两个用途:第一个:异步加载服务器组件,第二个:把组件和项目分包

官方详细文档:https://cn.vuejs.org/guide/components/async.html

平时我们编译的时候都是整体打一个包,但有些场景下是需要分别打包的

PS:有时候都放一个包里面会导致首页这类内容多的页面特别卡,有分包需求

先看个正常案例:

<!-- App.vue -->
<template>
    <div>
        <AppHeader></AppHeader>
        <AppItem></AppItem>
    </div>
</template>

<script>
import AppItem from './components/AppItem.vue';
import AppHeader from './components/AppHeader.vue';

export default {
    components: {
        AppItem,
        AppHeader
    }
}
</script>

子组件没写什么内容,就分别写了一个文本

build之后:整体打包成一个js文件了

如果我们要把header单独打包呢?==> 这时候异步组件用处就来了

我们先导入defineAsyncComponent,然后处理下导入的对象并在父组件中注册一下

import { defineAsyncComponent } from 'vue';

const appHeaderAsync = defineAsyncComponent(() => {
    // 异步加载服务器的组件、分包打包项目某个组件
   return import('./components/AppHeader.vue');
});

export default {
    components: {
        AppHeader: appHeaderAsync // 注册下
    }
}

贴一下App.vue

<template>
    <div>
        <AppHeader></AppHeader>
        <AppItem></AppItem>
    </div>
</template>

<script>
import AppItem from './components/AppItem.vue';

import { defineAsyncComponent } from 'vue';

const appHeaderAsync = defineAsyncComponent(() => import('./components/AppHeader.vue'));

export default {
    components: {
        AppItem,
        AppHeader: appHeaderAsync
    }
}
</script>

重新npm run build之后:appheader被单独打包了

预览效果:npm run preview

的确变成两个js文件了

异步组件平时项目用的不多(用路由懒加载),还有一些扩展内容可以看官方文档

3.9.组件的v-model★

1.语法糖探究

单独拎出来讲肯定是和之前v-model不一样的,我们先不管他,按照之前使用习惯来做个测试:

App.vue里面对AppItem子组件绑定了一个title

然后我们去appItem中使用:

打开浏览器后发现vue提示我们没在AppItem中定义

其实v-model就是一个语法糖,我们写v-model="title",相当于写成了这样:

<AppItem :modelValue="title" onUpdate:modelValue=回调函数 >

这段语句相当于我们自己通过属性传参,然后定义一个自定义方法用来接收子组件$emit('自定义方法',数据)的值

2.v-model实现的本质

v-model可以双向绑定,那么必然可以相互间的通信,那就可以简化步骤:

  1. 父组件先把数据传递给子组件 ==> 属性传值
    1. eg:父组件::title="title"、子组件:props: ['title']
  2. 子组件得到数据并展示出来,当子组件修改数据后再把数据重新传给父组件,eg:
    1. 父组件:@myEvent="updateTitle"定义一个自定义事件myEvent,并定义一个事件触发的处理函数
    2. 子组件:this.$emit('myEvent', 新数据);(加上:emits: ['myEvent']
  3. 父组件接收到新数据后,对data中的变量重新赋值
    1. eg:updateTitle(data){ this.title = data; }

代码比较简单,截个图:

刚才说v-model是语法糖,相当于vue帮我们写了这个:<AppItem :modelValue="title" Update:modelValue=回调函数 >

PS:相当于上面代码不变的情况下,只要把我们之前定义的:title换成:modelValue,自定义事件@myEvent换成Update:modelValue就行了

还是上面代码,我们改写下:App.vue

<template>
    <div>
        <!-- <AppItem :title="title" @myEvent="updateTitle"></AppItem> -->
        <AppItem :modelValue="title" @update:modelValue="updateTitle"></AppItem>
    </div>
</template>
<script>
import AppItem from './components/AppItem.vue';
export default {
    components: { AppItem },
    data() {
        return {
            title: '我是一个标题'
        }
    },
    methods: {
        updateTitle(data) {
            console.log('App update:', data);
            this.title = data;
        }
    },
}
</script>

AppItem.vue

<template>
    <div>
        <h2> {{ modelValue }}</h2>
        <button @click="updateTitle">修改title</button>
    </div>
</template>

<script>
export default {
    props: ['modelValue'],
    emits: ['update:modelValue'],
    methods: {
        updateTitle() {
            console.log('AppItem update');
            this.$emit('update:modelValue', '我是被AppItem修改过的值');
        }
    },
}
</script>

点击后效果:

上面案例就是v-model的本质了,我们现在可以使用v-model进一步简化App.vue中自定义组件AppItem的写法:

<AppItem :modelValue="title" @update:modelValue="title = $event"></AppItem>

App.vue:基本上没变,就是把:modelValue="title"换成了v-model="title"

回调函数这边我就简写了:title = $event(回调函数上面也可以这么写,我上面只是为了你进一步理解才分开写的)

<template>
    <div>
        <!-- <AppItem :title="title" @myEvent="updateTitle"></AppItem> -->
        <!-- <AppItem :modelValue="title" @update:modelValue="title = $event"></AppItem> -->
        <AppItem v-model="title" @update:modelValue="title = $event"></AppItem>
    </div>
</template>
<script>
import AppItem from './components/AppItem.vue';
export default {
    components: { AppItem },
    data() {
        return {
            title: '我是一个标题'
        }
    },
}
</script>

AppItem还是之前的内容:

3.父组件中多个子组件

再继续深究,如果我父组件中有多个AppItem,我又该怎么写呢?

AppItem无需修改:props中写下modelValue,emit里面写update:modelValue(只要写一份就行了)

我们来看下App.vue

<template>
    <div>
        <AppItem v-model="title" @update:modelValue="title = $event"></AppItem>
        <AppItem v-model="title2" @update:modelValue="title2 = $event"></AppItem>
        <AppItem v-model="title3" @update:modelValue="title3 = $event"></AppItem>
    </div>
</template>
<script>
import AppItem from './components/AppItem.vue';
export default {
    components: { AppItem },
    data() {
        return {
            title: '我是一个标题',
            title2: '我是另一个标题',
            title3: '我是第三个标题'
        }
    },
}
</script>

分别点击都可以生效:

4.自定义v-model名称

如果就要像之前我们自己手写各种自定义名称呢?

vue其实也有提供方法:v-model:自定义名称

PS:现在不奇怪为什么自定义事件是个update:modelValue了吧,默认的v-model,其实就是v-model:modeValue

看案例:AppItem.vueprops: ['title']$emit('update:myEvent', 内容)

<template>
    <div>
        <h2>{{ title }}</h2><button @click="changeTitle">修改标题</button>
    </div>
</template>

<script>
export default {
    props: ['title'],
    emits: ['update:myEvent'],
    methods: {
        changeTitle() {
            this.$emit('update:myEvent', '这个AppItem修改的内容');
        }
    },
}
</script>

App.vue<AppItem v-model:title="title" @update:myEvent="title = $event">

<template>
    <div>
        <AppItem v-model:title="title" @update:myEvent="title = $event"></AppItem>
    </div>
</template>

<script >
import AppItem from './components/AppItem.vue';
export default {
    components: { AppItem },
    data() {
        return {
            title: '这是一个标题'
        }
    },
}
</script>

点击后就可以修改了:

5.子组件中绑定多个v-model

那么已经有了别名了,是不是可以绑定多个v-model呢?==> 可以的 (平时很少这样干,完全可以传递个对象或者数组过去)

AppItem.vue:如果是整体批量处理可以在子组件定义一个自定义事件即可

<template>
    <div>
    {{ name }} - {{ age }} - {{ gender }}
    <button @click="changeData">修改内容</button>
    </div>
</template>

<script>
export default {
    props: ['name','age','gender'],
    emits: ['update:name','update:age','update:gender'],
    methods: {
        changeData(){ // 如果是这种整体修改的,可以用一个自定义事件统一处理
            this.$emit('update:name','王二');
            this.$emit('update:age',33);
            this.$emit('update:gender','女');
        },
    },
}
</script>

App.vue:实际使用中往往是传个对象或者数组过去,很少这么传的

<template>
    <div>
        <AppItem v-model:name="name" v-model:age="age" v-model:gender="gender" 
        	@update:name="name = $event" @update:age="age = $event"
                 @update:gender="gender = $event"></AppItem>
    </div>
</template>

<script >
import AppItem from './components/AppItem.vue';
export default {
    components: { AppItem },
    data() {
        return {
            title: '这是一个标题',
            name: '张三',
            age: 22,
            gender: '男'
        }
    },
}
</script>

3.10.混入Mixins

目前我们是使用组件化的方式在开发整个Vue的应用程序,但是组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。

在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成,Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能(这块以前Vue2中大量使用,Vue3基本不用 ==> 使用setup函数)

  1. 一个Mixin对象可以包含任何组件选项:

  2. 当组件使用Mixin对象时,所有Mixin对象的选项将被混合进入该组件本身的选项中

说明:Mixins 在 Vue3 支持主要是为了兼容Vue2和生态中其他库。在新的应用中应避免使用 mixin,特别是全局mixin

这边简单说下案例:我们先在混入对象中配置一些函数和字段(平时怎么用,这个里面都可以写)

App.vue:注册一下AppItem

AppItem组件里面该怎么样还是怎么用,如果字段和方法和混入里面的重名了,vue会以组件为准

效果:msg我组件里面并没有定义,方法也没有些,但是通过myMixin配置下就都有了

Vue3基本上不用这个,其他内容我一笔带过:

如果mixin里面的内容和组件重名了,组件本身的优先级更高

生命周期函数比较特殊:会都放在一个数组里面,最后mixin中的生命周期函数和组件的都会调用

也可以放全局,让每个组件都混入,但Vue3不推荐这么做,所以就不介绍了

官方文档https://v2.cn.vuejs.org/v2/guide/mixins.html


4.Vue3组件开发(常用)★

组合API代码演示:https://gitee.com/lotapp/BaseCode/tree/master/javascript/3.Vue/5vue3composition

之前为了从过渡和兼容Vue2,从html单文件混合开发,到选项API的Vue单文件开发,现在该回归Vue官方推荐的开发方式了 ==> 组合APIComposition APIsetup函数

★注意setup中不要使用this,setup属性特点:定义数据 + 函数 然后以对象方式return

setup函数主要有两个参数:propscontext

  1. props:父组件传递过来的属性会放到props中
  2. context:SetupContext中包含三个属性:
    1. attrs:所有非prop传递的attribute属性
    2. slots:父组件传递过来的插槽
    3. emit:子组件传递数据给父组件需要用到,只不过不再是this.$emit了,直接context.emit

4.1.组合API引入

先不谈性能方面的优化,光开发方面我们看个案例就知道:来个官方经常说的计数器案例

PS:创建新项目:cnpm create vue@latest,安装依赖:cnpm install,运行:cnpm run dev

先看之前vue2风格的Options API的代码:

<template>
  <div>
    {{ count }}&nbsp;<button @click="addCount">count++</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    addCount() {
      this.count++;
    }
  },

}
</script>

新的Composition API

<template>
  <div>
    <!-- 模板这边vue会帮我们自动解包,就不用写count.value了 -->
    {{ count }}&nbsp;<button @click="addCount">count++</button>
  </div>
</template>

<script>
// 从vue中导入ref方法
import { ref } from 'vue';

export default {
  setup() {
    // 把数据包裹一下就变成响应式的数据了
    let count = ref(0);
    const addCount = () => {
      count.value++; // 被包裹后获取里面的值需要通过.value来获取
    };
    // 把相关定义的实例返回
    return {
      count,
      addCount
    }
  }
}
</script>

点三下的效果都一样:

image-20230807093851104

选项API开发逻辑是分散开的,而组合API是可以把一块逻辑放在一起的,setup函数官方还提供了语法糖,我们看下对比:

模板方面都一样:

image-20230807095750536

现在只是一个简单的事件,后期如果有computed、watch、生命周期钩子,一个count相关的逻辑可能就分散到N个地方,后期维护和协同开发都会有影响。

组合API可以把相关逻辑都放在一段代码里面处理,可以像一个函数一样去编程,后期维护都会非常方便,官方提供的语法糖更是简化了这个过程

image-20230807100042623

PS:setup中的返回值,就相当于之前的data选项中数据,都是为了给template使用的


扩:ES6的导入和导出★

本段演示代码:https://gitee.com/lotapp/BaseCode/tree/master/javascript/1.ES6/es6-demo

1.导入导出语法

ES6模块化规范中定义:ES6模块化规范浏览器端和服务器端通用的模块化开发规范

  1. 每一个JS文件都是一个独立的模块
  2. 导入其它模块成员使用 import 关键字
  3. 向外共享模块成员使用 export 关键字

ES6的模块化主要就是这3种用法:

  1. 默认导出默认导入
    1. 默认导出语法:export default 默认导出的成员
    2. 默认导入语法:import 接收名称 from '模块标识符'
  2. 按需导出按需导入
    1. 按需导出语法:export 导出的属性或方法
    2. 按需导入语法:import {对应的属性名或方法} from '模块标识符'
  3. 直接导入执行模块中的代码
    1. 直接导入并执行:import '模块标识符'

2.默认导出、导入

来个案例,我们创建一个空的vue项目,然后演示下(不相关的东西都可以删掉)

默认导出语法:export default 默认导出的成员

// util/export_test.js
let name = '小明';
let age = 22;
function show() {
    console.log(name, age)
    // console.log(this); // {name: '小明', show: ƒ}
}
// 公开name和show方法
export default {
    name,
    show
}

默认导入语法:import 接收名称 from '模块标识符'

<script setup>
// App.vue
// 1.默认导入
import test from './util/export_test'; // test可以,test1也可以,随你

console.log(test); // 里面没有age,因为age没有被导出
console.log(test.name); // 输出name值
test.show(); // 调用show方法
</script>

<template></template>

输出:test里面没有age,因为age没有被导出

image-20230812164031829

注意事项
  1. 每个模块中,只允许使用唯一的一次 export default,否则会报错
  2. 默认导入时的接收名称可以是任意合法名称(不要以数字开头)

3.按需导出、导入案例

按需导出语法:export 导出的属性或方法

//export_need.js
export let name = '小明';
let age = 22;
export let gender = '男';
export function show() {
    console.log(name, age, gender);
}

按需导入语法:import {对应的属性名或方法} from '模块标识符'

<script setup>
// App.vue
// 2.按需导入
import {name,show} from './util/export_need'; // 我只需要name和show,gender我用不到可以不导入
console.log(name);
show();
</script>

<template></template>

输出:

小明
小明 22 男
注意事项
  1. 每个模块中可以使用多次按需导出
  2. 按需导入的成员名称必须和按需导出的名称保持一致
  3. 按需导入时,可以使用 as 关键字进行重命名
  4. 按需导入可以和默认导入一起使用

还是上面的案例,我们给show方法取个别名info:as

import {name,show as info} from './util/export_need'; // 我只需要name和show,gender我用不到可以不导入
console.log(name);
info();

按需导入可以和默认导入一起使用

//export_need.js
export let name = '小明';
let age = 22;
export let gender = '男';
export function show() {
    console.log(name, age, gender);
}
// 默认导出name和show
export default {
    name,
    show
}

App.vue

<script setup>
// 2.按需导入
import need from './util/export_need';
import {  gender } from './util/export_need';

console.log(need.name);
console.log(gender);
need.show();
</script>

<template></template>

输出:

小明
男
小明 22 男
4.直接导入案例

直接导入执行模块中的代码:import '模块标识符'

// util/exec_test.js
let name = '小明';
let age = 22;
console.log(name, age)

App.vue:里面就导入下即可:import './util/exec_test'

输出:小明 22


4.2.响应式数据

还是上面的例子,如果我们不导入ref方法,直接当函数使用,会发生什么事情呢?

image-20230807103642746

效果:显示是没有问题的,修改后值也是变了,但是页面并没有重新渲染(vue不知道这个count需要重新渲染)

image-20230807103534975

1.ref函数★

1.1.让数据变成响应式★

使用ref包裹后有个东西需要注意,js中如果要使用包裹的数据需要从value中获取(template正常使用,只要是ref对象,vue都会自动解包

image-20230807104856345

setup语法糖

<template>
    <div>
        {{ count }}&nbsp;<button @click="addCount">count++</button>
    </div>
</template>

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

let count = ref(0);
const addCount = () => {
    console.log(`++之后:${count.value}`);
    count.value++;
    console.log(`++之后:${count.value}`);
}
</script>
1.2.模板引用-ref属性 ☆

注意:ref属性前面不用加:

1.获取普通元素dom

之前说ref是获取dom的一种方式,现在ref函数也是可以的(一般不使用),来个简单演示:

在setup函数里面先定义inputTextRef的对象,然后在元素或者组件中绑定ref属性,ref的属性值就是刚刚定义的inputTextRef

<template>
    <div>
        <input type="text" ref="inputTextRef">
    </div>
</template>

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

export default {
    setup() {
        let inputTextRef = ref(null);
        onMounted(() => {
            // setup中获取不到dom,mounted就可以了
            console.log(inputTextRef.value);
        })

        return { inputTextRef }
    }
}
</script>

演示:image-20230807165339088

setup语法糖

<template>
    <div>
        <input type="text" ref="inputTextRef">
    </div>
</template>

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

let inputTextRef = ref(null);

onMounted(() => {
    // setup中获取不到dom,mounted就可以了
    console.log(inputTextRef.value);
})
</script>
2.获取子组件dom

之前是通过,$refs.ref名.$el获取组件DOM、$refs.ref名.xxx获取组件数据、$refs.ref名.xxx()调用组件方法

现在方法和这个不太一样了,我们一起看下:App.vue:

<script setup>
import { ref, onMounted } from 'vue';
import AppItem from './components/AppItem.vue';

const itemRef = ref();

onMounted(() => {
    console.log(itemRef.value);
    console.log(itemRef.value.title);
    itemRef.value.updateTitle();
})
</script>

<template>
    <div>
        <!-- 这个地方是ref,而不是`:ref` -->
        <AppItem ref="itemRef"></AppItem>
    </div>
</template>

AppItem.vue:

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

let title = ref('this is title')
const updateTitle = () => {
    title.value = '这是一个新标题'
}
// // 暴露出去哪些内容
// defineExpose({
//     title,
//     updateTitle
// })
</script>

<template>
    <div>
        {{ title }}&nbsp;<button @click="updateTitle">修改标题</button>
    </div>
</template>

默认是没有任何东西,而且也不能访问子组件的属性和方法的(vue默认不让访问

image-20230808145524897

vue也提供了一种手动暴露的方法:defineExpose()

默认情况下在<script setup>语法糖下组件内部的属性和方法是不开放给父组件访问的, 可以通过defineExpose编译宏指定哪些属性和
方法允许访问

我们把上面注释掉的部分正常执行:其他部分不用修改

image-20230808145323915

效果:能访问到title和调用updateTitle方法了(不用点按钮就能程序调用方法)

image-20230808145656125

2.reactive函数★

data选项、ref函数其实内部都是使用的reactive函数,reactive不支持简单数据类型,只针对复杂类型

PS:平时基本上都是使用ref,ref即支持简单类型又支持复杂类型

来个测试案例:如果包裹简单数据类型,控制台会提示不能生成响应式数据

image-20230807111518148

还是上面的案例,如果硬是要用reactive变成响应式数据也可以,就是使用起来比较麻烦:

<template>
    <div>
        {{ count.count }}&nbsp;<button @click="addCount">count++</button>
    </div>
</template>

<script>
import { reactive } from 'vue';

export default {
    setup() {
        let count = reactive({
            count: 0
        });
        const addCount = () => {
            console.log(`++之后:${count.count}`);
            count.count++;
            console.log(`++之后:${count.count}`);
        }
        return { count, addCount }
    }
}
</script>

效果一样,就是调用起来太麻烦,template部分vue不好自动解包,需要手动调用

image-20230807112847500

3.复杂类型案例

3.1.reactive函数

reactive可以用于复杂类型,但不能用于简单数据类型,来个例子:

<template>
    <div>
        {{ student.name }}-{{ student.age }}&nbsp;<button @click="updateName">updateName</button>
    </div>
</template>

<script>
import { reactive } from 'vue';

export default {
    setup() {
        let student = reactive({
            name: '小明',
            age: 23
        });
        const updateName = () => {
            console.log('修改之前:', student.name);
            student.name = '小华';
            console.log('修改之后:', student.name);
        }
        return { student, updateName }
    }
}
</script>

效果:

image-20230807113300981

setup语法糖

<template>
    <div>
        {{ student.name }}-{{ student.age }}&nbsp;<button @click="updateName">updateName</button>
    </div>
</template>

<script setup>
import { reactive } from 'vue';

let student = reactive({
    name: '小明',
    age: 23
})

const updateName = () => {
    console.log('修改之前:', student.name);
    student.name = '小华';
    console.log('修改之后:', student.name);
}
</script>
3.2.ref函数

ref也支持复杂类型,上面的案例用ref就是这样的:template没有变化,就是下面js部分要加value(开发中基本上还是以ref为主

<template>
    <div>
        {{ student.name }}-{{ student.age }}&nbsp;<button @click="updateName">updateName</button>
    </div>
</template>

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

export default {
    setup() {
        let student = ref({
            name: '小明',
            age: 23
        });
        const updateName = () => {
            console.log('修改之前:', student.value.name);
            student.value.name = '小华'; // student.value
            console.log('修改之后:', student.value.name);
        }
        return { student, updateName }
    }
}
</script>
3.3.应用场景

效果同上,是不是有点疑惑,复杂数据不是reactive更方便吗?怎么说ref用的最多呢?==> 数据如果是从服务器获取的,ref更方便,如果是本地表单提交(本地数据+数据有些关联),可以使用reactive

本地+数据有关联 ==> reactive,其他时候 ==> ref

来个模拟加载服务器数据的案例:

<template>
    <div>
        <ul>
            <li v-for="student in students" :key="student.id">
                {{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
            </li>
        </ul>
    </div>
</template>

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

export default {
    setup() {
        let students = ref([]);
        // 模拟后端数据获取
        onMounted(() => {
            students.value = [
                { id: 1, name: '张三', age: 22, gender: '男' },
                { id: 2, name: '李四', age: 33, gender: '男' },
                { id: 3, name: '王二', age: 34, gender: '女' }
            ];
        });
        return { students };
    }
}
</script>

ref对象有个value属性,我们可以直接把获取的数据赋值给它,而reactive则需要自己处理

image-20230807120151443

setup语法糖:template部分不变

<template>
    <div>
        <ul>
            <li v-for="student in students" :key="student.id">
                {{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
            </li>
        </ul>
    </div>
</template>

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

let students = ref([]);
// 模拟后端数据获取
onMounted(() => {
    students.value = [
        { id: 1, name: '张三', age: 22, gender: '男' },
        { id: 2, name: '李四', age: 33, gender: '男' },
        { id: 3, name: '王二', age: 34, gender: '女' }
    ];
});
</script>

4.readonly函数

来个场景:父组件里面好几个子组件,父组件给他们传递了个数据,这时候有个子组件把数据直接改了,这时候有两个问题:

这个其实是不合理的,修改数据应该emit提交给父组件,让父组件来修改(这样就知道是谁要改,然后还可以对其他组件做单独处理)

  1. 所有子组件都受影响了
  2. 父组件不知道是谁修改的

这个平时一般是通过开发规范直接规避掉了,所以也用的不多,这边介绍下:

语法:readonly(普通对象 | ref对象 | reactive对象)

App.vue:里面两个子组件,分别把student传递给了他们

<template>
    <div>
        <AppItem :student="student"></AppItem>
        <AppHeader :student="student"></AppHeader>
    </div>
</template>

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

import AppHeader from './components/AppHeader.vue';
import AppItem from './components/AppItem.vue';

export default {
    components: { AppItem, AppHeader },
    setup() {
        const student = ref({ id: 1, name: '张三', age: 22, gender: '男' });
        return {
            student,
        }
    }
}
</script>

AppHeader.vue:简单一个展示

<template>
    <div>
        <h2>AppHeader</h2>
        {{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
    </div>
</template>

<script>

export default {
    props: ['student'] // 接收父组件传递过来的student属性值
}
</script>

AppItem.vue:定义了一个修改的单击事件

<template>
    <div>
        <h2>AppItem</h2>
        {{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
        <button @click="student.name = '李四'">修改Name测试</button>
    </div>
</template>

<script>

export default {
    props: ['student']
}
</script>

点一下修改就全部修改掉了

image-20230807144040397

下面我们用readonly试下,代码没什么变换就是把ref换成了readonly(import { readonly } from 'vue';

image-20230807145623055

再点击修改就改不掉了:

image-20230807145500002

5.其他API函数

其实还有很多Vue提供的函数,这边就简单列举下:

5.1.工具函数

1.响应式 API:工具函数

  1. isRef():检查某个值是否为ref★
  2. unref():如果参数是 ref,则返回内部值,否则返回参数本身★
    1. isRef的语法糖:val = isRef(val) ? val.value : val
  3. toRef():将值、reactive属性转换为ref
  4. toValue():将值、refs 或 getters 规范化为值(3.3+
    1. eg:toValue(1)、toValue(ref(1))、toValue(() => 1) 的结果都是1
  5. toRefs():把响应式对象的每一个属性都变成ref对象(内部是每个属性都toRef一下)
    1. PS:详细可以查看案例1
  6. isProxy():检查一个对象是否是由reactive()、readonly()shallowReactive()、shallowReadonly()创建的代理
  7. isReactive():检查一个对象是否是由reactive()shallowReactive()创建的代理
  8. isReadonly():检查传入的值是否由readonly()shallowReadonly()创建的只读代理
    1. 只读对象的属性可以改,但不能通过传入的对象直接赋值。而是让父方法去修改(xxx.value=xx)

详细可以参考官方文档:https://cn.vuejs.org/api/reactivity-utilities.html

案例1:toRefs()使用场景

let student = reactive({
    name: '小明',
    age: 22
});

// 如果对象被ES6解构语法给解构了,它们就不是响应式的数据了
let { name, age } = student;

// 2s后修改数据,看看页面数据有没有修改
setTimeout(() => {
    name = '小黑';
    age = 22;
    console.log(name, age)
}, 2000);

2s后页面数据并没有修改:image-20230808095642879

而如果使用toRefs包裹一下就可以了:

<template>
    <div>
        {{ name }}-{{ age }}
    </div>
</template>

<script>
import { reactive, toRefs } from 'vue';

export default {
    setup() {
        let student = reactive({
            name: '小明',
            age: 22
        });

        // 如果对象被ES6解构语法给解构了,它们就不是响应式的数据了
        let { name, age } = toRefs(student);

        // 2s后修改数据,看看页面数据有没有修改
        setTimeout(() => {
            name.value = '小黑';
            age.value = 22;
            console.log(name, age)
        }, 2000);

        return {
            name, age,
        }
    }
}
</script>

2s后页面数据刷新了,name和age都是ref类型了

image-20230808100017173

setup语法糖

<template>
    <div>
        {{ name }}-{{ age }}
    </div>
</template>

<script setup>
import { reactive, toRefs } from 'vue';

let student = reactive({
    name: '小明',
    age: 22
});

let { name, age } = toRefs(student);

setTimeout(() => {
    name.value = '小黑';
    age.value = 22;
    console.log(name, age)
}, 2000);
</script>

5.2.进阶函数

2.响应式 API:进阶

  1. shallowRef():创建一个浅层的ref()对象☆
    1. ref默认是把里面所有属性都变成deep响应式的,shallowRef则是浅层的
  2. shallowReactive():创建一个浅层的reactive() 对象☆
  3. shallowReadonly():创建一个浅层的readonly() 对象☆
  4. toRaw()reactivereadonly代理的原始对象☆
  5. triggerRef():手动触发shallowRef、shallowReactive、shallowReadonly相关联的副作用
    1. 强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用
  6. customRef():创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式
  7. markRaw():将一个对象标记为不可被转为代理。返回该对象本身
  8. effectScope():创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理
  9. getCurrentScope():如果有的话,返回当前活跃的 effect 作用域
  10. onScopeDispose():在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数

详细可以参考官方文档:https://cn.vuejs.org/api/reactivity-advanced.html


4.3.computed计算属性

1.简单案例

计算属性用法差不多,先看个简单例子:

template:

<template>
    <div>
        {{ student.name }}-{{ student.age }}-{{ genderStr }}
    </div>
</template>

script:

<script>
import { computed, reactive } from 'vue';

export default {
    setup() {
        let student = reactive({
            name: '张三',
            age: 22,
            gender: 1
        });

        const genderStr = computed(() => {
            return student.gender == 1 ? '男' : '女';
        });

        console.log(genderStr);

        return {
            student, genderStr
        }
    }
}
</script>

setup语法糖

<script setup>
import { computed, reactive } from 'vue';

let student = reactive({
    name: '张三',
    age: 22,
    gender: 1
});

const genderStr = computed(() => {
    return student.gender == 1 ? '男' : '女';
});

console.log(genderStr);
</script>

输出:张三-22-男

2.computed的get和set

如果我们打印一下computed对象(console.log(genderStr))会发现,他其实是一个ref的对象

image-20230807161901249

那我们来尝试看看能不能直接修改:

image-20230807162639199

就算把genderStr属性变成let也是不可以修改的

image-20230807162613291

Computed有两个方法:get、set,get默认就是readonly类型,而set可以去修改(但不推荐去修改,我这边只是写个案例)

template:

<template>
    <div>
        {{ student.name }}-{{ student.age }}-{{ genderStr }}<br />
        <button @click="changeGender">修改性别</button>
    </div>
</template>

script:

<script>
import { computed, reactive } from 'vue';

export default {
    setup() {
        let student = reactive({
            name: '张三',
            age: 22,
            gender: 1
        });

        const genderStr = computed({
            set: function (newValue) {
                console.log(newValue)
                if (newValue == '男') {
                    student.gender = 1;
                } else if (newValue == '女') {
                    student.gender = 0;
                }
            },
            get: function () {
                return student.gender == 1 ? '男' : '女';
            }
        });
        const changeGender = () => {
            genderStr.value = '女';
        }

        return {
            student, genderStr, changeGender
        }
    }
}
</script>

setup语法糖

<script setup>
import { computed, reactive } from 'vue';

let student = reactive({
    name: '张三',
    age: 22,
    gender: 1
});

const genderStr = computed({
    set: function (newValue) {
        console.log(newValue)
        if (newValue == '男') {
            student.gender = 1;
        } else if (newValue == '女') {
            student.gender = 0;
        }
    },
    get: function () {
        return student.gender == 1 ? '男' : '女';
    }
});
const changeGender = () => {
    genderStr.value = '女';
}
</script>

4.4.watch侦听器★

从vue中导入watch函数,然后用法和之前类似:watch(数据源,回调函数,{参数})

1.数据源:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组

2.参数:immediate:true立即执行deep:true深度侦听

1.普通属性侦听

侦听title属性案例:

<template>
    <h2>{{ title }}</h2>
</template>
<script>
import { ref, watch } from 'vue';

export default {
    setup() {
        let title = ref('this is title');
        watch(title, (newValue, oldValue) => {
            console.log(`oldValue:${oldValue}`);
            console.log(`newValue:${newValue}`);
        });

        // 模拟修改操作
        setTimeout(() => {
            title.value = '这是一个标题';
        }, 2000);
        return {
            title
        }
    }
}
</script>

setup语法糖

<template>
    <h2>{{ title }}</h2>
</template>
<script setup>
import { ref, watch } from 'vue';

let title = ref('this is title');
watch(title, (newValue, oldValue) => {
    console.log(`oldValue:${oldValue}`);
    console.log(`newValue:${newValue}`);
});

// 模拟修改操作
setTimeout(() => {
    title.value = '这是一个标题';
}, 2000);
</script>

如果属性不是响应式的会提示: [Vue warn]: Invalid watch source: this is title A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types

image-20230808112007130

2.对象属性侦听

对象是引用传递的,所以这块old和new是同一个值,来看下:

<template>
    <div>
        {{ student.name }}-{{ student.age }}-{{ student.gender }}
    </div>
</template>

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

export default {
    setup() {
        let student = reactive({
            name: '张三',
            age: 22,
            gender: '男'
        })
        watch(student, (newValue, oldValue) => {
            // 对象是引用传递的,所以这块old和new是同一个值
            console.log(oldValue);
            console.log(newValue);
        }, {
            immediate: true
        });

        setTimeout(() => {
            student.name = '李四';
        }, 2000);

        return {
            student
        }
    }
}
</script>

效果:newValue和oldValue的确是一样的

image-20230808114122206

我们来进一步验证下,让它刚开始就执行一下:immediate: true

image-20230808113742080

刚开始名字的确是张三,因为是引用传递,在watch里面的newValue和oldValue就都一样了

image-20230808113710894

3.深层次的侦听

通过watch监听的ref对象默认是浅层侦听的,直接修改嵌套的对象属性不会触发回调执行 ==> 可以通过deep来深层侦听

来个案例,我们把上面案例从reactive换成ref就发现没被侦听到:

<template>
    <div>
        {{ student.name }}-{{ student.age }}-{{ student.gender }}
    </div>
</template>

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

export default {
    setup() {
        let student = ref({
            name: '张三',
            age: 23,
            gender:'男'
        });

        watch(student, (newValue, oldValue) => {
            console.log(newValue);
            console.log(oldValue);
        });

        setTimeout(() => {
            student.value.name = '李四';
        }, 2000);

        return{
            student
        }
    }
}
</script>

2s后内容被修改了,但是没有侦听到

image-20230808115327897

我们添加一个deep参数就可以了:(平时尽量不要用deep,有性能损耗的)

image-20230808115510221

因为对象是引用传递,所以新旧还是和之前一样,都是一个指向

image-20230808115454247

那ref就不能像reactive一样快乐的玩耍了吗?NoNoNo,我们别监听student,监听student.value就可以了

PS:注意这里的student.value不是一个普通的值,也是一个代理对象

image-20230808115917313

setup语法糖

<template>
    <div>
        {{ student.name }}-{{ student.age }}-{{ student.gender }}
    </div>
</template>

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

let student = ref({
    name: '张三',
    age: 23,
    gender: '男'
});

watch(student.value, (newValue, oldValue) => {
    console.log(newValue);
    console.log(oldValue);
});

setTimeout(() => {
    student.value.name = '李四';
}, 2000);
</script>

这样效果就和上面的reactive一样了

image-20230808120001743

4.监听某个属性☆

不能直接侦听响应式对象的属性值,例如:

<template>
    <div>
        {{ student.name }}-{{ student.age }}-{{ student.gender }}
        <span v-for="item in student.likes" :key="item">
            ~{{ item }}~
        </span>
    </div>
</template>

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

let student = ref({
    name: '张三',
    age: 23,
    gender: '男',
    likes: []
});

watch(student.value.likes, (newValue, oldValue) => {
    console.log(newValue);
    console.log(oldValue);
});

setTimeout(() => {
    student.value.likes = ['唱歌', '跳舞', '睡觉'];
}, 2000);
</script>

没有侦听到:image-20230808122337431

不想用deep,又想侦听怎么办?==> 使用getter函数,就把watch这块修改下,其他不用动:

image-20230808122739706

效果:

image-20230808122834117

5.多个属性侦听

直接贴下案例:

<template>
    <div>
        {{ name }}-{{ age }}
    </div>
</template>

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

let name = ref('张三');
let age = ref(22);

watch([name, age],
    ([nameNewValue, ageNewValue], [nameOldValue, ageOldValue]) => {
        console.log('name and age newValue');
        console.log(nameNewValue, ageNewValue);
        console.log('name and age oldValue');
        console.log(nameOldValue, ageOldValue);
    })

setTimeout(() => {
    name.value = '李四';
}, 1000);
setTimeout(() => {
    age.value = 33;
}, 2000);
</script>

效果:image-20230808123611408

watch文档:https://cn.vuejs.org/guide/essentials/watchers.html or https://cn.vuejs.org/api/reactivity-core.html#watch

6.watcheffect☆

watcheffect立即运行一个函数(immediate),同时响应式地追踪其依赖,并在依赖更改时重新执行

PS:它会自己跟踪它用到的响应式数据,并在其改变后再执行方法(可以理解为一个灵活的watch)

来个案例:

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

let title = ref('this is title')

// 它会自己跟踪它用到的响应式数据,并在其改变后再执行方法
watchEffect(() => {
    console.log(title.value);
})

setTimeout(() => {
    title.value = '我被修改了';
}, 2000);
</script>

<template>
    <h2>{{ title }}</h2>
</template>

效果:刚开始就执行了,title被修改后也执行了

image-20230808150252557

watcheffecthttps://cn.vuejs.org/api/reactivity-core.html#watcheffect

4.5.生命周期钩子★

先贴下组合式API和选项API的对应关系

PS:组合式API没有beforeCreate和created,与之对应的是setup。其他生命周期一样,就是名字前面加on(小驼峰命名)

image-20230807172249828

其实setup会在beforeCreate之前,我们可以看下官网和自己编个demo:

<template></template>
<script>
export default {
    setup() {
        console.log('setup');
    },
    beforeCreate() {
        console.log('beforeCreate');
    }
}
</script>

输出:image-20230807201217234

官方周期顺序:

image-20230807201350412

生命周期同样来个例子:

PS:生命周期函数是可以执行多次的,eg:你弄2个onMounted,就会依次执行2次onMounted

<template>
    <div v-if="isShow">
        {{ title }}<button @click="updateTitle">修改+消失</button>
    </div>
</template>

<script>
import {
    onBeforeMount, onMounted, onBeforeUpdate,
    onUpdated, onBeforeUnmount, onUnmounted
} from 'vue';

import { ref } from 'vue';

export default {
    setup() {
        onBeforeMount(() => {
            console.log('onBeforeMount');
        })
        onMounted(() => {
            console.log('onMounted');
        })
        onBeforeUpdate(() => {
            console.log('onBeforeUpdate');
        })
        onUpdated(() => {
            console.log('onUpdated');
        })
        onBeforeUnmount(() => {
            console.log('onBeforeUnmount');
        })
        onUnmounted(() => {
            console.log('onUnmounted');
        })

        let isShow = ref(true);
        let title = ref('这是一个标题');
        const updateTitle = () => {
            title.value = '这是一个新标题!';
            // 2s后注销组件
            setTimeout(() => {
                isShow.value = false;
            }, 2000)
        }
        return { title, updateTitle, isShow }
    }
}
</script>

效果图:

image-20230807203016903

setup语法糖

<template>
    <div v-if="isShow">
        {{ title }}<button @click="updateTitle">修改+消失</button>
    </div>
</template>

<script setup>
import {
    onBeforeMount, onMounted, onBeforeUpdate,
    onUpdated, onBeforeUnmount, onUnmounted
} from 'vue';

import { ref } from 'vue';

onBeforeMount(() => {
    console.log('onBeforeMount');
})
onMounted(() => {
    console.log('onMounted');
})
onBeforeUpdate(() => {
    console.log('onBeforeUpdate');
})
onUpdated(() => {
    console.log('onUpdated');
})
onBeforeUnmount(() => {
    console.log('onBeforeUnmount');
})
onUnmounted(() => {
    console.log('onUnmounted');
})

let isShow = ref(true);
let title = ref('这是一个标题');
const updateTitle = () => {
    title.value = '这是一个新标题!';
    // 2s后注销组件
    setTimeout(() => {
        isShow.value = false;
    }, 2000)
}
</script>

有新增的很多钩子,具体可看文档:https://cn.vuejs.org/api/composition-api-lifecycle.html

4.6.组件通信

1.父组件发信息给子组件★

之前在父组件里面使用子组件的案例说不少,现在这种新组合API的编程也来个demo:(用起来基本上差不多

回顾下之前怎么传递的:父组件中通过属性把数据绑给子组件,子组件props里面写下绑定的属性名

父:import 子组件 + <AppItem :title="title">,子:defineProps(['title'])

AppItem.vue:defineProps(['title'])

<template>
    <div>
        <h2>{{ title }}</h2>
        {{ count }}&nbsp;<button @click="addCount">count++</button>
    </div>
</template>
<script setup>
import { ref } from 'vue';

// defineProps(['title'])
const props = defineProps({
    title: {
        type: String,
        default: '我是默认值'
    },
})

let count = ref(0);
const addCount = () => {
    count.value++;
}
</script>

App.vue:导入子组件并绑定一个:title属性即可

<template>
    <div>
        <AppItem></AppItem>
        <AppItem :title="title"></AppItem>
    </div>
</template>

<script setup>
import { ref } from 'vue';
// 子组件导入即可,不用再通过components注册子组件了
import AppItem from './components/AppItem.vue';

let title = ref('this is a title test');
</script>

单击三次的效果:

image-20230808152126759

2.子组件发信息给父组件★

回顾下之前怎么传递的:父组件中给子组件标签通过@绑定事件子组件内部通过 $emit 方法触发事件

  1. 子组件emits:[xxx]里面写下自定义事件名 ==> 现在: const emit = defineEmits(['add']);

  2. 然后通过$emit('自定义事件',值)触发了自定义事件 ==> 现在:emit ('自定义事件',值)

  3. 回调函数通过event拿到数据并修改 或者 函数传过来的payload获取 ==> 现在:函数的参数payload获取

AppItem.vue:通过defineEmits得到emit方法

<template>
    <div>
        <input v-model="name" type="text" @keyup.enter="addStudent" />
    </div>
</template>

<script setup>
import { ref } from 'vue';
// 通过defineEmits得到emit方法
const emit = defineEmits(['add']);

let name = ref(''); // v-model绑定的属性

const addStudent = () => {
    emit('add', name.value);
    console.log(name.value)
    name.value = ''; //文本框清空
}
</script>

App.vue:<AppItem @add="addStudent">

<template>
    <div>
        <AppItem @add="addStudent"></AppItem>
        <ul>
            <li v-for="name in students" :key="name">
                {{ name }}
            </li>
        </ul>
    </div>
</template>

<script setup>
import AppItem from './components/AppItem.vue';

import { ref } from 'vue';

let students = ref(['张三', '李四', '王二']);

const addStudent = (payload) => {
    students.value.push(payload);
}
</script>

输入小李并回车:

image-20230807155142402

3.祖孙通信 - 依赖注入

主要就是这两个方法:provide(依赖):提供数据、inject(注入):获取数据

provide('key',普通数据 | ref(数据) | 函数) // 顶层组件提供普通数据/响应式数据

const msg = inject('key') // 获取普通数据/ref响应式数据

可以传递普通数据和响应式的数据,这边以响应式数据和传递方法来演示:

嵌套的树结构是:App.vue >> AppList.vue >> AppItem.vue,App.vue提供数据(provide),AppItem接收数据(inject

App.vue:

<script setup>
import AppList from './componets/AppList.vue';
import { provide, ref } from 'vue';

let count = ref(0);
const updateCount = () => {
    count.value++;
}

provide('count', count);
provide('updateCount', updateCount)
</script>

<template>
    <AppList></AppList>
</template>

AppList.vue:

<script setup>
import AppItem from './AppItem.vue';
</script>

<template>
    <AppItem></AppItem>
</template>

AppItem.vue

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

// 注入
const count = inject('count');
const updateCount = inject('updateCount');
</script>

<template>
    <div>
        <!-- template里面的数据会自己解包 -->
        {{ count }}&nbsp;<button @click="updateCount">点我调用祖辈方法</button>
    </div>
</template>

点3下效果:image-20230808160236601

PS:不相关组件之间的通信就不演示了,实际项目中不会用的,都是用Pinia状态管理库

4.10.组件v-model★

v-model的本质就是父组件通过:属性=数据传值给子组件,子组件通过props接收值,然后需要修改的时候使用emit(自定义事件,值)这时候会触发父组件里面的回调函数,对值进行修改

1.简单案例

App.vue:

<script setup>
import AppItem from './components/AppItem.vue';

import { ref } from "vue";

let student = ref({
    name: '小李',
    age: 33,
    gender: '女'
})
</script>

<template>
    <div>
        <AppItem v-model:student="student"></AppItem>
    </div>
</template>

AppItem.vuedefineProps(['name'])接收name属性,通过defineEmits(['update:name'])获取emit

<script setup>
defineProps(['name'])
const emit = defineEmits(['update:name'])

// 模拟修改
setTimeout(() => {
    // 调用自定义事件,并把小黑传递过去
    emit('update:name', '小黑');
}, 2000);
</script>

<template>
    <div>
        {{ name }}
    </div>
</template>

运行后效果:2s前:小张,2s后:小黑

其实<AppItem v-model:student="student">的语法糖就相当于,帮我们自动改成了这样:

<AppItem :student="student" @update:student="$event"></AppItem>

我们来看下:AppItem.vue 不修改,就把App.vue微调下,效果和上面一样

image-20230809082649567

2.对象案例

对象是引用传递的,如果在子组件中想对某个属性进行修改,不能只传递该属性,而是整个对象都要传递过来。

我们来看个问题案例

App.vue:

image-20230809083044459

AppItem.vue:

image-20230809083212790

如果直接传递小黑字符串,相当于把student给替换掉了,而且类型也不对。结果也是有问题的:刚开始小李-33-女,2s后--

修改其实就只需要把AppItem中js修改下就可以了:

<script setup>
const props = defineProps(['student'])
const emit = defineEmits(['update:student'])

// 模拟名称被修改,看看是否是双向绑定
setTimeout(() => {
    // 如果student是个对象,这边也要传递对象
    emit('update:student', {
        name: '小黑',
        age: props.student.age,
        gender: props.student.gender
    });
}, 2000);
</script>

<template>
    <div>
        {{ student.name }}-{{ student.age }}-{{ student.gender }}
    </div>
</template>

刚开始:小李-33-女,2s后:小黑-33-女

5.Vue3 Router

官方文档:https://router.vuejs.org/zh/guide

5.1.前后端路由概述

在项目里面安装路由可以添加vue-router的包:cnpm install vue-router

PS:也可以刚创建项目的时候选下路由选项(router那项按下右箭头,然后回车)

创建项目的时候可以添加

image-20230809131400016

也可以后期安装:

image-20230809131224457

我们先初始化一个含路由的项目,运行一下自己观察:来回切换URL和内容在变,但没有Ajax

这个意味着什么?==> 大大增加了后端服务器并发能力(缓解服务器压力)

image-20230809092814397

路由的本质都是映射关系表,我们以前的后端路由其实都是根据你浏览器输入的URL,然后服务器接收到后,根据正则匹配,看要访问哪个控制器(Controller)然后由这个控制器获取到数据(Model),发送到对应的视图(View)进行数据绑定和页面渲染,然后服务器返回渲染之后的DOM页面

PS:我上面说的就是我们后端开发的传统MVC(路由映射你可以理解为家里路由器中的路由表,url相当于ip,控制器/组件相当于电脑的mac地址)

那为什么要前后端分离呢?==> 后端程序员既要操作数据,又要写前端页面,然后前端语言中又混了后端的语法,各种糅合,没有那么的责任清晰和存粹。现在基本上都是后端专注数据和业务逻辑,然后通过WebAPI把数据给到前端开发,前端开发工程师专注交互和可视化。彼此之间高效协同工作,而且还可以多端共用一套API(PC、APP、Mini)

5.2.前端路由实现

前端路由的核心目的就是 ==> 改变URL,但页面不进行整体的刷新。那么实现这目的,就是要做到URL和内容有个映射,目前有两种主流方案:

PS:路由用于设定访问路径,将路径和组件映射起来。vue-router的单页面应用中,页面的路径改变就是组件的切换

  1. URL哈希:就是锚点#和其后面的内容(没有#则为空字符串)
    1. PS:本质是通过改变window.location.hash来切换组件(这样URL也会随之改变)
  2. H5的History:它有六种模式改变URL都不会刷新页面(location.pathname
    1. pushState:使用新的路径★
    2. replaceState:替换原来的路径
    3. popState:路径的回退
    4. go:向前或向后改变路径
    5. forward:向前改变路径
    6. back:向后改变路径

History比较清晰,URL哈希比较含糊,我们看个案例:

console.log(window.location.href);
console.log(window.location.hash);
window.location.hash='#hhhh'
console.log(window.location.href);
console.log(window.location.hash);

手动在浏览器上输入:http://localhost:5173/#mmd然后url就变成了http://localhost:5173/#hhhh

image-20230809100012040

5.3.Vue Router配置

先来分析下刚刚创建的含路由的项目

发现里面有新建的router文件夹,然后里面有个index.js来配置路由

image-20230809131841794

然后发现路由是在main.js中导入和use

image-20230809132154380

然后App.vue里面有配置路由占位路由跳转

image-20230809132335303

分析完毕,我们先总结下知识点:

  1. router文件夹index.js:创建路由const router = createRouter({})并返回export default router
    1. history:实现方式:HistorycreateWebHashHistory)or HashcreateWebHashHistory)★
    2. routespath路径、redirect跳转、component组件(懒加载:() => import('../views/View.vue'))★
    3. name:给每一个route设置一个独一无二的name,RouterLink 也可以直接 to name
    4. meta:自定义数据
  2. src文件夹main.js:导入刚刚的路由配置jsimport router from './router',并使用app.use(router)
  3. vue文件中占位<RouterView>)和跳转<RouterLink to="/xxx">

在我们的项目里面配置下路由:

roter/index.js:另一个实现就把createWebHashHistory()替换为:createWebHistory()

ps:如果替换为History的方式,url就变成:http://localhost:5173/Homehttp://localhost:5173/About

import { createRouter } from 'vue-router';
import { createWebHashHistory } from 'vue-router';

import HomeView from '../views/HomeView.vue';

const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: '/',
            redirect: '/Home'
        },
        {
            path: '/Home',
            component: HomeView
        },
        {
            path: '/About',
            component: () => import('../views/AboutView.vue') // 懒加载,用到才会去加载(避免所有组件全部怼页面上了)
        }
    ]
})

export default router

main.js:app.use(router)

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

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

App.vue<RouterLink to="/xxx"><RouterView />

<script setup>
import { RouterLink, RouterView } from 'vue-router';
</script>

<template>
  <div>
    App:<br />
    <RouterLink to="/Home">Home</RouterLink> |
    <RouterLink to="/About">About</RouterLink>
  </div>
  <hr />
  <RouterView></RouterView>
</template>

效果:点About,URL和下面组件都会切换

image-20230809140219924

5.4.Router-link属性

router-link有一些属性可以配置:

  1. to属性:是一个字符串to="/xxx",或者是一个对象:to={ path:'/xxx'}
    1. 也可以在每个route里面指定一个独一无二的name,然后to=name名就可以跳转到对应配置的组件
  2. replace属性:当点击时,会调用router.replace(),而不是默认的router.push()
    1. 不推荐使用,默认我们可以点浏览器前进后退切换到对应页面的,如果用了replace就直接over了
  3. active-class属性:设置激活a元素后应用的class,默认名:router-link-active
  4. exact-active-class属性:链接精准激活时,应用于渲染的<a>的class,默认是router-link-exact-active

简单说下,代码和之前一样,就把App.vue微调下:

image-20230809143909270

点到哪个,自动给你改成自定义类名:(exact-active-class是嵌套路由用的,如果没有嵌套,就会在这个上面两个类都加上)

image-20230809143841868

5.5.动态路由传值

我们打印一下route对象查看下:

import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route)

image-20230812091019225

获取参数基本上就这三个比较多:paramsqueryhash

PS:hash的就是获取从url#开始到最后的内容($route.hashuseRoute().hash

1.params传参

先说知识点,然后来个项目里面的常用案例:

  1. 配置路径path: '/User/:id'
  2. 链接传参<RouterLink :to="/User/${student.id}">编辑</RouterLink>
    1. 或者<RouterLink :to="{ path: /User/${student.id} }">编辑</RouterLink>
  3. 获取参数:template获取参数:{{ $route.params.id }},JS获取参数:useRoute().params.id
    1. OptionsAPI直接通过this.$route.params获取id
    2. 用户A直接切换到用户B,页面不刷新useRoute()获取不到可以通过路由的onBeforeRouteUpdate来获取(99%用不到)

下面贴代码+演示一下:router/index.js

import { createRouter, createWebHistory } from 'vue-router';

import ListView from '../views/ListView.vue';

const router = createRouter({
    history: createWebHistory(), // 是个方法
    routes: [
        {
            path: '/',
            component: ListView
        }, 
        {
            path: '/List',
            component: ListView
        },
        {
            path: '/User/:id',
            component: () => import('../views/UserView.vue')
        }
    ]
})

export default router

App.vue:

<script setup>
import { RouterLink, RouterView } from 'vue-router';
</script>

<template>
    <div>
        <h2>App Main</h2>
        <RouterLink to="/List" active-class="active">List</RouterLink>
        <hr>
        <RouterView></RouterView>
    </div>
</template>

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

ListView.vue/User/${student.id}

<script setup>
import { ref, onMounted } from 'vue';
import { RouterLink } from 'vue-router';

let students = ref([]);
onMounted(() => {
    // 模拟一下数据获取
    students.value = [
        { id: 1, name: '小张', age: 22 },
        { id: 2, name: '小李', age: 33 },
        { id: 3, name: '小王', age: 44 }
    ]
})
</script>

<template>
    <h3>List View</h3>
    <ul>
        <li v-for="student in students" :key="student.id">
            {{ student.id }}.{{ student.name }}-{{ student.age }}&nbsp;
            <!-- <RouterLink :to="{ path: `/User/${student.id}` }">编辑</RouterLink> -->
            <RouterLink :to="`/User/${student.id}`">编辑</RouterLink>
        </li>
    </ul>
</template>

User.vue:JS部分:useRoute().params.id、Template部分:{{ $route.params.id }}

<script setup>
import { ref, onMounted } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router'

const route = useRoute();
const userId = route.params.id; // 获取传递过来的id
console.log('User Id:', userId)

let student = ref({});
// 模拟获取数据
onMounted(() => {
    const data = [{ id: 1, name: '小张', age: 22 }, { id: 2, name: '小李', age: 33 }, { id: 3, name: '小王', age: 44 }];
    student.value = data.find(item => item.id == userId);
})
</script>

<template>
    <div>
        <h2>User Id:{{ $route.params.id }}</h2>
        {{ student.id }}.{{ student.name }}-{{ student.age }}
    </div>
</template>

打开后鼠标停留在ID为2的超链接上,这个链接就是:/User/2

image-20230809164812174

点进去就可以传递id,并显示相关内容:

image-20230809164907497

官方文档:https://router.vuejs.org/zh/guide/essentials/dynamic-matching.htm

2.query传参

和params差不多,就路由配置,和获取的地方不太一样:

参考:https://router.vuejs.org/zh/guide/essentials/passing-props.html#函数模式

/router/index.js

import { createRouter, createWebHistory } from 'vue-router';

import ListView from '../views/ListView.vue';

const router = createRouter({
    history: createWebHistory(), // 是个方法
    routes: [
        {
            path: '/',
            component: ListView
        },
        {
            path: '/List',
            component: ListView
        },
        {
            path: '/User',
            props: (route) => ({ id: route.query.id }), // 将query参数传递给User组件
            component: () => import('../views/UserView.vue')
        }
    ]
})

export default router

App.vue不变:

<script setup>
import { RouterLink, RouterView } from 'vue-router';
</script>

<template>
    <div>
        <h2>App Main</h2>
        <RouterLink to="/List" active-class="active">List</RouterLink>
        <hr>
        <RouterView></RouterView>
    </div>
</template>

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

ListView.vue:就改了这:/User?id=${student.id}

<script setup>
import { ref, onMounted } from 'vue';
import { RouterLink } from 'vue-router';

let students = ref([]);
onMounted(() => {
    // 模拟一下数据获取
    students.value = [
        { id: 1, name: '小张', age: 22 },
        { id: 2, name: '小李', age: 33 },
        { id: 3, name: '小王', age: 44 }
    ]
})
</script>

<template>
    <h3>List View</h3>
    <ul>
        <li v-for="student in students" :key="student.id">
            {{ student.id }}.{{ student.name }}-{{ student.age }}&nbsp;
            <RouterLink :to="`/User?id=${student.id}`">编辑</RouterLink>
        </li>
    </ul>
</template>

UserView.vue:,通过:route.query来获取id

<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router'

const route = useRoute();
const userId = route.query.id; // User?id=1
console.log('User Id:', userId)

let student = ref({});

onMounted(() => {
    // 模拟获取数据
    const data = [{ id: 1, name: '小张', age: 22 }, { id: 2, name: '小李', age: 33 }, { id: 3, name: '小王', age: 44 }];
    student.value = data.find(item => item.id == userId);
})
</script>

<template>
    <div>
        <h2>User Id:{{ $route.query.id }}</h2>
        {{ student.id }}.{{ student.name }}-{{ student.age }}
    </div>
</template>

效果:

image-20230811103546700

5.6.NotFound配置

/router/index.js:在最下面加个规则/:pathMatch(.*)

PS:name是唯一的,一旦重复后面的会把之前同名的name路由给覆盖掉

import { createRouter, createWebHistory } from "vue-router";

import HomeView from '../views/HomeView.vue';
import NotFoundView from '../views/NotFoundView.vue';

const router = createRouter({
    history: createWebHistory(),
    routes: [
        {
            path: '/',
            name: 'index', // name是唯一的,一旦重复后面的会把之前同名的name路由给删掉
            component: HomeView
        },
        {
            path: '/Home',
            name: 'home',
            component: HomeView
        },
        {
            path: '/About',
            name: 'about',
            component: () => import('../views/AboutView.vue')
        },
        {
            path: '/:pathMatch(.*)', // 在【最下面】加
            name: '404',
            component: NotFoundView
        }
    ]
})

export default router

App.vueHomeAbout里面没东西,就写了Home和About的字符串

<script setup>
import { RouterLink, RouterView } from 'vue-router';
</script>

<template>
    <div>
        <RouterLink to="/Home">Home</RouterLink> | 
        <RouterLink to="/About">About</RouterLink>
        <hr>
        <RouterView></RouterView>
    </div>
</template>

NotFound.vue:URL地址:$route.path,JS中页面跳转:useRouter().push('/Home')

<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';

// 来个倒计时
let time = ref(3);
setInterval(() => {
    if (time.value == 0) {
        return;
    }
    time.value--;
}, 1000);

// 三秒后跳转到Home
const router = useRouter(); // 这个要放setTimeout外面
setTimeout(() => {
    router.push('/Home'); // 跳转到Home主页
}, 3000);
</script>

<template>
    <div>
        <h2>没有找到页面{{ $route.path }}:{{ time }}后跳转到首页</h2>
    </div>
</template>

<style scoped>
h2 {
    color: red;
}
</style>

效果,如果不是Home和About页面,就会到404 Not Found页面

image-20230811153029535

5.7.嵌套路由

大致流程

  1. 父路由配置中新增children属性(path前面不用加/
  2. 父组件中新增<router-view>
  3. 通过路径跳转到嵌套组件:<router-link to='/xx/xx'>

1.案例演示

来个树结构示意图:

image-20230812104931839

这块主要是看路由配置:children:[]

import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';

const router = createRouter({
    history: createWebHistory(),
    routes: [
        {
            path: '/',
            redirect: '/Home'
        },
        {
            path: '/Home',
            name: 'home',
            component: HomeView,
            children: [
                {
                    path: '', // 链接如果是/Home就默认显示BottomView
                    name:'hson',// 要起个和home不一样的名字
                    component: () => import('../views/BottomView.vue')
                },
                {
                    path: 'Bottom', // 不用以`/`开头
                    name: 'bottom',
                    component: () => import('../views/BottomView.vue')
                },
                {
                    path: 'Ad',
                    name: 'ad',
                    component: () => import('../views/AdView.vue')
                }
            ]
        },
        {
            path: '/About',
            name: 'about',
            component: () => import('../views/AboutView.vue')
        }
    ]
});

export default router

App.vue:

<script setup></script>

<template>
    <div>
        <RouterLink to="/Home" active-class="active">Home</RouterLink> |
        <RouterLink to="/About" active-class="active">About</RouterLink>
    </div>
    <RouterView></RouterView>
</template>

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

HomeView.vue:

<script setup>
</script>

<template>
    <h2>Home</h2>
    <div>
        <RouterLink to="/Home/Bottom" active-class="son-active">Bottom</RouterLink> |
        <RouterLink to="/Home/Ad" active-class="son-active">Ad</RouterLink>
    </div>
    <RouterView></RouterView>
</template>

<style scoped>
.active {
    color: red;
}

.son-active {
    color: green;
}
</style>

BottomView.vueAdView.vue没有东西,就h3标签中加个文字

效果:

image-20230812110155861

2.样式说明

RouterLink有两个样式属性,active-class是模糊匹配,只要包含就显示自定义样式,exact-active-class是精确匹配,才显示自定义样式:

代码不修改,只是把App.vue中的active-class换成了exact-active-class就是这样的效果了:

image-20230812104523973

官方文档:https://router.vuejs.org/zh/guide/essentials/nested-routes.html

5.8.编程式导航

这个要通过Router里面的函数实现,我们打印router对象看下:

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

console.log(router)

1.路由跳转方法

跳转(push、replace)、前进后退(forward、back)、向前向后N步(go)

router.back() ==> go(-1)router.forward() ==> go(1)

image-20230812090504810

来个案例:

/router/index.js

import { createWebHistory } from "vue-router";
import { createRouter } from "vue-router";
import HomeView from '../View/HomeView.vue';

const router = createRouter({
    history: createWebHistory(),
    routes: [
        {
            path: '/',
            component: HomeView
        },
        {
            path: '/Home',
            name: 'home',
            component: HomeView
        },
        {
            path: '/About',
            name: 'about',
            component: () => import('../View/AboutView.vue')
        }
    ]
})

export default router

App.vue:template里面直接调用$router.方法即可,js中需要先实例化一个当前router对象,然后再使用对应的方法useRouter().方法

<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();

const back = () => {
    router.back(); // 相当于go(-1)
}
const goAbout = () => {
    // router.push('/About');
    router.push('about'); // 直接通过路由名跳转也行
}
</script>

<template>
    <div>
        <RouterLink to="/Home">Home</RouterLink> |
        <RouterLink to="/About">About</RouterLink>
    </div>
    <div>
        <RouterView></RouterView>
        <button @click="$router.forward()">向前一步</button>&nbsp;
        <button @click="back">向后一步</button>&nbsp;
        <button @click="$router.push('/Home')">跳转到Home</button>&nbsp;
        <button @click="goAbout">跳转到About</button>&nbsp;
        <button @click="$router.go(-2)">回跳两步</button>&nbsp;
    </div>
</template>

可以实现来回跳和指定页面的跳转

image-20230812093019522

2.跳转并传参

push的时候可以通过params或者query来传参,如果是使用的path,会自动忽略params,只能使用query来传递。如果使用的是路由name,则都可以

PS:开发中推荐使用query来传参

来个案例:

<script setup>
import { useRouter } from 'vue-router';

const router = useRouter();
const getUser = () => {
    router.push({
        path: '/User',
        query: {
            name: '张三',
            age: 23
        }
    });
}
</script>

<template>
    <div>
        <button @click="getUser">获取学生信息</button>
    </div>
</template>

UserView.vue:通过route.query获取值

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

<template>
    <div>
        {{ $route.query.name }}-{{ $route.query.age }}
    </div>
</template>

页面输出:张三-23,控制台输出:张三 23

官方文档:https://router.vuejs.org/zh/guide/essentials/navigation.html

3.动态路由方法

动态添加一个路由:eg,后端管理需要动态创建路由权限

官方文档:https://router.vuejs.org/zh/guide/advanced/dynamic-routing.html

const router = useRouter();
// 动态添加路由:
const adminRoute = {
    path: '/admin',
    name: 'admin',
    component: () => import('../admin/Home.vue')
}
router.addRoute(adminRoute); // 动态添加一个路由

动态删除一个路由

  1. 通过添加同名路由,来替换之前的,eg:router.addRoute(xxx)
    1. 如果动态添加的路由名和列表其他路由名一样,那之前的就会被覆盖掉
  2. 根据路由名直接删除router.removeRoute(路由名)
  3. 回调添加路由的返回函数:添加路由的时候会返回一个回调函数,调用下就可以删掉路由
    1. eg:const removeAdminRoute = router.addRoute(adminRoute); removeAdminRoute(); // 如果存在就删除

检查和获取路由

  1. router.hasRoute():检查路由是否存在
  2. router.getRoutes():获取包含所有路由记录的数组

5.9.路由导航守卫

1.导航守卫简述

这个一般和动态添加路由一起使用,eg:某些路径需要登陆后才能访问

router/index.js:如果访问的是/admin页面就让他到登录页面

PS:这种有问题,可以绕过,eg:/admin,/admin?id=111就可以绕过,我这边只是展示,下面有实战demo

// to是去哪个地方,from是从哪里来的
router.beforeEach((to, from) => {
    const isLogin = window.sessionStorage.getItem('login');
    if(to.path==='/Admin'&&isLogin!=='ok'){
        return '/Login' // 不能是router.push,直接return路径字符串
    }
})

基本上都是用beforeEach,其他的我贴下官网的,用到可以去官网查询:

完整的导航解析流程

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

2.后台管理登录验证案例

下面来个后台管理的权限验证案例:

比如现在有这些页面,所有Admin相关的页面都必须登录才能访问

image-20230813203828771

先配置先router:

// /router/index.js
import { createRouter, createWebHistory } from "vue-router";
import LoginView from '../views/LoginView.vue';
import HomeView from '../views/HomeView.vue';

const router = createRouter({
    history: createWebHistory(),
    routes: [
        {
            path: '/',
            component: HomeView
        },
        {
            path: '/Home',
            name: 'home',
            component: HomeView
        },
        {
            path: '/Login',
            name: 'login',
            component: LoginView
        },
        {
            path: '/Admin',
            name: 'admin',
            meta: { requireAuth: true }, // 等会靠它判断要不要验证登录
            component: () => import('../views/AdminView.vue'),
            children: [
                {
                    path: 'Main',
                    name: 'main',
                    component: () => import('../views/AdminMainView.vue')
                },
                {
                    path: 'Menu',
                    name: 'menu',
                    component: () => import('../views/AdminMenuView.vue')
                },
            ]
        },
        {
            path: '/:pathMatch(.*)',
            name: '404',
            component: () => import('../views/NotFoundView.vue')
        }
    ]
})

// 登录权限验证
router.beforeEach((to, from) => {
    const isLogin = window.localStorage.getItem('login');
    if (to.meta.requireAuth && isLogin !== 'ok') {
        // 跳转到登录页面,登录成功后可以借此跳转回来
        return {
            path: '/Login',
            query: { redirect: to.fullPath }
        }
        // return '/Login';
    }
})

export default router

App.vue:就是一个路由的占位

<script setup>
</script>

<template>
    <div>
        <RouterView></RouterView>
    </div>
</template>

AdminView.vue:就是来个路由跳转的链接和路由占位

<template>
    <div>
        <h3>Admin</h3>
        <RouterLink to="/Admin/Menu">AdminMenu</RouterLink> |
        <RouterLink to="/Admin/Main">AdminMain</RouterLink>
    </div>
    <div>
        <RouterView></RouterView>
    </div>
</template>

AdminMainViewAdminMenuView里面没东西,就一段文本(Admin > MainAdmin > Menu

HomeView.vue:就是判断下是否登录,如果没登录就有个登录的链接,如果登录了就有个退出登录的链接

<template>
    <div>
        Home(
        <a v-if="isLogin == 'ok'" href="#" @click="outLogin">退出登录</a>
        <a v-else href="#" @click="$router.push('/Login')">前往登录</a>
        )
    </div>
</template>

<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const isLogin = ref(window.localStorage.getItem('login'));
const router = useRouter();

const outLogin = () => {
    window.localStorage.removeItem('login');
    router.push('/Login');
}
</script>

LoginView.vue:登录页面就模拟一个登录验证并把登录结果保存在sessionStorage

<script setup>
import { ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';

let username = ref('');
let password = ref('');
let msg = ref('');
const router = useRouter();
const route = useRoute();

const login = () => {
    // 模拟后台效验
    if (username.value == 'dnt' && password.value == 'dnt') {
        window.localStorage.setItem('login', 'ok')
        console.log('redirect:',route.query.redirect)
        // redirect不为空,并且只是站内跳转
        if (!route.query.redirect) {
            // 跳转到默认的管理页面
            router.push('/Admin')
        } else {
            router.push(route.query.redirect)
        }
    } else {
        msg.value = '用户名或密码错误!';
    }
}
</script>

<template>
    <h2>{{ msg }}</h2>
    <div>
        <input v-model="username" type="text" /><br />
        <input v-model="password" type="password" @keyup.enter="login" /><br />
        <button @click="login">登录</button>
    </div>
</template>

<style scoped>
h2 {
    color: red;
}
</style>

NotFoundView.vue:访问不存在的页面就到NotFound页面,然后三秒钟后跳转到主页

<script setup>
import { useRouter } from 'vue-router';

// 在外面实例化下,箭头函数访问不到useRouter
const router = useRouter(); 
setTimeout(() => {
    router.push('/Home')
}, 3000);
</script>

<template>
    <h2>
        {{ $route.path }} 404 NotFound
    </h2>
</template>

<style>
h2 {
    color: red;
}
</style>

效果与演示:打开首页后会让你去登录,如果我们不登录手动访问一个后端管理页面,就会被跳转到登录页,登录成功后会重新跳转到刚刚页面,如果在访问过程中,另一个页面点了退出登录,等你重新访问admin相关页面的时候(操作/刷新)就会又跳转到登录页面e2cb7c34-a4be-46aa-9d9d-badd1952e885

文档:https://router.vuejs.org/zh/guide/advanced/navigation-guards.html | 路由元信息meta

6.Vue3状态管理-Pinia

官网:https://pinia.vuejs.org/zh

首先说下状态管理,管理的是什么?==> 程序中的各种数据(服务器返回数据、缓存数据、用户操作产生的数据、各种UI状态eg:是否选择、是否动效、当前分页等)

以前在setup中会通过ref、reactive之类的方法定义各种数据(state),然后在template里进行展示(view),接着可能通过一些事件对他们进行修改(actions)然后会更新template里面的数据

以前vue自带的这套状态管理,可以进行简单的管理,但当我们程序里面数据非常多的时候,都放一个组件里面的时候,组件就比较臃肿了,维护和扩展就比较麻烦了(有些还不是父子组件,传递数据就更麻烦了)

所以官方就推出了Vuex和pinia,pinia在Vuex基础上进一步简化并实现了下一代Vuex5想实现的全部功能,所以官方干脆就不弄vuex5了,直接主推pinia

使用之前先看下官方说的哪些数据适合被全局管理(常用数据),哪些应该组件自己管理(UI状态

image-20230814083730800

6.1.初探pinia官方案例

我们新建一个vue的项目,选下pinia

image-20230814075801737

我们打开main.js,发现官方是这样注册pinia的:

image-20230814092521647

然后在stores文件夹里面有个计数的store:里面定义了一个state ==> count的data数据、一个getters ==> doubleCount的计算属性、一个actions ==> increment的方法(导出的方法一般都是以usexxxStore命名)image-20230814092700083

官方没有写调用案例,我们补充下:

<script setup>
import { useCounterStore } from '../stores/counter'
// useCounterStore返回的是一个方法,我们调用下可以得到实例
// 里面的东西就是return的内容`return { count, doubleCount, increment }`
const counterStore = useCounterStore();
</script>

<template>
    <div>
        count值:{{ counterStore.count }}、count*2值:{{ counterStore.doubleCount }}
        <button @click="counterStore.increment">Count++</button>
    </div>
</template>

点3下效果:image-20230814093742463

6.2.手写简单案例

我们以空项目自己手动引入创建为例:cnpm install pinia

然后在src目录下新建一个stores的文件夹,里面和路由一样新建一个index.js

import { createPinia } from "pinia";

const pinia = createPinia();

export default pinia

三句话就结束了,然后在main.js中use一下:(你也可以把这三句话就写在main.js中,这个无所谓的)

import { createApp } from 'vue'
import pinia from './stores';
import App from './App.vue'

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

上面都一样,下面演示下两个风格的pinia写法:

1.组合API风格

student.js:

// stores>student.js
import { ref, computed } from 'vue';
import { defineStore } from "pinia";

// 创建一个useStudentStore的方法
export const useStudentStore = defineStore('student', () => {
    //state
    let name = ref('张三');
    let age = ref(22);
    let gender = ref(1);
    // getter
    const genderStr = computed(() => {
        return gender.value == 1 ? '男' : '女';
    });
    //actions
    const changeName = (newName) => {
        name.value = newName;
    }
    // 一定要记得返回
    return { name, age, gender, genderStr, changeName }
})

view:实例化student对应的store后,里面return的方法和属性都是有代码提示的

<script setup>
import { useStudentStore } from '../stores/sudent';
let studentStore = useStudentStore(); // 获取studentStore对象

</script>

<template>
    <div>
        {{ studentStore.name }}-{{ studentStore.age }}-{{ studentStore.genderStr }}
        <button @click="studentStore.changeName('李四')">改变名称</button>
    </div>
</template>

输出效果:点击后名称就变了

image-20230814101537453

2.选项API风格

view不用修改,就把student.js修改下:

import { defineStore } from "pinia";

export const useStudentStore = defineStore('student', {
    state() {
        return {
            name: '小明',
            age: 22,
            gender: 1

        }
    },
    getters: {
        genderStr: (state) => {
            // 访问state里面的数据
            return state.gender == 1 ? '男' : '女';
            // 访问自己的其他getter可以使用this.属性名
            // 访问其他stone中的数据需要usexxx实例化下再访问
        }
    },
    actions: {
        changeName(newName) {
            // 这块别用箭头函数,不然访问不到this了
            this.name = newName;
        }
    }
})

输出效果:点击后名称就变了

image-20230814101537453


6.3.选项 vs 组合

怎么选,其实官方已经有些说辞了,加上官方案例基本上都是选项API的,而且官方的组合API和vue的setup函数差不多需要导入很多东西,也没那么简洁。加之可以同时有多个store ==> 具体还是看个人习惯,官方两个都支持

可以考虑:Pinia里面可以直接使用选项API的风格编程,Vue3中依旧使用组合API风格的编程(setup)

image-20230814094733121

6.4.组件中操作State

Vuex中是不允许直接操作State的(实际上也可以改,只是它要求你按照它的一系列规定来)pinia官方主打方便,删除了很多冗余和不合理的东西,通过直接对state的操作,以及多个store的定义,让我们开发非常方便:

官方文档:StoreStateGetterAction

我们平时操作的基本上都是state:https://pinia.vuejs.org/zh/core-concepts/state.html

还是上面的示例,我们直接在组件中修改即可:<button @click="changeName">组件改信息</button>

const changeName = () => {
    // 直接对state操作即可,state对象是reactive的,不用.value就可以直接操作
    studentStore.name = '小华';
    studentStore.gender = 0;
}

完整代码如下:students.js还是之前那样的

<script setup>
import { useStudentStore } from '../stores/student';
let studentStore = useStudentStore();

const changeName = () => {
    // 直接对state操作即可,state对象是reactive的,不用.value就可以直接操作
    studentStore.name = '小华';
    studentStore.gender = 0;
}
</script>

<template>
    <div>
        {{ studentStore.name }}-{{ studentStore.age }}-{{ studentStore.genderStr }}
        <button @click="studentStore.changeName('李四')">改变名称</button>
        <button @click="changeName">组件改信息</button>
    </div>
</template>

效果:

image-20230814105232110

其它State方法

简单提一下,也就$patch可能用一下,其他基本上不用的

  1. 重置State:xxxStore.$reset
  2. 改多个State:xxxStore.$patch({})
  3. 替换State:xxxStore.$state={}

之前对name和gender进行了修改,我们也可以这样批量修改:

const changeName = () => {
    // 直接对state操作即可,state对象是reactive的,不用.value就可以直接操作
    // studentStore.name = '小华';
    // studentStore.gender = 0;
    studentStore.$patch({
        name: '小华',
        gender: 0
    })
}

扩展:如果你是用选项API的方式创建Store有重置的方法,如果是组合APIsetup的方式创建的store就没有这个方法了

image-20230814110431533

7.网络请求 - Axios

axiosAjax i/o system(安装:cnpm install axios

源码:https://github.com/axios/axios 官网:https://axios-http.com 中文:https://www.axios-http.cn/docs/intro

7.1.语法概述

1.get请求

get请求,url后面参数可以放在params对象中

// 简写
axios.get('/api/students/getstudent', {
        params: {
            id: id.value
        }
    })

完整写法

axios({
        url:'/api/students/getstudent',
        method:'get',
        params:{
            id: id.value
        }
    })

2.post请求

简写:url后面参数就是data,不用单独写个data:{}

axios.post('/api/students/update',
{
    id: student.value.id,
    name: student.value.name,
    age: student.value.age,
    gender: student.value.gender
})

完整写法

axios({
        url: '/api/students/update',
        method: 'post',
        data: {
            id: student.value.id,
            name: student.value.name,
            age: student.value.age,
            gender: student.value.gender
        }
    })

3.自定义配置

常见配置选项:

  1. url:请求地址
  2. method:请求类型
  3. params:URL查询对象
  4. data:post提交对象(request body)
  5. baseURL:请求根路径
  6. headers:请求头
  7. timeout:超时时间设置(默认不超时)
  8. transformRequest:请求前的数据处理
  9. transformResponse:请求后的数据处理

可以使用自定义配置新建一个实例,后面请求使用这个实例即可:

const myAxios = axios.create({
  baseURL: 'https://localhost:7002',
  timeout: 3000,
  headers: { 'Timestamp': timestamp }
});

4.请求响应拦截器

拦截器就两个,一个是请求前拦截,一个是响应前拦截

myAxios.interceptors.request.use((config) => {
    console.log('请求前我都会拦截一下的:\r\n', config);
    return config;
}, (error) => {
    console.log('请求失败拦截:\r\n', error);
    return Promise.reject(error);
})
myAxios.interceptors.response.use((res) => {
    console.log('响应前我都会拦截一下的:\r\n', res.data);
    return res.data; // 返回res.data后,后面通过myAxios请求后返回的res就直接是res.data了
}, (error) => {
    console.log('响应失败拦截:\r\n', error);
    return Promise.reject(error);
})

7.2.get和post简单案例

来个简单案例,后台现在封装了3个api接口:getall可以获取所有学生信息,getstudent?id=x可以获取指定学生信息,update可以修改某个学生信息

PS:我们平时开发其实就get和post,删也是post(都是数据状态改变的软删除,数据库的数据不会删的)

image-20230814140325987

template部分:上面部分就是遍历下students,中间是一个文本框点击按钮可以获取student信息,并显示在最下面,下面的按钮可以把更新后的内容发送到数据库,并更新本地数据

<template>
    <div>
        <div>
            <ul>
                <li v-for="student in students" :key="student.id">
                    {{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
                </li>
            </ul>
        </div>
        <div>
            <input type="text" v-model="id"><button @click="getData">get请求获取id为{{ student.id }}的学生</button>
        </div>
        <div v-if="student.id">
            <input v-model="student.id" type="hidden" /><br />
            <input v-model="student.name" type="text" /><br />
            <input v-model="student.age" type="text" /><br />
            <input v-model="student.gender" type="text" /><br />
            <button @click="updateData">修改id为{{ student.id }}的信息</button>
        </div>
    </div>
</template>

script部分:

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

const id = ref(1); // 查找的id
const student = ref({}); // getData和update所用的模型
const students = ref([]); // 所有学生列表

// 加载所有的students
axios.get('https://localhost:7002/api/students/getall').then(res => {
    students.value = res.data;
});

const getData = () => {
    axios.get('https://localhost:7002/api/students/getstudent', {
        params: {
            id: id.value
        }
    }).then(res => {
        student.value = res.data;
    }).catch(error => {
        console.log(error.message); // axios提示
        console.log(error.response); // 返回结果
    })
}

const updateData = () => {
    // 简略写法,student和我们要发送过去的字段同名,我们可以便捷的这样写
    axios.post('https://localhost:7002/api/students/update',
        {
            // id: student.value.id,
            // name: student.value.name,
            // age: student.value.age,
            // gender: student.value.gender
            ...student.value
        })
        .then(res => {
            if (res.status == 200) {
                console.log('更新成功:', res.data)
                // id和索引不见得对应,所以要手动获取下
                // const index = students.value.findIndex(item => item.id === student.value.id);
                // if (index !== -1) {
                //     students.value[index] = student.value;
                // }
                // 如果服务器有把更新后的数据返回,可以这么写
                const index = students.value.findIndex(s => s.id == res.data.id);
                if (index != -1) {
                    students.value[index] = res.data;
                }
            }
        }).catch(error => {
            console.log(error);
        })
}
</script>

运行后效果:

image-20230814145552508

PS:如果你本地信息没更新过来,说明你忘记更新了

不推荐直接把下面表单帮students中的某个元素,万一你更新失败,本地已经更新,远程其实并没有更新成功的,所以需要先远程反馈成功,然后你本地才能更新

if (res.status == 200) {
    console.log('更新成功:', res.data)
    // id和索引不见得对应,所以要手动获取下
    const index = students.value.findIndex(item => item.id === student.value.id);
    if (index !== -1) {
         students.value[index] = student.value;
    }
}

本小节案例:https://gitee.com/lotapp/BaseCode/blob/master/javascript/3.Vue/8vue-axios/src/views/AxiosView.vue

7.3.自定义配置案例

上面案例,我们简化下:一个文本框输入id点按钮后会把该id的学生显示出来,修改后点更新按钮可以更新数据

<template>
    <div>
        <div>
            <input type="text" v-model="id"><button @click="getDataById">get请求获取id为{{ student.id }}的学生</button>
        </div>
        <div v-if="student.id">
            <input v-model="student.id" type="hidden" /><br />
            <input v-model="student.name" type="text" /><br />
            <input v-model="student.age" type="text" /><br />
            <input v-model="student.gender" type="text" /><br />
            <button @click="updateData">修改id为{{ student.id }}的信息</button>
        </div>
    </div>
</template>

script部分:用法和之前没区别,就是使用自己创建的实例对象myAxios发起请求,这个请求里面会自动在开头加配置的baseURL,并添加headers

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

const id = ref(1); // 查找的id
const student = ref({}); // getData和update所用的模型

// 创建一个自定义配置的axios实例
const myAxios = axios.create({
    baseURL: 'https://localhost:7002',
    // timeout: 6000,
    // headers后面的对象,不是列表哦
    headers: {
        'RegisterNo': 'JD-TEST',
        'Timestamp': new Date().getTime()
    },
});
// 根据id获取学生信息
const getDataById = () => {
    myAxios.get('/api/students/getstudent', {
        params: {
            id: id.value
        }
    }).then(res => {
        student.value = res.data;
    }).catch(error => {
        console.log(error.message); // axios提示
        console.log(error.response); // 返回结果
    })
}
// 更新学生数据
const updateData = () => {
    myAxios.post('/api/students/update',
        {
            ...student.value
        })
        .then(res => {
            console.log(res.data);
        }).catch(error => {
            console.log(error);
        })
}
</script>

效果:

image-20230814152218556

请求头也自动加上了:

image-20230814153029995

本小节案例:https://gitee.com/lotapp/BaseCode/blob/master/javascript/3.Vue/8vue-axios/src/views/AxiosConfigView.vue

7.4.拦截器案例

拦截器用的还是挺多的,比如请求前显示loading动画 ,响应前取消loading动画。再比如响应前把res变成res.data,这样获取数据不用res.data.dataxxx了

还是以7.3的案例来说事,我们简单改下:

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

const id = ref(1); // 查找的id
const student = ref({}); // getData和update所用的模型

// 创建一个自定义配置的axios实例
const myAxios = axios.create({
    baseURL: 'https://localhost:7002',
    // timeout: 6000,
    // headers后面的对象,不是列表哦
    headers: {
        'RegisterNo': 'JD-TEST',
        'Timestamp': new Date().getTime()
    },
});

myAxios.interceptors.request.use((config) => {
    console.log('请求前我都会拦截一下的:\r\n', config);
    return config;
}, (error) => {
    console.log('请求失败拦截:\r\n', error);
    return Promise.reject(error);
})
myAxios.interceptors.response.use((res) => {
    console.log('响应前我都会拦截一下的:\r\n', res.data);
    return res.data; // 返回res.data后,后面通过myAxios请求后返回的res就直接是res.data了
}, (error) => {
    console.log('响应失败拦截:\r\n', error);
    return Promise.reject(error);
})

// 根据id获取学生信息
const getDataById = () => {
    myAxios.get('/api/students/getstudent', {
        params: {
            id: id.value
        }
    }).then(res => {
        student.value = res;//res.data;
    }).catch(error => {
        console.log(error.message); // axios提示
        console.log(error.response); // 返回结果
    })
}
// 更新学生数据
const updateData = () => {
    myAxios.post('/api/students/update',
        {
            ...student.value
        })
        .then(res => {
            console.log(res);
            // console.log(res.data);
        }).catch(error => {
            console.log(error);
        })
}
</script>

<template>
    <div>
        <div>
            <input type="text" v-model="id"><button @click="getDataById">get请求获取id为{{ student.id }}的学生</button>
        </div>
        <div v-if="student.id">
            <input v-model="student.id" type="hidden" /><br />
            <input v-model="student.name" type="text" /><br />
            <input v-model="student.age" type="text" /><br />
            <input v-model="student.gender" type="text" /><br />
            <button @click="updateData">修改id为{{ student.id }}的信息</button>
        </div>
    </div>
</template>

效果:查询id2的学生名字叫王二麻1,我们把名字后面的1删掉并提交 ==> 拦截器全程都可以捕获到的

image-20230814155400603

本小节案例:https://gitee.com/lotapp/BaseCode/blob/master/javascript/3.Vue/8vue-axios/src/views/AxiosAopView.vue

7.5.封装简易网络库

封装的目的 ==> 解耦:eg:防止以后第三方库不更新了,或者大更新对项目有影响。

PS:可以写一个封装的库,项目直接引用自己封装的库,后期有变更的话只要调整自己的库就可以了

我这边封装了一个简陋的网络库来作为示例:

使用封装

import axios from "axios";

// 使用前直接配置好
const myAxios = axios.create({
    baseURL: '',
    timeout: 10000,
    headers: {}
})

// 请求拦截
myAxios.interceptors.request.use(config => {
    return config;
}, error => {
    return Promise.reject(error);
})
// 响应拦截
myAxios.interceptors.response.use(res => {
    return res.data;
}, error => {
    return Promise.reject(error);
})

export default myAxios

解耦Class类封装

import axios from "axios";

// 如果需要指定baseURL、timeout、headers,可以new一个Class配置
// import { MyAxiosClass } from '../http'
// eg:const myAxios = new MyAxiosClass('https://jd.com:7788')
export class MyAxiosClass {
    constructor(baseURL = '', timeout = 0, headers = {}) {
        // 为当前实例创建一个axios对象
        this.myAxios = axios.create({
            baseURL: baseURL,
            timeout: timeout,
            headers: headers
        })
    }
    request(config) {
        console.log(config)
        return new Promise((resolve, reject) => {
            this.myAxios.request(config).then(res => {
                resolve(res.data);// 操作成功,返回res.data
            }).catch(error => {
                reject(error);// 请求失败返回错误
            });
        })
    }
    get(url, config = { params: {} }) {
        return this.request({ url, ...config, method: 'get' });
        // return new Promise((resolve, reject) => {
        //     this.myAxios.get(url, config).then(res => {
        //         resolve(res.data);// 操作成功,返回res.data
        //     }).catch(error => {
        //         reject(error);// 请求失败返回错误
        //     });
        // });
    }
    post(url, data = {}, config = {}) {
        return this.request({ url, data, ...config, method: 'post' });
        // return new Promise((resolve, reject) => {
        //     this.myAxios.post(url, data, config).then(res => {
        //         resolve(res.data);// 操作成功,返回res.data
        //     }).catch(error => {
        //         reject(error);// 请求失败返回错误
        //     });
        // });
    }
}

// 如果平时使用直接导入包即可使用:import myAxios from '@/http';
export default new MyAxiosClass()

1.直接使用

如果没有全局配置的话,直接导入包即可创建一个实例import myAxios from '../http';还是上面的案例,用法其实一样的:

PS:就是then的时候,获取数据不用res.data了,直接res即可(我库里面做了处理:resolve(res.data)

<script setup>
import { ref } from 'vue';
import myAxios from '../http';

const id = ref(1); // 查找的id
const student = ref({}); // 学生信息
const students = ref([]); // 学生列表

// 获取学生列表
myAxios.get('https://localhost:7002/api/students/getall').then(res => {
    // 这边写res即可,不用res.data了,自定义库里面处理过了
    students.value = res;
}).catch(error => {
    console.log(error.message);
})

// 根据id获取学生信息
const getDataById = () => {
    myAxios.get('https://localhost:7002/api/students/getstudent', {
        params: {
            id: id.value
        }
    }).then(res => {
        student.value = res;
    }).catch(error => {
        console.log(error.message);
    });
}
// 更新学生信息
const updateData = () => {
    myAxios.post('https://localhost:7002/api/students/update', {
        ...student.value
    }).then(res => {
        const index = students.value.findIndex(s => s.id == res.id);
        if (index != -1)
            students.value[index] = res;
    }).catch(error => {
        console.log(error.message);
    });
}
</script>

<template>
    <div>
        <div>
            <ul>
                <li v-for="student in students" :key="student.id">
                    {{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
                </li>
            </ul>
        </div>
        <div>
            <input type="text" v-model="id"><button @click="getDataById">get请求获取id为{{ id }}的学生</button>
        </div>
        <div v-if="student.id">
            <input v-model="student.id" type="hidden" /><br />
            <input v-model="student.name" type="text" /><br />
            <input v-model="student.age" type="text" /><br />
            <input v-model="student.gender" type="text" /><br />
            <button @click="updateData">修改id为{{ student.id }}的信息</button>
        </div>
    </div>
</template>

效果和7.2案例一样:

image-20230814145552508

本节使用请求代码:https://gitee.com/lotapp/BaseCode/blob/master/javascript/3.Vue/8vue-axios/src/views/MyAxiosInstanceView.vue

2.自定义配置

代码没有什么变换,就是导入一个类,然后实例化的时候配置下baseUrl,然后后面请求的url头都不用写了
image-20230815093640149

js封装库和上面一样,view也一样,就头和url不一样,我贴下:

<script setup>
import { ref } from 'vue';
// import myAxios from '../http';
import { MyAxiosClass } from '../http';
// 创建axios对象,并配置baseurl
const myAxios = new MyAxiosClass('https://localhost:7002/api');

const id = ref(1); // 查找的id
const student = ref({}); // 学生信息
const students = ref([]); // 学生列表

// 获取学生列表
myAxios.get('/students/getall').then(res => {
    // 这边写res即可,不用res.data了,自定义库里面处理过了
    students.value = res;
}).catch(error => {
    console.log(error.message);
})

// 根据id获取学生信息
const getDataById = () => {
    myAxios.get('/students/getstudent', {
        params: {
            id: id.value
        }
    }).then(res => {
        student.value = res;
    }).catch(error => {
        console.log(error.message);
    });
}
// 更新学生信息
const updateData = () => {
    myAxios.post('/students/update', {
        ...student.value
    }).then(res => {
        const index = students.value.findIndex(s => s.id == res.id);
        if (index != -1)
            students.value[index] = res;
    }).catch(error => {
        console.log(error.message);
    });
}
</script>

<template>
    <div>
        <div>
            <ul>
                <li v-for="student in students" :key="student.id">
                    {{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
                </li>
            </ul>
        </div>
        <div>
            <input type="text" v-model="id"><button @click="getDataById">get请求获取id为{{ id }}的学生</button>
        </div>
        <div v-if="student.id">
            <input v-model="student.id" type="hidden" /><br />
            <input v-model="student.name" type="text" /><br />
            <input v-model="student.age" type="text" /><br />
            <input v-model="student.gender" type="text" /><br />
            <button @click="updateData">修改id为{{ student.id }}的信息</button>
        </div>
    </div>
</template>

效果:同上

本节代码:https://gitee.com/lotapp/BaseCode/blob/master/javascript/3.Vue/8vue-axios/src/views/MyAxiosView.vue

7.6.重试机制

有时候我们有重试的需求,在Axios中可以通过axios-retry的插件进行实现:cnpm i axios-retry

import axiosRetry from 'axios-retry';

// 添加axios-retry插件,配置重试策略
axiosRetry(myAxios, {
    retries: 3, // 重试次数  
    retryDelay: (retryCount) => {
        // 使用指数退避策略,每次重试的时间间隔依次递增  
        return retryCount * 1000; // 
    },
    shouldResetTimeout: true, // 每次重试都重置超时时间
    // retryCondition: () => true // 始终重试
    // 判断是否为网络错误或者超时错误才进行重试
    retryCondition: (error) => {
        return axiosRetry.isNetworkOrIdempotentRequestError(error);
    },
});

贴一下简单版的HTTP封装库:

import axios from "axios";
import axiosRetry from 'axios-retry';

// 跨域访问首先要服务端设置了正确的 CORS(跨源资源共享)
const myAxios = axios.create({
    baseURL: 'xxx/services',
    timeout: 5000, // 5s内没响应就当失败了
    headers: {}
})

// 添加axios-retry插件,配置重试策略
axiosRetry(myAxios, {
    retries: 3, // 重试次数  
    retryDelay: (retryCount) => {
        return retryCount * 1000; // 重试时间累加
    },
    shouldResetTimeout: true, // 每次重试都重置超时时间
    // retryCondition: () => true // 始终重试
    // 判断是否为网络错误或者超时错误才进行重试
    retryCondition: (error) => {
        return axiosRetry.isNetworkOrIdempotentRequestError(error);
    },
});

// 请求拦截
myAxios.interceptors.request.use(config => {
    console.debug("request AOP", config);
    // TODO: eg,每个请求都要携带的headers...
    return config;
}, error => {
    return Promise.reject(error);
})
// 响应拦截
myAxios.interceptors.response.use(res => {
    console.debug("response AOP status:", res.status);
    // TODO:状态非200的处理
    return res.data;
}, error => {
    return Promise.reject(error);
})

export default myAxios

附录:官方完整Axios配置

我们不会用这么多,先贴一下官方的配置:简单看下就行

这些是创建请求时可以用的配置选项。只有 url 是必需的。如果没有指定 method,请求将默认使用 GET 方法。

{
  // `url` 是用于请求的服务器 URL
  url: '/students/getall',

  // `method` 是创建请求时使用的方法
  method: 'get', // 默认值

  // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
  baseURL: 'https://localhost:7002/api',

  // `transformRequest` 允许在向服务器发送前,修改请求数据
  // 它只能用于 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  // 数组中最后一个函数必须返回一个字符串, 一个Buffer实例,ArrayBuffer,FormData,或 Stream
  // 你可以修改请求头
  transformRequest: [function (data, headers) {
    // 对发送的 data 进行任意转换处理
    // TODO:【请求拦截器逻辑】
    return data;
  }],

  // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
  transformResponse: [function (data) {
    // 对接收的 data 进行任意转换处理
	// TODO:【响应拦截器逻辑】
    return data;
  }],

  // 自定义请求头【后面是对象的形式】
  headers: {'X-Requested-With': 'XMLHttpRequest'},

  // `params` 是与请求一起发送的 URL 参数【eg:url?id=xxx】
  // 必须是一个简单对象或 URLSearchParams 对象
  params: {
    id: 12345
  },

  // `paramsSerializer`是可选方法,主要用于序列化`params`【基本上用不到】
  // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
  paramsSerializer: function (params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'})
  },

  // `data` 是作为请求体被发送的数据
  // 仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
  // 在没有设置 `transformRequest` 时,则必须是以下类型之一:
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 浏览器专属: FormData, File, Blob
  // - Node 专属: Stream, Buffer
  data: {
    firstName: 'Fred'
  },
  
  // 发送请求体数据的可选语法【用上面的{}对象形式请求】
  // 请求方式 post
  // 只有 value 会被发送,key 则不会
  data: 'Country=Brasil&City=Belo Horizonte',

  // `timeout` 指定请求超时的毫秒数。
  // 如果请求时间超过 `timeout` 的值,则请求会被中断
  timeout: 1000, // 默认值是 `0` (永不超时)

  // `withCredentials` 表示跨域请求时是否需要使用凭证
  withCredentials: false, // default

  // `adapter` 允许自定义处理请求,这使测试更加容易。
  // 返回一个 promise 并提供一个有效的响应 (参见 lib/adapters/README.md)。
  adapter: function (config) {
    /* ... */
  },

  // `auth` HTTP Basic Auth
  auth: {
    username: 'janedoe',
    password: 's00pers3cret'
  },

  // `responseType` 表示浏览器将要响应的数据类型【现在基本上都是json】
  // 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
  // 浏览器专属:'blob'
  responseType: 'json', // 默认值

  // `responseEncoding` 表示用于解码响应的编码 (Node.js 专属)
  // 注意:忽略 `responseType` 的值为 'stream',或者是客户端请求
  // Note: Ignored for `responseType` of 'stream' or client-side requests
  responseEncoding: 'utf8', // 默认值

  // `xsrfCookieName` 是 xsrf token 的值,被用作 cookie 的名称
  xsrfCookieName: 'XSRF-TOKEN', // 默认值

  // `xsrfHeaderName` 是带有 xsrf token 值的http 请求头名称
  xsrfHeaderName: 'X-XSRF-TOKEN', // 默认值

  // `onUploadProgress` 允许为上传处理进度事件
  // 浏览器专属
  onUploadProgress: function (progressEvent) {
    // 处理原生进度事件
  },

  // `onDownloadProgress` 允许为下载处理进度事件
  // 浏览器专属
  onDownloadProgress: function (progressEvent) {
    // 处理原生进度事件
  },

  // `maxContentLength` 定义了node.js中允许的HTTP响应内容的最大字节数
  maxContentLength: 2000,

  // `maxBodyLength`(仅Node)定义允许的http请求内容的最大字节数
  maxBodyLength: 2000,

  // `validateStatus` 定义了对于给定的 HTTP状态码是 resolve 还是 reject promise。
  // 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),
  // 则promise 将会 resolved,否则是 rejected。
  validateStatus: function (status) {
    return status >= 200 && status < 300; // 默认值
  },

  // `maxRedirects` 定义了在node.js中要遵循的最大重定向数。
  // 如果设置为0,则不会进行重定向
  maxRedirects: 5, // 默认值

  // `socketPath` 定义了在node.js中使用的UNIX套接字。
  // e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。
  // 只能指定 `socketPath` 或 `proxy` 。
  // 若都指定,这使用 `socketPath` 。
  socketPath: null, // default

	// `httpAgent '和` httpsAgent '定义执行http时要使用的自定义代理和https请求。这允许添加如下选项
	// `keepAlive ',默认情况下不启用。
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),

  // `proxy` 定义了代理服务器的主机名,端口和协议。【有时候有外网或者内网的数据需要访问,就需要用到代理】
  // 您可以使用常规的`http_proxy` 和 `https_proxy` 环境变量。
  // 使用 `false` 可以禁用代理功能,同时环境变量也会被忽略。
  // `auth`表示应使用HTTP Basic auth连接到代理,并且提供凭据。
  // 这将设置一个 `Proxy-Authorization` 请求头,它会覆盖 `headers` 中已存在的自定义 `Proxy-Authorization` 请求头。
  // 如果代理服务器使用 HTTPS,则必须设置 protocol 为`https`
  proxy: {
    protocol: 'https',
    host: '127.0.0.1',
    port: 9000,
    auth: {
      username: 'mikeymike',
      password: 'rapunz3l'
    }
  },

  // see https://axios-http.com/zh/docs/cancellation
  cancelToken: new CancelToken(function (cancel) {
  }),

  // `decompress '指示是否应对响应正文进行自动解压缩。
  //来自所有解压缩响应的响应对象-仅节点(XHR无法关闭解压缩)
  decompress: true // 默认值
}

一个请求的响应包含以下信息:

{
  // `data` 由服务器提供的响应
  data: {},

  // `status` 来自服务器响应的 HTTP 状态码
  status: 200,

  // `statusText` 来自服务器响应的 HTTP 状态信息
  statusText: 'OK',

  // `headers` 是服务器响应头
  // 所有的 header 名称都是小写,而且可以使用方括号语法访问
  // 例如: `response.headers['content-type']`
  headers: {},

  // `config` 是 `axios` 请求的配置信息
  config: {},

  // `request` 是生成此响应的请求
  // 在node.js中它是最后一个ClientRequest实例 (in redirects),
  // 在浏览器中则是 XMLHttpRequest 实例
  request: {}
}

当使用 then 时,您将接收如下响应:

axios.get('/user/12345')
  .then(function (response) {
    console.log(response.data);
    console.log(response.status);
    console.log(response.statusText);
    console.log(response.headers);
    console.log(response.config);
  });
posted @ 2023-08-11 18:34  鲲逸鹏  阅读(203)  评论(2)    收藏  举报