07-Vue3组件&Vite&指令&动画

image-20200723170734421

参考资料:

本文全部基于 vue3。

vue3插件:

  • VSCode 插件:Vue(Offical) 语法格式检查和着色

  • Chrome 插件:Vue.js devtools 7.7.7 监控组件、路由等数据流

一、vue3进阶

1.1 单文件组件

组件定义

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常 常被组织成层层嵌套的树状结构:

image-20260101113621312

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
app.component("tabbar”,{
template:'
<div style="color:blue">
<ul>
<li> 首页 </li>
<li> 列表 </li>
</ul>
<child></child>
<tabbarchild></tabbarchild>
</div>

//局部定义
components:{
tabbarchild:{
template:'<div>
tabbarchild
</div>'
)
)
})

组件带来的好处 (1)结构清晰 (2)复用性增加 (3)封装性

当前写法的吐槽: (1)dom高亮和代码提示没有 (2)css只能行内

单文件组件(SFC)

Vue的单文件组件(即*.vue文件,英文Single-File Component,简称SFC)是一种特殊的文件格式,使我们能够 将一个Vue组件的模板、逻辑与样式封装在单个文件中。下面是一个单文件组件的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script>
export default {
data() {
return {
greeting: 'Hello World!'
};
}
};
</script>
<template>
<p class="greeting">{{ greeting }}</p>
</template>
<style>
.greeting {
color: red;
font-weight: bold;
}
</style>

Vue的单文件组件是网页开发中HTML、CSS和JavaScript三种语言经典组合的自然延伸。

  • 使用熟悉的HTML、CSS和JavaScript语法编写模块化的组件
  • 让本来就强相关的关注点自然内聚]
  • 预编译模板,避免运行时的编译开销
  • [组件作用域的CSS]
  • [在使用组合式API时语法更简单]
  • 通过交叉分析模板和逻辑代码能进行更多编译时优化
  • 更好的IDE支持],提供自动补全和对模板中表达式的类型检查
  • 开箱即用的模块热更新(HMR)支持

创建项目: Vue-CLI

Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统(已进入维护模式,官方推荐 Vite),提供:

  • 通过@vue/cli实现的交互式的项目脚手架。

  • 通过@vue/cli + @vue/cli-service-global实现的零配置原型开发。

  • 一个运行时依赖( @vue/cli-service ),该依赖:

  • 可升级;

  • 基于webpack构建,并带有合理的默认配置;

  • 可以通过项目内的配置文件进行配置;

  • 可以通过插件进行扩展。

一个丰富的官方插件集合,集成了前端生态中最好的工具。

一套完全图形化的创建和管理Vue.js项目的用户界面。

Vue CLI 致力于将Vue生态中的工具基础标准化。它确保了各种构建工具能够基于智能的默认配置即可平稳衔接, 这样你可以专注在撰写应用上,而不必花好几天去纠结配置的问题。与此同时,它也为每个工具提供了调整配置的 灵活性,无需eject。

安装:

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

确认版本:

1
2
3
4
5
6
7
8
9
> nvm list
20.15.0
* 16.20.2 (Currently using 64-bit executable)
> node -v
v16.20.2
> npm -v
8.19.4
> vue -V
@vue/cli 5.0.8

创建一个项目:

1
2
3
4
5
6
7
8
> vue create my-project
#OR vue ui
Vue CLI v5.0.8
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel
? Choose a version of Vue.js that you want to start the project with 3.x
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

创建项目: Vite-官方推荐(★)

官方文档:https://cn.vitejs.dev/

Vite (法语意为”快速的”,发音/vit/,发音同”veet”)是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

一个开发服务器,它基于原生ES模块提供了丰富的内建功能,如速度快到惊人的模块热更新(HMR)。

一套构建指令,它使用Rollup打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。

Vite意在提供开箱即用的配置,同时它的插件API和JavaScript API带来了高度的可扩展性,并有完整的类型支持。 官方图文说明

