Vue3简单快速基础入门(三)

Vue3快速入门3 -组件篇

1. 基于Vite创建Vue3项目

Vite是一个快速的前端开发环境,它使用原生ES模块,并提供丰富的内置功能,如零配置的HMR(Hot Module Replacement)、ESLint、TypeScript支持、CSS预处理等。

Vite官方网址


在合适的目录下,打开终端,输入以下命令:

npm create vite@latest

成功后会出现以下提示:

# 项目名称
✔ Project name: demo
# 选择项目框架
✔ Select a framework: › Vue
# 选择项目语言
✔ Select a variant: › JavaScript

然后回车,等待项目创建完成。


创建完成后,按照提示输出

cd demo
npm install
npm run dev

npm install 安装依赖包,执行完成后可以在项目目录看到node_modules文件夹以及package-lock.json文件。
npm run dev 启动项目
这样就成功创建了Vue3项目,打开浏览器访问 http://localhost:5173 即可看到项目运行效果。

2. 新建一个vue文件

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

    const web = reactive({
        show:true
    })

    const toggle = ()=>{
        web.show =!web.show
    }

</script>

<template>
    <h1>{{ web.show }}</h1>
    <p v-show="web.show">十一月的早晨</p>
    <button @click="toggle">切换显示</button>

</template>

<style scoped>
    
</style>

以上代码是一个简单的Vue3组件,它定义了一个名为webreactive对象,并定义了两个方法toggleshow,其中toggle方法用来切换show属性的值,show属性用来控制p标签的显示与否。

3. 组件化开发

组件化开发是Vue3的核心理念之一,它可以将复杂的页面拆分成多个小组件,每个组件只负责一部分功能,更加方便维护和复用。

3.1 定义组件

在components文件夹下新建一个名为header.vue的文件,内容如下:

<script setup>

</script>

<template>
    <h1> 十一月的早晨 </h1>
</template>

<style scoped>
    
</style>

同样方式,创建一个名为footer.vue的文件,内容如下:

<script setup>

</script>

<template>
    <h1> 吹进卧室的风 </h1>
</template>

<style scoped>
    
</style>

3.2 导入并使用组件

App.vue文件中导入并使用组件,内容如下:

<script setup>
    // 导入组件  注意这里 组件名首字母必须大写
    import Header from './components/header.vue'
    import Footer from './components/footer.vue'


    import {reactive} from 'vue'

    const web = reactive({
        show:true
    })

    const toggle = ()=>{
        web.show =!web.show
    }

</script>

<template>
    <!-- 使用组件 -->
    <Header/>

    <h1>{{ web.show }}</h1>
    <button @click="toggle">切换显示</button>

    <!-- 使用组件 -->
    <Footer/>
</template>

<style scoped>
    
</style>

导入组件后,此时设计一个概念,父子组件,父组件可以包含多个子组件,子组件只能有一个父组件。在这里,App.vue是父组件,header.vuefooter.vue则是子组件。

3.3 父子组件通信

父子组件通信是Vue3的重要特性之一,它可以让父组件向子组件传递数据,也可以让子组件向父组件传递数据。

3.3.1 父向子传递数据

可以直接在子组件上设置属性,子组件中用definProps接收属性,并在模板中使用。如例:

<!-- 首先在父组件中为子组件设置属性 -->
<script setup>
    // 导入组件
    import Header from './components/header.vue'
    import Footer from './components/footer.vue'


    import {reactive} from 'vue'

    // 使用reactive创建响应式数据
    const web = reactive({
        user:"旺旺",
        age:25
    })

    const addAge = () =>{
        web.age++
    }

</script>

<template>
    <!-- 通过设置属性传递数据 -->
    <Header propsName="十一月" propsUrl="https://blog.xy21lin.cn" />

    <h1>学习Vue3 父子组件传值  </h1>
    <button @click="addAge">增加年龄</button>
    
    <!-- 使用v-bind绑定属性传递数据 -->
    <Footer :="web" />
</template>


<!-- 子组件使用defineProps接收父组件传递的数据 -->
<!-- defineProps可以用两种方式来接收,一种是对象,另一种是数组。 -->
<!-- Header子组件接收 -->
<script setup>
    //使用数组接收传递的数据
    const prop = defineProps(["propsName","propsUrl"])
</script>

<template>
    <h1> 十一月的早晨 </h1>
    <h2>数组接收父组件传来的值  |   propsName:{{propsName}} | propsUrl:{{propsUrl}}</h2>
