别再用关键字搜了!手搓一个Vite插件,为页面上的标签打上标记

背景

相信大家都遇到过类似的场景,前端业务开发的过程中,产品提了个修改文案或者某个页面某个按钮的样式要做一下调整。首先应该定位到修改的代码位置,一般比较常规的方案就是复制页面上要修改部分的关键字,然后在Code中查找,不过这个方法有一个常见的问题,就是可能这个要修改的地方有很多,会搜索出很多拥有这个关键字的文件。但可能我们只需要修改其中的某一个或者一部分。因此,定位要修改的过程,会存在大量的人力成本。

最好的解决办法就是在页面上获取一个唯一的id,然后直接可以在Code中定位,由于文件绝对路径本身是唯一的,因此我们需要在要修改的地方就可以拿到这个位置所在文件的绝对路径,这样就不会存在重复的情况。

通用解决方案

其实,业界上对于该场景已经落地了相关的产品,比如Vite的code-inspector-plugin

js 复制代码
npm install code-inspector-plugin -D

在vite.config.js中做如下配置:

js 复制代码
// vite.config.js
import { defineConfig } from 'vite';
import { codeInspectorPlugin } from 'code-inspector-plugin';

export default defineConfig({
  plugins: [
    codeInspectorPlugin({
      bundler: 'vite',
    }),
  ],
});

使用说明:

  • 启动开发服务器
  • 按下 Alt + Shift (Mac 用户使用 Option + Shift)
  • 点击页面上任意元素,即可自动跳转到对应源码位置

摘自原文:www.cnblogs.com/mengqc1995/...

这是现有的解决方案,通过这个插件我们可以在开发时很轻松地定位到要修改的代码位置。

接下来,我打算手动实现一个类似的插件,这个插件要实现一个核心功能------干预Vite的编译,给组件的模板代码打标记。以达到开发者可以直接在控制台拿到这个标记去找到这个修改点的文件位置。

建议大家可以手动写一下这个逻辑,锻炼一下自己的js代码编写能力,过程中会涉及到一些字符串的处理,体会直接干预编译源码过程的魅力。

手动实现

Vite插件主函数

干预Vite编译,需要我们写一个自定义的Vite插件,Vite 插件是一种扩展 Vite 功能的方式。通过编写或使用插件,你可以在 Vite 的编译、打包、开发服务器等多个阶段中执行自定义逻辑,实现特定的功能。

OK,先创建一个js文件,我这里命名为ViteFileTagPlugin.js,在该文件内默认导出一个函数。

js 复制代码
export default function ViteFileTagPlugin() {
  return {
    load(id) {},
  };
}

这里我们需要在返回的对象中写一个load方法,该方法是一个Vite插件的钩子,用于在加载模块时,提供自定义的加载逻辑。

在load方法中可以得到一个参数id,该参数是编译的目标文件路径,我们通过这个路径可以读取文件内的源代码字符串。这一步很重要,我们只有获得源码才能对其进行相关操作。不过,需要注意的是,不是所有的文件都需要进行Debug标记,通常我们仅需要对业务代码组件做标记,因此我们需要做判断,判断文件后缀是否符合我们预期要处理的文件类型。

js 复制代码
export default function ViteFileTagPlugin() {
  return {
    load(id) {
      // 对于react项目,我们需要对.jsx和.tsx后缀的文件进行处理
      if (id.endsWith('.jsx') || id.endsWith('.tsx')) {
        try {
          // 读取文件内容
          const content = fs.readFileSync(id, 'utf-8');
          // 将文件内容和路径作为参数传递给标识函数
          return addComponentTag(content, id);
        } catch (error) {
          console.log('解析失败', error);
        }
      }
    },
  };
}

大概框架写好了,下面我们就来重点处理一下addComponentTag这个方法。

组件标识方法

首先在写这部分逻辑之前,可以思考几个问题:

  1. 如何找到标识的插入位置?
  2. 如果存在对应标识,该怎么处理?
  3. 如何尽可能多的覆盖不同的标识插入场景?

这会涉及到字符串的加工处理,归根到底,在该场景下,对源码的操作本质上就是字符串的操作。

