模块/组件分层设计
上层模块依赖下层模块,下层模块不反向依赖上层模块。
Ui层
组件层(components): 页面展示,用户交互(按钮表单弹窗)
业务逻辑层
逻辑层(Composables/Stores/Servers):处理业务规则, 状态管理,API调用协调。
Composables - Vue3的组合式函数
- 作用:封装可复用的响应式逻辑。
- 场景:管理响应式状态/处理生命周期逻辑/跨组件共享状态/处理副作用(API 事件监听等)
- 通常不依赖全局状态(无法单元测试,避免在组合式函数依赖全局store数据)通过局部或传参驱动
Stores - 全局状态管理
- 作用:管理跨组件共享的状态(如用户登录信息,购物车,全局配置)
- 工具:Vue3的Pinia,Vue2的Vuex
- 通常支持Actions发异步请求,getters 计算器属性
Servers(服务层)- 业务协调与API封装
- 作用:封装业务规则+协调多个API调用,避免组件直接调用API
- 特点:通常为纯函数不依赖UI,易于单元测试,可包含复杂的业务逻辑。(无响应式数据)
为什么需要逻辑层?
无逻辑层的问题 | 逻辑层如何解决 |
---|---|
组件代码臃肿(API+状态+逻辑混在一起) | 逻辑抽离,单一职责,组件只负责渲染 |
相同逻辑重复写(如多个页面都需要搜索用户) | Composable复用 |
状态在各个组件,传值繁琐 | Store集中管理,跨多层组件传值 |
业务规则写在组件里,无法测试 | Service是纯函数,可单元测试 |
修改一个业务组件要改多个文件 | 只需要改Composables或Service |
项目最佳实践
- 组件:调用Composable
- Compsable:调用Service+更新局部状态
- Store:管理全局状态,也可以调用Service
- Service:只处理数据和业务,不碰UI和响应式
分层后的效果
- Service只关心做什么(What)
- Composables/Store关心 怎么做(How)+状态怎么变(State)
- Component只关心怎么展示(View)
案例 - 用户搜索功能
1️⃣Service 层(无响应式)
js
// services/userService.ts
export async function searchUsers(keyword: string): Promise<User[]> {
if (!keyword.trim()) return []
const res = await api.get('/users', { params: { q: keyword } })
return res.data
}
✅ 只负责:调用 API + 返回数据 ,不碰 ref
、不管理状态。
2️ Composable 层(定义响应式变量)
js
// composables/useUserSearch.ts
import { ref, computed } from 'vue'
import { searchUsers } from '@/services/userService'
export function useUserSearch() {
// 👇 响应式变量定义在这里!
const keyword = ref('')
const loading = ref(false)
const results = ref<User[]>([])
const isEmpty = computed(() => results.value.length === 0)
const search = async () => {
loading.value = true
try {
// 调用 service,拿到普通数据
results.value = await searchUsers(keyword.value)
} finally {
loading.value = false
}
}
return {
keyword, // ref<string>
loading, // ref<boolean>
results, // ref<User[]>
isEmpty, // ComputedRef<boolean>
search // function
}
}
✅ 响应式状态(keyword/loading/results)定义在 Composable 中,因为它:
- 与 UI 交互强相关(输入框、加载动画、列表渲染)
- 可能被多个组件复用
- 生命周期通常与组件绑定(组件销毁,状态自动清理)
3️⃣ Store 层(如果是全局状态
js
// stores/userStore.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
// 👇 响应式状态定义在 state 中
state: () => ({
profile: null as User | null,
isLoggedIn: false
}),
actions: {
async fetchProfile() {
// 调用 service
this.profile = await getUserProfileService()
this.isLoggedIn = !!this.profile
}
}
})
✅ 全局共享的响应式状态放在 Store。
4️⃣ 组件层(尽量少定义)
js
<script setup>
import { useUserSearch } from '@/composables/useUserSearch'
// 从 composable 拿到响应式变量,直接在模板中使用
const { keyword, loading, results, search } = useUserSearch()
</script>
<template>
<input v-model="keyword" @input="search" />
<div v-if="loading">搜索中...</div>
<ul v-else>
<li v-for="user in results" :key="user.id">{{ user.name }}</li>
</ul>
</template>
✅ 组件不定义 keyword
、loading
等状态,只消费 Composable 提供的响应式数据。
数据访问层
API层/数据请求层(Axios/fetch的封装):与后端接口通信,只负责怎么发请求,怎么响应。