🤔 v-mortal 是什么?!我只知道 v-model 啊!

昨天,我正在 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 和前端开发领域。

欢迎关注我的微信公众号「小何不会写代码」,我会不定期分享一些开发心得、最佳实践以及技术探索等内容,希望能够帮到你!

相关推荐
vx-程序开发12 小时前
【计算机毕设】51300童装超市管理系统的设计与实现
java·vue.js·spring boot·源码·课程设计·毕设·大作业
ym hyd 11112 小时前
题库建立与考试系统源码 Java+SpringBoot+Vue3 前后分离
java·开发语言·vue.js·spring boot·毕设
YZ12255214 小时前
【Docker专题】使用Docker部署Vue-Flask项目前后端分离版【前端Docker部署】
前端·vue.js·docker
专业程序开发源15 小时前
springboot古城景区管理系统88564-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·php·课程设计
小蒜学长16 小时前
基于SSM+VUE的电影售票平台的设计与实现(代码+数据库+LW)
java·vue.js·spring boot·后端·ssm框架·电影售票平台
Q264336502316 小时前
【有源码】基于Spring Boot+Vue的骑行爱好者平台的设计与实现-计算机毕设选题推荐
java·vue.js·spring boot·毕业设计·课程设计·数据可视化·源代码
FYKJ_201016 小时前
SpringSecurity教室智能预约系统73972-计算机课程设计、毕业设计
java·vue.js·spring boot·python·mysql·spring·django
专业程序开发源16 小时前
springboot酒店会员点餐系统22339-计算机课程设计、毕业设计
java·javascript·vue.js·spring boot·后端·php·课程设计
FYKJ_201016 小时前
springboot游泳馆管理系统79222-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·mysql·django·课程设计
现任明教教主~18 小时前
SpringBoot+Vue学生宿舍管理系统源码 带部署文档
vue.js·spring boot·后端