</template>



<!-- Footer子组件接收 -->
<script setup>
    //使用对象接收传递的数据
    const prop = defineProps({
        user:String,
        age:Number,
        //可以在这里进行更细致的设置 必填 默认值等
        sex:{
            type:String,
            // required:true,
            default:"男"
        }
    })
</script>

<template>
    <h1> 吹进卧室的风 , 名字{{prop.user}} , 年龄{{prop.age}} , 性别{{prop.sex}}</h1>
</template>

3.3.2 子向父传递数据

子组件使用defineEmits来定义自己可以触发的事件,父组件中用v-on监听事件并调用相应的方法。如例:

<script setup>
    // 导入组件
    import Header from './components/header.vue'
 
    import {reactive} from 'vue'
    const web = reactive({
        url:"www.baidu.com",
        name:"无名",
        age:10
    })

    //监听 子组件定义的自定义事件getWeb并绑定 emitsWebUrl方法上 来处理
    const emitsWebUrl = (data) =>{
        console.log("接收到子组件传来的值",data)
        web.url = data.url
    }

    //监听 子组件定义的自定义事件addUser 并绑定到 emitsAddUser 方法上来处理
    const emitsAddUser = (data) =>{
        console.log("接收到子组件传来的值",data)
        web.age += data
    }

</script>

<template>
    <Header @getWeb="emitsWebUrl" @addUser="emitsAddUser" />
    <h2>{{web.url}} | {{web.name}} | {{web.age}}</h2>
</template>


<!-- 子组件使用defineEmits定义自定义事件 -->
<script setup>
    // 使用defineEmits 定义了名为 getWeb 和 addUser 的自定义事件
    const emit = defineEmits(['getWeb','addUser'])
    // 调用自定义事件,并传入对象参数
    emit("getWeb", {name:"十一月",url:"https://blog.xy21lin.cn"})
   
    // 定义 addUser 方法,调用自定义事件 addUser,传入参数值20
   const addUser = () =>{
       emit("addUser",20)
   }
</script>

<template>
    <h1> 十一月的早晨 </h1>
    <button @click="addUser">添加用户</button>
</template>

4. 跨组件通信

跨组件通信是Vue3的重要特性之一,它可以让不同组件之间通信,实现功能的解耦。
父组件可以传递给子组件之下的子组件,但是子组件不能直接向父组件传递数据。


在这里,使用的是provideinject来实现跨组件通信。
provide可以向下传递数据,也就是可以把父组件里的数据传递给子组件及之下的组件。
inject则是用于接收provide传递的数据。


使用App.vue作为父组件,header.vue作为子组件,nav.vue作为子组件之下的子组件。(没有文件就创建)

<!-- App.vue 父组件使用provide传递数据 -->
<script setup>
    // 导入组件
    import Header from './components/header.vue'

    import {reactive,ref,provide} from 'vue'

    const web = reactive({
        name:"十一月",
        url:"https://blog.xy21lin.cn/"
    })

    // 传递web对象 参数是key和valu  也就是键值对  建议键名带上provide前缀 方便后期维护
    provide("provideWeb",web)

    const user = ref(0)
    provide('provideUser',user)


    const addUser = ()=>{
        user.value++
    }

    // 传递函数
    provide('provideFuncAddUser',addUser)
</script>

<template>
    <h1>App.vue | Top 组件 | user: {{user}}</h1>
    <button @click="addUser">父组件 Top 增加</button>
    <Header />
</template>


<!-- header.vue 子组件 使用inject接收 -->
 <script setup>
    import Nav from './nav.vue'
    // 子组件需要使用 inject 接收父组件 provide 传递的数据
    import {inject} from 'vue'

    // 使用inject 通过 provide 设置的键名 来获取传递的数据
    const web = inject('provideWeb')
    console.log("provideWeb", web)

    const user = inject('provideUser')
    console.log("provideUser", user)

    const addUser = inject('provideFuncAddUser')
    console.log("provideFuncAddUser", addUser)
</script>

<template>
    <h1> header.vue | Middle 组件</h1>
    <button @click="addUser">子组件Middle添加</button>
    <Nav />
</template>

<!-- nav.vue 子组件之下的子组件 使用inject接收 -->
<script setup>
    import {inject} from 'vue'
    const addUser = inject("provideFuncAddUser")

</script>