首先,这个标识应该是一个标签属性,因为它可以体现在页面上,我们可以根据属性值找到对应的源码文件,那么属性值就应该是源码绝对路径。因此,标识形式应该是data-[name]=[value]这种形式。

确定函数参数,首先我们肯定需要源代码、对源代码上标签的属性标识、标记的位置以及标记名称。下面我们一一介绍这几个参数存在的意义。

我们可以先写出一个函数框架,首先需要区分如果要处理的源代码中包括对应的标识,那么我们应该略过这个文件。

js 复制代码
function addComponentTag(code, path, mark = 'div', property = 'data-compath') {
    if (code.indexOf(property) !== -1) {
      console.log(`>>> ${path} 该文件已添加${property}属性`);
      return;
    }
    // 主逻辑...
}

如果没有匹配到property属性,那么开始查找标识的首个插入点,如果找到,则继续,否则直接返回源码,不做任何处理。找到后,我们需要获取一个范围,即插入位置为起始位置(起始索引),字符'>'为结束位置(截止索引),大家可以脑补一下为什么要这么做,这里我们可以简单做一个演示。

js 复制代码
const firstMarkIndex = code.search(`<${mark}`);
if (firstMarkIndex !== -1) {
  // 处理后的源码
  return code;
}
// 未处理的源码
return code;

这里理解了,我们继续往下走,收集完起始索引和截止索引之后,将二者相加,能得到相对源码的插入标签后的实际内容的起始位置索引。这里不太好理解,我们就简单认为这里的索引是操作完插入动作后剩下源码的开始位置就行。

js 复制代码
if (insertIndex !== -1) {
    const rawIndex = insertIndex + firstMarkIndex;
}

可能这里你会问,为什么我要获取这么多索引,因为我们后面会拿着这些索引去操作字符串,这些索引就是操作的位置标志。

下面,我们就进入到源码操作的核心模块,这里我们需要再定义一个新的方法,这个方法包括源码内容、文件路径、标记索引和标记名称四个参数。

js 复制代码
function rawDeepSearch(content, filePath, tagIdx, property) {
  // 主逻辑...
  return content;
}

这里我们第一步,先对源码进行正则匹配,找到第一个匹配组,这个匹配组就是当前源码的插入匹配,若匹配到,就进行实际的标识插入操作。

js 复制代码
const group = /<(\w+)[ ]+/.exec(content.slice(tagIdx));
if (group?.index !== -1 && group?.[0]?.length) {
    const dataCompath = `${property}='${filePath.slice(
      filePath.indexOf('src/'),
    )}'`;
}

这里我们拼出来一个key=value,key为传入的自定义属性名,值则是当前文件从src开始截取的绝对路径。 然后开始将dataCompath插入进源码。

js 复制代码
content = `${content.slice(0, tagIdx + group.index)}${content.slice(
      tagIdx + group.index,
      tagIdx + group.index + group[0].length,
    )}${dataCompath} ${content.slice(tagIdx + group.index + group[0].length)}`;

这里本质上就是字符串的拼接,我们将字符串分割成插入标签前的段插入标签开始索引至插入点的段实际插入内容段插入之后的剩余代码段,然后将拼接完的源码字符串重新赋值,完成源码操作的替换。

然后,更新标记索引,将标记索引指向插入的标记字符串末尾,为下一次标识插入做准备。

js 复制代码
tagIdx += group.index + group[0].length + dataCompath.length;

由于html标签是嵌套的,因此我们需要递归进行标记插入操作。

js 复制代码
function rawDeepSearch(content, filePath, tagIdx, property) {
  const group = /<(\w+)[ ]+/.exec(content.slice(tagIdx));
  if (group?.index !== -1 && group?.[0]?.length) {
    // 插入逻辑
    return rawDeepSearch(content, filePath, tagIdx, property);
  }
  return content;
}

核心插入方法到这里差不多就实现完成了,然后我们回到刚才的addComponentTag方法,调用rawDeepSearch,返回deepResult,然后我们将完成插入的源码字符串和不参与插入逻辑的部分源码进行最后拼接,就可以得到最终属性标识插入后的完整源码,最后将结果返回。

js 复制代码
const deepResult = rawDeepSearch(
  code.slice(rawIndex),
  path,
  0,
  property,
);
const result = `${code.slice(0, rawIndex)}${deepResult}`;
console.log(chalk.green(`>>> ${path} 完成标记写入`));
return result;

