一篇文章彻底搞懂前端架构层面分层设计

模块/组件分层设计

上层模块依赖下层模块,下层模块不反向依赖上层模块。

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>

✅ 组件不定义 keywordloading 等状态,只消费 Composable 提供的响应式数据。

数据访问层

API层/数据请求层(Axios/fetch的封装):与后端接口通信,只负责怎么发请求,怎么响应。

相关推荐
王六岁4 分钟前
🐍 前端开发 0 基础学 Python 入门指南:数字与字符串篇
前端·python·全栈
tiantian_cool23 分钟前
HarmonyOS 开发环境配置指南 - macOS 版
前端
写不来代码的草莓熊42 分钟前
vue前端面试题——记录一次面试当中遇到的题(10)
前端·vue.js·面试
tiantian_cool1 小时前
正确的 .gitignore 配置
前端·github
三小河1 小时前
封装 classNames:让 Tailwindcss 类名处理更优雅
前端·javascript
起这个名字1 小时前
ESLint 导入语句的分组排序
前端·javascript
踩着两条虫1 小时前
VTJ.PRO低代码快速入门指南
前端·低代码
Lazy_zheng1 小时前
一场“数据海啸”,让我重新认识了 requestAnimationFrame
前端·javascript·vue.js
crary,记忆1 小时前
MFE: React + Angular 混合demo
前端·javascript·学习·react.js·angular·angular.js
Asort1 小时前
JavaScript设计模式(十七)——中介者模式 (Mediator):解耦复杂交互的艺术与实践
前端·javascript·设计模式