项目实践-vue3开发中常用功能汇总示例

1,019 阅读8分钟

cfb9523971594f1a8aba8567c7ff5597_tplv-k3u1fbpfcp-zoom-crop-mark_3024_3024_3024_1702.webp

前言

vue3已经发布2年多了,目前已经在几个项目中使用过,跟vue2相比,除了在模板和样式上的编写差别不大外,核心JavaScript部分写法变化还是比较大的,毕竟是完全重写了,接下来将把在项目中所用到的相关功能梳理出来,仅供大家学习和参考!

vue3官方文档

setup语法糖

下面示例中javascript部分代码均使用setup语法糖编写, 毕竟setup写起来更加丝滑, 部分代码会省略...

<template>
    <div>{{ title }}</div>
    <div @click="changeTitle"></div>
</template>
<script setup>
    import { ref } from 'vue'
    const title = ref('hello world')
    const changeTitle = () => {
        title.value = 'new hello world'
    }
</script>

全局属性配置

在vue2中全局属性是通过在vue原型上挂载属性实现, 在路由组件中通过this直接使用

vue.prototype.globalData = { title: "hello world" }

在vue3中完全模块化了,没有实例,所以写法完全不一样, 相比起来感觉还麻烦一点点...😒

主入口main.js配置

import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
// 配置全局的属性
app.config.globalProperties.globalData = { title: "hello world" };
app.mount("#app");

路由组件中获取

import { getCurrentInstance } from "vue";
const { globalData } = getCurrentInstance().appContext.config.globalProperties;
console.log(globalData.title) // hello world

生命周期钩子

项目中常用钩子

vue3中提供了12个生命周期钩子,但在项目上常用的有如下8个

import {
  onBeforeMount,
  onMounted,
  onBeforeUnmount,
  onUnmounted,
  onBeforeUpdate,
  onUpdated,
  onActivated,
  onDeactivated,
} from "vue";
onBeforeMount(() => {
    // todo 组件挂载前回调
})
onMounted(() => {
    // todo 组件挂载完成回调
})
onBeforeUnmount(() => {
    // todo 组件被卸载前回调
})
onUnmounted(() => {
    // todo 组件被卸载时回调
})
onBeforeUpdate(() => {
    // todo 组件数据更新前回调
})
onUpdated(() => {
    // todo 组件数据更新完成回调
})
onActivated(() => {
    // 组件实例被<keepAlive>包裹,插入DOM中时调用
})
onDeactivated(() => {
   // 组件实例被<keepAlive>包裹,在DOM中被移除时调用
})

父子组件生命周期执行顺序

父子组件在不同时期生命周期执行的顺序

  1. 组件加载时: onBeforeMount(父)➡onBeforeMount(子)➡onMounted(子)➡onMounted(父)➡onActivated(子)➡onActivated(父)
  2. 组件数据更新时:onBeforeUpdate(父)➡onBeforeUpdate(子)➡onUpdated(子)➡onUpdated(父)
  3. 组件卸载时:onBeforeUnmount(父)➡onBeforeUnmount(子)➡onUnmounted(子)➡onUnmounted(父)

响应式数据定义

基础类型ref

ref定义原始数据类型的响应式数据,在<script>中访问的时候需要使用.value, 主要是因为proxy只能代理非原始数据对象,在处理时对原始数据进行了包装,<template>中绑定的时候直接使用属性

<template>
    <div>{{ title }}</div>
</template>
<script setup>
    import { ref } from 'vue'
    const title = ref('hello world')
    console.log(title.value)
</script>

复杂类型reactive

<template>
    <div>{{ state.title }}</div>
</template>
<script setup>
    import { ref, reactive } from 'vue'
    const state = reactive({title: 'hello world'})
</script>

如果使用reactive定义基础类型的数据, 代码可以运行,但是vue3中会有警告信息value cannot be made reactive: hello world,不建议使用

import { reactive } from 'vue'
const state = reactive('hello world')
console.log(state)

不过对于复杂数据类型,也可以使用ref进行定义

