昨天,我正在 review 代码,突然看到几个 v-mortal 出现在 Diff 中。 嗯?很陌生啊,完全没见过,搜索引擎没有结果,AI 也说不知道,问我是不是拼写错误,可能是 v-model ?
于是我找到这段代码的作者,同事小爽,说:"这里是不是拼写错了,应该是 v-model 吧?"
小爽嘿嘿一笑:"兄弟,这个是我自研的指令,很高级,跟 v-model 一样能双向绑定数据!"
原来,小爽的英文名叫 Mortal,所以他写了一个 Vite 插件可以把所有的 v-mortal 在编译时替换为 v-model 。
这样做很巧妙,Vue 编译器无需处理 v-mortal ,没有额外的运行时开销,一起来看看他是怎么实现的吧!
编写插件
以 Vite 8 为例,查阅 插件 API 简单示例 文档, 先新建一个 VMortalPlugin 插件函数:
ts
import type { Plugin } from "vite";
export function VMortalPlugin(): Plugin {
return {
name: "v-mortal",
enforce: "pre",
transform: {
filter: {
id: /\.vue$/
},
handler(code) {
// ...
}
}
};
}
- 使用
v-mortal作为name标识插件,这个名称会出现在日志和错误信息中; - 因为需要在 Vue 编译器之前处理,所以将
enforce设置为pre,参考 插件顺序; transform.filter.id传/\.vue$/表示只处理 Vue 文件,通常来说只有.vue文件中才会使用指令。
接下来在 transform.handler 中使用 vue/compiler-sfc 提供的 parse() 解析 Vue 代码,并提取出 template 代码块:
ts
import { parse } from "vue/compiler-sfc";
const { template } = parse(code).descriptor;
然后遍历 template.ast 找到所有的 v-mortal 指令,并使用 MagicString 替换为 v-model :
ts
const visit = (node: TemplateChildNode) => {
// 只处理元素节点(例如 <input>、<CustomComponent> 等)
if (node.type === NodeTypes.ELEMENT) {
for (const prop of node.props) {
// 查找自定义指令 v-mortal
if (prop.type === NodeTypes.DIRECTIVE && prop.name === MORTAL) {
const start = prop.loc.start.offset;
// 将 v-mortal 替换为 v-model
// 只替换指令名称部分,保留参数和值等内容
ms.overwrite(start, start + V_MORTAL.length, "v-model");
}
}
}
// 如果当前节点包含 children,则继续递归遍历子节点
if ("children" in node) {
for (const child of node.children) {
// 过滤掉简单表达式节点,只处理可能包含模板结构的节点
if (
typeof child === "object" &&
child.type !== NodeTypes.SIMPLE_EXPRESSION
) {
visit(child);
}
}
}
};
// 从模板 AST 的根 children 开始遍历所有节点
for (const child of template.ast.children) {
visit(child);
}
- 使用 AST 遍历可以很精确快速地找到
v-mortal指令,而不必使用正则考虑太多边界条件,也不会错误修改字符串或者注释中的v-mortal; - MagicString 是一个轻量且高效的工具,用于操作字符串以及生成源码映射(source-map)。
处理完毕,最后返回 code 和 map(source-map):
ts
return {
code: ms.toString(),
map: ms.generateMap({ hires: true })
};
至此,插件编写完成,现在 Vue 代码中的所有 v-mortal 指令都会在编译阶段被自动替换为 v-model 。

完整代码如下:
ts
// v-mortal.ts
import type { TemplateChildNode } from "@vue/compiler-core";
import { NodeTypes } from "@vue/compiler-core";
import type { Plugin } from "vite";
import { MagicString, parse } from "vue/compiler-sfc";
const MORTAL = "mortal";
const V_MORTAL = `v-${MORTAL}`;
export function VMortalPlugin(): Plugin {
return {
name: "v-mortal",
enforce: "pre",
transform: {
filter: {
id: /\.vue$/
},
handler(code) {
const { template } = parse(code).descriptor;
if (template == null || template.ast == null) {
return;
}
const ms = new MagicString(code);
const visit = (node: TemplateChildNode) => {
if (node.type === NodeTypes.ELEMENT) {
for (const prop of node.props) {
if (prop.type === NodeTypes.DIRECTIVE && prop.name === MORTAL) {
const start = prop.loc.start.offset;
ms.overwrite(start, start + V_MORTAL.length, "v-model");
}
}
}
if ("children" in node) {
for (const child of node.children) {
if (
typeof child === "object" &&
child.type !== NodeTypes.SIMPLE_EXPRESSION
) {
visit(child);
}
}
}
};
for (const child of template.ast.children) {
visit(child);
}
return {
code: ms.toString(),
map: ms.generateMap({ hires: true })
};
}
}
};
}
智能提示
大家平时在写 v-model 的时候,输入 v-m IDE 就会智能提示 v-model ,那么怎样才能让 v-mortal 也有智能提示呢?
小爽很聪明,他又翻阅了 Vue 官方文档,找到 为自定义全局指令添加类型 部分的说明,他仿照提供了一个 v-mortal.d.ts 。
ts
// v-mortal.d.ts
import type { Directive } from "vue";
export type MortalDirective = Directive<HTMLElement, any>;
declare module "vue" {
export interface GlobalDirectives {
vMortal: MortalDirective;
}
}
这样 v-mortal 也会有智能提示啦,开发体验直接拉满!

总结回顾
通过这个小案例,我们了解了 Vite 插件开发的基本原理以及使用 TypeScript 提升开发体验的工程实践。
通过编译时转换来扩展 Vue 的语法能力,让开发者拥有更符合团队习惯的写法,同时又不引入额外的运行时成本。
v-mortal 虽然只是一个并不恰当的简单示例,但它展示了前端工程化中非常重要的思想:将复杂逻辑尽可能提前到构建阶段处理,把运行时的负担转移到编译器和工具链。
注意,这个案例只是用于演示插件开发,日常工作中不推荐大家这样做,以免被热心同事"亲切问候"~ 😁
写在最后
我是小何 xiaohe0601,热爱代码,目前专注于 AI 和前端开发领域。
欢迎关注我的微信公众号「小何不会写代码」,我会不定期分享一些开发心得、最佳实践以及技术探索等内容,希望能够帮到你!