Vue3中vue文件和composables的分工

🎯 核心原则

Vue 文件只做「展示和交互」,Composables 做「逻辑和数据」

📋 明确分类清单

✅ 应该写在 Vue 文件中的内容

1. UI 相关的状态
vue 复制代码
<script setup>
// ✅ UI 交互状态
const isModalOpen = ref(false)        // 弹窗开关
const isDropdownVisible = ref(false)  // 下拉菜单
const activeTab = ref(0)              // 当前激活的 tab
const isHovering = ref(false)         // 鼠标悬停状态
const selectedRowId = ref(null)       // 当前选中的行

// ✅ 表单输入绑定(UI 层)
const formData = reactive({
  name: '',
  email: '',
  password: ''
})

// ✅ 动画/过渡相关
const isAnimating = ref(false)
const transitionName = ref('fade')
</script>
2. 纯 UI 逻辑(不涉及数据获取)
vue 复制代码
<script setup>
// ✅ 切换 UI 元素
const toggleModal = () => {
  isModalOpen.value = !isModalOpen.value
}

// ✅ 表单验证(同步、前端验证)
const validateForm = () => {
  if (!formData.name) {
    errorMessage.value = '请输入姓名'
    return false
  }
  if (formData.password.length < 6) {
    errorMessage.value = '密码至少6位'
    return false
  }
  return true
}

// ✅ 格式化显示(纯展示用)
const formatDate = (date) => {
  return new Date(date).toLocaleDateString('zh-CN')
}

// ✅ UI 交互逻辑
const handleScroll = (e) => {
  const scrollTop = e.target.scrollTop
  isNavbarTransparent.value = scrollTop < 100
}

// ✅ 本地计算属性(基于 UI 状态)
const isFormValid = computed(() => {
  return formData.name && formData.password.length >= 6
})

const buttonText = computed(() => {
  return isLoading.value ? '提交中...' : '提交'
})
</script>
3. 组件生命周期钩子(仅调用逻辑)
vue 复制代码
<script setup>
import { useUser } from '@/composables/useUser'
import { useAnalytics } from '@/composables/useAnalytics'

const { fetchUser, user } = useUser()
const { trackPageView } = useAnalytics()

// ✅ 组件挂载时调用 composable 的方法
onMounted(() => {
  fetchUser()        // 调用 composable 的逻辑
  trackPageView()    // 调用 composable 的逻辑
})

// ✅ 监听路由变化
watch(() => route.params.id, (newId) => {
  fetchUser(newId)   // 调用 composable 的逻辑
})
</script>
4. 模板 ref(DOM 引用)
vue 复制代码
<script setup>
// ✅ 引用 DOM 元素
const inputRef = ref(null)
const modalRef = ref(null)
const chartRef = ref(null)

// ✅ 直接的 DOM 操作(简单的)
const focusInput = () => {
  inputRef.value?.focus()
}

const scrollToTop = () => {
  window.scrollTo({ top: 0, behavior: 'smooth' })
}
</script>

<template>
  <input ref="inputRef" />
  <div ref="modalRef">弹窗内容</div>
  <canvas ref="chartRef"></canvas>
</template>

✅ 应该写在 Composables 中的内容

