Ⅰ. 组件及组件化
一、为什么需要组件?
1. 思考
以可折叠面板为例,现要展示3个,如何操作?
可折叠面板案例的代码:
<script setup>
import { ref } from 'vue'
const visible = ref(false)
</script>
<template>
<h3>可折叠面板</h3>
<div class="panel">
<div class="title">
<h4>自由与爱情</h4>
<span class="btn" @click="visible = !visible"> {{ visible ? '收起' : '展开' }} </span>
</div>
<div class="container" v-show="visible">
<p>生命诚可贵,</p>
<p>爱情价更高。</p>
<p>若为自由故,</p>
<p>两者皆可抛。</p>
</div>
</div>
</template>
<style lang="scss">
body {
background-color: #ccc;
}
#app {
width: 400px;
margin: 20px auto;
background-color: #fff;
border: 4px solid green;
border-radius: 1em;
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.5);
padding: 1em 2em 2em;
}
#app h3 {
text-align: center;
}
.panel {
.title {
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #ccc;
padding: 0 1em;
}
.title h4 {
line-height: 2;
margin: 0;
}
.container {
border: 1px solid #ccc;
padding: 0 1em;
border-top-color: transparent;
}
.btn {
cursor: pointer;
}
}
</style>2. 解决方案
-
把需要复用的一段标签,抽离并封装到一个单独的
vue文件里,连同相关JS和CSS放到一起 -
哪里要用这个组件,哪里导入,当做标签使用即可
新建一个 src/components/MyPanel.vue 文件:
<script setup>
import { ref } from 'vue'
const visible = ref(false)
</script>
<template>
<div class="panel">
<div class="title">
<h4>自由与爱情</h4>
<span
class="btn"
@click="visible = !visible">
{{ visible ? '收起' : '展开' }}
</span>
</div>
<div
class="container"
v-show="visible">
<p>生命诚可贵,</p>
<p>爱情价更高。</p>
<p>若为自由故,</p>
<p>两者皆可抛。</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.panel {
.title {
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #ccc;
padding: 0 1em;
}
.title h4 {
line-height: 2;
margin: 0;
}
.container {
border: 1px solid #ccc;
padding: 0 1em;
border-top-color: transparent;
}
.btn {
cursor: pointer;
}
}
</style>然后在 App.vue 导入并使用该组件:
<script setup>
// 导入
import MyPanel from './components/MyPanel.vue'
</script>
<template>
<h3>可折叠面板</h3>
<!-- 使用 -->
<MyPanel />
<MyPanel />
<MyPanel />
</template>
<style>
body {
background-color: #ccc;
}
#app {
width: 400px;
margin: 20px auto;
background-color: #fff;
border: 4px solid green;
border-radius: 1em;
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.5);
padding: 1em 2em 2em;
}
#app h3 {
text-align: center;
}
</style>
二、组件及组件化
1. 组件
组件是一个 独立的、可复用 的 Vue 实例,也是一段 独立的 UI 视图 ,代码上体现在是一个独立的 .vue 文件,包含 JS + HTML + CSS 三部分组成。
类似乐高和积木一样,我们可以通过任意的乐高或积分进行组合,拼装成我们需要的成品。

2. 组件化
定义: 一种代码的开发思想,体现在一个页面可以拆分成一个个组件,每个组件有着自己独立的结构、样式、行为;通过 组件的组合与拼装 形成一个完整的页面,本质是代码的一种拆分思想,化大为小、化繁为简、分而治之 。
好处: 各自独立、便于复用
比如:下面这个页面,可以把所有的代码都写在一个页面中,但是这样显得代码比较混乱,难以维护。我们可以按模块进行组件拆分

三、根组件 App.vue
1. 根组件
根组件是整个应用最上层的组件,包裹所有普通小组件,如下图所示:

2. 组件是由三部分构成
-
template:HTML 结构
-
script:JS 逻辑
-
style:CSS 样式(可支持less/scss,需要装包)
- 让组件支持less/scss
style标签,lang="less/scss"开启less/scss功能
装包:
npm i less less-loader -D或者npm i sass -D
四、组件的使用
组件的一系列场景命名规范如下所示:
| 场景 | ✅推荐写法 | 可选写法 | 说明 |
|---|---|---|---|
| 文件名 | MyHeader.vue | my-header.vue | 官方推荐 PascalCase ,和类名一样清晰 |
| import 引入 | import MyHeader from './MyHeader.vue' | import Haha from './MyHeader.vue' | 变量名随意,但最好和文件名一致 ,便于阅读 |
| 局部注册 | components: { MyHeader } | components: { Haha } | 名字和模板里的标签要对应 |
| 模板 (SFC 内部) | Vue 自动识别两种形式 | ||
| HTML 文件直接写 (非 .vue) | ❌ 不支持 |
HTML 标签不区分大小写,必须用短横线 |
1. 创建组件
新建 .vue 文件,编写组件的三部分代码
2. 导入组件
在需要的 vue 文件中的 <script setup> 中导入需要的组件即可!
注意:导入的组件对象名称,推荐保持和文件名一致 。
import 组件对象 from '相对路径'
// 例子:
import MyPanel from './components/MyPanel.vue'3. 注册组件💥
💥注意:局部组件无需注册 ,全局组件要在 main.js中注册 。
💡 之所以局部组件不需要注册,是因为
vue3中<script setup>自动完成了局部注册 ,如果是vue2或者普通的<script>的话,则需要手动注册局部组件,如下面所示:
<script>
import MyHeader from './components2/MyHeader.vue'
// 如果是 vue2 或者普通的 <script> 的话,则需要手动注册局部组件
export default {
components: {
MyHeader
}
}
</script>
<template>
<my-header></my-header>
</template>4. 使用组件
把组件当做自定义标签 使用:(单双标签均可)
<组件名></组件名>
<组件名 />
// 例子:
<!-- **大驼峰** 双标签 -->
<MyPanel></MyPanel>
<!-- 大驼峰 自闭合的单标签 -->
<MyPanel />
<!-- **烤串法(更推荐这种)** 双标签 -->
<my-panel></my-panel>
<!-- 烤串法 自闭合的单标签 -->
<my-panel />5. 练习
components2/MyHeader.vue文件:
<script setup></script>
<template>
<div class="my-header">我是my-header</div>
</template>
<style>
.my-header {
height: 100px;
line-height: 100px;
background-color: #8064a2;
}
</style>components2/MyMain.vue文件:
<script setup></script>
<template>
<div class="my-main">我是my-main</div>
</template>
<style>
.my-main {
height: 400px;
margin: 20px 0;
line-height: 400px;
background-color: #f79646;
}
</style>components2/MyFooter.vue文件:
<script setup></script>
<template>
<div class="my-footer">我是my-footer</div>
</template>
<style>
.my-footer {
height: 100px;
line-height: 100px;
background-color: #4f81bd;
}
</style>App.vue文件:
<template>
<my-header></my-header>
<my-main></my-main>
<my-footer></my-footer>
</template>
<script setup>
import MyHeader from './components2/MyHeader.vue';
import MyMain from './components2/MyMain.vue';
import MyFooter from './components2/MyFooter.vue';
</script>
<style>
* {
margin: 0;
}
#app {
height: 100vh;
padding: 10px;
background: skyblue;
font-size: 30px;
color: #fff;
text-align: center;
}
</style>五、组件的全局注册
💡 全局注册的组件,在项目的任何组件中都能使用
1. 步骤
-
创建
.vue组件(三个组成部分) -
main.js中进行全局注册
2. 使用组件(与局部组件一样)
当成 HTML 标签直接使用:
-
双标签:
<组件名></组件名> -
自闭合的单标签:
<组件名 />
3. 注意
组件名规范:大驼峰命名法(推荐) 或 烤串法 ,如 MyHeader 或 my-header。
4. 语法
在 main.js 中通过导入组件,并且使用 app.component() 方法注册组件:
**// main.js文件**
import MyPanel from './components/MyPanel.vue'
// 注册全局组件语法:
// **app.component('组件名', 组件对象)**
// **大驼峰组件名** (**更推荐这种** ,因为使用标签时候既可以用大驼峰,也可以用烤串法)
app.component('MyPanel', MyPanel)
// 烤串法组件名(需要注意:使用标签时候,只能用烤串法,而不能再用大驼峰了)
app.component('my-panel', MyPanel)注意,上面代码中的 app 对象,是 createApp(App) 得到的应用对象!
5. 练习
components3/MyButton.vue文件:
<script setup></script>
<template>
<button class="my-button">通用按钮</button>
</template>
<style scoped>
.my-button {
height: 50px;
line-height: 50px;
padding: 0 15px;
background-color: #3bae56;
border-radius: 5px;
font-size: 16px;
color: white;
border: none;
cursor: pointer;
}
</style>在 main.js 中,导入并全局注册:
import { createApp } from 'vue'
import App from './App.vue'
import MyButton from './components3/MyButton.vue'
const app = createApp(App) // 拿到应用对象
app.component("MyButton", MyButton)
app.mount('#app') // 最后再去挂载即可components3/MyHeader.vue文件:
<script setup></script>
<template>
<div class="my-header">
我是my-header
<my-button />
</div>
</template>
<style>
.my-header {
height: 100px;
line-height: 100px;
background-color: #8064a2;
}
</style>components3/MyMain.vue文件:
<script setup></script>
<template>
<div class="my-main">
我是my-main
<my-button />
</div>
</template>
<style>
.my-main {
height: 400px;
margin: 20px 0;
line-height: 400px;
background-color: #f79646;
}
</style>components3/MyFooter.vue文件:
<script setup></script>
<template>
<div class="my-footer">
我是my-footer
<my-button />
</div>
</template>
<style>
.my-footer {
height: 100px;
line-height: 100px;
background-color: #4f81bd;
}
</style>App.vue文件:
<template>
<my-header></my-header>
<my-main></my-main>
<my-footer></my-footer>
</template>
<script setup>
import MyHeader from './components3/MyHeader.vue';
import MyMain from './components3/MyMain.vue';
import MyFooter from './components3/MyFooter.vue';
</script>
<style>
* {
margin: 0;
}
#app {
height: 100vh;
padding: 10px;
background: skyblue;
font-size: 30px;
color: #fff;
text-align: center;
}
</style>Ⅱ. 组件生命周期
一、生命周期介绍
一个 Vue 实例(组件)从创建到卸载的整个过程,称为其生命周期。从宏观角度来看,一共有四个阶段,如下所示:
-
创建 :初始化
props、data、methods等响应式数据。 -
挂载 :渲染模板
template,把虚拟 DOM 渲染成真实 DOM,显示在页面上。 -
更新 :当数据变化时,重新渲染视图。
-
卸载 :组件销毁、释放资源。
二、组件生命周期钩子
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据监听,编译模板,挂载实例到真实 DOM 树上,以及在数据改变时更新 DOM。
在上述过程中,会自动运行一些函数,这些函数被称为【Vue生命周期钩子】 。
作用:钩子函数在特定时机会自动执行,这给了开发者在特定阶段添加自己代码的机会。