创建项目

Vite需要Node.js版本14.18+, 16+。然而,有些模板需要依赖更高的Node版本才能正常运行,当你的包管理器发出警告时,请注意升级你的Node版本。

node 与 vite 版本对应关系:参考说明

Vite 7 对应node版本:node 20.19+ 或者 22.12+

Vite 6 对应node版本:node 18+ 或者 20+

Vite 5 对应node版本:node 18+ 或者 20+

Vite 4 对应node版本:node 14、18+ 或者 16+

Vite 3 对应node版本:node 14、18+ 或者 16+

Vite 2 对应node版本:node 大于或等于 12.2.0

执行命令 npm view create-vite versions 查看当前vite所有版本,然后找到对应大版本下的小版本就好了。

查询当前通过脚手架可以指定的安装的版本:输入npm view create-vue versions

使用NPM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
npm create vite@latest
#根据自己的node版本安装指定的版本,如 vite4.0.0
npm init vite@4.0.0
#选择与输出
Need to install the following packages:
create-vite@4.0.0
Ok to proceed? (y)
√ Project name: ... myappvite
√ Select a framework: » Vue
√ Select a variant: » JavaScript
Scaffolding project in E:\work\webProjects\vue\3.0\myappvite...
Done. Now run:
cd myappvite
npm install
npm run dev

使用Yarn:

1
yarn create vite

使用PNPM:

1
pnpm create vite

启动流程

1
2
3
4
#运行前先安装一下依赖,会生成 node_modules, npm i 即 npm install
npm i
#运行,默认端口 5173
npm run dev

1.2 组件基础

1.2.1 父传子-接收传值props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 写法1:传值属性单独写 -->
<Navbar mytitle='我的电影' left='返回' right='首页'></Navbar>
<!-- 写法2:传值属性打包一起写,传值时会拆分为标签属性 -->
<Navbar v-bind="{
mytitle:'我的电影',
left:'返回',
right:'首页'
}" ></Navbar>
<!-- 写法3:传值属性打包一起写,传值时会拆分为标签属性 -->
<Navbar v-bind="propObj" ></Navbar>
<script>
export default {
data() {
return {
propObj: {
mytitle: '当前',
left: '返回',
right: '更多'
}
}
}
}
</script>

子组件接收:(如果对接收的值不满意,可以通过 computed 计算属性进行二次计算、赋值给自己定义的属性值放dom上使用)

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>
<button>{{ left }}</button>
Navbar-{{ mytitle }}
<button>{{ right }}</button>
</div>
</template>

<script>
export default {
props: ["mytitle", "left", "right"],
}
</script>

注意:

所有的props都遵循着单向绑定原则,props因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

另外,每次父组件更新后,所有的子组件中的props都会被更新到最新值,这意味着你不应该在子组件中去更改一个prop。若你这么做了,Vue会在控制台上向你抛出警告:

1
2
3
4
5
6
export default {
props: ['foo'],
created:{
this.foo = 'bar' // X警告! prop是只读的!
}
}
props属性验证&默认属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export default {
props: {
//基础类型检查,给出'null'和'undefined'值则会跳过任何类型检查 propA: Number,
//多种可能的类型 propB: [String, Number],
//required必传,且为String类型
propA: {
type: null,
},
propB: {
type: [String, Number],
required: true,
}, // String, Number类型的默认值
propC: {
type: String,
required: true,
}, // required此项为必填项
propD: {
type: Number,
default: 100,
},
propE: {
type: Object,
},
//对象类型的默认值,对象或者数组应当用工厂函数返回。,工厂函数会收到组件所接收的原始props,作为参数
default(rawProps) {
return { message: "hello" }
},
propF: {
validator(value) { //自定义类型校验函数
// The value must match one of these strings
return ["success", "warning", "danger"].includes(value)
},
},
},
}

当验证失败时,Vue会在控制台发出警告,但默认情况下不会阻止数据的传递和显示。

