组件允许我们将 UI 划分为独立的、可重用的部分来思考。组件在应用程序中常常被组织成层层嵌套的树状结构
这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件数据模型,使我们可以在每个组件内封装自定义内容与逻辑
我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC), 它是一种特殊的文件格式,使我们能够将一个 Vue 组件的模板、逻辑与样式封装在单个文件中
<!-- 逻辑处理部分, 有JavaScript提供该能力 -->
<script setup>
</script>
<!-- 视图模版, 有HTML和自定义组件或者Web组件提供该能力 -->
<template>
<p class="greeting">{{ greeting }}</p>
</template>
<!-- 元素样式, 由css提供该能力 -->
<style>
</style>
很显然 该文件并不是个合法的html文件, 并不能别浏览器直接加载, 它是一种特殊的文件格式, 定义该文件的语法被叫做: SFC 语法
该语法需要被转换成标准的HTML文件才能被浏览器加载, @vue/compiler-sfc 就是用于干这个事儿的, 当然它不是独立工作的, vite提供了构建功能, vue以插件的形式提供sfc语法转换逻辑(compiler-sfc), 比如下面的vite配置:
import { fileURLToPath, URL } from "url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
一个编译后的 SFC 是一个标准的 JavaScript(ES) 模块,这也意味着通过适当的构建配置,你可以像导入其他 ES 模块一样导入 SFC
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent
}
}
当然,有些场景下使用 SFC 有些过犹不及。当不使用构建步骤时, 一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
// 或者 `template: '#my-template-element'`
}
当然由于跳过的构建,没有编译器的支撑 很多编译器提供的功能,比如$ref,defineProps等 我们就不能使用了, 这会是语法比较繁琐, 一般不这样用
我们分别定义:
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
<style scoped>
</style>
通过Import导入后, 通过名称直接使用
<template>
<div>
<!-- 在单文件组件中, 推荐为子组件使用 PascalCase 的标签名 -->
<ButtonCounter style="width: 220px" />
<!-- 也可以使用原生 HTML 标签命名风格(单词-单词), 但是为了和标准HTML元素区分开, 并不推荐这样使用 -->
<!-- <button-counter></button-counter> -->
</div>
</template>
<script setup>
// 通过 <script setup>,导入的组件都在模板中直接可用
import ButtonCounter from "@/components/ButtonCounter.vue";
</script>
向上面那样直接使用时局部导入
你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入
import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)
有的需求会想要在两个组件间来回切换:
vue 提供一个component元素, 它由一个属性: is, is的值可以为:
比如:
<!-- currentTab 改变时组件也改变 -->
<component :is="currentComponentName"></component>
vue 内置了一些组建, 可以理解为官方提供的一些标准库, 由于内置组建是全局组册的, 因此我们不需要Import, 可以在模版中直接使用
下面演示下组建缓存:
组件是vue中用于复用的最小UI单元, 你可以把它理解为后端编程里面的一个函数, 只是这个函数不仅有逻辑还有界面:
func Component() {
// 定义UI 展示
// 定义数据处理逻辑
}
现在我们的组建既没有参数,也获取不到组件的返回, 这使得我们的组件很难复用, 比如:
func ComponentA() {
// 显示 文章A的标签
}
func ComponentB() {
// 显示 文章B的标签
}
// 使用参数进行统一抽象
func Component(title, callbackFunc) {
// 根据传人的title参数 进行显示
title
// 如果用户修改了title 使用callbackFunc函数回调通知
callbackFunc(titile)
}
vue 为我们提供组件通讯的机制有:
传递props, 主要用于 父组件向子组件通讯:
<父组件> ---> <子组件>
如果向组件传入参数, 首先组件需要定义参数: defineProps 宏就是用于干这个的
<script setup>
// 使用 defineProps 来进行组件属性(参数)的定义。
// defineProps() 是一个宏由编译器负责处理, 无需引入
const props = defineProps({
title: String,
likes: Number
})
console.log(props.foo)
</script>
最终我们可以通过组建的属性 来进行参数的传递:
<ComponentName title="文章A" likes=10 />
defineProps({
greetingMessage: String
})
一般情况下都会使用 PascalCase 作为组件标签名,因为这提高了模板的可读性,能很好地区分出 Vue 组件和原生 HTML 元素。 然而这对于传递 prop 来说收效并不高,因此我们选择对其进行转换
<ComponentName greeting-message="文章A" />
很多时候我们传递的属性都是和我们的响应式数据绑定的, 组件和元素HTML在属性绑定上并没有啥区别, 都是用v-bind执行来进行响应式数据绑定
<!-- 使用v-bind指令 绑定变量name -->
<ComponentName v-bind:greeting-message="name" />
<!-- v-bind可以缩写为: 缩写模式 -->
<ComponentName :greeting-message="name" />
定义属性时处理可以指定type以外,还可以有:
defineProps({
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})
所有的 prop 都遵循着单向绑定原则,prop 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改了父组件的状态,不然应用的数据流就会变得难以理解了
监听事件, 主要用于 子组件向父组件通讯:
<父组件> <--- <子组件>
组件要触发的事件可以显式地通过 defineEmits() 宏来声明
<script setup>
// 其中事件的名称可以使用数组来进行定义
const emit = defineEmits(['inFocus', 'submit'])
</script>
然后我们调用声明来进行事件的触发, 触发时 可以携带任何类型的数据
// 把当前的值通知给父组件
const doClick = () => {
count.value++;
emit("submit", count.value);
};
和其他正常的组件一样我们使用v-on(@)+事件名称 来监听具体的事件(当然是父组件来监听):
<template>
<!-- 定义一个事件处理函数来处理 submit 事件 -->
<button-counter
@submit="submitEventHandler"
style="width: 220px"
></button-counter>
</template>
<script setup>
const submitEventHandler = (e) => {
console.log(e);
};
</script>
父组件 --属性传递--> 子组件
父组件 <--触发事件-- 子组件
通过上面我们已经可以组件实现数据的双向绑定了:
<script setup>
// defineProps() 是一个宏由编译器负责处理, 无需引入
const props = defineProps({
count: Number,
});
// 其中事件的名称可以使用数组来进行定义
const emit = defineEmits(["update_count"]);
const doClick = () => {
// 把修改的值传递给父组件, 由父组件来更新count props
emit("update_count", props.count + 1);
};
</script>
<template>
<button @click="doClick">You clicked me {{ count }} times.</button>
</template>
<template>
<div class="about">
<button-counter
@update_count="updateCountEventHandler"
:count="count"
style="width: 220px"
></button-counter>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import ButtonCounter from "@/components/ButtonCounter.vue";
// 通过 v-bind 绑定动态属性
// 通过 v-on 监听来自于子组件的update_count事件
const count = ref(0);
const updateCountEventHandler = (e) => {
count.value = e;
};
</script>
很显然上面的写法很累赘, 我们事件处理函数的处理逻辑也很单调, 仅仅是赋值, 像元素的HTML元素 都可以直接使用v-model来实现双向绑定, 比如:
<input v-model="searchText" />
那能不能把组件也通过这个语法直接变成双向绑定的喃?
<ButtonCounter v-model="count" />
为了使组件能像这样工作,组件必须:
<script setup>
// defineProps() 是一个宏由编译器负责处理, 无需引入
const props = defineProps({
modelValue: Number,
});
// 其中事件的名称可以使用数组来进行定义
const emit = defineEmits(["update:modelValue"]);
const doClick = () => {
// 把修改的值传递给父组件, 由父组件来更新count props
emit("update:modelValue", props.modelValue + 1);
};
</script>
<template>
<button @click="doClick">You clicked me {{ modelValue }} times.</button>
</template>
这样我们的父组件就能完成双向通行了
<ButtonCounter v-model="count" />
有上面可以看出 v-model作用与自定义组件时, 相当于固定了modelValue属性和update:modelValue方法
由于直接使用v-model 就等于直接绑定了modelValue属性和update:modelValue事件, 而且还只针对一个属性, 如果有多个自定义属性怎么办?
v-model提供了一个参数: v-model:propsName, 比如如果要绑定的属性名称为 count, 则:
<!-- 如果有多个属性需要绑定就写多个v-model:attr就可以了 -->
<ButtonCounter v-model:count="count" />
因此修改自组件的属性和事件命名:
<script setup>
// defineProps() 是一个宏由编译器负责处理, 无需引入
const props = defineProps({
count: Number,
});
// 其中事件的名称可以使用数组来进行定义
const emit = defineEmits(["update:count"]);
const doClick = () => {
// 把修改的值传递给父组件, 由父组件来更新count props
emit("update:count", props.count + 1);
};
</script>
<template>
<button @click="doClick">You clicked me {{ count }} times.</button>
</template>
上面讲到的都是父子组件的通讯, 如果组件并不是父子关系, 二是多层的关系,比如这样, 难道我们需要把props一层一层的往下传递?:
注入到父组件的变量, 所有的后面的子组件才能获取到, 就像golang里面的ctx
要为组件后代供给数据,需要使用到 provide() 函数, 由于组件本身就是树状结构, 只要我们注入到根组件,那么后续所有组件都能访问到
修改App.vue
<script setup>
import { RouterLink, RouterView } from "vue-router";
import HelloWorld from "@/components/HelloWorld.vue";
import { ref, provide } from "vue";
// 如果的变量可以是响应式的
const count = ref(0);
provide(/* 注入名 */ "count", /* 值 */ count);
</script>
通过inject获取父组件注入的变量:
<script setup>
import { inject } from "vue";
// 这里也可以获取默认值: inject(<变量名称>, <变量默认值>), 如果获取不到变量 就使用默认值
const count = inject("count");
const doClick = () => {
count.value++;
};
</script>
<template>
<button @click="doClick">You clicked me {{ count }} times.</button>
</template>
依然注入约等于共享内存,因此灵活度很高
上面的方式 必须像通过上下文 来传递变量, 变编程中 有一种更常用的 贡献内存通信的方式: 全局变量
我们可以定义一个全局变量, 在需要的地方直接导入,然后访问该变量的状态
在vue中, 我们很少直接定义这种全局变量, 它提供了一种 通过定义一个函数来 访问该全局变量的状态的方式: 组合式函数
“组合式函数”是一个利用 Vue 组合式 API 来封装和复用有状态逻辑的函数,
鼠标跟踪器示例: 获取当前鼠标的位置坐标:
我们通过编写一个js的模块, 导出一个函数给外部使用(工具函数), 该函数维护这2个响应式变量(x, y), 分别代码光标的 x,y位置
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通过返回值暴露所管理的状态
return { x, y }
}
组合式函数就是一个带有响应式数据的普通函数, 并没有特殊之处, 我们按照js模块引入规范,导入使用即可:
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
定义一个保护响应式数据的公共模块
// store/global.js
import { reactive } from "vue";
export const store = reactive({
count: 0,
});
button组件引入并使用:
<script setup>
import { store } from "@/stores/global";
const doClick = () => {
store.count++;
};
</script>
<template>
<button @click="doClick">You clicked me {{ store.count }} times.</button>
</template>
其他组件也可以引入并使用:
import { store } from "@/stores/global";
// 访问strore
store.count
但是显然我们基于stroe进行数据共享,但是我们store并没有持久化, 页面一刷新就没了, 有没有一种能持久化 并且是响应式的数据源工具喃? 这就不得不讲到 vue的 公共库: vueuse
首先我们安装上这个库:
npm i @vueuse/core
然后倒入我们需要的函数使用
import { useMouse } from '@vueuse/core'
const { x, y, sourceType } = useMouse()
由于VueUse的出现, vue3终于可以比肩并(react Hooks)且有超越react的趋势
在VueUse的中有个这样的库: useStorage, 他可以把浏览器的Localstorage 包装成一个响应式对象
import { useStorage } from "@vueuse/core";
// 第一个参数是key, 第二个参数是vulue
const count = useStorage("count", 0);
插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:
接下来我们讲解vue使用最多的几种插件:
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。