其中 code.slice(rawIndex)代表要参与插入逻辑的源代码段,code.slice(0, rawIndex)代表不需要参与插入逻辑的源代码段。

最后我们将addComponentTag放入主函数中调用并返回,得到的最终结果就是编译后的带自定义标识的源码字符串,我们通过自己的逻辑干预了Vite源码的编译,并将该编译结果传递给后续的插件钩子继续执行其他编译逻辑。 启动Vite,随便找个页面,控制台看下效果⬇️

可以看到,每个标签上都已经打上了data-compath的属性了,当然,这个属性名可以通过参数定义,我们直接在vite.config.js中进行定义。

js 复制代码
import viteFileTagPlugin from './ViteFileTagPlugin';
export default defineConfig({
    plugins: [viteFileTagPlugin({
      mark: 'div',
      property: 'data-compath',
    })]
})
完整代码
js 复制代码
/** ViteFileTagPlugin.js */
const fs = require('fs');
const chalk = require('chalk');

function addComponentTag(code, path, mark = 'div', property = 'data-compath') {
  try {
    if (code.indexOf(property) !== -1) {
      console.log(chalk.red(`>>> ${path} 该文件已添加${property}属性`));
      return;
    }
    const firstMarkIndex = code.search(`<${mark}`);
    if (firstMarkIndex !== -1) {
      const firstMark = code.slice(firstMarkIndex);
      const insertIndex = firstMark.indexOf('>');
      if (insertIndex !== -1) {
        const rawIndex = insertIndex + firstMarkIndex;
        const deepResult = rawDeepSearch(
          code.slice(rawIndex),
          path,
          0,
          property,
        );
        const result = `${code.slice(0, rawIndex)}${deepResult}`;
        console.log(chalk.green(`>>> ${path} 完成标记写入`));
        return result;
      }
      return code;
    }
    return code;
  } catch (error) {
    console.log(chalk.red('>>> 读取失败:', error));
  }
}

function rawDeepSearch(content, filePath, tagIdx, property) {
  const group = /<(\w+)[ ]+/.exec(content.slice(tagIdx));
  if (group?.index !== -1 && group?.[0]?.length) {
    const dataCompath = `${property}='${filePath.slice(
      filePath.indexOf('src/'),
    )}'`;
    content = `${content.slice(0, tagIdx + group.index)}${content.slice(
      tagIdx + group.index,
      tagIdx + group.index + group[0].length,
    )}${dataCompath} ${content.slice(tagIdx + group.index + group[0].length)}`;
    tagIdx += group.index + group[0].length + dataCompath.length;
    return rawDeepSearch(content, filePath, tagIdx, property);
  }
  return content;
}

export default function ViteFileTagPlugin({ mark, property }) {
  return {
    load(id) {
      if (id.endsWith('.jsx') || id.endsWith('.tsx')) {
        try {
          const content = fs.readFileSync(id, 'utf-8');
          return addComponentTag(content, id, mark, property);
        } catch (error) {
          console.log(chalk.red('解析失败', error));
        }
      }
    },
  };
}
相关推荐
YL雷子24 分钟前
纯前端使用ExcelJS插件导出Excel
前端·vue·excel
什么什么什么?32 分钟前
el-table高度自适应vue页面指令
前端·javascript·elementui
码上暴富4 小时前
axios请求的取消
前端·javascript·vue.js
JefferyXZF4 小时前
Next.js 初识:从 React 到全栈开发的第一步(一)
前端·全栈·next.js
一只韩非子5 小时前
AI时代,程序员如何优雅地搞定页面设计?
前端·ai编程
新中地GIS开发老师5 小时前
2025Mapbox零基础入门教程(14)定位功能
前端·javascript·arcgis·gis·mapbox·gis开发·地理信息科学
tager5 小时前
Vue 3 组件开发中的"双脚本"困境
前端·vue.js·代码规范
烛阴6 小时前
Int / Floor
前端·webgl
excel6 小时前
使用 PWA 时,为什么你必须手动添加更新逻辑,否则会报错?
前端
Moment6 小时前
Node.js 这么多后端框架,我到底该用哪个?🫠🫠🫠
前端·后端·node.js