1. 所有 HTTP 请求和 API 调用
javascript 复制代码
// composables/useUsers.js
export function useUsers() {
  const users = ref([])
  const loading = ref(false)
  
  // ✅ HTTP 请求逻辑
  const fetchUsers = async () => {
    loading.value = true
    try {
      const { data } = await axios.get('/api/users')
      users.value = data
    } finally {
      loading.value = false
    }
  }
  
  const createUser = async (userData) => {
    const { data } = await axios.post('/api/users', userData)
    users.value.unshift(data)
    return data
  }
  
  return { users, loading, fetchUsers, createUser }
}
2. 全局/跨组件状态
javascript 复制代码
// composables/useAuth.js
export function useAuth() {
  // ✅ 全局认证状态
  const user = ref(null)
  const isAuthenticated = ref(false)
  const token = ref(localStorage.getItem('token'))
  
  // ✅ 登录/登出逻辑
  const login = async (credentials) => {
    const { data } = await authApi.login(credentials)
    token.value = data.token
    user.value = data.user
    isAuthenticated.value = true
    localStorage.setItem('token', data.token)
  }
  
  const logout = () => {
    token.value = null
    user.value = null
    isAuthenticated.value = false
    localStorage.removeItem('token')
  }
  
  return { user, isAuthenticated, login, logout }
}
3. 复杂的业务逻辑
javascript 复制代码
// composables/useShoppingCart.js
export function useShoppingCart() {
  const cart = ref([])
  
  // ✅ 计算总价(业务逻辑)
  const totalPrice = computed(() => {
    return cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  })
  
  // ✅ 库存检查
  const checkStock = async (productId, quantity) => {
    const { data } = await inventoryApi.checkStock(productId)
    return data.available >= quantity
  }
  
  // ✅ 添加购物车(含业务规则)
  const addToCart = async (product, quantity = 1) => {
    // 检查库存
    const hasStock = await checkStock(product.id, quantity)
    if (!hasStock) {
      throw new Error('库存不足')
    }
    
    // 检查是否已存在
    const existingItem = cart.value.find(item => item.id === product.id)
    if (existingItem) {
      existingItem.quantity += quantity
    } else {
      cart.value.push({ ...product, quantity })
    }
    
    // 保存到本地存储
    saveCartToLocal()
  }
  
  return { cart, totalPrice, addToCart }
}
4. 数据持久化和缓存
javascript 复制代码
// composables/useLocalStorage.js
export function useLocalStorage(key, defaultValue) {
  const data = ref(defaultValue)
  
  // ✅ 读取本地存储
  const loadData = () => {
    const stored = localStorage.getItem(key)
    if (stored) {
      data.value = JSON.parse(stored)
    }
  }
  
  // ✅ 保存到本地存储
  const saveData = () => {
    localStorage.setItem(key, JSON.stringify(data.value))
  }
  
  // ✅ 监听变化自动保存
  watch(data, saveData, { deep: true })
  
  loadData()
  return { data, saveData, loadData }
}
5. 副作用和订阅(WebSocket、定时器等)
javascript 复制代码
// composables/useWebSocket.js
export function useWebSocket(url) {
  const messages = ref([])
  const isConnected = ref(false)
  let ws = null
  
  // ✅ WebSocket 连接逻辑
  const connect = () => {
    ws = new WebSocket(url)
    ws.onopen = () => { isConnected.value = true }
    ws.onmessage = (event) => { messages.value.push(JSON.parse(event.data)) }
  }
  
  const send = (data) => {
    ws?.send(JSON.stringify(data))
  }
  
  // ✅ 清理逻辑
  const disconnect = () => {
    ws?.close()
    isConnected.value = false
  }
  
  return { messages, isConnected, connect, send, disconnect }
}
6. 可复用的辅助函数
javascript 复制代码
// composables/useDebounce.js
export function useDebounce(fn, delay = 300) {
  let timer = null
  
  // ✅ 防抖逻辑
  const debouncedFn = (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn(...args), delay)
  }
  
  return debouncedFn
}

// composables/usePagination.js
export function usePagination(itemsPerPage = 10) {
  const currentPage = ref(1)
  const totalItems = ref(0)
  
  // ✅ 分页计算逻辑
  const totalPages = computed(() => Math.ceil(totalItems.value / itemsPerPage))
  const startIndex = computed(() => (currentPage.value - 1) * itemsPerPage)
  const endIndex = computed(() => startIndex.value + itemsPerPage)
  
  const nextPage = () => {
    if (currentPage.value < totalPages.value) currentPage.value++
  }
  
  const prevPage = () => {
    if (currentPage.value > 1) currentPage.value--
  }
  
  return { currentPage, totalPages, startIndex, endIndex, nextPage, prevPage }
}

🎨 实战对比示例

完整组件示例(展示正确的分工)

vue 复制代码
<!-- UserProfile.vue - 只负责展示和交互 -->
<template>
  <div class="user-profile">
    <!-- UI 状态控制显示 -->
    <div v-if="loading" class="loading-spinner">
      加载中...
    </div>
    
    <div v-else-if="error" class="error-message">
      错误: {{ error }}
      <button @click="retry">重试</button>
    </div>
    
    <div v-else class="user-info">
      <!-- 展示数据 -->
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
      
      <!-- UI 交互元素 -->
      <button @click="toggleEditMode">
        {{ isEditMode ? '取消' : '编辑' }}
      </button>
      
      <!-- UI 状态控制的表单 -->
      <form v-if="isEditMode" @submit.prevent="handleUpdate">
        <input v-model="editForm.name" />
        <input v-model="editForm.email" />
        <button type="submit">保存</button>
      </form>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useUser } from '@/composables/useUser'