不过,可以通过自定义逻辑来处理不满足条件的参数,使其不显示或者以默认值替代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>
{{ datalist }}
</div>
</template>
<script>
export default{
props:{
data:{
validator(value) {
console.log(value)
return ['success', 'warning', 'danger'].includes(value)
}
}
},
data(){
return{
datalist:this.data
}
},
created(){
if(!['success', 'warning', 'danger'].includes(this.datalist)){
this.datalist="默认消息" //不满足条件的时候显示默认值或设置为空
}
}
}
</script>

校验选项中的type可以是下列这些原生构造函数:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

1.2.2 禁止属性透传

inheritAttrs: false,封装的组件在被其他组件使用时,不想被其他组件(即父组件)给污染了属性或样式,因此会设置禁止属性透传。

1
2
3
export default {
inheritAttrs:false, //禁止透传(属性/事件):因默认会从父组件透传到子组件的根节点上
}

1.2.3 子传父-自定义事件 $emit

在组件的模板表达式中,可以直接使用$emit方法触发自定义事件(例如:在v-on的处理函数中):

1
2
<!-- MyComponent,直接在标签上写可以不用 this. -->
<button @click="$emit('someEvent')">click me</button>

emit()方法在组件实例上也同样以 this.emit()方法在组件实例上也同样以this. emit()方法在组件实例上也同样以this.emit()的形式可用:

1
2
3
4
5
6
7
8
export default {
methods: {
submit() {
this.$emit('someEvent')
//this.$emit('someEvent', 1, 2) //传参1和2
}
}
}

父组件可以通过v-on (缩写为@)来监听事件:

1
2
3
4
5
6
7
8
9
10
11
12
<MyComponent @some-event="callback" />

<script>
export default {
methods: {
//@some-event="handleEvent" 此时方法名不能写小括号,如果带小括号则需要显式写形参 handleEvent($event, p1, p2)
handleEvent(p1, p2) {
console.log("App event. param=", p1, p2)
}
}
}
</script>

同样,组件的事件监听器也支持 .once 修饰符、也支持同时监听多个事件

1
<MyComponent @some-event.once="callback" @some-event2="callback2" />

1.2.4 $refs-父组件的强权

  • ref 如果绑定在dom节点上,拿到的就是原生dom节点
  • ref 如果绑定在组件上,拿到的就是组件对象,可以实现通信功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Field label="用户名" ref="myusername"></Field>
<Field label="密码" type="password" ref="mypassword"></Field>
<Field label="年龄" type="number"></Field>

<script>
export default {
methods:{
handleRegister(){
console.log(this.$refs.myusername.value, this.$refs.mypassword.value)
},
handelReset(){
this.$refs.myusername.value=""
this.$refs.mypassword.value=""
}
}
}
</script>

1.2.5 $parent & root-子组件无法无天

在子组件中通过 $parent 访问父组件,通过 $root 访问根组件

1
2
3
4
5
6
methods:{
handleClick(){
console.log(this.$parent.title) //直接访问父组件 data() 中的 title 值
console.log(this.$root.title) //直接访问根组件 data() 中的 title 值
}
}

1.2.6 provide() & inject-跨级通信

非响应式: 子孙组件可以inject注入到值,但无法去修改。

image-20260101181746830

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//app.vue - 非响应式(即:子孙组件可以inject注入到值,但无法去修改)
//祖组件挂载数据:
data() {
return {
message:"hello jerry"
}
},
provide() {
return {
message: this.message
}
}

//child.vue grandchild.vue
//孙组件接收数据:
export default {
inject:["message"]
}

1.2.7 模拟订阅发布模式

跨级组件通信方案,模拟订阅发布模式的简易实现。

store.js

1
2
3
4
5
6
7
8
9
10
// 订阅发布模式
export default {
datalist: [],
subscribe(callback) {
this.datalist.push(callback)
},
publish(param) {
this.datalist.forEach(callback => callback(param))
}
}

调用订阅的组件:

1
2
3
4
5
6
7
8
import store from "./store"
export default {
mounted() {
store.subscribe((value) => {
console.log("订阅被触发了", value)
})
}
}

调用发布的组件:

1
2
3
4
5
6
7
8
import store from "./store"
export default {
methods: {
handleClick() {
store.publish(this.xxx)
}
}
}

1.2.8 component 动态组件

在切换时创建新的组件实例通常是有意义的,但在这个例子中,我们的确想要组件能在被“切走”的时候保留它们的状态。要解决这个问题,我们可以用<KeepAlive><keep-alive>内置组件将这些动态组件包装起来。

  • include 属性,指定包含的组件,必须写成组件的名字(script setup 时会自动有组件名字,否则需要自己指定组件名字)
  • exclude 属性,指定不包含的组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--非活跃的组件将会被缓存!-->
<KeepAlive>
<component :is="whichComponent" />
</KeepAlive>

<!--以英文逗号分隔的字符串,只在a和b组件中保持活跃-->
<KeepAlive include="a,b">
<component :is="whichComponent" />
</KeepAlive>

<!--正则表达式儒使用 v-bind 动态绑定-->
<KeepAlive :include="/a | b/">
<component :is="whichComponent" />
</KeepAlive>

<!--数组需使用 v-bind 动态绑定-->
<KeepAlive :include="['a', 'b']">
<component :is="whichComponent" />
</KeepAlive>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import store from "./store"
export default {
data() {
return {
whichComponent: "Home"
}
},
mounted() {
let obj = {
首页: "Home",
列表: "List",
我的: "Center",
}
//store.js 订阅发布模式
store.subscribe(value => {
this.whichComponent = obj[value]
})
}
}
v-model 在组件中通信(原理)

v-model 的展开形式如下:

1
<componet :modelValue="inputText" @update:modelValue="newValue => inputText = newValue">

因此在

  • 父传子 时,父使用 v-model,子组件的 props 中需要使用 modelValue 来接收值。
  • 子传父 时,子组件通过 @input 事件 $emit 去触发 update:modelValue 事件,并带上输入的参数。
1
2
3
<!-- 子组件 Field.vue -->
<label>{{label}}</label>
<input :type="type" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
1
2
<!-- 父组件 xxx.vue -->
<Field label="用户名" v-model="myvalue"></Field>

1.2.9 异步加载组件(★)

在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时(如点击时)再从服务器加载相关组件。

  • defineAsyncComponent(() => import(path)) Vue提供了该方法来实现此功能(注意:*需要删除手动导入组件的方式 import … from …*)。
1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {
AdminPage: defineAsyncComponent(()=>
import('./components/AdminPageComponent.vue')
)
}
}
</script>
<template>
<AdminPage />
</template>

加载与错误提示