import { ref } from 'vue'
const state = ref({title: 'hello world'})
console.log(state.value.title)

计算属性模块

例如在子组件中对父组件传递的金额做转换处理

<template>
    <div>{{ _money }}</div>
</template>
<script setup>
    import { computed } from 'vue'
    const props = defineProps({
        money: {
            type: String,
            default: ''
        }
    })
    const _money = computed(() => {
        return `¥${props.money}`
    })
</script>

监听属性模块

监听单个属性变化

import { ref, watch } from 'vue'
const title = ref('hello world')
watch(title, (n, o) => {
    // todo
})

监听对象变化

import { reactive, watch } from 'vue'
const state = reactive({title: 'hello world'})
watch(() => state, (n, o) => {
    // todo
},
{ deep: true, immediate: true })

监听对象某个属性变化

import { reactive, watch } from 'vue'
const state = reactive({title: 'hello world'})
watch(() => state.title, (n, o) => {
    // todo
})

监听对象所有子属性变化

import { reactive, watch } from 'vue'
const state = reactive({title: 'hello world', content: "you is a word"})
watch(() => ({ ...state }), (n, o) => {
    // todo
})

数据状态管理-vuex

主模块store/index.js

import { createStore } from "vuex";
import other from "./other"; // 引入other模块
export default createStore({
  modules: { other },
  state: {
    title: 'hello world'
  },
  getters: {},
  mutations: {
    setTitle(state, value) {
      state.title = value
    },
  },
  actions: {
    changeTitle({ commit }, value) {
      commit("setTitle", value);
    },
  },
});

other模块store/other.js

export default {
  namespaced: true,
  state: {
    otherTitle: "i am other hello world",
  },
  mutations: {
    setTitle(state, value) {
       state.otherTitle = value
    },
  },
  actions: {
    changeTitle({ commit }, value) {
       commit('setTitle', value)
    }
  }
};

程序入口main.js

import { createApp } from "vue";
import App from "./App.vue";
import store from "./store";
const app = createApp(App);
app.use(store);
app.mount("#app");

路由组件中使用

<template>
    <div>{{ title }}</div>
    <div>{{ otherTitle }}</div>
    <div @click="setTitle('我是新的title')">设置title</div>
    <div @click="setTitle('我是新的otherTitle')">设置otherTitle</div>
</template>
<script setup>
    import { toRefs } from 'vue'
    import { useStore } from 'vuex'
    const store = useStore()
    const { title } = toRefs(store.state)
    const { otherTitle } = toRefs(store.state.other)
    // 调用mutations中的方法 - mutations中处理同步的方法
    const setTitle = (title) => {
       store.commit('setTitle', title)
       store.commit('other/setTitle', title)
    }
    // 调用actions中的方法 - actions中处理异步的方法
    const setTitle = (title) => {
       store.dispatch('changeTitle', title)
       store.dispatch('other/changeTitle', title)
    }
</script>

数据状态管理-pinia【推荐】

从vue2开始vuex一直都是与vue生态相结合的状态管理模块,也是官方推荐的,并且在vue3中也做了版本更新适配, 而pinia它是最新一代的轻量级状态管理插件,按照vue作者尤雨溪的说法,vuex 将不再接受新的功能,所以如果是新的项目建议使用pinia作为状态管理的插件

😘相比而言在代码层面piniavuex更加简洁方便, 并且在pinia中移除了mutations

入口文件pinia/index.js

import { createPinia } from "pinia";
const pinia = createPinia();
export default pinia;

数据模块pinia/ohter.js

对于复杂的应用需要多个模块管理数据的,还可以创建其他模块文件

import { defineStore } from "pinia";
const useOther = defineStore("other", {
  state: () => ({
     title: 'hello world',
     content: 'i am a content'
  }),
  getters: {
  },
  actions: {
    setTitle(value) {
      this.title = value;
    },
  },
});
export default useOther;

程序入口main.js

import { createApp } from "vue";
import App from "./App.vue";
import pinia from "./pinia";
const app = createApp(App);
app.use(pinia);
app.mount("#app");

