Vue3 项目编码规范:基于Composable的清晰架构实践

前言

在大型Vue3项目中,随着业务复杂度的提升,组件代码极易变得臃肿且难以维护。数据来源多样、逻辑分散、提交行为不清晰是常见痛点。为解决这些问题,我们制定并推行一套以 "职责分离""函数式架构" 为核心的编码规范,旨在提升代码的可读性、可测试性与团队协作效率。

一、 核心数据命名与分类规范

强制要求按照数据的来源与用途,在组件中声明以下三类数据,禁止混用:

  1. pageStaticData - 静态/配置数据

    • 定义:在组件生命周期内不会改变的初始数据。
    • 举例:字典项、固定的选项列表、页面默认配置、本地图标映射。
    • 特点:通常为常量或从全局配置/枚举导入。
  2. pageStatusData - 状态/响应式数据

    • 定义:驱动视图变化的动态数据,主要来自异步请求。
    • 举例:从后端API获取的列表数据、表单的临时填写状态、弹窗的开关状态。
    • 特点 :必须是响应式的(ref/reactive),其变化直接关联UI更新。
  3. pageSubmitData - 提交数据

    • 定义:专门用于向服务端提交的数据对象。
    • 举例:表单提交的DTO、查询条件的参数集。
    • 特点 :应保持结构纯净,与接口契约严格一致。它是唯一可以被"提交"函数修改的数据。

二、 逻辑与视图分离:Composable架构

我们采用 "一个视图组件对应一个逻辑Composable" 的架构。

  • 文件结构 :为每个Page.vue或复杂组件创建一个同名的usePage.ts(或Page.composable.ts)文件,并放置在同一目录下。或者放在composables目录中按页面模块组织。

    bash 复制代码
    src/
    ├── views/
    │   └── UserManagement/
    │       ├── UserList.vue          # 视图组件
    │       └── useUserList.ts        # 专属逻辑Composable
  • 职责划分

    • .vue文件 :专注于视图。包括模板结构、样式、以及事件/声明的绑定。原则上不包含具体的业务逻辑实现。
    • .ts (Composable)文件 :专注于逻辑。集中管理所有数据、方法、生命周期钩子、以及异步操作。
  • 组件集成 :在Vue组件中,通过setup()<script setup>导入并使用Composable。

    vue 复制代码
    <!-- UserList.vue -->
    <script setup lang="ts">
    import { useUserList } from './useUserList'
    
    const {
      pageStaticData,
      pageStatusData,
      pageSubmitData,
      handleSearch,
      handleReset,
      handleSubmit
    } = useUserList()
    </script>

三、 页面初始化函数:initPage

为保证数据初始化的统一性和可预测性,每个Composable必须包含一个 initPage 函数,用于集中初始化三类核心数据。

initPage 的职责:

  1. 初始化静态数据 (pageStaticData):设置字典、配置项等固定数据
  2. 初始化状态数据 (pageStatusData):设置响应式数据的初始值
  3. 初始化提交数据 (pageSubmitData):设置提交数据结构的默认值
  4. 执行初始数据获取:如页面首次加载时需要的字典数据

实现模式:

typescript 复制代码
// useUserList.ts
import { ref, reactive } from 'vue'
import { getUserList, getDictData } from '@/api'

interface PageStaticData {
  statusOptions: Array<{ label: string; value: string }>
  genderDict: Record<string, string>
}

interface PageStatusData {
  loading: boolean
  tableData: User[]
  total: number
}

interface PageSubmitData {
  page: number
  pageSize: number
  keyword: string
  status?: string
}

export function useUserList() {
  // 1. 声明三类数据
  const pageStaticData = reactive<PageStaticData>({
    statusOptions: [],
    genderDict: {}
  })
  
  const pageStatusData = reactive<PageStatusData>({
    loading: false,
    tableData: [],
    total: 0
  })
  
  const pageSubmitData = reactive<PageSubmitData>({
    page: 1,
    pageSize: 10,
    keyword: '',
    status: ''
  })

  // 2. 初始化函数
  async function initPage() {
    // 步骤1:初始化静态数据(可同步可异步)
    await _initStaticData()
    
    // 步骤2:初始化状态数据
    _initStatusData()
    
    // 步骤3:初始化提交数据
    _initSubmitData()
    
    // 步骤4:执行初始数据加载
    await loadTableData()
  }

  // 私有初始化函数
  async function _initStaticData() {
    // 从接口获取字典数据
    const dictResult = await getDictData(['user_status', 'gender'])
    pageStaticData.statusOptions = dictResult.user_status || []
    pageStaticData.genderDict = dictResult.gender || {}
    
    // 本地静态配置
    // pageStaticData.someConfig = LOCAL_CONFIG
  }

  function _initStatusData() {
    // 设置状态初始值
    pageStatusData.loading = false
    pageStatusData.tableData = []
    pageStatusData.total = 0
  }

  function _initSubmitData() {
    // 设置提交数据初始值
    pageSubmitData.page = 1
    pageSubmitData.pageSize = 10
    pageSubmitData.keyword = ''
    pageSubmitData.status = ''
  }

  // 3. 页面加载时调用initPage
  onMounted(() => {
    initPage()
  })

  // 4. 其他业务函数...
  async function loadTableData() {
    pageStatusData.loading = true
    try {
      const result = await getUserList(pageSubmitData)
      pageStatusData.tableData = result.list
      pageStatusData.total = result.total
    } finally {
      pageStatusData.loading = false
    }
  }

  return {
    pageStaticData,
    pageStatusData,
    pageSubmitData,
    initPage, // 暴露initPage以便外部调用
    loadTableData,
    // ... 其他公共函数
  }
}