1
2
3
4
5
6
7
8
9
10
11
12
13
const AsyncComp = defineAsyncComponent({
//加载函数
loader: () => import('./Foo.vue'),
//加载异步组件时使用的组件,自定义 Loadingcomponent 组件
loadingcomponent: Loadingcomponent,
//展示加载组件前的延迟时间,默认为200ms
delay: 200,
//加载失败后展示的组件,自定义 ErrorComponent 组件
errorComponent: ErrorComponent,
//如果提供了一个timeout时间限制,并超时了
//也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})

1.3 组件插槽

1.3.1 slot 插槽基本应用

<slot>元素是一个插槽出口(slot outlet),标示了父元素提供的插槽内容(slot content)将在哪 里被渲染。

image-20260102100656501

注意:

插槽内容可以访问到父组件的数据作用域。(比如在父组件中放按钮直接控制父组件的属性来改变其他子组件的显示隐藏)

插槽内容无法访问子组件的数据。

Vue模板中的表达式只能访问其定义时所处的作用域,这和JavaScript的词法作用域规则是一致的。

换言之:父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

1.3.2 #name 具名插槽

v-slot:name 简写 #name ,插槽名字。

1
2
3
4
5
6
7
8
9
10
11
12
//子组件:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
//父组件:
<BaseLayout>
<template #header>
<h1>这是header</h1>
</template>
<template #default>
<p>这是default</p>
</template>
<template #footer>
<p>这是footer</p>
</template>
</BaseLayout>

1.3.3 作用域插槽

在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。

image-20260102102538290

1
2
3
4
5
6
7
8
9
10
11
12
<Nowplaying v-slot="myprops">
<ul>
<li v-for="data in myprops.mylist" :key="data.id">
<div v-if="data.nm.includes(text) && text!== ''" style="font-size: 30px;">
{{data.nm}}
</div>
<div v-else>
{{data.nm}}
</div>
</li>
</ul>
</Nowplaying>

解构写法 + 具名插槽简写:

1
2
3
4
5
<!-- myprops可以结构写为 {mylist},遍历时直接使用 mylist 就不需要 myprops. 了 -->
<Nowplaying #movie="{mylist}">
<ul>
<li v-for="data in mylist" :key="data.id">
......
1
2
3
4
5
6
7
8
<!-- Nowplaying.vue子组件开放了 datalist 给父组件可以进行传入数据(如果没有使用mylist插槽内默认显示自己的内容) -->
<slot :mylist="datalist" name="movie">
<ul>
<li v-for="data in datalist" :key="data.id">
{{data.nm}}
</li>
</ul>
</slot>

1.4 生命周期

每个Vue组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。

  1. beforeCreate() 会在实例初始化完成、props解析之后、data()和computed等选项处理之前立即调用。

  2. created() 当这个钩子被调用时,以下内容已经设置完成:响应式数据、计算属性、方法和侦听器。然而,此时挂载阶段还未开始,因此$el属性仍不可用。

    • 场景:初始化
  3. beforeMount() 当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建DOM节点。它即将 首次执行DOM渲染过程。

  4. mounted() 所有同步子组件都已经被挂载。这个钩子通常用于执行需要访问组件所渲染的DOM树相关的作用。—— 最常用

    • 场景:订阅发布、ajax、setInterval、访问dom
  5. beforeUpdate() 这个钩子可以用来在Vue更新DOM之前访问DOM状态。在这个钩子中更改状态也是安全的。

  6. updated() 这个钩子会在组件的任意DOM更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的DOM,请使用 nextTick() 作为替代。

    • 场景:echarts的resize()操作等
  7. beforeUnmount() 当这个钩子被调用时,组件实例依然还保有全部的功能。

  8. unmounted() 在一个组件实例被卸载之后调用。

    • 场景:解绑事件如窗口大小变动监听window.onresize=null

img

vue2 & vue3 生命周期对比

vue2 vue3
beforeCreate setup(()=>{ })
created setup(()=>{ })
beforeMount onBeforeMount(()=>{})
mounted onMounted(()=>{})
beforeUpdate onBeforeUpdate(()=>{})
updated onUpdated(()=>{})
beforeDestroy onBeforeUnmount(()=>{ })
destroyed onUnmounted(()=>{ })

总结: Vue2和Vue3钩子变化不大,beforeCreate 、created 两个钩子被 setup() 钩子来替代。

结合单文件组件使用的组合式 API,推荐通过 <script setup> ... </script> 以获得更加简洁及符合人体工程学的语法。

示例(echarts与生命周期):

1
npm i echarts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<template>
<div>
<button @click="handleWidth">点击变宽</button>
<div id="main" :style="{ width: mywidth, height: '400px' }"></div>
</div>
</template>

<script>
//导入所有方法聚集为 echarts 对象
import * as echarts from "echarts"

export default {
data() {
return {
option: {},
mywidth: "600px",
}
},
created() { // 做初始化数据操作
this.option = {
title: {
text: "ECharts 入门示例",
},
tooltip: {},
legend: {
data: ["销量"],
},
xAxis: {
data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
},
yAxis: {},
series: [
{
name: "销量",
type: "bar",
data: [5, 20, 36, 10, 10, 20],
},
],
}
},
mounted() { // 做访问dom结构操作
this.myChart = echarts.init(document.getElementById("main"))
this.myChart.setOption(this.option)
},
updated() {
this.myChart.resize() //必须在updated后进行resize,因为此时自定义的宽度 mywidth 数据才渲染到dom树
},
methods: {
handleWidth() {
this.mywidth = "1024px"
},
},
}
</script>

1.5 组件的封装-swiper

1.5.1 纯静态轮播 .html

  • 【注意】一定要在dom数据都上树(渲染到dom节点上)后再 new Swiper()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="https://unpkg.com/swiper@8/swiper-bundle.min.css">
<script src="https://unpkg.com/swiper@8/swiper-bundle.min.js"> </script>
<style>
.swiper {
width: 600px;
height: 300px;
}
</style>
</head>

<body>
<h1>
静态页面 swiper
</h1>
<div class="swiper">
<div class="swiper-wrapper">
<div class="swiper-slide">Slide 1</div>
<div class="swiper-slide">Slide 2</div>
<div class="swiper-slide">Slide 3</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>

<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>

<!-- 如果需要滚动条 -->
<!-- <div class="swiper-scrollbar"></div> -->
</div>

<script>
var mySwiper = new Swiper('.swiper', {
//direction: 'vertical', // 垂直切换选项
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
// scrollbar: {
// el: '.swiper-scrollbar',
// },
})
</script>
</body>

</html>

1.5.2 模块化轮播 .vue-推荐(★)

基于 swiper12 引入,安装 swiper 指定版本,如12版本:npm i swiper@12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<template>
<!-- Swiper 容器:必须设置宽度/高度,否则无法显示 -->
<div class="simple-swiper-container">
<!-- Swiper 核心组件 -->
<!-- 自动播放:3秒/次,交互后不停止 -->
<!-- 分页器:可点击切换 -->
<!-- 循环播放 -->
<!-- 自动轮播2. 关键:注册Autoplay模块 -->
<Swiper
:autoplay="{ delay: 2000, disableOnInteraction: false }"
:pagination="{ clickable: true }"
:loop="true"
:modules="[Autoplay]"
class="swiper"
>
<!-- 轮播项:替换为你的图片/内容即可 -->
<SwiperSlide>
<div class="slide-item" style="background: #f0f8ff">轮播图1</div>
</SwiperSlide>
<SwiperSlide>
<div class="slide-item" style="background: #e6f7ff">轮播图2</div>
</SwiperSlide>
<SwiperSlide>
<div class="slide-item" style="background: #f6ffed">轮播图3</div>
</SwiperSlide>
</Swiper>
</div>
</template>

<script setup>
// 核心导入:Swiper12 需单独导入组件和样式(关键!)
import { Swiper, SwiperSlide } from "swiper/vue"
// 自动轮播1. 新增:导入Autoplay功能模块(核心修复点)
import { Autoplay } from "swiper/modules"
// 导入 Swiper 核心样式和所需模块样式
import "swiper/css" // 基础样式
import "swiper/css/autoplay" // 自动播放模块样式
import "swiper/css/pagination" // 分页器模块样式

// 无需额外逻辑:Swiper12 已封装为 Vue 组件,配置通过 props 传递即可
</script>

<style scoped>
/* 仅需设置容器和轮播项的基础尺寸,无多余样式 */
.simple-swiper-container {
width: 100%; /* 宽度自适应 */
height: 200px; /* 轮播图高度(可根据需求调整) */
}

.swiper {
width: 100%;
height: 100%;
}

.slide-item {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #333;
}
</style>

1.5.3 自定义轮播组件封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
<div>
<!-- v-if 可以确保在有数据时才会创建和渲染 -->
<MySwiper v-if="datalist.length" :loop="false" @slideChange="onSlideChange">
<MySwiperItem v-for="(data, index) in datalist" :key="index">
{{ data }}
</MySwiperItem>
</MySwiper>
</div>
</template>

<script>
import MySwiper from './MySwiper.vue';
import MySwiperItem from './MySwiperItem.vue';

export default {
components: {
MySwiper,
MySwiperItem
},
data() {
return {
datalist: []
};
},
mounted() {
setTimeout(() => {
this.datalist = [11, 22, 33, 44];
}, 2000);
},
methods: {
onSlideChange(index) {
console.log(index);
}
}
}
</script>

<style>
.swiper {
width: 600px;
height: 400px;
}
</style>

MySwiper.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div class="swiper">
<div class="swiper-wrapper">
<slot></slot>
</div>
<!--如果需要分页器-->
<div class="swiper-pagination"></div>
</div>
</template>

<script>
import Swiper from 'swiper/bundle';
import 'swiper/css/bundle';

export default {
props: ["loop"],
mounted() {
this.mySwiper = new Swiper('.swiper', {
loop: this.loop,
//如果需要分页器
pagination: {
el: '.swiper-pagination'
},
on: {
slideChange: () => {
this.$emit("slideChange", this.mySwiper.activeIndex);
}
}
});
}
}
</script>

MySwiperItem.vue

1
2
3
4
5
<template>
<div class="swiper-slide">
<slot></slot>
</div>
</template>

二、自定义指令

2.1 指令写法

除了 Vue内置的一系列指令(比如v-model或v-show)之外,Vue还允许你注册自定义的指令(Custom Directives)。

  • 自定义指令主要是为了重用涉及普通元素的底层DOM访问的逻辑

全局

1
2
3
4
5
const app = createApp(App)
//使v-focus在所有组件中都可用
app.directive('focus', {
/* ... */
})

局部

1
2
3
4
5
6
7
8
const focus = { 
mounted: (el) => el.focus()
}
export default {
directives: {
focus //在模板中启用v-focus
}
}

2.2 指令钩子-生命周期

1
2
3
4
5
6
7
8
9
const myDirective = {
created(el, binding, vnode, prevVnode) {}, //在绑定元素的attribute前,或事件监听器应用前调用
beforeMount(el, binding, vnode, prevVnode) {}, //在元素被插入到DOM前调用
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) {} //绑定元素的父组件卸载后调用
}

简写形式

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

1
<div v-jerrycolor="yellow"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
//全局简写
app.directive('jerrycolor', (el, binding) => {
//这会在'mounted'和'updated'时都调用
el.style.color = binding.value
})
//局部简写
export default {
directives: {
jerrycolor(el, binding) {
el.style.color = binding.value
}
}
}

三、过渡动画

Vue 提供了两个内置组件,可以帮助你制作基于状态变化的过渡和动画:

  • Transition 会在一个元素或组件进入和离开DOM时应用动画。
  • TransitionGroup 会在一个 v-for 列表中的元素或组件被插入,移动,或移除时应用动画。

3.1 过渡效果

image-20260102125712394

  1. v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
  2. v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。
  3. v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。
  4. v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。
  5. v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。
  6. v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。

最简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>
<button @click="show = !show">Toggle</button>
<Transition>
<p v-if="show">hello</p>
</Transition>
</div>
</template>

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

const show = ref(true)
</script>

<style scoped>
/* 下面我们会解释这些 class 是做什么的 */
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>

可以给Transition传一个name属性来声明一个过渡效果名:

1
2
3
<Transition name="fade">
...
</Transition>

class样式则可以使用过渡效果名称 name:

1
2
3
4
5
6
7
8
9
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}

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