路由组件中使用

<template>
    <div>{{ title }}</div>
    <div @click="setTitle('我是新的title')">设置title</div>
    <div @click="setPatch">批量设置属性</div>
</template>
<script setup>
    import useOther from "./pinia/other"
    import { storeToRefs } from "pinia";
    const otherStore = useOther()
    const { title } = storeToRefs(otherState)
    const setTitle = (title) => {
        otherStore.setTitle(title)
    }
    // 批量修改状态管理数据
    const setPatch = () => {
        otherStore.$patch((state) => {
            state.title = 'new hello world'
            state.content = 'i am a new content'
        })
    }
</script>

组件交互

父子组件交互

在实际项目中父子组件常用的交互场景有:

1. 父组件传值给子组件以及子组件调用父组件自定义方法

父组件

<template>
    <div>
      <demo :title="title" @change="change"></dmeo> 
    </div>
</template>
<script setup>
    import { ref } from 'vue'
    import Demo from 'Demo.vue'
    const title = ref('hello world')
    const change = () => {
        title.value = 'im a new hello world'
    }
</script>

子组件Demo.vue

<template>
    <div>{{ title }}</div>
    <div @click="change">设置标题</div>
</template>
<script setup>
    const emits = defineEmits(['change'])
    const props = defineProps({
        title: {
            type: String,
            default: 'hi'
        }
    })
    const change = () => {
        emits('change')
    }
</script>

2. 父组件访问子组件属性和方法

父组件

<template>
    <demo ref="demoRef"></dmeo> 
    <div @click="change">设置title</div>
</template>
<script setup>
    import { ref } from 'vue'
    import Demo from 'Demo.vue'
    const demoRef = ref(null) // 获取子组件实例
    const change = () => {
        console.log(demoRef.value.title) // 访问子组件属性
        demoRef.value.change() // 调用子组件方法
    }
</script>

子组件Demo.vue

<template>
    <div>{{ title }}</div>
</template>
<script setup>
    import { ref } from 'vue'
    const title = ref('hello world')
    const change = () => {
       title.value = 'i am a new hello world'
    }
    // 子组件暴露出方法和属性父组件才能访问到
    defineExpose({
        title,
        change
    })
</script>

3. 子组件通过插槽传值到父组件

父组件

<template>
    <demo>
        <template #default="item">
            <div>标题: {{ item.title }} </div>
            <div>内容: {{ item.content }}</div>
        </template>
    </dmeo> 
</template>
<script setup>
    import Demo from 'Demo.vue'
</script>

子组件Demo.vue

<template>
    <div>
        demo组件
        <slot :title="title" :content="content"></slot>
    </div>
</template>
<script setup>
    import { ref } from 'vue'
    const title = ref('hello world')
    const content = ref('i am a content')
</script>

父子组件双向绑定

父子组件之间的数据双向绑定也算是父子组件交互的一类,但是它是通过v-model指令来实现的,这里单独梳理出来,🎇在vue2中每个组件只能绑定一个v-model,而在vue3中可以绑定多个,更加方便

父组件

<template>
    <demo v-model="title" v-model:content="content">
    </dmeo> 
</template>
<script setup>
    import { ref } from 'vue'
    import Demo from 'Demo.vue'
    const title = ref('hello world')
    const content =  ref('i am a content')
</script>

v-model为默认的数据绑定modelValue

子组件Demo.vue

<template>
   <div>{{ title }} {{ content }}</div>
   <div @click="change">修改值</div>
</template>
<script setup>
   const emits = defineEmits(['update:modelValue', 'update:content'])
   const props = defineProps({
       modelValue: {
          type: String,
          default: "",
      },
      content: {
          type: String,
          default: "",
      }
   })
   const change = () => {
       emits('update:modelValue', 'new hello world')
       emits('update:content', 'this is a new content')
   }
</script>

兄弟组件交互

