🤔 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)。

处理完毕,最后返回 codemap(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 和前端开发领域。

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

相关推荐
xm_xm_xm_12 小时前
Vue 3 项目开发常用操作详细汇总
前端·vue.js
雪芽蓝域zzs3 小时前
Vue3+Vite 本地数据模拟 JSON 读取和Mock 方案完整讲解总结
前端·vue.js
:-)3 小时前
idea中的vue文件没有高亮显示
前端·javascript·vue.js·ecmascript·intellij-idea
Southern Wind4 小时前
从一段文字到一部动态漫:如何打造全流程分镜创作工作台 StoryCanvas AI
前端·javascript·vue.js·人工智能
Mh7 小时前
听说我兄弟喜欢跑车,所以必须安排上
前端·javascript·vue.js
zttbee7 小时前
vue学习(白话功能版)
前端·vue.js·学习·前端框架
nicole bai7 小时前
前端封装el-table及注意点
前端·javascript·vue.js
晨枫阳7 小时前
Vue 前端构建工具链完全理解指南
前端·javascript·vue.js
ly76891 天前
Vue 3 从入门到工程实践:基础语法、组件化、路由、状态管理与项目部署
前端·javascript·vue.js