选项式 API 的代码示例
新建 components/LifeCycle.vue文件:
<script>
export default {
// 提供响应式数据
data() {
return {
count: 0
}
},
// 提供方法/函数
methods: {
fn() {
console.log('fn 函数执行了')
}
},
setup() {
console.log('0-setup')
},
**// 创建阶段(第一阶段):Vue组件创建/出生阶段:**
// 创建前:此时无法访问 data 数据,也无法调用 methods 方法
beforeCreate() {
console.log('1-beforeCreate')
console.log(this.count) // undefined
console.log(this.fn) // undefined
},
// 创建后:此时可以访问 data 数据,也可以调用 methods 方法
created() {
console.log('2-created')
console.log(this.count) // 0
console.log(this.fn) // 访问到函数
this.fn()
// 开启定时器
// 给当前组件实例新增了一个 timerId 属性,保存了当前定时器的 id 值
this.timerId = setInterval(() => {
console.log(this.count)
}, 1000)
},
**// 挂载阶段(第二阶段):模版渲染阶段**
// 挂载前:此时写在 template 下的标签还没有变成真实DOM,故而无法获取DOM
beforeMount() {
console.log('3-beforeMount')
console.log(document.querySelector('p')) // null
},
// 挂载后:此时写在 template 下的标签已经变成了真实DOM,故而可以获取DOM(是最早可以操作DOM的时机)
mounted() {
console.log('4-mounted')
console.log(document.querySelector('p'))// <p>0</p>
document.querySelector('p').style.color = 'red'
},
**// 更新阶段(第三阶段):数据变了,组件重新渲染的过程**
// 更新前
beforeUpdate() {
console.log('5-beforeUpdate')
// console.log(this.count)
console.log(document.querySelector('p').innerText)// 旧内容(以前的内容)
},
// 更新后
updated() {
console.log('6-updated')
// console.log(this.count)
console.log(document.querySelector('p').innerText)// 新内容
},
**// 卸载阶段(第四阶段):组件移除阶段**
beforeUnmount() {
console.log('7-beforeUnmount')
// 用于 **依赖 DOM 或组件存在的清理逻辑** ,比如要解除某个挂在组件 DOM 上的事件监听。
},
unmounted() {
console.log('8-mounted')
// 这里尝试访问 this.$el 或 document.querySelector('#xxx')
// 已经拿不到了,因为 DOM 被销毁了
// 但是可以关闭定时器之类非 DOM 的资源
clearInterval(this.timerId)
}
}
</script>
<template>
<div>
<p>{{ count }}</p>
<button @click="count++">+1</button>
</div>
</template>App.vue文件:
<script setup>
import { ref } from 'vue'
import LifeCycle from './components/LifeCycle.vue'
const isAlive = ref(true)
</script>
<template>
<life-cycle v-if="isAlive"/>
</template>三、组合式API生命周期钩子
1. 与 Vue2 钩子函数的对比
| 阶段 | Vue2(选项式) | Vue3(组合式) |
|---|---|---|
| 创建阶段 | beforeCreate、created | setup (网络请求) |
| 挂载阶段 | beforeMount、mounted | onBeforeMount、onMounted (操作DOM) |
| 更新阶段 | beforeUpdate、updated | onBeforeUpdate、onUpdated |
| 销毁阶段 | beforeUnmount、unmounted | onBeforeUnmount、onUnmounted (清理工作) |
其中 vue3 的 <script setup> 就直接包括了 vue2 中的 setup()、beforeCreate()、created(),所以可以直接在 <script setup>中进行网络请求 !
2. 代码实例
<script setup>
import { onMounted, onUnmounted } from 'vue'
// 开启定时器
const timer = setInterval(() => {
console.log('Hello World')
}, 1000)
// 组件挂载后
onMounted(() => {
// console.log(document.querySelector('p'))
document.querySelector('p').style.color = 'green'
})
// 组件卸载后
onUnmounted(() => {
// 关闭定时器
clearInterval(timer)
})
</script>四、案例-生命周期钩子应用
1. 在 setup 中发起网络请求并渲染数据
① 安装axios
npm i axios② 完整代码
<script setup>
// 安装 axios
// 导入模块
// 定义请求函数并调用
import axios from 'axios'
import { ref } from 'vue'
const i = ref(0)
// 图片列表
const images = ref([])
getBannerData()
**async** function getBannerData() {
// 发请求,调接口
const resp = **await** **axios** ({
**method** : 'GET', // 请求方式
**url** : 'http://localhost:4000/api/banner' // 请求路径
})
// 保存数据
console.log(resp);
**images.value = resp.data.data**
}
const prev = () => {
i.value--
if (i.value <= -1) {
i.value = images.value.length - 1
}
}
const next = () => {
i.value++
if (i.value >= images.value.length) {
i.value = 0
}
}
let timer = null
const play = () => {
timer = setInterval(() => {
next()
}, 3000)
}
play()
const stop = () => {
clearInterval(timer)
}
</script>
<template>
<div
class="banner"
@mouseenter="stop"
@mouseleave="play">
<ul>
<li
v-for="(url, index) in images"
:class="{ active: index === i }">
<img :src="url" />
</li>
</ul>
<div class="indicator">
<span
v-for="(n, index) in images.length"
:key="n"
:class="{ active: index === i }"
@click="i = index">
</span>
</div>
<div class="ctrl">
<a
href="javascript:;"
class="btn prev"
@click="prev"
><</a
><a
href="javascript:;"
class="btn next"
@click="next"
>></a
>
</div>
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
}
a {
text-decoration: none;
color: #000;
}
.banner {
position: relative;
width: 1200px;
height: 337px;
margin: 150px auto;
}
.banner ul {
width: 100%;
height: 100%;
list-style: none;
}
.banner ul li {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 1s;
}
.banner ul li.active {
opacity: 1;
}
.banner .indicator {
display: flex;
justify-content: center;
position: absolute;
left: 0;
bottom: 20px;
width: 100%;
}
.banner .indicator span {
width: 30px;
height: 2px;
margin: 0 5px;
cursor: pointer;
background: rgba(255, 2555, 255, 0.6);
}
.banner .indicator span.active {
background: rgba(255, 255, 255, 1);
}
.btn {
position: absolute;
top: 50%;
width: 30px;
height: 30px;
line-height: 30px;
margin-top: -15px;
font-size: 14px;
text-align: center;
background: rgba(0, 0, 0, 0.3);
color: #fff;
border-radius: 50%;
}
.btn.prev {
left: 15px;
}
.btn.next {
right: 15px;
}
</style>2. 在 onMounted 中操作 DOM
目标:打开网页的时候,输入框自动聚焦
<script setup>
import { onMounted } from 'vue'
// 组件挂载后
onMounted(() => {
// 获取 input 元素
const input = document.querySelector('input')
// 调用 focus 聚焦
input.focus()
})
</script>
<template>
<div class="container">
<img
width="150"
src="https://th.bing.com/th/id/ODL.ce819d0be740fd3c6b5e42d538119fab?w=310&h=198&c=7&rs=1&bgcl=ffff14&r=0&o=6&dpr=1.3&pid=AlgoBlockDebug"
alt="logo" />
<div class="search-box">
<input type="text" />
<button>搜 索</button>
</div>
</div>
</template>
<style>
html,
body {
height: 100vh;
}
.container {
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.container .search-box {
display: flex;
}
.container img {
margin-bottom: 30px;
}
.container .search-box input {
width: 512px;
height: 17px;
padding: 12px 16px;
font-size: 16px;
margin: 0;
vertical-align: top;
outline: 0;
box-shadow: none;
border-radius: 10px 0 0 10px;
border: 2px solid #c4c7ce;
background: #fff;
color: #222;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
}
.container .search-box button {
width: 112px;
height: 44px;
line-height: 42px;
background-color: #ad2a27;
border-radius: 0 5px 5px 0;
font-size: 17px;
box-shadow: none;
font-weight: 400;
margin-left: -1px;
border: 0;
outline: 0;
letter-spacing: normal;
color: white;
cursor: pointer;
}
body {
background: #f1f2f3 no-repeat center / cover;
}
</style>