initPage 使用注意事项:

  1. 调用时机 :通常在组件的onMounted中调用,或在路由守卫中调用
  2. 可重置性:应支持多次调用,用于重置页面到初始状态
  3. 错误处理:内部应包含适当的错误处理,避免因初始化失败导致页面崩溃
  4. 加载状态:可考虑添加初始化加载状态,供UI显示

四、 函数定义与结构规范

1. 公私分明

  • 公共函数 (Public Functions) :在Composable中return出去,供组件调用的函数。它们通常是事件处理函数或生命周期函数。
  • 私有函数 (Private Functions) :仅限Composable内部使用的函数。必须 以下划线 _ 开头命名(如 _validateForm)。

2. 核心铁律:私有函数纯度原则

所有私有函数必须是"纯函数"或"仅用于获取数据的函数"。

  • 严禁 在私有函数中直接修改 pageSubmitData 或执行副作用(如API调用)。
  • 必须 将处理结果返回给调用它的公共函数,由公共函数决定如何修改状态或执行提交。

3. 标准三步走结构

每个具备一定复杂度的公共函数(及其调用的主要私有函数),其内部逻辑应抽象为三个清晰步骤:

typescript 复制代码
// 公共函数示例
async function handleSubmit() {
  // 【步骤1:校验参数】- 通常是一个私有纯函数
  if (!_validateSubmitData(pageSubmitData)) {
    message.error('表单校验失败')
    return
  }

  // 【步骤2:逻辑处理】- 准备提交数据,处理业务逻辑
  const payload = _buildApiPayload(pageSubmitData, pageStaticData.someMapping)

  // 【步骤3:返回结果】- 执行副作用,处理结果
  await _performApiSubmit(payload)
}

// 对应的私有函数示例
function _validateSubmitData(data: SubmitDTO): boolean {
  // 1. 校验参数:检查数据完整性、格式等
  return !!data.name && data.age > 0
}

function _buildApiPayload(submitData: SubmitDTO, staticMap: Record<string, string>): ApiPayload {
  // 2. 逻辑处理:数据转换、映射、合并等
  return {
    ...submitData,
    typeCode: staticMap[submitData.type]
  }
}

async function _performApiSubmit(payload: ApiPayload): Promise<void> {
  // 3. 返回结果:发起网络请求
  try {
    await api.user.update(payload)
    message.success('提交成功')
    // 可能触发数据重新拉取等后续操作
  } catch (error) {
    message.error('提交失败')
    throw error
  }
}

五、 补充实践建议与特例说明

1. 关于简单页面的特例

关于简单的组件,其ts可直接写在.vue中:

  • 强烈建议 :即使是最简单的页面,也优先考虑 使用Composable,哪怕它只包含一个fetch函数和initPage函数。这能保持项目架构的一致性,并预留未来扩展的空间。
  • 特例范围 :仅限完全静态无任何交互无任何异步数据的纯展示页面。一旦需要添加一个按钮或一个API调用,应立即重构为Composable模式。

2. 类型定义 (TypeScript)

  • pageStaticDatapageStatusDatapageSubmitData分别定义清晰的接口或类型。
  • 所有函数,尤其是私有函数,应严格标注参数和返回值类型。这能极大增强代码的可靠性和开发体验。

3. 代码风格统一

  • 结合团队ESLint和Prettier配置,统一缩进、分号、引号等风格。
  • 在Composable中,将三类数据的声明、initPage函数、公共函数、私有函数进行分组,并用空行分隔,增强可读性。

总结

本规范的核心价值在于 "强制建立一种清晰的代码组织心智模型"

  • 看到变量名,就知道它的来源和用途。
  • 看到.vue文件,就知道它只负责展示。
  • 看到initPage函数,就知道这是页面初始化的入口。
  • 看到_开头的函数,就知道它不会产生副作用。
  • 看到一个复杂函数,就能按"校验-处理-提交"的路径快速理解。

通过实践这套规范,我们可以将Vue3组件的开发,从"面向模板"的编写,提升为"面向架构"的设计,从而有效管理复杂度,构建出更稳健、更易协作的前端应用。

相关推荐
小酒星小杜2 小时前
在AI时代,技术人应该每天都要花两小时来构建一个自身的构建系统 - Build 篇
前端·vue.js·架构
zengyufei2 小时前
2.4 watch 监听变化
vue.js
m0_471199632 小时前
【小程序】订单数据缓存 以及针对海量库存数据的 懒加载+数据分片 的具体实现方式
前端·vue.js·小程序
貂蝉空大3 小时前
vue-pdf-embed分页预览解决文字丢失问题
前端·vue.js·pdf
ss2733 小时前
RuoYi-App 本地启动教程
前端·javascript·vue.js
用户248257824813 小时前
vue3快速入门
vue.js
涵涵(互关)3 小时前
JavaScript 对大整数(超过 2^53 - 1)的精度丢失问题
java·javascript·vue.js
天府之绝4 小时前
uniapp 中使用uview表单验证时,自定义扩展的表单,在改变时无法触发表单验证处理;
开发语言·前端·javascript·vue.js·uni-app
xkxnq4 小时前
第二阶段:Vue 组件化开发(第 20天)
前端·javascript·vue.js