单文件组件的核心结构:三部分如何协同工作
单文件组件(SFC)的本质是把组件的模板、逻辑、样式封装在一个.vue
文件里,这种结构让组件的职责更清晰。我们先看一个最基础的SFC结构:
<template>
<div class="user-card">
<img :src="user.avatar" alt="avatar" />
<div class="user-info">
<h3>{{ user.name }}</h3>
<p>{{ user.bio }}</p>
<button @click="handleFollow">关注</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 响应式数据
const user = ref({
name: 'Alice',
avatar: 'https://via.placeholder.com/100',
bio: '前端开发者'
})
// 事件处理函数
const handleFollow = () => {
alert(`关注了${user.value.name}`)
}
</script>
<style scoped>
.user-card {
display: flex;
gap: 16px;
padding: 16px;
border: 1px solid #eee;
border-radius: 8px;
}
.user-info h3 {
margin: 0 0 8px;
font-size: 18px;
}
.user-info p {
margin: 0 0 12px;
color: #666;
}
button {
padding: 4px 12px;
background: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
这三部分的分工很明确:
– 模板(<template>
):负责组件的结构和交互逻辑(比如@click
事件),支持Vue的模板语法(如插值、指令、slot);
– 脚本(<script setup>
):负责组件的逻辑(数据、方法、生命周期),setup
语法糖是Vue 3的推荐写法,能简化代码(比如不用写export default
);
– 样式(<style scoped>
):负责组件的外观,scoped
属性会让样式只作用于当前组件,避免污染其他组件。

需要注意的是,script setup
里的变量和函数会自动暴露给模板,不用再像Vue 2那样写data
或methods
——这是很多新手容易混淆的点。
样式隔离的正确姿势:避免组件样式污染
你有没有遇到过这样的情况:给组件写了样式,结果意外修改了其他组件的样式?这就是样式污染,而scoped
属性是解决这个问题的关键。
scoped
的工作原理
当你给<style>
加scoped
时,Vue会自动给组件的根元素添加一个唯一的data-v-xxxx
属性,然后把样式选择器编译成带这个属性的形式。比如上面的.user-card
会被编译成:
.user-card[data-v-123456] {
display: flex;
gap: 16px;
/* ... */
}
这样样式就只会作用于当前组件的元素,不会影响其他组件。
什么时候需要“穿透”scoped?
但有时候你需要修改第三方组件的样式(比如Element Plus的el-button
),这时候scoped
会阻止样式生效——因为第三方组件的元素没有当前组件的data-v
属性。这时候需要用样式穿透:
– Vue 3用::v-deep
;
– Vue 2用/deep/
或>>>
。
比如修改Element Plus按钮的背景色:
<style scoped>
/* 穿透scoped,修改ElButton的样式 */
::v-deep .el-button--primary {
background-color: #ff6600;
border-color: #ff6600;
}
</style>
注意:不要过度使用穿透——能不用就不用,否则会破坏样式隔离的初衷。
逻辑复用的秘密:从mixins到Composition API
在Vue 2中,我们常用mixins
来复用逻辑,但它有两个致命问题:
1. 命名冲突:如果多个mixins有相同的变量名,会覆盖彼此的值;
2. 逻辑不透明:组件使用mixins后,你不知道变量或方法来自哪个mixin,维护起来很麻烦。
Vue 3的Composition API(配合script setup
)完美解决了这些问题——通过组合式函数(Composable Functions)来复用逻辑。
什么是组合式函数?
组合式函数是用setup
语法写的可复用函数,通常以use
开头(比如useFetch
、useCounter
)。举个例子,我们写一个获取数据的useFetch
:
// src/composables/useFetch.js
import { ref, onMounted } from 'vue'
export function useFetch(url) {
const data = ref(null) // 存储请求结果
const loading = ref(true) // 加载状态
const error = ref(null) // 错误信息
// 挂载后发起请求
onMounted(async () => {
try {
const response = await fetch(url)
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
})
// 返回需要暴露的变量
return { data, loading, error }
}
然后在SFC中使用这个函数:
<template>
<div class="article-list">
<div v-if="loading">加载中...</div>
<div v-else-if="error">请求失败:{{ error.message }}</div>
<ul v-else>
<li v-for="article in data" :key="article.id">
{{ article.title }}
</li>
</ul>
</div>
</template>
<script setup>
import { useFetch } from '@/composables/useFetch'
// 复用useFetch逻辑
const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts')
</script>
这样做的好处很明显:
– 逻辑透明:你能清楚看到data
来自useFetch
;
– 无命名冲突:每个组件可以独立使用useFetch
,变量不会互相干扰;
– 类型友好:配合TypeScript能自动推导类型,减少错误。
mixins vs Composition API的对比
特性 | mixins | Composition API |
---|---|---|
命名冲突 | 容易出现 | 完全避免 |
逻辑可读性 | 差(不知道来源) | 好(明确来自组合式函数) |
类型支持 | 弱 | 强(TypeScript友好) |
复用灵活性 | 低(只能全局或局部引入) | 高(可传参定制逻辑) |
实战技巧:提升SFC开发效率的5个小秘诀
掌握了基础和逻辑复用,我们再讲几个能直接提升开发效率的技巧——这些都是我在实际项目中常用的。
1. 用defineProps
/defineEmits
简化Props和事件
在script setup
中,不用再写props
或emits
选项,直接用defineProps
和defineEmits
:
<template>
<button @click="handleClick">{{ label }}</button>
</template>
<script setup>
// 定义Props(类型校验)
const props = defineProps({
label: {
type: String,
required: true
}
})
// 定义Emits(事件名称)
const emit = defineEmits(['click'])
// 事件处理函数
const handleClick = () => {
emit('click', '按钮被点击了')
}
</script>
这样写比Vue 2的props
/emits
选项简洁很多,而且支持TypeScript类型校验(比如用defineProps<{ label: string }>
)。
2. 用Teleport
处理模态框
模态框(Modal)是常见的组件,但如果把它写在组件内部,会被父组件的overflow: hidden
影响(比如无法全屏)。这时候可以用Teleport
把模态框“传送”到body
标签下:
<template>
<button @click="showModal = true">打开模态框</button>
<Teleport to="body">
<div v-if="showModal" class="modal-backdrop">
<div class="modal-content">
<h3>这是模态框</h3>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
<style scoped>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
padding: 24px;
background: white;
border-radius: 8px;
}
</style>
Teleport
的to
属性可以指定目标元素(比如body
或.app
),这样模态框就不会被父组件的样式影响了。
3. 用Suspense
处理异步组件
当组件需要加载异步数据(比如从服务器获取列表)时,Suspense
能帮你处理加载状态和错误状态:
<template>
<Suspense>
<!-- 异步组件 -->
<template #default>
<AsyncArticleList />
</template>
<!-- 加载中状态 -->
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
</template>
<script setup>
// 动态导入异步组件(Webpack会自动拆分代码)
const AsyncArticleList = defineAsyncComponent(() => import('./AsyncArticleList.vue'))
</script>
AsyncArticleList.vue
里可以用useFetch
获取数据,Suspense
会自动显示fallback
内容直到数据加载完成。
常见坑点避坑:解决你遇到的SFC痛点
最后我们来解决几个新手常遇到的问题,帮你少走弯路。
坑点1:setup
里无法访问this
script setup
里没有this
——因为setup
是在组件实例创建前执行的(对应Vue 2的beforeCreate
和created
生命周期)。如果你需要响应式数据,直接用ref
或reactive
:
<script setup>
// 错误:setup里没有this
// const msg = this.msg
// 正确:用ref定义响应式数据
const msg = ref('Hello Vue!')
</script>
坑点2:scoped
样式不生效
如果scoped
样式不生效,先检查这几点:
– 是不是用了v-html
?v-html
插入的元素不会被scoped
样式影响(因为没有data-v
属性);
– 是不是修改了第三方组件的样式?需要用::v-deep
穿透;
– 是不是样式选择器写错了?比如类名拼错或层级不对。
坑点3:组合式函数的变量没响应式
如果组合式函数返回的变量没有响应式,说明你没用到ref
或reactive
:
// 错误:没有用ref,数据变化不会更新视图
export function useCounter() {
let count = 0 // 非响应式
const increment = () => count++
return { count, increment }
}
// 正确:用ref,数据变化会更新视图
export function useCounter() {
const count = ref(0) // 响应式
const increment = () => count.value++
return { count, increment }
}
记住:setup
里的响应式数据必须用ref
(基本类型)或reactive
(对象/数组)。
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/197