<template>
    <h1> nav.vue | Bottom 组件</h1>
    <button @click="addUser">子组件 Bottom 添加</button>
</template>

5. 插槽

插槽:是指可以在父组件内定义模板片段,在子组件中可以将定义的模板片段插入到子组件的特定位置。
插槽使用的是solt属性

5.1 匿名插槽

匿名插槽:是指在父组件中定义的插槽,没有指定名称,只能有一个默认插槽。

<!-- App.vue 父组件 -->
<script setup>
    // 导入组件
    import Header from './components/header.vue'
</script>

<template>
    <Header>
        <h1 style="color: pink">Header组件插槽,这里是匿名插槽</h1>
    </Header>
</template>


<!-- header.vue 子组件使用匿名插槽 -->
 <script setup>

</script>

<template>
    <h1>Header组件</h1>

    <!-- 调用匿名插槽模板 -->
    <slot/>
</template>

5.2 具名插槽

具名插槽:是指在父组件中定义的插槽,指定了名称,可以有多个插槽。

<!-- App.vue 父组件 -->
<script setup>
    // 导入组件
    import Footer from './components/footer.vue'
</script>

<template>
    <Footer>
        <!-- 具名插槽,需要使用template标签,并在template标签上使用v-slot 指定插槽名称 这里指定模板名称为url -->
        <!-- <template v-slot:url> -->
        <!-- v-slot:url 可以简写为 #url -->
        <template #url>
            <h1 style="color:blue">Footer组件插槽,这里是具名插槽</h1>
        </template>
    </Footer>
</template>

<!-- footer.vue 子组件使用具名插槽 -->
 <script setup>

</script>

<template>
    <h1> Footer组件</h1>

    <!-- 调用具名插槽模板 -->
    <slot name="url"/>
</template>

5.3 作用域插槽

作用域插槽:子组件向父组件传递数据,并在父组件定义的模板中渲染

<!-- App.vue 父组件 -->
 <script setup>
    import Footer from './components/footer.vue'
</script>

<template>
    <Footer>
        <!-- 可以使用data对象 来接收子组件传递过来的数据 -->
        <!-- <template #url="data">
            <h1>这里是作用域插槽,数据是:{{ data.author }} | {{ data.url }}</h1>
        </template> -->

        <!-- 或者是直接用具体属性来接收 -->
        <template #url="{author,url}">
            <h1>这里是作用域插槽,数据是:{{ author }} | {{ url }}</h1>
        </template>
    </Footer>
</template>

<!-- footer.vue 子组件 -->
<script setup>

</script>

<template>
    <h1> Footer组件</h1>

    <!-- 调用具名插槽模板 并传递数据给父组件 -->
    <slot name="url" author="十一月" url="https://blog.xy21lin.cn"/>
</template>

<style scoped>
    
</style>

6. 生命周期函数

Vue3的生命周期函数分为beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed

6.1 创建阶段钩子函数

beforeCreate:在实例初始化之后,数据观测(data observer)和事件配置之前被调用。
created:在实例创建完成后被调用,此时实例已经完成了数据观测(data observer)和事件配置。
目前已基本废弃,使用setup函数替代。
setup 这是 Vue 3 新引入的钩子,它在组件实例创建之前调用,允许我们在组件实例创建之前进行复杂的初始化操作。使用场景:适合进行异步数据获取、状态管理、逻辑封装等。

6.2 挂载阶段钩子函数

beforeMount:在挂载开始之前被调用,相关的模板渲染函数都没有被调用。
mounted:在实例挂载之后被调用,相关的模板渲染函数被调用。

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

    const web = {
        name:"十一月",
        url:"https://blog.xy21lin.cn/"
    }

    // 实例挂载之后被调用
    onMounted(() => {
        console.log("实例挂载之后被调用")
    })

    // 实例挂载之前被调用
    onBeforeMount(() => {
        console.log("实例挂载之前被调用")
    })
</script>

<template>    
    <h1>App.vue | Top 组件</h1>
</template>

6.3 更新阶段钩子函数

beforeUpdate:在数据更新之前被调用,发生在虚拟 DOM 打补丁之前。
updated:在数据更新之后被调用,发生在虚拟 DOM 打补丁之后。

<script setup>
    import {onBeforeUpdate,onUpdated} from 'vue'

    const web = {
        name:"十一月",
        url:"https://blog.xy21lin.cn/"
    }

    // 实例数据更新之前被调用
    onBeforeUpdate(() => {
        console.log("实例数据更新之前被调用")
    })

    // 实例数据更新之后被调用
    onUpdated(() => {
        console.log("实例数据更新之后被调用")
    })