3.1.1 animate CSS动画库

官网:https://animate.style/

1
2
#安装
npm i animate.css
1
2
//导入
import "animate.css"

示例:

  • animate__animated 前缀必须添加,否则不生效。
  • animate__backInDownanimate__backOutDown 是从官网复制过来的效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<button @click="show = !show">Toggle</button>
<!-- animate__animated 前缀必须记得添加。 -->
<Transition
enter-active-class="animate__animated animate__backInDown"
leave-active-class="animate__animated animate__backOutDown"
>
<h1 v-if="show">hello</h1>
</Transition>
</div>
</template>

<script setup>
import { ref } from "vue"
import "animate.css"

const show = ref(true)
</script>

示例:多个并存或切换

  • 如果内部有多个元素,最好使用标签进行包裹。
  • v-if 和 v-else 切换
1
2
3
4
<Transition>
<h1 v-if="show">hello</h1>
<h1 v-else>world</h1>
</Transition>

3.1.2 JS钩子函数

1
2
3
4
5
6
7
8
9
10
11
12
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@enter-cancelled="onEnterCancelled"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
@leave-cancelled="onLeaveCancelled"
>
...
</Transition>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 在元素被插入到 DOM 之前被调用
// 用这个来设置元素的 "enter-from" 状态
function onBeforeEnter(el) {}
// 在元素被插入到 DOM 之后的下一帧被调用
// 用这个来开始进入动画
function onEnter(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done()
}
// 当进入过渡完成时调用。
function onAfterEnter(el) {}
// 当进入过渡在完成之前被取消时调用
function onEnterCancelled(el) {}
// 在 leave 钩子之前调用
// 大多数时候,你应该只会用到 leave 钩子
function onBeforeLeave(el) {}
// 在离开过渡开始时调用
// 用这个来开始离开动画
function onLeave(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done()
}
// 在离开过渡完成、
// 且元素已从 DOM 中移除时调用
function onAfterLeave(el) {}
// 仅在 v-show 过渡中可用
function onLeaveCancelled(el) {}