兄弟组件之间的交互没有父子那么直接,理论上可以通过兄弟组件共同的父组件来作为桥梁进行交互,但这种交互方式太繁琐了,项目上一般不会用,更直接的方式是引入事件总线来监听和触发,vue2中使用vue实例作为eventBus进行交互,vue3中我们可以使用mitt模块进行交互,使用方式大同小异

mitt是第三方模块,所以需要安装

cnpm i mitt -S

首先类似eventBus,需要定义一个中间模块mitt.js

import mitt from "mitt";
const emitter = mitt();
export default emitter;

兄弟组件A

<template>
   <div>我是A组件{{ title }}</div>
</template>
<script setup>
    import { ref } from 'vue'
    import emitter from "@/utils/mitt"; // 注意项目上的路径
    const title = ref("hello world")
    // emitter监听事件
    emitter.on("update", () => {
        title.value = 'this is a new hello world'
    });
</script>

兄弟组件B

<template>
   <div>我是B组件</div>
   <div @click="setTitle">修改A组件的title</div>
</template>
<script setup>
    import { ref } from 'vue'
    import emitter from "@/utils/mitt"; // 注意项目上的路径
    const setTitle = () => {
        // emitter触发事件
        emitter.emit('update')
    }
</script>

组件依赖注入

vue3中的依赖注入主要说的provide和inject函数,它们主要用于父子或跨层级(父➡孙)之间的数据交互

父组件

<template>
   <div>我是父组件</div>
   <div @click="change">修改值</div>
</template>
<script setup>
    import { reactive, provide } from 'vue'
    const provideMsg = reactive({title: 'hello world'})
    provide('provideMsg', provideMsg)
    const change = () => {
       provideMsg.title = 'new hello world'
    }
</script>

孙组件Demo.vue

<template>
   <div>我是孙组件</div>
   {{ provideText.title }}
</template>
<script setup>
    import { inject } from 'vue'
    const provideText = inject('provideText')
</script>

🎈 注意

  1. provide和inject的数据传递只能由父到子到孙级传递,不能反向
  2. provide提供的数据如果是响应式,那么inject接收到的也是响应式的,否则就不是

异步组件加载

正常情况下父子组件的加载顺序,是子组件优先于父组件加载完毕,如果子组件加载时间较长就会影响到父组件的加载,这种情况下可以异步加载子组件,不会阻塞父组件的加载

父组件

<template>
    <div>我是父组件</div>
    <demo></dmeo> 
</template>
<script setup>
    import { defineAsycnComponent } from 'vue
    const Demo = defineAsyncComponent(() => import('./Demo.vue'))
</script>

子组件Demo.vue

<template>
    <div>我是子组件</div>
</template>

自定义指令

指令是vue框架的一大特色,在<template>模板中我们通过使用内置指令,大大简化了Dom的操作,但是在某些场景下,需要自定义指令来满足我们的需求

自定义指令

定义指令非常简单方便,只需一个指令名称和声明对应的钩子函数即可,vue中提供了7个钩子函数,实际场景下可能只需要mounted和updated就可以满足我们的需求了

import { createApp } from "vue";
const app = createApp()
app.directive('focus', {
  created(el, binding, vnode, prevVnode) {
    // todo
  },
  beforeMount(el, binding, vnode, prevVnode) {
    // todo
  },
  mounted(el, binding, vnode, prevVnode) {
    // todo
  },
  beforeUpdate(el, binding, vnode, prevVnode) {
    // todo
  },
  updated(el, binding, vnode, prevVnode) {
    // todo
  },
  beforeUnmount(el, binding, vnode, prevVnode) {
   // todo
  },
  unmounted(el, binding, vnode, prevVnode) {
    // todo
  },
})

