Vue AI 编程神器:vuejs-ai/skills v19
Vue AI 编程神器:vuejs-ai/skills v19 解读
AI 写 Vue 代码有三个高频毛病:组件越写越胖、composable 越导越多、层级一深就靠 props 一层层往下捅。
这三件事,Vue 核心团队成员 SerKo 打算用一套 Skills 解决。vue-best-practices v19 发布后,Vue 作者尤雨溪转发了这条消息。

推文核心三句:处理 props/emits 过多的组件、避免 composable 过度导出、用 provide/inject 缓解 prop drilling 与 emit hell
一、这个项目是什么
vuejs-ai/skills 是一套面向 Vue 3 的 Agent Skills,给 Claude Code、Codex 这类 AI 编程工具补上 Vue 开发规范。
它不提供运行时,不提供组件库,也不改变 Vue 本身——它改变的是 AI 写 Vue 时的默认动作。

截图时约 3k Star、170 Fork、9 位贡献者
Agent Skills 的机制很朴素:把领域知识、检查清单、参考文档写成 Markdown,放在约定目录下,AI 执行任务时按需加载。所以这套东西的质量,取决于文档写得够不够细、规则有没有被验证过。
值得一提的是官方定位——仓库首页挂着一句很坦诚的说明:
Early Experiment / Community Project
这是一个早期实验。Skills 来源于真实问题,但受限于模型幻觉可能并不完整,欢迎反馈。如果确实有价值,作者计划提议把项目移交给 Vue 官方组织。
换句话说:能用,但别当成 Vue 官方标准。
二、8 个 Skills,覆盖日常开发主场景
| Skill | 适用场景 |
|---|---|
vue-best-practices |
Vue 3 + Composition API + TypeScript 的通用最佳实践、SSR、性能 |
vue-options-api-best-practices |
Options API 下的 this 上下文、生命周期、TypeScript |
vue-router-best-practices |
Vue Router 4 的导航守卫、路由参数、路由组件生命周期 |
vue-pinia-best-practices |
Pinia 的 Store 组织、响应式、状态模式 |
vue-testing-best-practices |
组件测试与 E2E:Vitest、Vue Test Utils、Playwright |
vue-jsx-best-practices |
Vue 中写 JSX,与 React JSX 的语法差异 |
vue-debug-guides |
运行时错误、警告、异步错误处理、水合(hydration)问题排查 |
create-adaptable-composable |
编写可复用 composable,MaybeRef / MaybeRefOrGetter 入参模式 |
基本把 Vue 日常开发的主要场景都覆盖了。
三、v19 到底改了什么
v19 的更新集中在三个方向:组件设计、逻辑封装、数据流。有意思的是,这三条都在纠正 AI 的同一个倾向——遇事不决就加抽象。
3.1 组件被塞了太多 Props / Emits
AI 生成组件时,习惯把相关的一切都塞进同一个组件的入参里:
<script setup lang="ts">
defineProps<{
user: User
loading: boolean
editable: boolean
permissions: string[]
settings: Settings
}>()
defineEmits<{
save: []
cancel: []
delete: []
refresh: []
}>()
</script>
问题不在于「属性多」,而在于职责混。这一个组件同时承担了数据展示、表单编辑、权限判断、持久化触发。任何一个需求变动都会波及它,改一处影响多处。
v19 的规则是 Decompose by responsibility——按职责拆分,让每个实现的契约显式。拆完大概是这样:
<!-- UserCard.vue:只负责展示 -->
<script setup lang="ts">
defineProps<{ user: User }>()
</script>
<!-- UserForm.vue:只负责编辑 -->
<script setup lang="ts">
defineProps<{ user: User; disabled?: boolean }>()
const emit = defineEmits<{
submit: [draft: UserDraft]
cancel: []
}>()
</script>
<!-- UserActions.vue:只负责动作与权限 -->
<script setup lang="ts">
defineProps<{ permissions: string[] }>()
const emit = defineEmits<{
remove: []
refresh: []
}>()
</script>
然后是父组件负责编排:
<script setup lang="ts">
const props = defineProps<{ id: string; permissions: string[] }>()
const emit = defineEmits<{ removed: [] }>()
const { state, save, refresh } = useUser(() => props.id)
async function handleRemove() {
await api.deleteUser(props.id)
emit('removed')
}
</script>
<template>
<template v-if="state.user">
<UserCard :user="state.user" />
<UserForm :user="state.user" @submit="save" @cancel="refresh" />
</template>
<UserActions :permissions="permissions" @remove="handleRemove" @refresh="refresh" />
</template>
但 v19 同时否定了另一种偷懒:按行数拆。
SKILL.md 里写得很直接:
Treat file length, UI section count, and API size as review signals, not extraction thresholds.
文件长度、UI 区块数量、API 大小,只是该复查一下的信号,不是拆分阈值。一个内聚的小功能就该留在一个 SFC 里,多加一层边界反而让复杂度藏得更深。
至于怎么判断「该不该拆」,它给的标准也值得记下来:
Judge cohesion by behavior that changes together or maintains a shared invariant. Being on the same page, reading the same data, or having only one consumer does not by itself justify one implementation.
内聚的判断依据是是否一起变化、是否维护同一个不变量;「在同一个页面」「读同一份数据」「只有一个使用者」,都不构成合并成一个实现的理由。
还有一条反向约束,专门治「为了减少 props 传递而强行拆分」:
If a split adds extensive forwarding or mutual callbacks, first move local state and DOM behavior to its UI owner, then identify genuinely shared dependencies and their lifetime.
如果拆完之后满屏都是转发和互相回调,说明拆分方向错了——先把本地状态和 DOM 行为还给它的 UI 宿主。
3.2 Composable 变成了一个巨型对象
这是 AI 生成 Vue 代码里最常见的第二个毛病:所有逻辑塞进 useXxx(),最后把内部状态和方法一股脑 return 出去。
// 反例:什么都往外吐
export function useUser() {
const user = ref<User | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
function fetchUser() {}
function updateUser() {}
function resetUser() {}
return { user, loading, error, fetchUser, updateUser, resetUser }
}
业务一复杂,一个 composable 能返回几十个字段。调用方看起来「什么都拿得到」,实际上没人知道哪些是契约、哪些是意外。
v19 的要求是区分「实现归属」和「对外门面」:
Distinguish implementation ownership from the public facade. A coordinator connects capabilities and implements cross-capability operations; it should not contain all their implementations or re-export all their internals.
协调者负责连接能力、实现跨能力操作,但它不该装下所有实现,也不该把内部细节原样透传出去。
改造后:
// 正例:对外只暴露消费者真正需要的那部分
export function useUser(id: MaybeRefOrGetter<string>) {
const state = reactive({
user: null as User | null,
loading: false,
error: null as Error | null,
})
// ---- 以下都是内部实现,不对外 ----
function patchState(patch: Partial<typeof state>) {
Object.assign(state, patch)
}
async function load() {
patchState({ loading: true, error: null })
try {
patchState({ user: await fetchUser(toValue(id)) })
} catch (e) {
patchState({ error: e as Error })
} finally {
patchState({ loading: false })
}
}
function reset() {
patchState({ user: null, error: null })
}
// ---- 门面:状态只读,动作有语义 ----
async function refresh() {
await load()
}
async function save(draft: UserDraft) {
patchState({ user: await api.updateUser(toValue(id), draft) })
}
onMounted(load)
return {
state: readonly(state),
refresh,
save,
}
}
几个细节值得注意:
- 用
readonly(state)把状态锁成只读,外部拿不到写权限,只能通过语义化动作改 fetchUser/reset/patchState全部留在闭包里,不进返回值- 入参用
MaybeRefOrGetter<string>,调用方传ref、getter还是普通字符串都行
至于判断信号,SKILL.md 给了很实用的一条:
If a consumer needs unrelated groups of internals, review that consumer's responsibilities too.
如果某个消费者需要一堆互不相关的内部状态,该复查的是这个消费者自己的职责,而不是把导出的口子开得更大。
另外还纠正了一个常见的性能误区:
Use
shallowReffor primitive reactive data.
对于不可变的整体替换型数据(从接口拿回的对象、列表),shallowRef 比 ref 合适——省掉深层响应式代理的开销。
3.3 Props Drilling 与 Emit Hell
第三个变化针对多层组件通信。典型结构:
App
└─ UserPage
└─ UserPanel
└─ UserForm
如果 UserForm 需要 App 里的数据,传统写法是 props 一层层往下传,事件一层层往上传。中间两层纯粹做搬运工。
v19 建议在合适场景下用 provide/inject,并且要求类型安全:
// keys.ts
import type { ComputedRef, InjectionKey } from 'vue'
export interface UserContext {
user: ComputedRef<User | null>
update: (patch: Partial<User>) => Promise<void>
}
export const userKey: InjectionKey<UserContext> = Symbol('user')
<!-- UserPage.vue:只负责提供,不关心谁来消费 -->
<script setup lang="ts">
const props = defineProps<{ id: string }>()
const { state, save } = useUser(() => props.id)
provide<UserContext>(userKey, {
user: computed(() => state.user),
update: (patch) => save({ ...state.user!, ...patch }),
})
</script>
<!-- UserForm.vue:直接取用,不必经过 UserPanel -->
<script setup lang="ts">
const { user, update } = inject(userKey)!
</script>
但有三个限定词,很容易被忽略。 SKILL.md 的原话是:
Use typed, scoped provide/inject for shared descendant dependencies when intermediate components only relay them; reassess component boundaries first.
拆开看:
- typed——必须带
InjectionKey,不能是裸字符串provide('user', user) - scoped——只在一个子树范围内提供,不要挂到根组件当全局状态用
- descendant + only relay——只适用于「中间层单纯在转发」的情况
最后半句更重要:reassess component boundaries first。先重新审视组件边界,再考虑用 provide/inject 绕过。
也就是说,provide/inject 是用来消除「无意义的转发层」的,不是用来掩盖「本来就该合并或重新划分的组件层级」的。如果中间层其实有自己的职责,只是恰好也在传数据,那该做的是重新划边界,而不是注入。
普通父子组件通信,v19 依然推荐 Props / Emits:
Make data flow explicit: Props down, Events up for most cases.
四、比「三条更新」更有价值的部分
翻 SKILL.md 会发现,v19 真正做的是把 Vue 开发流程固化成了 5 步工作流,其中前两步是强制(required)的。
第一步:写代码之前先确认架构
默认技术栈写死为 Vue 3 + Composition API + <script setup lang="ts">。项目明确用 Options API 或 JSX 的,加载对应 skill。
更关键的是这一条——动手前必须读四份核心参考文档,并且在整个任务期间保持在上下文里:
references/reactivity.mdreferences/sfc.mdreferences/component-data-flow.mdreferences/composables.md
原文强调:
Keep these references in active working context for the entire task, not only when a specific issue appears.
这一条其实点中了要害。AI 写 Vue 出问题,多数不是「不会写」,而是没读文档、凭记忆写。强制读参考,比任何 prompt 技巧都有效。
第二步:先划组件边界,再写代码
非平凡功能或结构性重构,要求先画出三样东西:职责划分、状态归属、跨边界的契约。
规则里还有一句很到位的判断:
Let root/route components compose features and coordinate page behavior.
根组件和路由组件负责编排,不要兼任实现。反过来也成立——把整个功能挪进一个子组件或者一个 useFeature() 函数里,并不算完成了拆解。
第三步:可选特性按需加载
Slots、fallthrough attrs、KeepAlive、Teleport、Suspense、Transition / TransitionGroup、自定义指令、异步组件、渲染函数、插件、状态管理模式——全部标注为「默认不加」:
Do not add these by default. Load the matching reference only when the requirement exists.
这条直接对治 AI 的另一个坏习惯——不管用不用得上,先把全家桶摆出来。
第四步:性能优化放在功能正确之后
Performance work is a post-functionality pass. Do not optimize before core behavior is implemented and verified.
它把性能工作明确归位成「功能完成后的独立一轮」,并给出对应场景的参考文档:大列表虚拟化、v-once / v-memo、列表热路径里避免过度抽象、updated 钩子的更新开销。
第五步:交付前自检
清单里几条值得抄进团队的 Code Review 规范:
- 响应式模型是否最小、可预测
- SFC 结构与模板规则是否遵守
- 拆出去的那部分是否真的藏住了复杂度,而不只是转发了一层
- 公共契约是否带类型、是否隐藏了内部实现
- 状态是否有明确的所有权和生命周期,副作用是否随宿主一起清理
- 重构是否保持了原有行为(包括未完成的草稿和跨功能操作)
最后一条特别提醒了一句:
API or file-size reductions alone are not evidence of success.
接口变小了、文件变短了,都不构成重构成功的证据。
五、它怎么证明自己有效
仓库里有一套我觉得比代码本身更值得学的机制。
先分类。 Skills 被分成两类:
- Capability(能力型):没有这个 skill,AI 根本做不对。对应版本特性、未文档化行为、训练数据之外的边界情况
- Efficiency(效率型):AI 能做,但做得不够好。提供更优模式、更一致的实现路径
再验证。 每条规则都要跑自动化评测的 A/B 对照:
| 基线(不装 skill) | 装载 skill | 处理 |
|---|---|---|
| 失败 | 通过 | 保留 |
| 通过 | 通过 | 考虑移除 |
A rule is kept only if it enables the model to solve problems it couldn't solve without it.
标准很硬:不是「加了更好」,而是「不加就错」。基线本来就能通过的规则,会被考虑删掉,避免上下文膨胀。
这套方法论其实可以直接搬到团队内部:写规范文档容易,难的是证明规范有用。
六、安装与使用
最简方式,一行命令:
npx skills add vuejs-ai/skills
按提示选择需要的 Skills 和目标 AI 工具即可。
Claude Code 用户也可以走 marketplace:
# 添加 marketplace
/plugin marketplace add vuejs-ai/skills
# 一次性装全部
/plugin install vue-skills-bundle@vue-skills
# 或者只装单个
/plugin install vue-best-practices@vue-skills
# 装多个
/plugin install vue-best-practices@vue-skills vue-router-best-practices@vue-skills
用的时候加一句前缀
官方给的建议很具体:prompt 开头加上 use vue skill。
Use vue skill, 帮我重构当前这个 Vue 3 页面
原因写在 README 里:
Without the prefix, skill triggering may be inconsistent depending on how closely your prompt matches the skill's description keywords.
不加前缀时,Skill 能否被触发,取决于你的 prompt 和 skill 描述关键词的匹配度——不稳定。加了前缀相当于显式调用。
一个完整的重构 prompt 可以这样写:
Use vue skill
帮我重构当前 Vue 3 页面:
1. 拆分承担了过多职责的组件,按「是否一起变化」判断内聚,不要按行数拆
2. 收敛 composable 的对外导出,区分实现归属和对外门面
3. 中间层只做转发的 props 改为类型安全的 provide/inject
4. 保持现有行为不变,改完给出边界划分的说明
Nuxt 项目也能用。 作者在评论区确认过,相关 Vue 规则同样适用。
七、两个官方 Demo
Demo 1:Todo App
输入一句 create a todo app,对比装载 skill 前后的产物:
- 代码可读性提升
- 组件被拆开
- 状态挪进 composable(
useTodos.ts) - 原始响应式数据改用
shallowRef
Demo 2:useHidden
输入 create a reusable composable for controlling hidden for a element,产出的签名是这样:
export interface UseHiddenOptions {
hidden?: MaybeRef<boolean>
initialHidden?: MaybeRefOrGetter<boolean>
syncAria?: boolean
}
export function useHidden(
target?: MaybeRefOrGetter<HTMLElement | null | undefined>,
options: UseHiddenOptions = {},
)
MaybeRef 和 MaybeRefOrGetter 的用法是这套 skill 的典型特征——让调用方自己决定传值还是传 getter,把响应式的灵活性留在边界上。
八、几点判断
通读文档和 demo 之后,说几个比较实际的结论。
它解决的是「默认行为」问题,不是「能力」问题。 装上之后 AI 不会突然懂 Vue 编译器内部实现,但它会先读文档、先划边界、交付前过一遍清单。对日常业务代码来说,这个差别已经很大。
最大的价值在老项目重构。 「这个组件该不该拆」「这个 composable 导出太多了」这类问题,以前靠人的经验,现在变成了可以逐条对照的检查清单。团队里水平不齐的时候,这个价值会被放大。
别当成 Vue 官方标准。 仓库自己标了 Early Experiment,规则里可能有受幻觉影响的部分。当作高水平的 Code Review 意见更合适,遇到明显不对的规则该跳过就跳过。
注意上下文成本。 它要求 AI 在整个任务期间保持四份参考文档在工作上下文里,这对上下文窗口是有占用的。项目大、任务长的时候,建议只装真正用得上的那一个 skill,而不是 vue-skills-bundle 全装。
九、相关链接
- GitHub:https://github.com/vuejs-ai/skills
- 同类项目:
- antfu/skills — Anthony Fu 维护的 Vue / Vite / Nuxt agent skills
- vueuse/vueuse-skills — VueUse 开发用 skills
- onmax/nuxt-skills — Nuxt 开发用 skills
License:MIT。
本文基于
vuejs-ai/skills仓库 README 与vue-best-practicesv19.1.0 的SKILL.md整理,规则原文引用自仓库文档。

浙公网安备 33010602011771号