过渡模式

1
2
3
<Transition mode="out-in">
...
</Transition>

组件间过渡

1
2
3
<Transition name="fade" mode="out-in">
<component :is="activeComponent"></component>
</Transition>

3.2 列表过渡

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

区别:

  • 默认情况下,它不会渲染一个容器元素。但你可以通过传入tag prop来指定一个元素作为容器元素来渲染。
  • 过渡模式在这里不可用,因为我们不再是在互斥的元素之间进行切换。
  • 列表中的每个元素都必须有一个独一无二的 key 字段,否则动画会不生效。
  • CSS过渡class会被应用在列表内的元素上,而不是容器元素上。
1
2
3
4
5
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item">
{{ item }}
</li>
</TransitionGroup>

移动动画

当某一项被插入或移除时,它周围的元素会立即发生“跳跃”而不是平稳地移动。我们可以通过添加一些额外的CSS 规则来解决这个问题:

1
2
3
4
5
6
7
8
/* 对移动中的元素应用的过渡 */
.list-move {
transition: all 0.5s ease;
}
/* 确保将离开的元素从布局流中删除,以便能够正确地计算移动的动画。 */
.list-leave-active {
position: absolute;
}

3.3 可复用过渡

得益于Vue的组件系统,过渡效果是可以被封装复用的。要创建一个可被复用的过渡,我们需要为Transition组 件创建一个包装组件,并向内传入插槽内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- MyTransition.vue -->
<template>
<!--包装内置的Transition组件-->
<Transition name="my-transition" @enter="onEnter" @leave="onLeave">
<slot></slot> <!--向内传递插槽内容-->
</Transition>
</template>

<script>
//JavaScript钩子逻辑…
</script>

<style>
/*
必要的css...
注意:避免在这里使用<style scoped>
因为那不会应用到插槽内容上
*/
</style>

现在MyTransition可以在导入后像内置组件那样使用了 :

1
2
3
<MyTransition>
<div v-if="show">Hello</div>
</MyTransition>

07-Vue3组件&Vite&指令&动画
https://janycode.github.io/2022/05/22/04_大前端/04_Vue/07-Vue3组件&Vite&指令&动画/
作者
Jerry(姜源)
发布于
2022年5月22日
许可协议