在所有钩子函数中都有4个参数, 一般情况下只需要用到前两个参数的数据

  • el:表示当前指令绑定的dom元素
  • bingding:一个对象,包括指令所传递的参数 {arg: '', dir: {}, modifiers: {}, value: '', oldValue: ''}
    • arg: 绑定的参数,例如在v-demo:type中,arg的值就是type,还可以写成动态的v-demo:[type]
    • modifiers:修饰符对象,例如在v-demo.type1.type2 中,修饰符对象是 { type1: true, type2: true }
    • value:指令绑定的值
    • oldValue:更新之前的值
    • dir:基本用不到
    • instance:基本用不到
  • vnode:代表绑定元素底层vnode 基本用不到
  • prevVnode:之前的渲染中代表指令所绑定元素的 VNode,仅在 beforeUpdate 和 updated 钩子中可用 基本用不到

自定义指令实例

在项目中有遇到一个非常简单的场景,在文本框中输入字符时,如果字符长度大于6则字体颜色变为红色,小于6时为黑色,于是选择用自定义指令实现,当然很多其他方式也能实现😂

<template>
    <input type="text" v-model="myText" v-setColor="myText">
</template>
<script setup>
    import { ref } from 'vue
    const myText = ref('')
    // 定义自定义指令
    const vSetColor = {
        mounted: (el, binding) => {
            setColor(el, binding)
        },
        updated: (el, binding) => {
            setColor(el, binding)
        }
    }
    const setColor = (el, binding) => {
       if (binding.value.length > 6) {
           el.style.color = 'red'
       } else {
          el.style.color = 'black'
       }
    }
    
</script>

🎈 此代码为在组件内定义自定义指令,全局注册指令参考上面代码即可

插槽

插槽算是一种非常便利的功能了,它让组件充满更多的活力

自定义插槽

父组件

<template>
    <div>
      <demo>
      <!--默认插槽-->
      <template #default>
          我是默认插槽内容
      </template>
      <!--自定义插槽-->
      <template #btn>
         我是自定义插槽内容
      </template>
      </dmeo> 
    </div>
</template>
<script setup>
    import Demo from 'Demo.vue'
</script>

子组件Demo.vue

<template>
    <div>
        我是子组件
        <slot></slot>
        <slot name="btn"></slot>
    </div>
</template>

获取插槽集合

在某些场景下,子组件定义了多个插槽,子组件可能需要判断在父组件访问时是否有传某个插槽来做一些逻辑判断处理

import { getCurrentInstance } from 'vue'
const { ctx } = getCurrentInstance()
console.log(ctx.$slots) // 此方法会返回父组件中使用了哪些插槽, 以上面代码为例会返回 { default:() => {} , btn: () => {} }

插件功能

插件功能在vue2版本中就已经有了,vue3中一样沿用,主要的作用是用来对第三方或封装的公共模块进行注册绑定

封装的模块tools.js

export default {
  install(app) {
   // 例如: 希望在vue3中添加一些全局的属性和方法
    app.config.globalProperties.globalData = { title: "hello world" }
  },
};

插件本质是一个模块,导出一个对象,里面有一个install方法,当使用app.use()的时候,会直接调用install方法并把app当做参数传递过去

主入口文件main.js

import { createApp } from "vue";
import tools from './tools.js'
import App from "./App.vue";
const app = createApp(App);
app.use(tools).mount("#app");

动态组件

在项目中如果有一个tab切换功能,并且在每一部分内容都非常多的情况下,一般我们会把每部分都单独封装成组件进行切换,你是否会想到使用v-if 或 v-show,当然这种方式可以实现,只不过还有更好的一种方式,使用<component>

<template>
    <div @click="changeCom('Demo1')"></div>
    <div @click="changeCom('Demo2')"></div>
    <component :is="comList[currentCom]"></component>
</template>
<script setup>
    import { ref, shallowReactive } from 'vue'
    import Demo1 from 'Demo1.vue'
    import Demo2 from 'Demo2.vue'
    const currentCom = ref('Demo1')
    const comList = shallowReactive({ Demo1, Demo2 })
    const changeCom = (com) => {
        currentCom.value = com
    }
</script>

内置组件

keepAlive

缓存路由组件

在项目app.vue文件中使用keep-alive缓存路由,需要在路由配置中设置meta.keepAlive属性