</script>

<template>
    <h1>App.vue | Top 组件</h1>
</template>

6.4 销毁阶段钩子函数

beforeDestroy:在实例销毁之前被调用。
destroyed:在实例销毁之后被调用。

<!-- 父组件App.vue -->
<script setup>
    import {reactive} from 'vue'
    import Header from './components/header.vue'
    const web = reactive({
        url: 'https://blog.xy21lin.cn',
        count:0,
        show:true
    })
</script>

<template>
    <Header v-if="web.show"/>
    <button @click="web.show=!web.show">显示切换</button>
</template>

<!-- 子组件header.vue -->
 <script setup>
    import { onUnmounted,onBeforeUnmount} from 'vue';
    onUnmounted(() => {
        console.log('组件销毁');
    });
    onBeforeUnmount(() => {
        console.log('组件即将销毁');
    });
</script>

<template>
    <h1>Header组件</h1>
</template>

7. toRef 和 toRefs

toRef:可以将响应式对象的某个属性转换为ref变量
toRefs:可以将响应式对象的所有属性转换为ref对象

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

    const web = reactive({
        url: 'https://blog.xy21lin.cn',
        count:0
    })

    // toRef  第一个参数 是要转换的对象,第二个参数 是要转换的属性名
    // let count = toRef(web, 'count')

    // toRefs  参数 是要转换的对象,返回的是一个对象,对象的属性名和值都是响应式的  这里使用解构进行接收
    let {url,count} = toRefs(web)

    const addCount = () =>{
        count.value++
    }
</script>

<template>
    <h1>count :{{ count }}</h1>
    <h2>url :{{ url }}</h2>
    <button @click="addCount">点赞</button>
</template>

8. Pinia

Pinia:是 Vue 3 官方推荐的状态管理库,它可以帮助我们管理应用的状态。
Pinia官方网址

可能还是有些不明白Pinia的作用,这里举个例子,以登陆为例:

  1. 使用Pinia创建一个userStore来集中管理用户的登陆状态和过期时间
  2. 用户登陆成功时,设置userStore中用户登陆状态为已登录,并设置过期时间
  3. 用户登出时,设置userStore中用户登陆状态为未登录,并清除过期时间

8.1 Pinia 和 组件通信 的区别

虽然Vue提供的父传子、子传父以及跨组件通信也可以用于状态共享,但在大型项目中,随着组件数量的增加,会导致以下问题:

  1. 组件之间传递大量的props,会使项目变得非常繁琐和难以维护
  2. 非父子组件间过度依赖provide/inject,使状态散落在各个组件之间

但是使用了Pinia 后可以解决的问题:

  1. 全局状态管理:所有组件都可以访问和修改状态,而不用在每个组件内部进行状态管理
  2. 简化组件之间的通信:避免在组件之间传递大量的props
  3. 状态持久化:可以将应用程序的状态保存到本地存储中,在应用程序重启后会保留状态,对于登录等场景非常有用

总的来说,Pinia可以处理大型项目中复杂的状态管理需求,而父传子、子传父以及跨组件通信,可以解决一些简单的状态传递问题,更适合小型项目

8.2 Pinia 和 localStorage的区别

虽然localStorage可以用于存储数据,但它只能存储字符串,不能存储复杂的数据结构,并且只能存储少量的数据,且有大小限制,通常为5MB左右
Pinia可以存储任意数据类型,包括对象、数组等,没有大小限制,可以存储大量的数据。并且可以持久化存储数据,可以用于存储登录状态等复杂数据。


总的来说,对于复杂的状态管理需求,使用Pinia是更好的选择,而对于简单的状态管理需求,使用localStorage是更简单的解决方案

8.3 安装并使用Pinia

项目目录下打开终端,输入以下命令:

npm install pinia

安装完成后打开package.json文件,可以看到以下代码,可以看到pinia版本为3.0.1:

// ...
  "dependencies": {
    "pinia": "^3.0.1",
    "vue": "^3.5.13"
  },
//   ...

main.js中导入createPinia方法用于创建Pinia实例:

import {createPinia} from 'pinia'


// 创建pinpa实例,在应用中集中管理状态
const pinia = createPinia()

// 创建vue实例,注册pinia实例到vue应用中,挂载到#app元素上
createApp(App).use(pinia).mount('#app')

