优势
基于 vue2 的知识,可以很快理解 vue3 的特性。使用 TypeScript 支持;
Vue2 组合式 API 对比 Vu3 选项式 API
早期 vu2 对于大项目的维护比较困难,原因是因为每个内容放在不同的区域中。若需要改动某项方法,则可能连带着其他区域的内容进行修改。
大量的代码都要上下滑动找到对应的内容。造成不必要的麻烦。
vue3 从 vue2 的 选项式 的代码书写,变成了 集中式 的代码书写。从而集中式管理;
代码书写风格
实现 点击按钮 让数字 + 1 的业务
vue3 的集中式管理使得 代码量更精简,从分散式维护转为集中式维护,更容易封装复用
认识 create-vue
对比 vue2 的 vue@cil 脚手架工具,官方提倡使用官方新的脚手架工具。因为其底层 从 wabpack 打包切换到了 vite (下一代构建工具),
为开发提供极速响应;
使用 create-vue 创建项目
确保已安装 Node.js 的 16.0 或者更高版本
nodejs:^16.0
创建一个 Vue应用
# 这将会安装并执行 create-vue
npm init vue@latest
配置项目 (初期学习全部勾选 否 即可)
√ 请输入项目名称: ... vue-demo
√ 是否使用 TypeScript 语法? ... 否 / 是
√ 是否启用 JSX 支持? ... 否 / 是
√ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是
√ 是否引入 Pinia 用于状态管理? ... 否 / 是
√ 是否引入 Vitest 用于单元测试? ... 否 / 是
√ 是否要引入一款端到端(End to End)测试工具? » 不需要
√ 是否引入 ESLint 用于代码质量检测? ... 否 / 是
正在构建项目 D:\smm\code\vue\vue3\vue-demo...
安装依赖、启动项目
# 跳转到项目目录 vue-demo 为你创建的项目名
cd vue-demo
# 安装依赖
npm install
# 运行项目
npm run dev
完成安装,Vue 启动后。项目默认部署在 http://127.0.0.1:5174 上
启动成功后访问项目的界面如下图所示:
目录介绍
Vue3 的项目目录和关键文件如下:
- vite.config.js – 项目的配置文件 基于 vite 的配置
- package.json – 项目包文件 核心依赖项变成了 Vue3.x 和 vite
- main.js – 入口文件 createApp 函数创建应用实例
- app.vue – 根组件 SFC 单文件组件 script-template-style
- 变化1:脚本 script 和模板 template 顺序调整
- 变化2:模板 template 不在要求唯一根元素
- 变化3:脚本 script 添加 setup 表示支持组合式 API
- index.html – 单页面入口 提供 id 为 app 的挂载点
vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
// 允许我们以 @ 方式去访问对应目录 (Vue2 中默认底层已配置)
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
package.json
{
"name": "vue-demo",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.11"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.2",
"vite": "^5.0.10"
}
}
mian.js
import './assets/main.css'
// 从 new Vue() 改成了 createApp() 创建应用实例
// 将来 创建路由 和创建 store 变成了
// createRoter()、createStore()
// 目的是为了保证每个实例的独立封闭性,不受其他模块的影响
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
App.vue
<!-- 逻辑 -->
<!-- setup 是允许在 script 中直接编写组合式 API -->
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'
</script>
<!-- 结构 -->
<!-- 允许根目录存在多元素 -->
<template>
<header>
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<!-- 局部注册 引入即用 -->
<HelloWorld msg="You did it!" />
</div>
</header>
<main>
<TheWelcome />
</main>
</template>
<!-- 样式 -->
<style scoped>
header {
line-height: 1.5;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
}
</style>
由于 Vue2 使用的 vuter 不适用于 Vue3 项目,需要禁用该插件并启用 Vue3 支持的插件 volar
组合式 API 的入口 就是 setup
setup 选项的写法和执行时机
setup 是一个钩子函数,在 vue 的生命周期中它的执行时机要先于 beforCreate ,因此 setup 中无法获取到 this
<script>
export default {
// setup 函数中是获取不到 this, this 是 undefined
setup() {
console.log('setup 函数', this);
},
beforeCreate() {
console.log('beforCreate 函数')
}
}
</script>
效果演示
setup 的函数和使用
由于 setup 函数是在 vue 创建前执行,若想讲 setup 中的实际数据提供给其他区域,需要直接 return 出来
<script>
export default {
setup() {
const message = 'Hello word';
const logMessage = () => {
console.log('这是一个提示');
}
// 将需要的数据 return 出去
return {
message,
logMessage
}
}
}
</script>
<template>
<div>{{ message }}</div>
<button @click="logMessage">按钮</button>
</template>
效果演示:
<script setup> 语法糖
为了方便开发者能简化上方步骤,即每次使用都需要 return,官方提供了语法糖来代替 return 的操作;
即:在 script 的左侧加上 setup 修饰,Vue3 会自动将加了 setup 修饰的 script 标签进行处理,并 return 出去
<script setup>
const message = 'Hello word';
const logMessage = () => {
console.log('这是一个提示');
}
</script>
<template>
<div>{{ message }}</div>
<button @click="logMessage">按钮</button>
</template>
<style>
body {
padding-left: 700px;
}
</style>
效果演示:
Vue3中,默认的数据并不是响应式的,使用 import 导入 reactive 模块,会将参数中的对象转成响应式并返回该对象
说明
接收对象类型数据的参数传入并返回一个响应式对象。
- 从 vue 包中导入 reactive 函数
- 在 <script setup> 中执行 reactive 函数并传入类型为对象的初始值,并使用遍历接收返回值
<script setup>
// 导入 reactive 模块
import { reactive } from 'vue';
// reactive 接收一个对象类型的数据,返回一个响应式的对象
const state = reactive({ count: 1 });
// 实现方法
const setCount = () => {
state.count++;
console.log(state.count);
}
</script>
<template>
<!-- 响应式的数据当发生改变时 会动态渲染界面 -->
<div>{{ state.count }}</div>
<button @click="setCount">+1</button>
</template>
效果演示
Vue3中,默认的数据并不是响应式的,使用 import 导入 ref 模块,会将参数中的对象(复杂类型)或简单类型数据转成响应式并返回该对象
推荐使用 ref 方法声明数据,因为该方法比起 reactive 方法更灵活实用
说明
接收 简单类型 或 复杂类型 的数据,返回一个响应式的对象。
本质上是再原有传入数据的基础上,外层包了一层复杂类型的对象。因此打印 ref 导出的内容时候,可以看到 ref 返回的是一个对象。
其中传入 ref 的值放在了对象成员的 value 中。
它底层的逻辑就是将简单数据包装成复杂类型之后,在借助 reactive 实现响应式数据。
<script setup>
import { ref } from 'vue';
// reactive 接收一个对象类型的数据,返回一个响应式的对象
const count = ref(0);
console.log(count);
</script>
- 从 vue 包中导入 ref 函数
- 在 <script setup> 中执行 ref 函数并传入类型为复杂或简单类型的数据,并使用遍历接收返回值
<script setup>
import { ref } from 'vue';
const count = ref(0);
// 实现方法
const setCount = () => {
count.value++;
console.log(count.value); // 通过 value 获取到数据
}
</script>
<template>
<!-- 响应式的数据当发生改变时 会动态渲染界面 -->
<!-- 在 template 中使用 ref 数据不需要添加 .value,因为vue 帮我们扒了一层 -->
<div>{{ count }}</div>
<button @click="setCount">+1</button>
</template>
效果演示
computed 是 vue3 的计算属性,它的计算属性基本思想和 vue2 的完全一致,只是在组合式API下修改了写法。
操作
- 导入 computed 函数,并创建一个只读的计算属性 ref;
- 指向函数 在 回调参数 中 return 基于响应式数据做计算的值,用变量接收
<script setup>
import { computed, ref } from 'vue';
const state = ref([1, 2, 3, 4]);
// 计算属性会在回调参数中的 return 计算值
const computedState = computed(() => {
// 基于响应式数据做计算后的值
return state.value.map(item => item * 2);
})
</script>
<template>
<!-- 响应式的数据当发生改变时 会动态渲染界面 -->
<div>原数组:{{ state }}</div>
<div>计算属性返回数组:{{ computedState }}</div>
</template>
效果演示
get set 操作
创建一个可写的计算属性 ref;但是一般情况下 计算属性不应该有副作用,并尽量避免直接修改计算属性的值
- 不建议增加副作用:例如去发起一些请求等操作,操作dom是不合理的,计算属性应该只做数据的展示
- 布建议去修改计算属性的值:因为计算属性建议仅用于 只读
<script setup>
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
</script>
watch 是用于声明在数据更改时调用的侦听回调;
作用:侦听一个或者多个数据的变化,数据变化时立即执行回调函数;拥有俩个额外参数:
- immediate (立即执行)
- deep (深度监听)
侦听单个数据
使用 watch(需要监听的响应式数据,(新值,旧值)=>{ // 操作的方法 }) 语法实现:
<script setup>
import { ref, watch } from 'vue';
const state = ref(0);
const addNum = function () {
state.value++
}
// 调用 watch 侦听变化
watch(state, (newValue, oldValue) => {
console.log(newValue, oldValue);
})
</script>
<template>
<div>{{ state }}</div>
<button @click="addNum">+1</button>
</template>
效果演示
侦听多个数据的变化
传入数组,同时监听多个响应式数据的变化,数组中任何一个数据的值发生变化时,均会触发 watch 侦听器的回调函数
<script setup>
import { ref, watch } from 'vue';
const num_1 = ref(0);
const num_2 = ref(0);
const addNum_1 = function () {
num_1.value++
}
const addNum_2 = function () {
num_2.value++
}
// 调用 watch 侦听变化
watch([num_1, num_2], (newValue, oldValue) => {
console.log('监听数据1:' + newValue[0], oldValue[0]);
console.log('监听数据2:' + newValue[1], oldValue[1]);
})
</script>
<template>
<div>{{ num_1 }}</div>
<div>{{ num_2 }}</div>
<button @click="addNum_1">+1</button>
<button @click="addNum_2">+1</button>
</template>
效果演示
初始化触发、深度监听
immediate 和 deep 参数 可以实现侦听器初始化时触发和深度监听对象数据的操作
- immediate 参数声明成 true 后,会在侦听器创建时候立即触发回调,响应式数据发生变化之后继续执行回调
- deep 为 true 时会开启深度监听,watch 默认是浅层监听,即:只有对象指向的复杂数据的下标发生改变时才触发
<script setup>
import { ref, watch } from 'vue';
const obj = ref({
name: '点赞数',
total: 0
})
const upTotal = function () {
obj.value.total++;
}
// 调用 watch 侦听变化 开启深度监听
watch(obj, (newValue, oldValue) => {
console.log(newValue, oldValue);
}, { deep: true })
</script>
<template>
<div>{{ obj.name }}</div>
<div>{{ obj.total }}</div>
<button @click="upTotal">+1</button>
</template>
效果演示
精确侦听对象属性
注意侦听的对象需要使用下方的固定写法:watch(()=> 值 ,()=>{ // 回调方法 })
若只需要监听对象中某个值的变化,在不开启 deep 的前提下能触发 watch 的回调函数,可以使用该操作:
<script setup>
import { ref, watch } from 'vue';
const obj = ref({
name: '点赞数',
total: 0
})
const upTotal = function () {
obj.value.total++;
}
// 调用 watch 侦听变化 精确侦听 obj.value.total 值
watch(
() => obj.value.total,
(newValue, oldValue) => {
console.log(newValue, oldValue);
})
</script>
<template>
<div>{{ obj.name }}</div>
<div>{{ obj.total }}</div>
<button @click="upTotal">+1</button>
</template>
效果演示
Vue3 也像 Vue2 一样,提供了 生命周期的 API,对照表如下:
说明 | 选项式API | 组合式API |
---|---|---|
初始化 | beforeCreate/created | setup |
挂载前 | beforeMount | onBeforeMount |
挂载完成 | mounted | onMounted |
更新前 | beforeUpdate | onBeforeUpdate |
更新完成 | updated | onUpdated |
销毁前 | beforeUnmount | onBeforeUnmount |
销毁完成 | unmounted | onUnmounted |
写法
多次调用相同的生命周期的钩子函数,会按照顺序依次执行,互不冲突;
<script setup>
import { onMounted } from 'vue';
// 挂载阶段 钩子函数
onMounted(() => {
console.log('mounted 生命周期函数');
})
// 进入页面就触发
console.log('setup 生命周期函数');
// 可以调用多次生命周期函数
onMounted(() => {
console.log('mounted 第二个生命周期函数');
})
</script>
效果演示
子组件引用 – 局部引入
一般组件定义在项目的 src/components 文件夹下,页面需要引用可使用 import 方式引用。
引用无需声明注册,可直接在 <template> 中使用
<script setup>
import item from './components/item.vue'
</script>
<template>
<div class="content">
<h1>父组件</h1>
<item></item>
</div>
</template>
<style scoped>
.content {
width: 400px;
height: 200px;
text-align: center;
border: 5px dashed green;
}
</style>
引用子级组件
<script setup>
</script>
<template>
<div class="compCont">
<h1>子组件</h1>
</div>
</template>
<style scoped>
.compCont {
display: inline-block;
padding: 20px 50px;
border: 4px dashed #66cc;
}
</style>
效果展示
父传子
父组件中给子组件绑定属性,子组件内部通过 defineProps() 提供的 编译器宏 来接收内容;
defineProps 原理:在编译阶段做一个标识,编译解析过程时会在对应区域进行编译转换
<script setup>
import { ref } from 'vue';
import item from './components/item.vue'
const sayHi = ref('hello word');
const num = ref(0);
const upNum = function () {
num.value++;
}
</script>
<template>
<div class="content">
<h1>父组件</h1>
<button @click="upNum">+1</button>
<!-- 父向子传参 -->
<item :sayHi="sayHi" :num="num"></item>
</div>
</template>
<script setup>
// 接收父级传递的参数
defineProps({
sayHi: String,
num: Number
})
</script>
<template>
<!-- 展示内容 -->
<div class="compCont">
<h1>子组件</h1>
<p>{{ sayHi }}</p>
<p>{{ num }}</p>
</div>
</template>
效果展示
子传父
子传父是通过事件来传递信号,由父级来直接操作数据;父组件中给子组件通过@绑定事件,子组件通过 emit 方法触发事件。
Vue3 规定了在子组件中需要定义 defineEmits() 编译器宏生成 emit 方法,通过编译器宏生成的 emit 创建对应的事件
<script setup>
import { ref } from 'vue';
import item from './components/item.vue'
const sayHi = ref('hello word');
const num = ref(0);
const upNum = function () {
num.value++;
}
// 触发子级组件的事件 并 接收子级传递的参数 进行赋值
const subNum = function (subValue) {
num.value = num.value + subValue;
}
</script>
<template>
<div class="content">
<h1>父组件</h1>
<button @click="upNum">+1</button>
<item :sayHi="sayHi" :num="num" @sub="subNum"></item>
</div>
</template>
<script setup>
const emit = defineEmits(['sub'])
defineProps({
sayHi: String,
num: Number
})
// 向父级页面触发事件 并传入参数 -1
const subCallFn = () => {
emit('sub', -1);
}
</script>
<template>
<div class="compCont">
<h1>子组件</h1>
<p>{{ sayHi }}</p>
<p>{{ num }}</p>
<button @click="subCallFn">-1</button>
</div>
</template>
效果演示
概念
通过 ref 标识获取真实的 dom对象 或者 组件实例对象
- 调用 ref 函数生成一个 ref 对象
- 通过 ref 标识绑定 ref 对象到标签
<script setup>
import { onMounted, ref } from 'vue';
// 变量名需要与 template 的标签的 ref 属性同名
const h1Ref = ref(null);
// 挂载完成事件
onMounted(() => {
// 获取到了对应 <h1> 的 Dom
console.log(h1Ref.value);
})
</script>
<template>
<div class="content">
<h1 ref="h1Ref">我是dom</h1>
</div>
</template>
效果演示
获取网页 Dom 元素
ref 绑定标签效果:实现点击让输入框聚焦
<script setup>
import { ref } from 'vue';
const iptRef = ref(null);
const focusFn = function () {
iptRef.value.focus();
console.log('已聚焦');
}
</script>
<template>
<div class="content">
<input ref="iptRef" type="text">
<button @click="focusFn">实现聚焦</button>
</div>
</template>
效果演示
获取 Vue 组件实例
通过 ref 去获取组件的实例
<script setup>
import { ref } from 'vue';
import testCom from './components/testCom.vue';
const testRef = ref(null);
const getCom = () => {
console.log(testRef.value);
}
</script>
<template>
<div class="content">
<button @click="getCom">获取组件</button>
<testCom ref="testRef"></testCom>
</div>
</template>
获取到组件后,理论上就可以拿到子组件中 defineExpose 内向外暴露的属性和方法
效果演示
Vue 组件实例向外暴露对应的属性和方法
默认情况下在 <script setup> 语法糖下的组件内部的属性和方法是不开放给父组件访问的,
可以通过 defineExpose 编译宏指定那些熟悉和方法允许访问。
<script setup>
const count = 1000;
const sayHi = function () {
console.log('我是子组件的方法');
}
defineExpose({
count,
sayHi
})
</script>
完成后,在打印组件时就会显示对应内容
作用和场景
除了 Vue 内置的一系列指令 (比如 v-model 或 v-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。
下面是一个自定义指令的例子,当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦:
局部注册
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
效果展示
全局注册
将一个自定义指令全局注册到应用层级
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
mounted(el, binding) {
/* ... */
}
})
指令钩子函数
一个指令的定义对象可以提供几种钩子函数 (都是可选的)
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
指令传参(简化)
通过 v-指令名:”参数” 绑定指令接收的参数,全局注册指令可通过 app.directive(el,binding) 参数2 的 binding.value 接收
<div v-color="color"></div>
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})
更多内容可以参考 官方文档
作用和场景
provide 和 inject 的效果是实现顶层组件向任意的底层组件传递数据和方法,实现跨层级组件通信
实现语法:
只要存在层级关系的组件,都可以跨层级通信
- 顶层组件通过 provide 函数提供数据
- 底层组件通过 inject 函数获取数据
provide('key',顶层组件中的数据)
const message = inject('key')
顶层给底层传数据
该方法是为了顶层组件能与底层组件能直接传递数据,下方是一个传递普通数据的案例:
<script setup>
import cententCom from '@/components/centent-com.vue';
import { provide } from 'vue';
// 1. 跨层传递普通数据
provide('theme-color','red');
</script>
<template>
<h1>我是顶层组件</h1>
<cententCom></cententCom>
</template>
<script setup>
import bottomCom from '@/components/bottom-com.vue'
</script>
<template>
<h2>我是中间组件</h2>
<bottomCom></bottomCom>
</template>
<script setup>
import { inject } from 'vue';
const themeColor = inject('theme-color')
</script>
<template>
<h3 :style="{ color: themeColor }">我是底层组件 {{ themeColor }}</h3>
</template>
效果演示
可以看到,顶层组件的 provide() 传入的参数被底层组件的 inject(); 接收到,并直接改变了对应内容;
合理的操作和传递
数据通过 provide 传到 底层组件,如果底层需要改变顶层传入的数据的值。不建议直接进行操作。
而是依靠顶层传入的方法来进行操作;
相当于外卖送了一个炸鸡,并附带一个手套。意思是由数据供应方提供处理数据的操作
底层调用顶层方法改数据
<script setup>
import cententCom from '@/components/centent-com.vue';
import { provide, ref } from 'vue';
const count = ref(10)
// 跨层传递响应式数据
provide('count', count);
// 跨层传递修改方法
provide('changeCount', (newVal) => {
count.value = newVal;
})
</script>
<template>
<h1>我是顶层组件</h1>
<span>顶层数据为 {{ count }}</span>
<cententCom></cententCom>
</template>
<script setup>
import bottomCom from '@/components/bottom-com.vue'
</script>
<template>
<h2>我是中间组件</h2>
<bottomCom></bottomCom>
</template>
<script setup>
import { inject } from 'vue';
const count = inject('count');
const chuangeFn = inject('changeCount')
function addFn() {
chuangeFn(count.value + 1);
}
</script>
<template>
<h3>我是底层组件</h3>
<span>接收数据为 {{ count }}</span>
<button @click="addFn">数据自增</button>
</template>
效果演示
新特性诞生背景
有 <script setup> 之前,如果定义 props,emits 可以轻而易举的添加一个与 setup 平级的熟悉;
<script>
export default {
setup() {
// ...
},
methods() {
// ...
},
props() {
// ...
}
}
</script>
但是用了 <script setup> 后,就没法这么干了 setup 属性已经没有了,自然无法添加与其平级的属性。
为了解决这个问题,引入了 defineProps 与 defineEmits 这俩个宏。但是只是解决了 props 与 emit 这俩个属性引入。
如果我们要在 <script setup> 中定义组件的 name 或者其他自定义的属性,就用到了 defineOptions;
Vue3.3 中新引入了 defineOptions 宏,用来定义 Options API 的选项。可以用 defineOptions 定义任意的选项。
(除了 props,emits,expose,slots 除外,因为这些已经预先存在对应的编辑器宏的函数)
<script setup>
defineOptions({
name: 'Demo',
inheritAttrs: false
// ...更多自定义属性
})
}
</script>
该新特性目前为实验性
新特性诞生的背景
在 vue3 中,自定义组件上使用 v-model 相当于传递了一个 modelValue属性,同时触发 update:modelValue 事件。
如果自定义的子组件要实现 v-model 双向绑定,则对应就得写一些比较麻烦的操作;而 defineModel 就是为了简化这样的操作而诞生的。
Vue 的指令
<script setup>
import { ref } from 'vue';
import sonCom from '@/components/son-com.vue';
const text = ref('你好~');
</script>
<template>
<sonCom v-model="text"></sonCom>
</template>
效果相当于
<script setup>
import { ref } from 'vue';
import sonCom from '@/components/son-com.vue';
const text = ref('你好~');
</script>
<template>
<sonCom :text="text" @update:text="text = $event"></sonCom>
</template>
若是以前需要实现类似 Vue2 的双向绑定就要先定义 Props,再定义 emits。其中有许多重复的代码,
子组件需要动态修改此值,还需要手动调用 emit 函数
父级定义 v-model
<script setup>
import { ref } from 'vue';
import myInput from '@/components/myInput.vue';
const text = ref('');
</script>
<template>
<div class="content">
<p>父级页面值:{{ text }}</p>
<myInput v-model="text"></myInput>
</div>
</template>
子级组件要实现 modelValue 的传值 和发起 update:modelValue 事件
<script setup>
import { ref } from 'vue';
// 接收参数
defineProps({
modelValue: String
})
// 定义 emit 事件
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
type="text"
:value="modelValue"
@input="e => emit('update:modelValue', e.target.value)" />
</template>
父级还要实现接收 update:modelValue 并修改对应数据,只不过 v-mode 相当于已经操作
效果展示
defineModel 的使用
1. 在 vite.config.js 文件中开启实验性的功能 defineModel
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
script: {
defineModel: true
}
}),
],
resolve: {
alias: {
// 允许我们以 @ 方式去访问对应目录 (Vue2 中默认底层已配置)
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
2. 父级使用 v-model 并传入数据
<script setup>
import { ref } from 'vue';
import myInput from '@/components/myInput.vue';
const text = ref('');
</script>
<template>
<div class="content">
<p>父级页面值:{{ text }}</p>
<myInput v-model="text"></myInput>
</div>
</template>
3. 使用实验性的方法 defineModel 获取到的 v-model 的值是支持直接修改的
<script setup>
import { defineModel } from 'vue';
// 通过 defineModel() 传过来的 v-model 数据支持直接修改
const modelValue = defineModel();
</script>
<template>
<!-- 直接修改父级传过来的值 -->
<input type="text" :value="modelValue" @input="e => modelValue = e.target.value" />
</template>
效果演示
vue3 也支持路由方式去实现单页面切换的效果,关于 vue2 Router 的介绍可以 参考笔记
定义 路由组
在部署 vue3 项目时,勾选 router 后,会额外配置 router 的演示内容
// createRouter 创建 router 对应实例
// createWebHistory 创建 history 模式的路由
import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/Login/index.vue'
import Layout from '@/views/Layout/index.vue'
import Home from '@/views/Home/index.vue'
import Category from '@/views/Category/index.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
// 默认指向路由
path: '/',
component: Layout,
children: [
// path 为空则为默认二级路由
{
path: '',
component: Home
},
{
// 接收 params 参数的路由
path: 'category/:id',
component: Category
}
]
},
{
path: '/login',
component: Login
}
],
// 扩展:每次切换路由 自动滚动到顶部
scrollBehavior() {
return {
top: 0
}
}
})
export default router
设置挂载点
通过 router-view 路由出口组件,将当期路由对应的内容挂载在目标位置,
若有二级或多级的路由,同样也要在对应的父级提供挂载点的位置;以便对应的内容提供
<template>
<!-- 路由出口组件 -->
<router-view></router-view>
</template>
跳转指定路径
在 setup 的环境中,需要引用 vue-router 提供的 useRouter 去实现路由方法
import { useRouter } from 'vue-router';
// 路由对象方法
const router = useRouter()
// 直接替换当前路由 不会添加新的历史记录
router.replace({ path: '/' })
// 使用路由器实例进行路由导航
router.push('/about');
接收参数
在 setup 的环境中,需要引用 vue-router 提供的 useRoute 方法去获取路由传参
(注意与上面 useRouter 的区别)
<template>
<RouterLink to="/Category/22"></RouterLink>
<RouterLink to="/Category?id=23"></RouterLink>
</template>
import { useRoute } from 'vue-router';
const route = useRoute()
// ...
// 获取 params 参数并传值
getCategory(route.params.id) // 22
// 获取 query 参数并传值
getCategory(route.query.id) // 23
同一个路由当参数更新时,不会触发界面的更新;
这意味着即使路由参数发生变化,组件实例仍然是相同的,因此不会触发组件的更新
onBeforeRouteUpdate
vue3 的 router 提供了 onBeforeRouteUpdate 函数用于路由切换时触发的回调。
<script setup>
// ...
import { onMounted, ref } from 'vue'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
const goods = ref({})
// 获取 route 参数
const route = useRoute()
const getGoods = async (id = route.params.id) => {
const res = await getDetail(id)
goods.value = res.result
}
onMounted(() => getGoods())
// 路由切换更新回调
onBeforeRouteUpdate(async (to) => {
// 重新获取数据 渲染界面
await getGoods(to.params.id)
})
</script>
Pinia 是 Vue 的最新的状态管理工具,是 Vuex 的替代品
效果:
- 提供了更简单的API (去掉了 mutation)
- 提供符合组合式风格的 API (和 Vue3 新语法统一)
- 去掉了 Modules 的概念,每个 store 都是一个独立的模块
- 配合 TypeScript 更加友好,提供可靠的类型推断
在实际开发项目的时候,关于 Pinia 的配置,可以在项目创建时候自动添加。但是为了让我们了解,建议先了解手动添加的操作;
官方提供了 Pinia 的安装文档:pinia 文档
创建项目目录
创建项目
npm create vue@latest
安装依赖
npm i
# 使用 yarn 下载 pinia
yarn add pinia
# 或者使用 npm
npm install pinia
配置 Pinia 到 Vue3 项目
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// 创建 pinia 实例
const pinia = createPinia()
const app = createApp(App)
// 安装 pinia插件 到 vue 实例
app.use(pinia)
app.mount('#app')
Pinia 可以创建多个仓库,并为多个仓库创建唯一标识。在 Pinia 当中多个仓库互相之间是独立的。当然最终也会挂载在同一个状态树上。
import { defineStore } from 'pinia'
// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,
// 同时以 `use` 开头且以 `Store` 结尾。
// (比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useAlertsStore = defineStore('alerts', {
// 其他配置...
})
组合式风格
与 Vue 的选项式 API 类似,我们也可以传入一个带有 state
、actions
与 getters
属性的 Option 对象
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
你可以认为 state
是 store 的数据 (data
),getters
是 store 的计算属性 (computed
),而 actions
则是方法 (methods
)。
为方便上手使用,Option Store 应尽可能直观简单
选项式风格
Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,
并且返回一个带有我们想暴露出去的属性和方法的对象。
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
在 Setup Store 中:
ref()
就是state
属性computed()
就是getters
function()
就是actions
Setup store 比 Option Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。
使用 pinia 创建数据,并提供给 Vue 页面。
创建数据仓库
import { defineStore } from "pinia";
import { ref } from "vue";
// 定义 store
export const userCounterStore = defineStore('counter', () => {
// 声明数据 state - count
const count = ref(0);
// 声明数据 state - msg
const msg = ref('hello pinia')
// 声明操作数据的方法 action (普通函数)
const upCount = () => count.value++
const subCount = () => count.value--
// 声明基于派生的计算属性 getters
return {
count,
msg,
upCount,
subCount
}
})
页面引用仓库
<script setup>
// ...
import { userCounterStore } from '@/store/count'
// 不要尝试对 pinia 数据进行解构,会丢失响应式效果
const counterStore = userCounterStore();
</script>
<template>
<div class="content">
<h1>APP根组件</h1>
<h4>{{ counterStore.msg }}</h4>
<p>{{ counterStore.count }}</p>
<!-- ... -->
</div>
</template>
其他子组件同理,引用 pinia 提供的数据是同步共享的
<script setup>
import { userCounterStore } from '@/store/count';
const counterStore = userCounterStore();
</script>
<template>
<div class="comContent">
<h3>Son1Com组件</h3>
<h4>{{ counterStore.msg }}</h4>
<p>{{ counterStore.count }}</p>
<!-- 通过 pinia 提供的方法去修改 store数据 -->
<button @click="counterStore.upCount">+1</button>
<button @click="counterStore.subCount">-1</button>
</div>
</template>
效果演示
使用计算属性
声明基于数据派生的计算属性 getters 可以使用 compted() 函数来创建
import { defineStore } from "pinia";
import { computed, ref } from "vue";
// 定义 store
export const userCounterStore = defineStore('counter', () => {
// 声明数据 state - count
const count = ref(0);
// 声明数据 state - msg
const msg = ref('hello pinia')
// 声明操作数据的方法 action (普通函数)
const upCount = () => count.value++
const subCount = () => count.value--
// 声明基于派生的计算属性 getters
const double = computed(() => count.value * 2)
return {
count,
msg,
double,
upCount,
subCount
}
})
组件引用计算属性时,相当于引用了计算属性 computed 函数 return 的值
<script setup>
import { userCounterStore } from '@/store/count';
const counterStore = userCounterStore();
</script>
<template>
<div class="comContent">
<h3>Son2Com组件</h3>
<h4>{{ counterStore.msg }}</h4>
<p>{{ counterStore.double }}</p>
<button @click="counterStore.upCount">+1</button>
<button @click="counterStore.subCount">-1</button>
</div>
</template>
效果演示
下载 axios 用于发起网络请求,关于 axios 的教程可以在这里查阅。
npm i axios
# 或使用 yarn 下载
yarn add axios
在 pinia 创建的仓库中定义对应的异步请求数据的方法和接收的变量
import axios from 'axios';
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useChannelStore = defineStore('channel', () => {
// 声明数据
const channelList = ref([]);
// 声明操作数据的方法
const getList = async () => {
const url = 'http://geek.itheima.net/v1_0/channels'
// 异步获取到数据 赋值给对应本地 state
const { data: { data } } = await axios.get(url);
channelList.value = data.channels
}
return {
channelList,
getList
}
})
页面调用 pinia 的数据和提供操作数据的方法实现页面的数据铺设
<script setup>
import { useChannelStore } from '@/store/channel'
const channelStore = useChannelStore();
</script>
<template>
<div class="content">
<h1>APP根组件</h1>
<button
@click="channelStore.getList"
>获取频道数据</button>
<ul>
<li
v-for="(item, index) in channelStore.channelList"
:key="index"
>
{{ item.id }} -- {{ item.name }}
</li>
</ul>
</div>
</template>
效果演示
当我们直接解构 pinia 提供的数据时候,会丧失数据的响应式效果。所以我们不应该直接去解构数据;
使用 pinia 提供的 storeToRefs 方法去包裹 pinia 返回的仓库。就可以去解构数据仍保留响应式效果;
使用 Store
虽然我们前面定义了一个 store,但在我们使用 <script setup>
调用 useStore()
(或者使用 setup()
函数,
像所有的组件那样) 之前,store 实例是不会被创建的:
<script setup>
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCounterStore()
</script>
你可以定义任意多的 store,但为了让使用 pinia 的益处最大化
(比如允许构建工具自动进行代码分割以及 TypeScript 推断),你应该在不同的文件中去定义 store。
如果你还不会使用 setup
组件,你也可以通过映射辅助函数来使用 Pinia。
一旦 store 被实例化,你可以直接访问在 store 的 state
、getters
和 actions
中定义的任何属性。
我们将在后续章节继续了解这些细节,目前自动补全将帮助你使用相关属性。
请注意,store
是一个用 reactive
包装的对象,这意味着不需要在 getters 后面写 .value
,
就像 setup
中的 props
一样,如果你写了,我们也不能解构它:
<script setup>
const store = useCounterStore()
// ❌ 这将不起作用,因为它破坏了响应性
// 这就和直接解构 `props` 一样
const { name, doubleCount } = store
name // 将始终是 "Eduardo"
doubleCount // 将始终是 0
setTimeout(() => {
store.increment()
}, 1000)
// ✅ 这样写是响应式的
// 💡 当然你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>
为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()
。它将为每一个响应式属性创建引用。
当你只使用 store 的状态而不调用任何 action 时,它会非常有用。
请注意,你可以直接从 store 中解构 action,因为它们也被绑定到 store 上:
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>
原理
使用 storeToRefs 方法,是为了保持响应式的效果。具体效果如下:
<script setup>
import { useChannelStore } from '@/store/channel'
import { storeToRefs } from 'pinia';
const store = useChannelStore();
const { getList } = store;
const { channelList } = storeToRefs(store);
</script>
<template>
<div class="content">
<h1>APP根组件</h1>
<button @click="getList">获取频道数据</button>
<ul>
<li v-for="(item, index) in channelList" :key="index">
{{ item.id }} -- {{ item.name }}
</li>
</ul>
</div>
</template>
效果演示
使用 pinia 持久化插件来实现 Pinia 数据的持久化存储;更多内容可查看 官方文档
安装
使用 npm 安装插件
npm i pinia-plugin-persistedstate
在 main.js 上全局挂载使用
import persist from 'pinia-plugin-persistedstate'
// ...
app.use(crearePinia().use(persist))
store 仓库,使用 persist:true 开启后,当前 store 将会使用默认持久化配置
import { defineStore } from 'pinia'
export const useStore = defineStore(
'main',
() => {
const someState = ref('你好 pinia')
return { someState }
},
{
persist: true,
},
)
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => {
return {
someState: '你好 pinia',
}
},
persist: true,
})
使用案例
对使用 pinia 制作的 加法器 进行 持久化存储;只需要在 defineStore 第三个参数传入配置对象
import { defineStore } from "pinia";
import { computed, ref } from "vue";
// 定义 store
export const userCounterStore = defineStore('counter', () => {
// 声明数据 state - count
const count = ref(0);
// 声明数据 state - msg
const msg = ref('hello pinia')
// 声明操作数据的方法 action (普通函数)
const upCount = () => count.value++
const subCount = () => count.value--
// 声明基于派生的计算属性 getters
const double = computed(() => count.value * 2)
return {
count,
msg,
double,
upCount,
subCount
}
},
{ persist: true } // 开启当前模块的持久化
)
效果演示
刷新界面,仍然保持数据的存活
原理 和 自定义
更多内容可以阅读 官方文档
pinia 是使用 localStorage 进行存储,并以 store.$id
作为 storage 默认的 key。
会自行对需要存储的数据进行 JSON.stringify
/JSON.parse
进行序列化/反序列化;
如若需要更多配置,可以给 persist 传入一个对象。进行自定义配置。
import { defineStore } from "pinia";
import { computed, ref } from "vue";
// 定义 store
export const userCounterStore = defineStore('counter', () => {
// 声明数据 state - count
const count = ref(0);
// 声明数据 state - msg
const msg = ref('hello pinia')
// 声明对象
const obj = ref({
a: 1
})
// ...
},
{
persist: {
key:'smm-counter', // 自定义传入的本地存储的 key
paths: ['count','obj.a'] // 指定哪些数据需要持久化
}
} // 开启当前模块的持久化
)
Element plus 是专门适用于 vue3 系列的样式框架 >>>官方文档
安装
推荐使用自动按需导入
# NPM
$ npm install element-plus --save
# Yarn
$ yarn add element-plus
# pnpm
$ pnpm install element-plus
安装 unplugin-vue-components
和 unplugin-auto-import
插件
npm install -D unplugin-vue-components unplugin-auto-import
配置 unplugin-vue-components
和 unplugin-auto-import
插件
// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
// ...
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
尝试使用官方文档的 el-button 按钮组件 进行测试
<template>
<el-button>Default</el-button>
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>
<el-button type="info">Info</el-button>
<el-button type="warning">Warning</el-button>
<el-button type="danger">Danger</el-button>
</template>
效果演示
通过安装 scss 修改覆盖 elementplus 默认的样式,并通知 Element 采用 scss 样式语言
安装
基于vite的项目默认不支持css预处理器,需要开发者单独安装
npm i sass -D
创建一个替换 elementplus 样式的 scss 样式表
/* 只需要重写你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
// 主色
'base': #27ba9b,
),
'success': (
// 成功色
'base': #1dc779,
),
'warning': (
// 警告色
'base': #ffb302,
),
'danger': (
// 危险色
'base': #e26237,
),
'error': (
// 错误色
'base': #cf4444,
),
)
)
通知 elementplus 使用 scss 方式,并导入到 elementplus 配置
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver({
importStyle: 'sass' // 配置 elementPlus 采用 sass 样式
})]
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
css: {
preprocessorOptions: {
scss: {
// 自动导入定制化样式文件进行样式覆盖
additionalData: `
@use "@/styles/element/index.scss" as *;
` // 上方指定的目录文件
}
}
}
})
效果展示
Vueuse 是一个功能强大的 Vue.js 生态系统工具库,它提供了一系列的可重用的 Vue 组件和函数,帮助开发者更轻松地构建复杂的应用程序。例如 获取DOM滚动高度,切换暗黑模式、获取鼠标位置等
官方地址:https://vueuse.org/
相关文档:https://blog.csdn.net/m0_69824302/article/details/136353352
依赖 Vueuse 监听元素是否到达视野区域,当到达该区域时,给 src 属性赋值
创建模块
// 定义懒加载插件
import { useIntersectionObserver } from '@vueuse/core'
export const lazyPlugin = {
install(app) {
// 懒加载指令逻辑
app.directive('img-lazy', {
mounted(el, binding) {
// el: 指令绑定的那个元素 img
// binding: binding.value 指令等于号后面绑定的表达式的值 图片url
console.log(el, binding.value)
const { stop } = useIntersectionObserver(
el,
(entries) => {
// 如果进度到视野区域 回调函数的下方参数返回 true
if (entries[0].isIntersecting) {
// 修改图片的 src 的值 实现延迟加载图片
el.src = binding.value
// 为避免重复触发 stop() 停止监听元素的交叉情况
stop()
}
}
)
}
})
}
}
在main.js 中全局注册该模块
import {lazyPlugin} from '@/directives'
// ...
app.use(lazyPlugin)
使用 v-img-lazy 使用自定义指令
<img v-img-lazy="xxx.jpg" alt="">
效果展示
依赖 VueUse 的 useMouseInElement(dom) 方法,获取鼠标的在元素中的具体相对位置
useMouseInElement 传入 dom 目标时,会提供 elementX, elementY, isOutside 这三个响应式数据
- elementX 相对于元素的X轴位置
- elementY 相对于元素Y轴的位置
- isOutside 鼠标光标是否不在元素中
案例演示
导入 vueuse 方法 并绑定对应用于的放大镜样式的元素
<script setup>
import { ref, watch } from 'vue'
import { useMouseInElement } from '@vueuse/core'
// 获取鼠标相对位置
const target = ref(null)
const { elementX, elementY, isOutside } = useMouseInElement(target)
</script>
实现业务逻辑
// ...
// 控制滑块跟随鼠标移动(监听elementX/Y变化,一旦变化 重新设置left/top)
const left = ref(0)
const top = ref(0)
const positionX = ref(0)
const positionY = ref(0)
// 监听每个值的变化,一旦某个值变化立即触发回调
watch([elementX, elementY, isOutside], () => {
console.log('xy变化了')
// 如果鼠标没有移入到盒子里面 直接不执行后面的逻辑
if (isOutside.value) return
console.log('后续逻辑执行了')
// 有效范围内控制滑块距离
// 横向
if (elementX.value > 100 && elementX.value < 300) {
left.value = elementX.value - 100
}
// 纵向
if (elementY.value > 100 && elementY.value < 300) {
top.value = elementY.value - 100
}
// 处理边界
if (elementX.value > 300) { left.value = 200 }
if (elementX.value < 100) { left.value = 0 }
if (elementY.value > 300) { top.value = 200 }
if (elementY.value < 100) { top.value = 0 }
// 控制大图的显示
positionX.value = -left.value * 2
positionY.value = -top.value * 2
})
绑定元素的背景图定位属性的参数
<template>
<!-- 左侧大图-->
<div class="middle" ref="target">
<img :src="prop.imageList[activeIndex]" alt="" />
<!-- 蒙层小滑块 -->
<div class="layer"
v-show="!isOutside"
:style="{ left: `${left}px`, top: `${top}px` }"
></div>
</div>
<div class="goods-image">
<!-- ... -->
<!-- 放大镜大图 -->
<div class="large" :style="[
{
backgroundImage: `url(xxx.jpg)`,
backgroundPositionX: `${positionX}px`,
backgroundPositionY: `${positionY}px`,
},
]" v-show="!isOutside"></div>
</div>
</template>
效果展示