<template>
  <router-view v-slot="{ Component }">
    <keep-alive>
      <component :is="Component" v-if="$route.meta.keepAlive" />
    </keep-alive>
    <component :is="Component" v-if="!$route.meta.keepAlive" />
  </router-view>
</template>

缓存属性

<KeepAlive>默认情况下会缓存所有组件,我们可以通过include 和 exclude属性来配置是否缓存某个组件, include表示包含某些组件会被缓存,exclude表示除了某些组件以外,其他的都被缓存, 匹配的值是组件的name,有三种写法:

  • 以逗号分割的字符串
  • 正则表达式
  • 数组:元素值可以是字符串或正则表达式
<template>
    <div @click="changeCom('Demo1')"></div>
    <div @click="changeCom('Demo2')"></div>
    <keep-alive :include="Demo1">
        <component :is="comList[currentCom]"></component>
    </keep-alive>
</template>
<script setup>
    import { ref, shallowReactive } from 'vue'
    import Demo1 from 'Demo1.vue'
    import Demo2 from 'Demo2.vue'
    const currentCom = ref('Demo1')
    const comList = shallowReactive({ Demo1, Demo2 })
    const changeCom = (com) => {
        currentCom.value = com
    }
</script>

上面示例只会缓存Demo1组件,Demo2组件每次都会被重新加载

组件name值

  • 在3.2.34版本或以后,使用<script setup></script>声明的单文件组件默认会以文件名生成对应的name选项

  • 在3.2.34版本之前需要手动的声明,可以使用vite-plugin-vue-setup-extend插件实现

  1. 安装模块
cnpm i vite-plugin-vue-setup-extend -D
  1. vite.config.js配置
import { defineConfig } from 'vite' 
import VueSetupExtend from 'vite-plugin-vue-setup-extend' 
export default defineConfig({ plugins: [ VueSetupExtend() ] })
  1. 组件配置

在script中声明name属性即可

<template>
    <div>{{ title }}</div>
</template>
<script setup name="demo">
    import { ref } from 'vue'
    const title = ref('hello world')
</script>

Teleport

teleport也叫做传送门, 它可以把元素挂载到页面任意的地方,挂载的目标可以是某个dom标签元素,id标签,class标签

<template>
    <div @click="open(true)">打开</div>
    <teleport to="body">
        <div @click="open(false)">我是弹出层</div>
    </teleport>
</template>
<script setup>
    import { ref } from 'vue'
    const isOpen = ref(false)
    const open = (val) => {
        isOpen.value = val
    }
</script>

动态绑定class

绑定对象

<template>
    <div :class="{ active: isActive, 'text-danger': hasError }"></div>
</template>
<script setup>
    import { ref } from 'vue'
    const isActive = ref(true)
    const hsaError = ref(false)
</script>
<style scoped>
    .active {
        font-weight: bold
    }
    .text-danger {
        color: red
    }
</style>

绑定数组

<template>
    <div :class="[{ active: isActive }, 'text-danger']"></div>
</template>
<script setup>
    import { ref } from 'vue'
    const isActive = ref(true)
</script>
<style scoped>
    .active {
        font-weight: bold
    }
    .text-danger {
        color: red
    }
</style>

动态设置style

绑定对象

<template>
    <div :style="{fontSize: size + 'px', color: 'red'}"></div>
    <div :style="objStyle"></div>
</template>
<script setup>
    const objStyle = {
        fontSize: '20px',
        color: '#999'
    }
</script>

总结

上面所梳理的功能点基本都是在项目中使用过的(还有很多功能没有使用过),后续也会不断的更新和完善,也作为自己学习的一个积累,vue2和vue3目前在项目中都有在用,总体感觉只是代码层面的写法变了而已,逻辑思维方式还是一样一样的,只要写熟练习惯就好,技术没有好坏,就相当于工具没有好坏一样,主要看是否用在合适的地方,对于新的技术我们应该抱着一颗学习和向往的心态,但是也不要盲目的跟风,适合自己以及能解决工作问题的才是最佳选择!!!