刚入行那会儿,我接手过一个Vue 2.x的老项目。第一次打开components文件夹时,我的表情大概是这样的:😱。一个3000多行的.vue文件里,template部分混着样式,methods里塞着各种业务逻辑,data里堆满了各种临时状态。最可怕的是,这个组件还被5个页面引用,每次修改都像是在拆炸弹。
这就是典型的"屎山"代码(我们私下都这么叫)。它的特征很明显:
Vue的单文件组件(SFC)就像瑞士军刀,它提供了天然的代码组织方案:
vue复制<template>
<!-- 视图层 -->
</template>
<script>
// 逻辑层
</script>
<style>
/* 样式层 */
</style>
但工具再好,不会用也是白搭。我见过不少开发者虽然用了SFC,但写出来的代码依然是"意大利面条式"的。关键是要掌握正确的拆分姿势。
一个组件应该只做一件事,并且做好。我常用这个判断标准:能不能用一句话清楚描述这个组件的功能?如果不能,就该拆了。
反面教材:
vue复制<!-- 这个组件既处理用户信息展示,又做表单验证,还负责API调用 -->
<UserDashboard />
正确姿势:
vue复制<UserAvatar />
<UserProfile />
<SecuritySettings />
我习惯把组件分为三类:
mermaid复制graph TD
A[Page] --> B[Container]
B --> C[Business]
C --> D[Presentational]
Vue的组件系统更适合组合模式。比如我们要做一个带搜索功能的表格:
vue复制<template>
<div>
<SearchBar @search="handleSearch" />
<DataTable :data="filteredData" />
</div>
</template>
而不是:
vue复制<template>
<div>
<!-- 把搜索和表格写在一个组件里 -->
</div>
</template>
对于复杂逻辑,我推荐这样组织:
vue复制<script>
import useUser from './composables/useUser'
import usePermissions from './composables/usePermissions'
export default {
setup() {
const { user, fetchUser } = useUser()
const { canEdit } = usePermissions()
return {
user,
canEdit,
fetchUser
}
}
}
</script>
避免全局样式污染:
vue复制<style scoped>
/* 这些样式只作用于当前组件 */
.button {
background: var(--primary);
}
</style>
对于可复用的样式,我更推荐:
vue复制<style module>
/* 可以通过$style对象访问 */
.success { color: green; }
</style>
使用这些技巧保持template清爽:
vue复制<template>
<!-- 好 -->
<UserCard :user="activeUser" />
<!-- 不好 -->
<div v-if="user" class="card">
<img :src="user.avatar"/>
<div>{{ user.name }}</div>
<!-- 更多用户信息... -->
</div>
</template>
对于老项目,我推荐"外科手术式"重构:
这些模式特别有用:
js复制// 策略模式示例
const strategies = {
admin: () => <AdminPanel />,
user: () => <UserDashboard />,
guest: () => <GuestWelcome />
}
const Component = strategies[user.role]()
组件拆分后要注意:
vue复制<template>
<component :is="dynamicComponent" v-memo="[dynamicComponent]" />
</template>
json复制{
"vetur.validation.template": true,
"eslint.validate": ["vue"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
组件不是越小越好。我见过有人把按钮都拆成三个组件(Icon、Text、Wrapper),这就过头了。判断标准是:
组件拆分后容易遇到状态同步问题。我的经验是:
js复制// 不好的做法:直接修改父组件状态
this.$parent.userData = newData
// 好的做法:通过事件通信
this.$emit('update', newData)
没有测试的重构等于蒙着眼睛拆炸弹。我的测试方案:
js复制// 组件测试示例
test('should emit search event', async () => {
const wrapper = mount(SearchBar)
await wrapper.find('input').setValue('vue')
expect(wrapper.emitted('search')[0]).toEqual(['vue'])
})
去年我主导重构了一个电商后台系统,数据很能说明问题:
| 指标 | 重构前 | 重构后 |
|---|---|---|
| 平均组件行数 | 1200 | 240 |
| 重复代码率 | 35% | 8% |
| 构建时间 | 45s | 22s |
| Bug率 | 23% | 6% |
关键重构步骤:
bash复制# 项目结构对比
# 重构前
src/
components/
MegaComponent.vue # 2800行
# 重构后
src/
components/
ui/
Button.vue
Table.vue
features/
Product/
List.vue
Form.vue
Order/
List.vue
Detail.vue
composables/
useProduct.js
useOrder.js
在我的团队,CR时重点关注:
每个组件必须包含:
vue复制<docs>
### 用途
用于展示用户基本信息卡片
### 最佳实践
```vue
<UserCard
:user="userData"
size="medium"
/>
我们每月有"代码花园日":
如果你刚接触Vue,记住这些原则:
我刚开始学Vue时做的傻事:
vue复制<!-- 把整个页面写在一个组件里 -->
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
<!-- 现在会这样做 -->
<template>
<div>
<AppHeader />
<RouterView />
<AppFooter />
</div>
</template>
好的组件像乐高积木。我设计组件API的原则:
vue复制<template>
<Pagination
:total="100"
:current="3"
:page-size="10"
@change="handlePageChange"
/>
</template>
一些立竿见影的技巧:
js复制const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
我用Vitepress自动生成文档:
markdown复制## Button
### Props
| 名称 | 类型 | 默认值 |
|------|------|--------|
| type | string | 'default' |
### 示例
```vue
<Button type="primary">提交</Button>
这些helper函数我几乎每个项目都用:
js复制// 组件props验证
export const dimensionProps = {
width: {
type: [Number, String],
default: null
},
height: {
type: [Number, String],
default: null
}
}
// 样式工具
export const useSpacing = (size) => {
return computed(() => ({
padding: `${size * 8}px`,
margin: `${size * 4}px`
}))
}
除了props/emit,这些方式也很好用:
js复制// eventBus.js
export default mitt()
// 组件A
eventBus.emit('search', query)
// 组件B
eventBus.on('search', handleSearch)
js复制const { state, send } = useMachine({
initial: 'idle',
states: {
idle: {
on: { FETCH: 'loading' }
},
loading: {
// ...
}
}
})
js复制// useCart.js
export default () => {
const items = ref([])
const addItem = (item) => { /* ... */ }
return { items, addItem }
}
// 多个组件共享同一状态
const { items } = useCart()
我习惯这样写组件:
js复制test('Button should emit click event', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
用这个配置捕获UI变化:
js复制import { mount } from '@vue/test-utils'
import { toMatchImageSnapshot } from 'jest-image-snapshot'
expect.extend({ toMatchImageSnapshot })
test('Button snapshot', async () => {
const wrapper = mount(Button)
expect(await wrapper.html()).toMatchImageSnapshot()
})
如果要开发自己的组件库,记住:
我的组件库目录结构:
code复制lib/
components/
Button/
index.ts
Button.vue
Button.spec.ts
README.md
styles/
variables.scss
mixins.scss
utils/
helpers.ts
types.ts
五年前我第一次看到整洁的组件代码时,感觉像发现了新大陆。现在回头看自己早期的代码,简直不忍直视。但这就是成长的过程。
几个让我顿悟的时刻:
记住,好的组件设计就像乐高积木 - 每个零件简单可靠,组合起来却能创造无限可能。