前言
在大型Vue3项目中,随着业务复杂度的提升,组件代码极易变得臃肿且难以维护。数据来源多样、逻辑分散、提交行为不清晰是常见痛点。为解决这些问题,我们制定并推行一套以 "职责分离" 与 "函数式架构" 为核心的编码规范,旨在提升代码的可读性、可测试性与团队协作效率。
一、 核心数据命名与分类规范
强制要求按照数据的来源与用途,在组件中声明以下三类数据,禁止混用:
-
pageStaticData- 静态/配置数据- 定义:在组件生命周期内不会改变的初始数据。
- 举例:字典项、固定的选项列表、页面默认配置、本地图标映射。
- 特点:通常为常量或从全局配置/枚举导入。
-
pageStatusData- 状态/响应式数据- 定义:驱动视图变化的动态数据,主要来自异步请求。
- 举例:从后端API获取的列表数据、表单的临时填写状态、弹窗的开关状态。
- 特点 :必须是响应式的(
ref/reactive),其变化直接关联UI更新。
-
pageSubmitData- 提交数据- 定义:专门用于向服务端提交的数据对象。
- 举例:表单提交的DTO、查询条件的参数集。
- 特点 :应保持结构纯净,与接口契约严格一致。它是唯一可以被"提交"函数修改的数据。
二、 逻辑与视图分离:Composable架构
我们采用 "一个视图组件对应一个逻辑Composable" 的架构。
-
文件结构 :为每个
Page.vue或复杂组件创建一个同名的usePage.ts(或Page.composable.ts)文件,并放置在同一目录下。或者放在composables目录中按页面模块组织。bashsrc/ ├── 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 的职责:
- 初始化静态数据 (
pageStaticData):设置字典、配置项等固定数据 - 初始化状态数据 (
pageStatusData):设置响应式数据的初始值 - 初始化提交数据 (
pageSubmitData):设置提交数据结构的默认值 - 执行初始数据获取:如页面首次加载时需要的字典数据
实现模式:
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 使用注意事项:
- 调用时机 :通常在组件的
onMounted中调用,或在路由守卫中调用 - 可重置性:应支持多次调用,用于重置页面到初始状态
- 错误处理:内部应包含适当的错误处理,避免因初始化失败导致页面崩溃
- 加载状态:可考虑添加初始化加载状态,供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)
- 为
pageStaticData、pageStatusData、pageSubmitData分别定义清晰的接口或类型。 - 所有函数,尤其是私有函数,应严格标注参数和返回值类型。这能极大增强代码的可靠性和开发体验。
3. 代码风格统一
- 结合团队ESLint和Prettier配置,统一缩进、分号、引号等风格。
- 在Composable中,将三类数据的声明、
initPage函数、公共函数、私有函数进行分组,并用空行分隔,增强可读性。
总结
本规范的核心价值在于 "强制建立一种清晰的代码组织心智模型"。
- 看到变量名,就知道它的来源和用途。
- 看到.vue文件,就知道它只负责展示。
- 看到
initPage函数,就知道这是页面初始化的入口。 - 看到
_开头的函数,就知道它不会产生副作用。 - 看到一个复杂函数,就能按"校验-处理-提交"的路径快速理解。
通过实践这套规范,我们可以将Vue3组件的开发,从"面向模板"的编写,提升为"面向架构"的设计,从而有效管理复杂度,构建出更稳健、更易协作的前端应用。