接下来,在src目录下创建文件夹stores,并在其中创建js文件web.js

import {reactive,ref} from 'vue'
import {defineStore} from 'pinia'

// 定义一个store,第一个参数是store的唯一标识,建议使用文件名,第二个参数使用setup()函数 或 option 对象,这里使用setup()函数
// 使用export const useXXXStore = defineStore('web',()=>{...}) 定义一个store,并导出一个useXXXStore函数,调用该函数可以获取到该store的实例
// useXXXStore 命名规范,在标识前加use,后加store
export const useWebStore = defineStore('web',()=>{
    const users = ref(100) // 定义一个响应式变量 用户数量,初始值为100
    const web = reactive({
        title:"枫月Blog",
        url:"https://blog.xy21lin.cn/"
    })

    // 定义增加用户的方法
    const userAdd = () =>{
        users.value++
    }

    //返回定义的数据
    return{
        users,
        web,
        userAdd
    }
})

App.vue中导入useWebStore函数,并在template标签中使用{{ }}插值语法,将web数据渲染到页面上:

<script setup>
    //导入useWebStore函数
    import {useWebStore} from './stores/web.js'

    // 获取webStore实例
    const webStore = useWebStore()

    console.log(webStore)
</script>

<template>
    <h1>users :{{ webStore.users }}</h1>
    <h2>url :{{ webStore.web.url }}</h2>
    <button @click="webStore.userAdd">添加用户</button>
</template>

8.4 Pinia持久化存储插件

Pinia提供了持久化存储插件,可以将Pinia的状态存储到本地存储中,也就是说在应用程序重启后会保留状态。
Pinia持久化存储插件网址


首先,安装Pinia持久化存储插件,在项目目录终端下输入命令:

npm i pinia-plugin-persistedstate

同Pinia一样,安装完成后,在package.json文件中可以看到插件的版本号,这里是"pinia-plugin-persistedstate": "^4.2.0"
然后,在main.js文件中导入createPersistedState方法,并注册到Pinia实例中:

import { createApp } from 'vue'
import {createPinia} from 'pinia'
//从 pinia-plugin-persistedstate 模块中导入 piniaPluginPersistedstate
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'


// 创建pinpa实例,在应用中集中管理状态
const pinia = createPinia()
// 注册 pinia-plugin-persistedstate 插件到pinia实例上
pinia.use(piniaPluginPersistedstate)

// 创建vue实例,注册pinia实例到vue应用中,挂载到#app元素上
createApp(App).use(pinia).mount('#app')

最后,在stores文件夹下的需要进行持久化存储的仓库中,进行配置:

import {reactive,ref} from 'vue'
import {defineStore} from 'pinia'

// 定义一个store,第一个参数是store的唯一标识,建议使用文件名,第二个参数使用setup()函数 或 option 对象,这里使用setup()函数
// 使用export const useXXXStore = defineStore('web',()=>{...}) 定义一个store,并导出一个useXXXStore函数,调用该函数可以获取到该store的实例
// useXXXStore 命名规范,在标识前加use,后加store
export const useWebStore = defineStore('web',()=>{
    const users = ref(100) // 定义一个响应式变量 用户数量,初始值为100
    const web = reactive({
        title:"枫月Blog",
        url:"https://blog.xy21lin.cn/"
    })

    // 定义增加用户的方法
    const userAdd = () =>{
        users.value++
    }

    //返回定义的数据
    return{
        users,
        web,
        userAdd
    }
},
   // 主要就是这一段  注意位置,是在defineStore第三个参数的位置
{
    persist:true // 开启持久化
})

这样,在应用程序重启后,web仓库的状态会被存储到本地存储中。可以在App.vue中进行测试,刷新页面后,可以看到web仓库的状态被保留。浏览器f12,点击应用,本地存储空间,可以看到web字段,里面存储了web仓库的状态。

注意:pinia持久化插件也是存储到localStorage中,为什么不直接使用localStorage?

  1. 自动状态同步:持久化插件自动将Pinia的状态存储到localStorage中,无需手动处理状态的读取和写入
  2. 易用性:无需手动处理localStorage的键值对存储、数据转换等繁琐过程
  3. 与Vue组件状态紧密集成,持久化插件与Vue组件的响应式数据完美结合,当状态改变时,依赖这些状态的组件会自动更新视图,与仅仅从localStorage中读取静态数据相比更加灵活和强大