Vue 3 组件重构实战:登录大组件的拆分与优化
在现代前端开发中,随着业务逻辑的增加,组件往往会变得臃肿不堪,难以维护。本文将以 UserAuth 登录组件为例,分享一次从"巨石组件"到"模块化组件"的重构过程。
1. 重构背景
在项目的初期开发阶段,为了快速实现功能,我将登录、注册、找回密码等逻辑全部写在一个 .vue 文件中。随着功能的完善,UserAuth.vue 逐渐暴露出了以下问题:
- 代码行数过长:单个文件包含数百行代码,阅读困难。
- 逻辑耦合严重:登录、注册、验证码获取等逻辑混杂在一起,修改一处容易影响其他功能。
- 状态管理混乱 :大量的
ref和reactive变量堆积,难以区分属于哪个表单。
为了解决这些问题,我决定对 UserAuth 组件进行拆分重构。
2. 重构策略
我们的核心策略是 "关注点分离" (Separation of Concerns):
- 父组件 (Container):只负责布局、背景样式以及子组件的切换调度。
- 子组件 (Presentational/Logical):负责具体的表单渲染、表单验证以及与后端的 API 交互。
- 类型定义 (Types):抽离公共类型,确保类型安全。
- 表单校验 (Validation):抽离校验规则,实现复用与动态校验。
2.1 目录结构变化
重构后的目录结构更加清晰:
text
src/
├── views/
│ └── UserAuth.vue # 父组件:容器与调度
├── components/
│ └── Auth/
│ ├── LoginForm.vue # 子组件:登录表单
│ ├── RegisterForm.vue # 子组件:注册表单
│ └── ForgotForm.vue # 子组件:找回密码表单
├── types/
│ └── Auth/
│ └── index.ts # 类型定义
└── utils/
└── rules/
├── auth.ts # 认证相关校验规则聚合
└── base.ts # 基础校验规则(邮箱、密码强度等)
3. 核心代码实现
3.1 父组件:UserAuth.vue
父组件主要充当"容器"的角色。它维护了一个 currentView 状态,用于控制显示哪个子组件。
vue
<template>
<div class="auth-wrapper">
<div class="auth-container">
<!-- 顶部标题区 -->
<div class="auth-header">
<h1 class="auth-title">Daystream Music</h1>
<p class="auth-subtitle">欢迎回来</p>
</div>
<!-- Tab 切换区 -->
<div class="auth-tabs">
<ElButton
class="auth-tab"
:class="{ active: currentView === 'login' }"
@click="currentView = 'login'"
text
>
登录
</ElButton>
<ElButton
class="auth-tab"
:class="{ active: currentView === 'register' }"
@click="currentView = 'register'"
text
>
注册
</ElButton>
</div>
<!-- 内容区:动态渲染子组件 -->
<div class="auth-content">
<!-- 使用 v-model 传递 currentView,允许子组件内部切换视图 -->
<LoginForm v-model="currentView" />
<RegisterForm v-model="currentView" />
<ForgotForm v-model="currentView" />
</div>
<!-- 底部社交登录等 -->
<div class="auth-divider">
<span class="divider-text">或使用其他方式登录</span>
</div>
<!-- ... -->
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import LoginForm from '@/components/Auth/LoginForm.vue'
import RegisterForm from '@/components/Auth/RegisterForm.vue'
import ForgotForm from '@/components/Auth/ForgotForm.vue'
// 定义当前视图状态
const currentView = ref<'login' | 'register' | 'forgot'>('login')
</script>
3.2 子组件:LoginForm.vue
子组件将业务逻辑完全内聚。以登录表单为例,它包含了表单验证、验证码获取、登录接口调用等逻辑。
值得注意的是,我使用了 Vue 3.4+ 的 defineModel 宏,极大简化了父子组件间的双向绑定。
vue
<template>
<ElForm
v-if="currentView === 'login'"
ref="loginRef"
:model="loginForm"
:rules="loginRules"
class="auth-form"
label-position="top"
>
<ElFormItem label="邮箱" prop="email">
<ElInput v-model="loginForm.email" type="email" class="form-input" placeholder="请输入邮箱" />
</ElFormItem>
<ElFormItem label="密码" prop="password">
<ElInput
v-model="loginForm.password"
type="password"
class="form-input"
placeholder="请输入密码"
show-password
/>
</ElFormItem>
<!-- 验证码区域 -->
<ElFormItem label="验证码" prop="captcha">
<div class="form-input-group captcha-group">
<ElInput v-model="loginForm.captcha" class="form-input" placeholder="请输入验证码" />
<div class="captcha-wrapper" @click="changeCaptcha()">
<div class="captcha-box">
<img :src="loginCaptchaUrl" alt="验证码" />
</div>
<span class="change-captcha-text">看不清换一张</span>
</div>
</div>
</ElFormItem>
<div class="form-actions">
<ElButton type="text" class="forgot-password" @click="currentView = 'forgot'">
忘记密码?
</ElButton>
</div>
<div class="btn-group">
<ElButton class="btn-primary" @click="handleLogin(loginRef)"> 登 录 </ElButton>
<ElButton class="btn-secondary" @click="currentView = 'register'"> 立即注册 </ElButton>
</div>
</ElForm>
</template>
<script lang="ts" setup>
import { reactive, ref, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import type { LoginForm } from '@/types/Auth'
import { useAuthRules } from '@/utils/rules/auth'
import type { FormInstance } from 'element-plus'
import { useRouter } from 'vue-router'
import { getCaptcha } from '@/api/auth/Login'
const currentView = defineModel<string>()
const loginRef = ref<FormInstance>()
const loginForm = reactive<LoginForm & { captcha: string }>({
email: '',
password: '',
captcha: '',
})
// 引入校验规则
const { loginRules } = useAuthRules(loginForm)
const router = useRouter()
const userStore = useUserStore()
const loginCaptchaUrl = ref('')
const captchaId = ref('')
// 登录逻辑
const handleLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
const LoginData = reactive({
...loginForm,
captchaUUId: captchaId,
})
userStore
.login(LoginData)
.then(() => {
formEl.resetFields()
ElMessage.success('登录成功')
router.push('/')
})
.catch((error) => {
console.log(error)
changeCaptcha() // 登录失败刷新验证码
})
}
})
}
// 获取验证码逻辑
const changeCaptcha = () => {
// ... 获取验证码的具体实现
}
onMounted(() => {
changeCaptcha()
})
</script>
3.3 类型定义:types/Auth/index.ts
将表单接口抽离到单独的类型文件中,可以在组件、Store 和 API 层之间共享,避免重复定义。
typescript
export interface LoginForm {
email: string
password: string
}
export interface RegisterForm {
username: string
email: string
password: string
newPassword: string // 确认密码
captcha: string
}
// ...
3.4 表单校验策略
为了避免在每个组件中重复编写校验规则,将校验逻辑抽离到了 utils/rules 目录下。
基础规则 (base.ts)
首先定义通用的原子规则,如邮箱格式、密码强度等:
typescript
// src/utils/rules/base.ts
import type { FormItemRule } from 'element-plus'
// 通用:邮箱格式
export const emailRule: FormItemRule[] = [
{ required: true, message: '请填写对应邮箱', trigger: 'blur' },
{ type: 'email', message: '邮箱格式不正确', trigger: ['blur', 'change'] },
]
// 通用:密码强度
export const passwordStrengthRule: FormItemRule[] = [
{ required: true, message: '请输入密码', trigger: 'blur' },
{
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[a-zA-Z\d!@#$%^&*]{8,20}$/,
message: '密码需8-20位,含大写字母、数字及特殊字符',
trigger: 'blur',
},
]
聚合规则与动态校验 (auth.ts)
然后,我们创建一个 Hook useAuthRules,它接收表单数据作为参数。这可以实现动态校验,例如"确认密码"需要与当前的"密码"字段进行比对。
typescript
// src/utils/rules/auth.ts
import { reactive } from 'vue'
import type { FormRules } from 'element-plus'
import { emailRule, passwordStrengthRule, captchaRule, nicknameRule } from './base'
export function useAuthRules(formData: Record<string, any>, pwdKey = 'password') {
// 动态校验器:检查两次密码是否一致
const validateConfirmPwd = (rule: any, value: string, callback: any) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== formData[pwdKey]) {
// 这里的 formData[pwdKey] 是响应式的,能获取到用户实时输入的密码
callback(new Error('两次输入密码不一致'))
} else {
callback()
}
}
const confirmPwdRule = [
...passwordStrengthRule,
{ validator: validateConfirmPwd, trigger: 'blur' },
]
// 登录规则
const loginRules = reactive<FormRules>({
email: emailRule,
password: passwordStrengthRule,
captcha: captchaRule,
})
// 注册规则
const registerRules = reactive<FormRules>({
email: emailRule,
username: nicknameRule,
captcha: captchaRule,
password: passwordStrengthRule,
newPassword: confirmPwdRule, // 使用组合后的规则
})
return {
loginRules,
registerRules,
// ...
}
}
3.5 注册组件应用 (RegisterForm.vue)
在注册组件中,只需要简单地调用 useAuthRules,即可获得包含复杂逻辑的校验规则:
vue
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import { useAuthRules } from '@/utils/rules/auth'
import type { RegisterForm } from '@/types/Auth'
const registerForm = reactive<RegisterForm>({
username: '',
email: '',
password: '',
newPassword: '',
captcha: '',
})
// 传入 registerForm,使得内部的 validateConfirmPwd 能访问到最新的 password
const { registerRules } = useAuthRules(registerForm)
const insertUser = async (formEl: FormInstance | undefined) => {
if (!formEl) return
try {
await formEl.validate()
// 校验通过,执行注册逻辑
// ...
} catch (error) {
console.log('表单校验未通过', error)
}
}
</script>
4. 重构收益
通过这次拆分,获得了以下收益:
- 代码可读性提升 :
UserAuth.vue从几百行缩减为专注于布局的容器组件,逻辑一目了然。 - 维护成本降低 :修改登录逻辑时,只需关注
LoginForm.vue,不会误触注册或找回密码的代码。 - 校验逻辑复用 :通过
useAuthRules,将校验逻辑从 UI 中剥离,不仅实现了复用,还优雅地解决了"确认密码"这种依赖表单状态的动态校验问题。 - 开发体验优化:配合 TypeScript,每个组件的 Props 和 Emits 定义更加清晰,IDE 提示更加友好。
5. 总结
组件拆分是前端工程化中的重要一环。不要等到组件变得不可维护时才想到重构。在开发初期,保持良好的组件设计思维,遵循单一职责原则(SRP),能让我们的代码库保持长久的生命力。