我最早对 VSCode snippets(代码模板)的印象,就是敲三个字母加一个 Tab,然后屏幕上冒出一行console.log()。当时觉得这功能挺可爱,但也就那样。真正让我改变看法的是接了一个前后端混杂的项目:同一天里我要写 Vue 3 的组件骨架、写 Node 的接口处理器、写几段 Python 数据处理脚本,还要顺手改一点 C++ 的测试用例。一天下来统计了一下,我敲的字符里有相当一部分是在重复同一批结构——<script setup>开头那几行、try/except的固定收尾、for循环的固定模板。这些代码没有任何智力含量,却实实在在吃掉了时间和注意力。
snippets 解决的正是这部分劳动。它不是代码补全,也不是 AI 生成,它是你自己预先写好的一段文本骨架,靠一个短前缀唤出来,把光标按你预留的位置一个个跳过去,让你只填真正需要动脑的部分。这篇文章想做的事情很具体:把 VSCode 里 snippets 从语法到落盘、从个人使用到团队共享、从写不出来到排查为什么不出效果,完整讲一遍。如果你已经用过 snippets 但只停留在「从插件市场装了个 Vue 3 snippets 包」的阶段,那这篇正好补齐你自己动手的那一半。
1. snippets 在编辑器里到底替代了哪部分劳动
1.1 补全、模板、AI 生成,解决的不是同一件事
很多人把这三者混在一起讨论,其实它们的触发逻辑完全不同,搞混了就会在错误的地方找解决方案。
IntelliSense 补全依赖语言服务(Language Server)对当前工程做语义分析。它能告诉你这个对象上有哪些方法、这个函数的参数是什么类型,前提是代码能被正确解析。所以当你在 C++ 项目里发现「写 C 没有代码提示」,问题通常出在编译配置或语言服务上,跟 snippets 一点关系都没有。
snippets依赖你自己定义的文本模式。它不理解语义,你把foo定义成一段while(1){},它照样给你吐出来。好处是绝对可靠、零延迟、离线可用;坏处是它不会替你判断这段模板在这里合不合适。
AI 补全依赖上下文概率,能生成你从没写过的东西,但结果不稳定,同一位置多按几次可能给出不同答案,而且在涉及内部规范、私有接口、特定业务约束时经常跑偏。
| 维度 | snippets | IntelliSense 补全 | AI 补全 |
|---|---|---|---|
| 判断依据 | 你定义的文本模式 | 语言服务语义分析 | 上下文概率预测 |
| 可靠性 | 完全确定 | 高,依赖工程可解析 | 波动较大 |
| 触发成本 | 极低 | 低 | 有网络与算力成本 |
| 适合场景 | 结构固定、写法统一的骨架 | 符号、方法、参数 | 探索性、非重复性代码 |
| 团队一致性 | 可以版本化统一 | 依赖工程配置 | 难以统一 |
看完这张表就能明白,snippets 的位置其实很清晰:只负责那些你已经确定「就应该这么写」的部分。
1.2 一个 snippet 从敲下前缀到展开的完整链路
想排查问题,先得知道正常流程长什么样。一次 snippet 展开大致经过这么几步:
- 你在编辑器里输入字符,编辑器收集当前光标前的词;
- 建议列表(Suggest Widget)根据当前语言过滤可用的 snippet 集合;
- 命中某个 snippet 的
prefix后,把body拿出来做渲染; - 渲染过程中求值所有内置变量,比如
TM_FILENAME、CURRENT_YEAR; - 光标跳到第一个 tabstop,你填内容,按 Tab 进入下一个;
- 全部 tabstop 走完后,光标落在
$0指定的位置。
这个链路里任何一环断了,表现都不一样:第 2 步断了是「建议列表里根本没有」;第 3、4 步断了是「展开出来是空的或者变量没被替换」;第 5 步断了是「展开后光标不动或者跳错位置」。你后面遇到问题时,先判断卡在哪一环,比盲目重启编辑器有用得多。
1.3 模板化省下的不只是按键数
我自己的体感是,snippets 的价值分三层。
第一层是省时间,这是最容易感知的,但通常也是最不值钱的一层。省下的那几百次按键,累计起来可能一天也就十几分钟。
第二层是降低认知切换成本。写惯了一种结构之后,大脑会自动把「新建一个组件」这件事打包成一个动作。如果你每次都要从零回忆「这个项目里 props 是写在 setup 前面还是后面」,那这个切换成本是持续的。模板把这段决策固化了,你的注意力可以全部放在业务逻辑上。
第三层是统一团队代码风格,这一层价值最大也最容易被忽略。当log在所有人的编辑器里都展开成同一套日志格式、apicall都展开成同一种错误处理结构时,代码评审里关于「这里写法不一致」的讨论会显著减少。这部分收益不用等到项目结束,第一周的 PR 就能看出来。
2. 手写一个 snippet:把 JSON 结构逐字段拆开看
2.1 prefix、body、description 各自的职责边界
VSCode 的 snippet 定义就是一个 JSON 对象,外层 key 是这个 snippet 的名字(只在配置界面里显示),内层三个字段各自有明确分工。
{ "Log to console": { "prefix": "log", "body": [ "console.log('$1');", "$0" ], "description": "输出一行日志到控制台" } }prefix是触发词,可以写成数组,比如["log", "clog", "consolelog"],这样一个模板能用多个入口唤出来。我个人的建议是不要贪多,同一模板给两三个前缀就够,给太多会污染建议列表,反而降低筛选效率。
body是真正的内容,支持字符串和数组两种写法。数组写法每一行是一个元素,可读性好得多,而且不用担心换行符和缩进转义的问题。只要模板超过两行,就老老实实用数组,这是我踩过几次坑之后的固定做法。
description会显示在建议列表右侧,写清楚它的用途,团队共享时能省掉大量沟通。
2.2 tabstop 与占位符:$1、$0、${1:默认值} 的行为差异
这是 snippets 里最容易写错、也最值得花十分钟搞明白的部分。
$1、$2、$3是光标停留点,按编号顺序跳转。同一个编号出现多次时,它们会联动编辑——你在第一个位置输入什么,其他同编号位置同步变化。这个特性非常有用,比如生成一对同名的变量和函数。$0是最终光标位置,不参与编号顺序,永远在最后。如果省略$0,光标会停在模板末尾。${1:默认内容}是带默认值的占位符,展开后默认内容被选中,你直接输入就替换掉,按 Tab 保留默认值。${1|红色,蓝色,绿色|}是可选项占位符,展开时给一个下拉列表让你选。
举个实际例子,写一个 Vue 3 组件的骨架:
{ "Vue 3 SFC skeleton": { "prefix": "v3s", "body": [ "<template>", " <div class=\"${1:container}\">", " $0", " </div>", "</template>", "", "<script setup lang=\"ts\">", "const props = defineProps<{", " ${2:title}: ${3:string}", "}>()", "", "</script>", "", "<style scoped lang=\"scss\">", " .${1:container} {", " }", "</style>" ], "description": "Vue 3 单文件组件骨架" } }注意这里$1用了两次:一次在模板的 class 名,一次在样式选择器里。你展开后输入card,两处同时变成card,这就是联动编辑的实用价值所在。这类写法在很多 Vue 3 snippets 扩展包里也能看到,但自己写一遍才知道它为什么这样组织。
2.3 内置变量:TM_ 系列、时间、剪贴板与随机值
内置变量是 snippets 真正拉开差距的地方。常用的有这么几类:
| 变量 | 含义 | 典型用途 |
|---|---|---|
TM_FILENAME | 当前文件名含扩展名 | 生成文件头注释 |
TM_FILENAME_BASE | 当前文件名不含扩展名 | 生成同名类名或测试名 |
TM_DIRECTORY | 当前文件所在目录的绝对路径 | 调试信息、模块路径 |
TM_FILEPATH | 当前文件完整路径 | 生成__FILE__类信息 |
RELATIVE_FILEPATH | 相对工作区根目录的路径 | 日志定位、文档链接 |
WORKSPACE_NAME | 当前工作区名称 | 多仓库场景下的标识 |
CURRENT_YEAR等 | 年月日时分秒 | 版权头、变更记录 |
CURRENT_DAY_NAME | 星期几 | 变更记录 |
LINE_COMMENT | 当前语言的行注释符号 | 跨语言通用注释模板 |
BLOCK_COMMENT_START/_END | 块注释符号 | 跨语言文档注释 |
CLIPBOARD | 系统剪贴板内容 | 粘贴路径、URL 做注释 |
RANDOM/RANDOM_HEX | 随机字符串 | 生成临时 ID |
UUID | 一个 UUID v4 | 需要唯一标识的场合 |
LINE_COMMENT这个变量特别值得说一句。它会让你的注释模板变成跨语言通用的:同一份 snippet,在 Python 里展开成#,在 C++ 里展开成//,在 SQL 里展开成--。我现在的文件头模板就是靠它做的,一份定义在七八种语言里都能用。
写一个我实际在用的文件头模板:
{ "File header": { "prefix": "fhdr", "body": [ "$LINE_COMMENT ${TM_FILENAME_BASE}", "$LINE_COMMENT", "$LINE_COMMENT Created: $CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE $CURRENT_HOUR:$CURRENT_MINUTE", "$LINE_COMMENT Author: $1", "$LINE_COMMENT", "$0" ], "description": "插入文件头注释" } }这个模板我放在用户级通用位置,不绑定具体语言,Python、C++、Shell 里都能直接fhdr+ Tab 展开。
2.4 转义:$、}和反斜杠在 JSON 里的双重身份
这部分是新手最容易翻车的地方,因为你在处理两层转义。
第一层是JSON 自身的转义:JSON 字符串里的双引号必须写成\",反斜杠必须写成\\。所以如果你的模板内容里有一个正则\d,在 JSON 里要写成\\d。
第二层是snippet 语法的转义:如果你想在模板里输出一个字面量$而不是变量,必须写\$。想输出字面量},写\}。
把两层叠起来就会出现很反直觉的写法。比如你想生成一段 Shell 脚本,里面有个$HOME:
{ "shell home": { "prefix": "shome", "body": ["echo \"\\$HOME = \\$HOME\""] } }第一次写的时候我盯着这行看了半天。理解方式很简单:先按 snippet 规则处理,\$变成字面$;剩下的\交给 JSON 解析器处理。写复杂模板时,我的建议是先在一个空文件里手写出你想要的最终结果,然后倒推着加转义,比正向硬想快得多。
3. snippet 放哪儿:作用域、优先级与拆分策略
3.1 三个层级的落盘路径
VSCode 的 snippet 有明确的层级,选错位置会出现「在我电脑上能用、同事那边没有」这类问题。
| 层级 | 存放位置 | 适用范围 |
|---|---|---|
| 用户级 · 语言专属 | 用户目录下snippets/<language>.json | 所有项目中的该语言文件 |
| 用户级 · 通用 | 用户目录下snippets/*.code-snippets | 所有项目、所有语言 |
| 工作区级 | 项目内.vscode/*.code-snippets | 仅当前项目 |
| 插件自带 | 扩展安装目录 | 取决于插件声明 |
用户目录的位置按系统区分:Windows 在%APPDATA%\Code\User\,macOS 在~/Library/Application Support/Code/User/,Linux 在~/.config/Code/User/。
最省事的入口不是手动找路径,而是按Ctrl+Shift+P(macOS 是Cmd+Shift+P)输入Snippets,选「配置用户代码片段」,编辑器会弹出语言列表让你点。选一项,它自动建好文件并打开。工作区级的同理,选「新建全局代码片段文件」或用项目里的.vscode目录手工建文件都行。
3.2 同名 prefix 撞车的时候谁赢
这是个很实际的问题。你装了一个 Vue 3 snippets 插件,自己又写了一个v3s,插件里恰好也有v3s,那按下去会出哪一个?
VSCode 的处理方式是把所有匹配的候选都列在建议列表里,顺序上更具体的作用域排得更靠前——工作区级优先级高于用户级,用户级语言专属高于用户级通用。但要注意,这个排序规则在版本迭代中有过调整,所以在依赖顺序做事之前,最稳妥的办法是实测一次:把两个同名 snippet 都定义好,展开看看谁在前面。
如果确实撞车,处理方式有三种:
- 改自己的
prefix,加个团队前缀,比如x-v3s; - 直接覆盖插件的行为,把插件里那个 snippet 的 key 抄过来改 body,因为同名 key 会覆盖;
- 禁用插件的 snippet 贡献,如果插件支持单独关闭的话。
我一般选第一种,代价最小,也最容易维护。
3.3 什么情况下值得把 snippet 拆成 .code-snippets 独立文件
.json结尾的是早期格式,一个文件对应一种语言,文件名就是语言 ID。.code-snippets结尾的是后来引入的格式,一个文件里可以混放多种语言的 snippet,靠每个条目里的scope字段来限定。
{ "API handler": { "scope": "typescript,javascript", "prefix": "apih", "body": [ "export async function ${1:handlerName}(req, res) {", " try {", " $0", " } catch (err) {", " res.status(500).json({ message: err.message })", " }", "}" ] } }我判断是否拆分的标准很简单:这份模板需不需要跨语言复用,或者需不需要跟项目走。跨语言复用就写.code-snippets放用户级;项目专属就写.code-snippets放.vscode目录,跟着仓库一起提交。单语言、纯个人的小模板,用语言专属.json就够了,不用折腾。
值得一提的是,项目内的.vscode/*.code-snippets会随代码库分发,新同事拉下代码就自动有了这套模板。这是团队统一模板最轻量的落地方式,比写文档、发配置文件都省事。
4. 让模板会「加工」:正则转换、条件插入与嵌套调用
4.1 转换语法${1/regex/format/options}逐段解释
基础占位符只能替换,转换语法能让模板对输入内容做加工。完整形式是:
${变量或占位符/匹配正则/替换格式/选项}四个部分逐个说清楚:
- 第一个斜杠前:被处理的源。既可以是编号占位符
$1,也可以是内置变量TM_FILENAME。 - 中间的正则:用来捕获源里的片段,用括号分组,分组在替换格式里用
$1、$2引用。注意这里的$1指的是正则捕获组,和上面的 tabstop 编号是两套东西,容易看混。 - 替换格式:可以是纯文本,也可以用
${1:/upcase}形式的大小写转换。 - 选项:
i忽略大小写,g全局匹配,m多行模式。
一个真实用例——把文件名转成驼峰命名,用来生成类名或函数名:
{ "Filename to camelCase": { "prefix": "fnc", "body": ["const ${TM_FILENAME_BASE/(.*)/${1:/camelcase}/} = $0"] } }假设当前文件叫user-profile.ts,展开后得到const userProfile =。这一招我在写测试文件时用得最多,测试文件名和被测对象名之间就靠它建立关联。
支持的格式修饰符有upcase、downcase、capitalize、camelcase、pascalcase、snakecase、kebabcase,覆盖了绝大多数命名风格转换需求。
需要注意一个坑:正则里的反斜杠在 JSON 里要写成双份。如果你想匹配点号.,正则应写\\.而不是\.,否则 JSON 解析阶段就会报错或者吞掉字符。我第一次写转换的时候就在这儿卡了快二十分钟。
4.2 条件插入:可选片段的两种写法
有时候模板里有些行是「可有可无」的。条件插入能根据占位符有没有值来决定插不插入。
${1:+text}:如果$1有值,就插入text;${1:-text}:如果$1没有值,就插入text。
举个实用例子,写一个带可选await的调用模板:
{ "Maybe await call": { "prefix": "maw", "body": [ "${1:const data = }${2:+await }${3:fetchData}()", "$0" ] } }如果你在第二个位置填了任意内容,await就会出现;直接按 Tab 跳过就变成同步调用。这种写法比我早期做的「同步版 + 异步版两个模板」优雅得多,维护成本也更低。
我个人的经验是,条件插入适合处理一到两个开关型差异。差异超过三个,模板的复杂度会急剧上升,写出来自己都记不住该怎么填,这时候拆成几个模板反而更清楚。
4.3 嵌套调用:composite snippet 的用法和它的天花板
带isFileTemplate: true的 snippet 会被标记为文件模板,可以在新建文件时直接套用。而在普通 snippet 的 body 里,也可以把一个 snippet 的名字当成变量来引用,实现嵌套。
{ "Component with header": { "prefix": "cmph", "body": [ "$LINE_COMMENT ${TM_FILENAME_BASE}", "$LINE_COMMENT", "import React from 'react'", "", "export default function ${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}() {", " return (", " <div>$0</div>", " )", "}" ] } }坦白说,我在实际项目里很少做深层嵌套。原因很直接:嵌套会把调试难度抬高一个量级。展开出来的东西不符合预期时,你很难判断是外层模板的问题还是内层模板的问题。我的做法是让每个 snippet 保持「一眼能看完」的体量,需要组合的场景用手动连续展开几次,而不是把它们焊死在一起。
这条经验是我在维护一个包含二十多个互相引用的模板集合之后总结出来的。那个集合最后的结局是被整体重写,拆成了十几个互不依赖的小模板。
5. 从个人快捷键到团队资产:命名、版本化与淘汰
5.1 把 prefix 当成 API 来设计
一旦 snippet 要分享给同事用,prefix的命名就不再是个人喜好的问题,而是接口设计问题。我踩过的坑是早期用了t、c、d这种单字母前缀,结果和编辑器自带的补全、语言服务的缩写严重冲突,按下去经常出不来我想要的东西。
现在我的命名规则大致是:
- 两到四个字符,太短容易冲突,太长影响输入效率;
- 语义可猜,看到
apih能猜到是 API handler,看到v3s能猜到是 Vue 3 骨架; - 加语义化前缀区分来源,团队规范相关的统一加某个字母开头,避免和插件市场装的包撞车;
- 不用纯数字和特殊符号,虽然语法上允许,但输入体验很差。
一个具体的小技巧:前缀里尽量包含一个不常见的字母组合。比如clg比log更好,因为log在很多人项目里是一个真实存在的变量名,你打字打一半它就跳出来干扰你,甚至可能在你想要那个变量的时候给你展开模板。
5.2 用工作区文件承载项目专属模板
个人模板和工作模板要分开放。我的分层是这样的:
| 内容类型 | 存放位置 | 例子 |
|---|---|---|
| 跨语言通用 | 用户级.code-snippets | 文件头注释、TODO 标记 |
| 语言通用 | 用户级语言.json | Python 的try/except骨架 |
| 项目专属 | 项目.vscode/*.code-snippets | 内部 API 调用模板、组件骨架 |
| 临时试验 | 先放用户级,稳定后再下沉 | 新增的日志格式模板 |
项目专属模板我基本都会跟着仓库提交。这样做的好处是模板和代码在同一份版本历史里,改模板这件事有记录、可追溯、可回滚。比把模板放在共享网盘里让大家手动下载靠谱得多。
需要注意的是,工作区级的.code-snippets里,每个条目要显式写明scope,否则它会对所有语言生效。我就干过忘记写scope,结果在 YAML 文件里敲apih也展开出一段 TypeScript 代码的蠢事。
5.3 模板的评审与淘汰机制
模板最大的风险不是不够多,而是悄悄过期。项目用的框架升级了、内部的 API 封装改名了、日志规范调整了,但模板还是老样子下发给所有人,那它就从提效工具变成了污染源。
我现在的做法是给模板加一道轻量的维护机制:
- 每份模板的
description里写清适用范围和最后更新意图,比如「适用于 v2 接口层,v3 迁移后作废」; - 在季度回顾时清一遍,把半年内没人用或者已经过期的删掉;
- 新模板先由一个人用一个月,稳定了再进工作区文件;
- 改动模板走普通 PR 流程,让它和其他代码改动一样接受评审。
这套做法看起来有点重,但实际操作起来成本很低——毕竟模板改动一年也就那么几次。真正省下的是「有人用着过期模板写出不符合规范的代码,然后在评审时被退回」这件事带来的额外往返。
6. snippet 不生效、乱弹出、展开错位:一条完整排查链路
6.1 第一步永远先确认语言 ID
这是最高频的原因,没有之一。VSCode 认的是语言 ID,不是文件后缀。
你建了个文件叫foo.tsx,但如果编辑器右下角显示的是Plain Text,那所有 TypeScript 相关的 snippet 都不会出现在建议列表里。同理,.vue文件如果没装对应的语言支持,它可能被识别成 HTML 而不是 Vue。
排查动作很简单:看编辑器状态栏右下角的语言标识,点一下能切换。如果这里不对,后面所有排查都是白费力气。
还有一个容易忽略的情况:.code-snippets文件里scope写的是typescript,但你的文件被识别成typescriptreact。这两个是不同的语言 ID。我见过不少人在这里反复折腾,其实只要把scope改成typescript,typescriptreact就解决了。
6.2 JSON 语法错误与转义问题的定位
snippet 文件是 JSON,但 VSCode 对它的容错比较宽松——有些错误它不报,只是默默让这个文件的部分或全部条目失效。这就是「我明明写了但就是不出现」的常见原因。
定位顺序:
- 看文件里有没有红色的波浪线,有就先解决;
- 检查最外层是不是一个合法的对象,逗号有没有多写或漏写,最后一项后面不能有逗号;
- 检查每个 snippet 的内层对象,
prefix、body、description之外的多余字段虽然不报错但也没用; - 检查字符串里的双引号有没有转义,路径里的反斜杠有没有写成双份;
- 检查
body数组里每一行是否都是合法字符串,换行是不是被误写成了真实换行符。
如果这些都看不出来,我的土办法是把可疑条目单独复制到一个新建的 snippet 文件里测试。如果单独放能用,说明是文件里其他条目的语法有问题;如果单独放也不能用,那就是这个条目自身的问题。二分法排查,比一行行盯快得多。
6.3 和补全源抢触发词
症状是:你输入前缀,建议列表出来了,但第一个不是你的 snippet,得往下翻好几条才能找到。或者更糟——列表里压根没有,被别的补全结果挤掉了。
原因通常有三类:
- 前缀和真实变量名、函数名冲突,语言服务把它当成已有符号优先推荐;
- 装了多个 snippet 扩展,前缀撞车;
- AI 补全插件的建议权重更高,把 snippet 压下去了。
处理手段有两个方向。一是改前缀,用更独特的组合,这是一劳永逸的做法。二是调整建议列表的排序策略,在设置里搜snippetSuggestions,可以设成top让 snippet 排在前面、bottom沉底、inline混排、none完全不显示。我一般设成top,因为能被我自己定义成 snippet 的东西,基本就是我确定要优先用的。
另外还有一个设置值得知道:editor.tabCompletion。把它设成onlySnippets或on之后,你可以不经过建议列表,直接按 Tab 展开匹配的 snippet。这个方式在你不确定建议列表会不会干扰的时候特别好用,缺点是容易误触,需要适应一段时间。
6.4 建议列表里没有但 Tab 能展开
这个现象很有意思,说明你的 snippet 是有效的、语言 ID 也没错,问题出在建议列表的过滤上。
常见原因是前缀里有特殊字符,或者你在设置里打开了某些过滤选项,导致建议列表的模糊匹配没把它算进来。比如某些配置下,建议列表对大小写和特殊符号的处理比较严格。
还有一种情况是 snippet 的body为空数组或空字符串,它不会报错,但展开出来什么都没有,看起来就像「没生效」。
6.5 远程 SSH、WSL、容器下的差异
现在很多人在远程开发模式或者 WSL 里写代码,这里有个关键点一定要记住:用户级 snippet 读的是你连上去的那一端的环境路径,不是本地机器。
也就是说,你本地编辑器的用户目录在哪儿,和远端机器上的用户目录在哪儿,是两个不同的位置。如果你在本地配了一堆 snippet,连上远端服务器后一个都没有,不要怀疑编辑器坏了,就是位置的问题。
解决方式有两种,看你的使用习惯:
- 远端机器单独配一份,适合长期连同一台机器的情况;
- 把项目相关的模板全放进项目内的
.vscode目录,这样它跟着代码库走,无论本地还是远端、无论谁拉下来都能用,这也是我更推荐的方式。
WSL 场景下还有一个细节:Windows 侧的用户目录和 WSL 内的用户目录也是分开的。如果你在 Windows 里的 VSCode 打开 WSL 里的文件,snippet 的加载取决于窗口是用哪种模式打开的。这个差异不太好用文字描述清楚,实测一次看效果最直接。
7. 哪些代码不该做成模板
7.1 会持续演进的业务逻辑
snippet 最大的误用,是把还在演进的业务逻辑固化下来。
判断标准很清晰:如果这段代码未来三个月内会因为需求变化而修改,就不要做成模板。比如某个接口的请求参数结构、某个页面的表单校验规则、某段状态机的流转逻辑。这些东西正在变化中,做成模板等于把「过时的写法」批量复制到项目各处。
我见过比较典型的反面案例是:有人把一个三行就能写完的组件定义做成了二十行的模板,里面包含了当时的 props 默认值、当时的样式变量命名、当时的埋点调用。半年后这些东西全变了,模板还在,新人用它生成组件,然后每一处都要手改回来。
我自己的筛选标准是——这段代码我连续三个月、在至少五个不同文件里写过几乎一模一样的版本,才考虑做成模板。这个门槛拦掉了很多冲动型的模板化需求。
7.2 snippet 和 AI 补全、LSP 的分工
现在的开发环境里,这三种能力是共存的,搞清楚分工比争论谁更好用实际得多。
语言服务负责语义层面的事:符号跳转、类型检查、重命名重构、参数提示。这些事 snippet 和 AI 都做不了,遇到「无法跳转到定义」这类问题,方向是检查语言服务配置、编译参数、项目索引是否正常,而不是去翻 snippet 配置。
snippet 负责确定性结构:结构固定、写法统一、几乎不随业务变化的骨架。它的核心价值是「确定」,你按下去就知道会得到什么。
AI 补全负责探索性内容:写一个没见过的算法、翻译一段逻辑、生成一批测试数据的变体。它的核心价值是「跳跃」,你按下去可能得到惊喜也可能得到垃圾。
我的实际组合方式是:项目脚手架和固定骨架用 snippet 打底,中间的业务逻辑用语言服务补全和 AI 交替推进。三者不冲突,它们在不同的抽象层级上工作。
在选择编辑器扩展时也要注意区分。插件市场里那些自动闭合标签、自动重命名配对标签的插件,属于编辑辅助行为,跟 snippets 是两回事——它们不需要你定义前缀,是直接作用在你输入过程中的。如果你遇到的问题是「标签没有自动闭合」,那该找的是这类插件,而不是 snippet 配置。
我在实际使用中的几点体会
写到最后,说几个我摸索了很久才想明白的点。
不要在第一天就把模板库做得很全。我最早的一版模板集合有四十多个条目,三个月后真正还在用的不到十个。先攒痛点,再用模板解决,而不是先建模板库再找地方用。我现在习惯是在正常写代码时,如果同一个结构一天内手打了三次以上,才停下来花两分钟加一个 snippet。
description一定要写,哪怕你觉得这个模板只有自己用。半年后你看到那个xyz前缀,真的想不起来它是干嘛的。写一句「生成带重试的 HTTP 请求封装」,未来能省下你翻文件查看的时间。
最后是一个小技巧:想把一段已经写好的代码变成 snippet,不用手打。选中那段代码,按Ctrl+Shift+P(macOS 是Cmd+Shift+P),输入Insert Snippet,再选「新建代码片段」,编辑器会帮你把选中内容预填进模板结构里,你只需要补上prefix和description。这个路径比从空文件开始写快很多,尤其适合处理那些结构复杂、缩进敏感的多行模板。