import { useNotification } from '@/composables/useNotification'

// ✅ 从 composable 获取数据和业务逻辑
const { user, loading, error, fetchUser, updateUser } = useUser()
const { showSuccess, showError } = useNotification()

// ✅ UI 状态(放在组件中)
const isEditMode = ref(false)
const editForm = reactive({
  name: '',
  email: ''
})

// ✅ UI 逻辑(放在组件中)
const toggleEditMode = () => {
  isEditMode.value = !isEditMode.value
  if (isEditMode.value) {
    // 复制数据到编辑表单
    editForm.name = user.value.name
    editForm.email = user.value.email
  }
}

// ✅ 调用 composable 的业务逻辑
const handleUpdate = async () => {
  try {
    await updateUser(editForm)
    showSuccess('更新成功')
    isEditMode.value = false
  } catch (err) {
    showError('更新失败')
  }
}

const retry = () => {
  fetchUser()
}

// ✅ 生命周期钩子(调用逻辑)
onMounted(() => {
  fetchUser()
})
</script>

对应的 Composable

javascript 复制代码
// composables/useUser.js - 包含所有业务逻辑
import { ref } from 'vue'
import axios from 'axios'

export function useUser() {
  // ✅ 数据状态
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  // ✅ HTTP 请求逻辑
  const fetchUser = async (userId) => {
    loading.value = true
    error.value = null
    
    try {
      const { data } = await axios.get(`/api/users/${userId || 'me'}`)
      user.value = data
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  // ✅ 业务逻辑
  const updateUser = async (userData) => {
    loading.value = true
    
    try {
      const { data } = await axios.put(`/api/users/${user.value.id}`, userData)
      user.value = data
      return data
    } finally {
      loading.value = false
    }
  }
  
  // ✅ 其他业务逻辑
  const changePassword = async (oldPassword, newPassword) => {
    // 密码修改逻辑
  }
  
  const uploadAvatar = async (file) => {
    // 头像上传逻辑
  }
  
  return {
    user,
    loading,
    error,
    fetchUser,
    updateUser,
    changePassword,
    uploadAvatar
  }
}

📝 快速判断清单(查表用)

内容类型 放 Vue 文件 放 Composables
弹窗开关状态
HTTP 请求
表单输入绑定
全局用户状态
本地计算属性(UI相关)
跨组件共享的计算属性
点击事件处理(简单)
复杂的业务规则
路由参数监听
WebSocket 连接
本地存储操作
表格选中行 ID
购物车总价计算
防抖/节流函数
定时器
表单验证(简单)
表单验证(复杂,需调接口)
动画状态

🎯 最终原则

  1. Vue 文件是「视图层」:只关心"显示什么"和"用户点了什么"
  2. Composables 是「逻辑层」:关心"数据从哪里来"和"业务怎么处理"
  3. 能复用的放 Composables:如果逻辑可能在多个组件用到,直接放 composable
  4. 和 UI 强相关的放组件:弹窗开关、tab 切换、hover 状态等

这样组织代码,Vue 组件会非常清爽,只专注于 UI 展示和交互,而所有复杂的数据处理、API 调用、业务逻辑都在 composables 中,易于测试和复用。

相关推荐
袋鼠云数栈UED团队2 小时前
基于 superpowers 实现复杂前端改造
前端
袋鼠云数栈前端2 小时前
基于 superpowers 实现复杂前端改造
前端·ai
sugar__salt2 小时前
LLM服务HTTP接口实战:前端HTTP请求全解与项目落地
前端·网络协议·http
十正2 小时前
Claude code源码精读之蜂群模式
javascript·人工智能·agent·claude code
薛先生_0992 小时前
vue-编程式跳转-基本跳转
前端·javascript·vue.js
微三云、小叶2 小时前
排队免单系统底层设计:四种分配算法拆解,无预支资金的合规营销架构方案
java·前端·软件开发·商业模式·本地生活·商业思维
copyer_xyf2 小时前
Python 内存分析:从栈和堆理解对象引用
前端·后端·python
whatever who cares2 小时前
大型 React 项目的文件结构
前端·react.js·前端框架
AI_零食2 小时前
健身室器材管理系统鸿蒙PC Electron框架编写深度解析
前端·javascript·学习·华为·electron·前端框架·鸿蒙