vue-sfc-structure
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVue SFC 结构规范
Vue SFC Structure Specification
这份规范用于本项目所有 文件,重点约束 的组织顺序与职责分层。
.vuescript setupThis specification applies to all files in this project, focusing on constraining the organization order and responsibility layering of .
.vuescript setup适用范围
Scope
- 文件匹配:。
**/*.vue - 适用对象:使用 的 Vue 单文件组件。
<script setup> - 不适用对象:无 的纯模板组件或非 SFC 写法。
<script setup>
- File matching: .
**/*.vue - Applicable objects: Vue single-file components using .
<script setup> - Non-applicable objects: Pure template components without or non-SFC writing methods.
<script setup>
目标
Objectives
- 让组件脚本结构稳定、可预测、便于扫描。
- 把类型定义、响应式状态、context provider、watch、生命周期分层放置,减少来回跳读。
- Make component script structures stable, predictable, and easy to scan.
- Layer type definitions, reactive states, context providers, watchers, and lifecycle hooks to reduce back-and-forth reading.
script setup 顺序
script setup Order
推荐顺序如下:
- import statements
defineOptions- props 类型定义或
import type defineProps- emits 类型定义或
import type defineEmits- slots 类型定义或
import type defineSlots- hooks / composables 初始化
- 组件业务逻辑,按语义分块
- 必要的 函数
init - context provider
- /
watchwatchEffect - 生命周期 hooks
defineExpose
Recommended order is as follows:
- import statements
defineOptions- props type definitions or
import type defineProps- emits type definitions or
import type defineEmits- slots type definitions or
import type defineSlots- hooks / composables initialization
- Component business logic, grouped by semantics
- Necessary functions
init - Context provider
- /
watchwatchEffect - Lifecycle hooks
defineExpose
详细说明
Detailed Explanations
defineOptions
defineOptions
- 尽量靠前,通常紧跟 imports 后。
defineOptions - 组件 遵循项目既有命名约定。
name
- Place as early as possible, usually right after imports.
defineOptions - Component follows the project's existing naming conventions.
name
props / emits / slots 类型定义
props / emits / slots Type Definitions
- 项目有 惯例时,优先从同级
types.ts导入类型。types.ts - 如果类型很简单,也可以在当前文件内直接声明小型 interface 或 tuple-style 类型。
- When the project has a convention, prioritize importing types from the sibling
types.ts.types.ts - If the type is very simple, you can directly declare a small interface or tuple-style type in the current file.
defineProps / defineEmits / defineSlots
defineProps / defineEmits / defineSlots
- 不使用时不传变量,直接 。
defineXxx<Type>() - 脚本里需要访问时,用 。
const xxx = defineXxx<Type>() - 即使脚本里保留了 对象,模板里也直接使用 prop 名,不写
props。props.xxx
- Do not pass variables when not in use, just use .
defineXxx<Type>() - When access is needed in the script, use .
const xxx = defineXxx<Type>() - Even if the object is retained in the script, directly use the prop name in the template instead of writing
props.props.xxx
hooks / composables 初始化
hooks / composables Initialization
- 、
useRoute这类初始化逻辑放在这里。useRouter - 这一段只做"拿能力"和"拿基础上下文",不要混入大量业务计算。
- Place initialization logic like and
useRoutehere.useRouter - This section only focuses on "acquiring capabilities" and "obtaining basic context", do not mix in a lot of business calculations.
组件业务逻辑
Component Business Logic
- 按业务语义分块,而不是按 API 类型混排。
- 相关的 、
ref、函数尽量靠近放置。computed - 对对象、数组、实例句柄等不需要深层响应式的数据,优先使用 而不是
shallowRef。ref
- Group by business semantics instead of mixing by API type.
- Place related ,
ref, and functions as close to each other as possible.computed - For data that does not require deep reactivity such as objects, arrays, instance handles, etc., prioritize using instead of
shallowRef.ref
响应式引用选择
Reactive Reference Selection
- 需要追踪基础值或确实依赖深层属性响应式更新时,使用 。
ref - 如果只关心 整体替换,而不需要内部深层属性自动追踪,优先使用
.value。shallowRef - 尤其是对象、数组、第三方实例、DOM 句柄、上下文状态容器这类值,默认先考虑 。
shallowRef - 只有需要依赖对象内部字段变化触发更新时,才更适合继续使用 。
ref
- Use when you need to track primitive values or truly rely on reactive updates of deep properties.
ref - If you only care about the overall replacement of and do not need automatic tracking of internal deep properties, prioritize using
.value.shallowRef - Especially for values like objects, arrays, third-party instances, DOM handles, context state containers, etc., consider by default.
shallowRef - Only continue to use when you need to rely on changes in object internal fields to trigger updates.
ref
init 函数
init Function
- 只有在确实存在初始化流程时才定义 。
init - 初始化逻辑集中到一个函数里,避免散落到多个生命周期钩子中。
- Only define when there is indeed an initialization process.
init - Concentrate initialization logic into one function to avoid scattering it across multiple lifecycle hooks.
context provider
Context Provider
- 这类 provider 放在业务逻辑之后、watch 与生命周期之前。
provideXxx - 先把要提供的数据准备好,再统一注入,避免 provider 之前和之后来回穿插定义状态。
- Place providers like after business logic and before watchers and lifecycle hooks.
provideXxx - Prepare the data to be provided first, then inject uniformly, avoid interleaving state definitions before and after providers.
watch / watchEffect
watch / watchEffect
- 、
watch放在 provider 之后。watchEffect - 没必要时不要引入 watcher;优先考虑 或直接事件流。
computed
- Place and
watchafter providers.watchEffect - Do not introduce watchers unnecessarily; prioritize or direct event flows.
computed
生命周期 hooks
Lifecycle hooks
- 生命周期钩子放在 script 末段。
- 若存在 ,通常在这里或等价创建阶段调用,保持初始化入口集中。
init()
- Place lifecycle hooks at the end of the script.
- If exists, it is usually called here or in an equivalent creation phase to keep the initialization entry centralized.
init()
defineExpose
defineExpose
- 放在脚本最后。
defineExpose - 只有组件确实需要暴露实例 API 时才使用。
- Place at the very end of the script.
defineExpose - Only use it when the component really needs to expose instance APIs.
template 绑定函数
Template Binding Functions
- template 中绑定到事件、插槽参数回调或属性的函数实现,必须写在 里,或从外部显式导入后再在 template 中引用。
script setup - 不要在 template 里直接写内联箭头函数、匿名函数或承载业务逻辑的函数实现;template 只负责绑定已经在脚本中定义好的函数。
- 允许在 template 中给脚本内已定义的函数传参,例如 ;不允许直接写
@click="handleSelect(item.id)"这类内联实现。@click="() => handleSelect(item.id)"
- The implementation of functions bound to events, slot parameter callbacks, or attributes in the template must be written in , or explicitly imported from outside before being referenced in the template.
script setup - Do not directly write inline arrow functions, anonymous functions, or function implementations carrying business logic in the template; the template only binds functions already defined in the script.
- Allowed to pass parameters to functions defined in the script in the template, such as ; not allowed to write inline implementations like
@click="handleSelect(item.id)".@click="() => handleSelect(item.id)"
attrs 继承
attrs Inheritance
- 若 template 只有一个非 slot 根标签,并且 attrs 本来就应该整体落到这个根标签上,则不要设置 ,也不要为了透传根 attrs 额外写
inheritAttrs: false+useAttrs()。v-bind="attrs" - 优先使用 Vue 默认的 attrs 继承,让调用方传入的普通 attrs 自动绑定到根标签,减少样板代码和无意义的转发层。
- 只有在以下场景才使用 或手动消费
inheritAttrs: false:需要把 attrs 拆分到多个节点;需要过滤或改写 attrs;需要把 attrs 转交给非根节点;需要在脚本逻辑中显式读取 attrs;或者组件本身不是单个可直接承接 attrs 的根标签结构。useAttrs()
- If the template has only one non-slot root tag and attrs should naturally fall on this root tag as a whole, do not set , and do not write extra
inheritAttrs: false+useAttrs()to pass through root attrs.v-bind="attrs" - Prioritize using Vue's default attrs inheritance, allowing ordinary attrs passed by the caller to be automatically bound to the root tag, reducing boilerplate code and meaningless forwarding layers.
- Only use or manually consume
inheritAttrs: falsein the following scenarios: need to split attrs into multiple nodes; need to filter or rewrite attrs; need to pass attrs to non-root nodes; need to explicitly read attrs in script logic; or the component itself is not a single root tag structure that can directly承接 attrs.useAttrs()
其他
Others
- 若组件非常简单,不必为了凑顺序硬塞空分区;顺序是为了增强可读性,不是制造样板代码。
- If the component is very simple, do not force empty sections just to follow the order; the order is to enhance readability, not to create boilerplate code.
检查问题
Inspection Checklist
- 是否足够靠前?
defineOptions - props / emits / slots 类型与 /
defineProps/defineEmits是否成对且顺序稳定?defineSlots - hooks 初始化、业务逻辑、provider、watch、生命周期是否分层清楚?
- 如果 template 是单个可承接 attrs 的非 slot 根标签,是否避免了不必要的 和手动
inheritAttrs: false透传?useAttrs() - template 中绑定的函数是否都已经在 中定义或导入,而不是写成内联实现?
script setup - 不需要深层响应式的状态是否优先使用了 ?
shallowRef - 是否为了形式保留了不必要的 、watch、
init?defineExpose
- Is placed early enough?
defineOptions - Are props / emits / slots types paired with /
defineProps/defineEmitsand in a stable order?defineSlots - Are hooks initialization, business logic, providers, watchers, and lifecycle hooks clearly layered?
- If the template is a single non-slot root tag that can承接 attrs, have unnecessary and manual
inheritAttrs: falsepassing been avoided?useAttrs() - Are all functions bound in the template defined or imported in instead of written as inline implementations?
script setup - Have been prioritized for states that do not require deep reactivity?
shallowRef - Have unnecessary , watchers, and
initbeen retained just for formality?defineExpose
完整示例
Complete Example
以下示例展示上述顺序与最佳实践在一个典型组件中的落地:
vue
<script setup lang="ts">
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import type { OptionItem } from './types';
import { provideFilterContext } from './context';
// 1. defineOptions
defineOptions({ name: 'FilterPanel' });
// 2-3. props 类型定义 + defineProps
interface Props {
modelValue: string;
options: OptionItem[];
}
const props = defineProps<Props>();
// 4-5. emits 类型定义 + defineEmits
interface Emits {
(e: 'update:modelValue', value: string): void;
(e: 'change', value: OptionItem): void;
}
const emit = defineEmits<Emits>();
// 6. hooks / composables 初始化
const route = useRoute();
const router = useRouter();
// 7. 组件业务逻辑(按语义分块,shallowRef 用于不需要深层响应式的数据)
const listRef = shallowRef<OptionItem[]>(props.options);
const keyword = ref('');
const filtered = computed(() => {
const kw = keyword.value.trim().toLowerCase();
return kw ? listRef.value.filter(item => item.label.toLowerCase().includes(kw)) : listRef.value;
});
// 8. template 绑定的函数:在脚本中定义,模板只负责引用
function handleSelect(item: OptionItem) {
emit('update:modelValue', item.value);
emit('change', item);
}
// 9. init 函数(仅在确实存在初始化流程时)
function init() {
const initial = route.query.keyword;
if (typeof initial === 'string') {
keyword.value = initial;
}
}
// 10. context provider
provideFilterContext({ keyword, filtered });
// 11. watch / watchEffect(仅在必要时引入)
watch(keyword, value => {
router.replace({ query: { ...route.query, keyword: value || undefined } });
});
// 12. 生命周期 hooks
onMounted(() => {
init();
});
// 13. defineExpose(仅在需要暴露实例 API 时)
defineExpose({ reset: () => (keyword.value = '') });
</script>
<template>
<div class="filter-panel">
<input v-model="keyword" placeholder="搜索" />
<ul>
<li
v-for="item in filtered"
:key="item.value"
@click="handleSelect(item)"
>
{{ item.label }}
</li>
</ul>
</div>
</template>要点回顾:
- 紧跟 imports,后续按 props → emits → hooks → 业务逻辑 → provider → watch → 生命周期的稳定顺序排布。
defineOptions - 不需要深层响应式的 使用
listRef;需要追踪输入值的shallowRef使用keyword。ref - template 中 引用的是脚本中已定义的函数,未使用内联箭头函数。
@click="handleSelect(item)" - 单根标签结构且 attrs 应整体落到根标签,未设置 。
inheritAttrs: false
The following example demonstrates the implementation of the above order and best practices in a typical component:
vue
<script setup lang="ts">
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import type { OptionItem } from './types';
import { provideFilterContext } from './context';
// 1. defineOptions
defineOptions({ name: 'FilterPanel' });
// 2-3. props 类型定义 + defineProps
interface Props {
modelValue: string;
options: OptionItem[];
}
const props = defineProps<Props>();
// 4-5. emits 类型定义 + defineEmits
interface Emits {
(e: 'update:modelValue', value: string): void;
(e: 'change', value: OptionItem): void;
}
const emit = defineEmits<Emits>();
// 6. hooks / composables 初始化
const route = useRoute();
const router = useRouter();
// 7. 组件业务逻辑(按语义分块,shallowRef 用于不需要深层响应式的数据)
const listRef = shallowRef<OptionItem[]>(props.options);
const keyword = ref('');
const filtered = computed(() => {
const kw = keyword.value.trim().toLowerCase();
return kw ? listRef.value.filter(item => item.label.toLowerCase().includes(kw)) : listRef.value;
});
// 8. template 绑定的函数:在脚本中定义,模板只负责引用
function handleSelect(item: OptionItem) {
emit('update:modelValue', item.value);
emit('change', item);
}
// 9. init 函数(仅在确实存在初始化流程时)
function init() {
const initial = route.query.keyword;
if (typeof initial === 'string') {
keyword.value = initial;
}
}
// 10. context provider
provideFilterContext({ keyword, filtered });
// 11. watch / watchEffect(仅在必要时引入)
watch(keyword, value => {
router.replace({ query: { ...route.query, keyword: value || undefined } });
});
// 12. 生命周期 hooks
onMounted(() => {
init();
});
// 13. defineExpose(仅在需要暴露实例 API 时)
defineExpose({ reset: () => (keyword.value = '') });
</script>
<template>
<div class="filter-panel">
<input v-model="keyword" placeholder="搜索" />
<ul>
<li
v-for="item in filtered"
:key="item.value"
@click="handleSelect(item)"
>
{{ item.label }}
</li>
</ul>
</div>
</template>Key Recap:
- follows immediately after imports, followed by a stable order of props → emits → hooks → business logic → provider → watch → lifecycle.
defineOptions - , which does not require deep reactivity, uses
listRef;shallowRef, which needs to track input values, useskeyword.ref - The in the template references a function already defined in the script, no inline arrow functions are used.
@click="handleSelect(item)" - With a single-root tag structure where attrs should fall on the root tag as a whole, is not set.
inheritAttrs: false