vite_react 插件 find_code 最終版本
當初在開發一個大型項目的時候,第一次接觸 vite 構建,由于系統功能很龐大,在問題排查上和模塊開發上比較耗時,然后就開始找解決方案,find-code 插件方案就這樣實現出來了,當時覺得很好使,開發也很方便,于是自己開始琢磨自己開發一下整個流程 現如今也是零碎花費了兩天時間做出了初版本的 find_code 插件
源碼如下
// index.ts
import fs from "fs/promises";
import parser from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
export const processFile = async (filePath: string, filePathIndexMap: any) => {try {// 讀取文件內容const code = await fs.readFile(filePath, "utf8");// 解析代碼生成 ASTconst ast = parser.parse(code, {sourceType: "module",plugins: ["jsx", "typescript"],});// 遍歷 AST(traverse as any).default(ast, {JSXOpeningElement(path: any) {const line = path?.node?.loc?.start?.line;const value = `${filePath}:${line}`;const index = `${Object.keys(filePathIndexMap)?.length || 0}`;filePathIndexMap[index] = value;const pathAttribute = {type: "JSXAttribute",name: { type: "JSXIdentifier", name: "data-path" },value: {type: "StringLiteral",value: index,},};// 檢查是否已經存在 path 屬性,如果不存在則添加const existingPathAttribute = path.node.attributes.find((attr: any) => {return (attr?.name &&attr?.name.type === "JSXIdentifier" &&attr?.name.name === "data-path");});if (!existingPathAttribute) {path.node.attributes.push(pathAttribute);}},});// 生成新代碼,設置 retainLines 為 true 避免生成不必要的轉義序列const { code: newCode } = (generate as any).default(ast, {retainLines: true,jsescOption: {minimal: true,},});return newCode;} catch (error) {console.error("處理文件時出錯:", error);}
};
// vite-plugin-react-line-column.ts
import { createFilter } from "@rollup/pluginutils";
import { execSync } from "child_process";
import type { Plugin } from "vite";
import { processFile } from "./index";
import { parse } from "url";const vitePluginReactLineColumn = (): Plugin => {const filePathIndexMap = {} as any;return {// 定義插件名稱name: "vite-plugin-react-line-column",// 設置插件執行順序為 'post',在其他插件之后執行enforce: "pre",// 僅在開發環境執行apply: "serve",// 轉換代碼的 hookasync transform(code, id) {const filter = createFilter(/\.(js|jsx|ts|tsx)$/);if (!filter(id)) {return null;}const transformedCode = (await processFile(id, filePathIndexMap)) as any;return {code: transformedCode,map: null,};},async configureServer(server) {// 提供接口獲取文件路徑和索引的映射server.middlewares.use("/getPathIndexMap", (req, res) => {res.setHeader("Content-Type", "application/json");res.end(JSON.stringify(filePathIndexMap));});// 提供接口給一個路徑跳轉到 vscodeserver.middlewares.use("/jumpToVscode", (req, res) => {const query = parse(req?.url as string, true).query;const filePath = query.path;console.log(filePath, "filePath");if (!filePath || filePath == "undefined") {res.statusCode = 400;return res.end(JSON.stringify({ success: false, message: "缺少路徑參數" }));}try {// 構建打開文件的命令const command = `code -g "${filePath}"`;// 同步執行命令execSync(command);res.setHeader("Content-Type", "application/json");res.end(JSON.stringify({ success: true }));} catch (error) {res.statusCode = 500;res.end(JSON.stringify({ success: false, message: "打開文件失敗" }));}});},};
};export default vitePluginReactLineColumn;
// 創建選擇框
function createSelector() {const selector = document.createElement("div");selector.style.cssText = `position: fixed;border: 2px solid #007AFF;background: rgba(0, 122, 255, 0.1);pointer-events: none;z-index: 999999;display: none;`;document.body.appendChild(selector);return selector;
}// 初始化選擇器
const selector = createSelector();
let isSelecting = false;
let selectedElement = null;
let pathIndexMap = {};const init = async () => {const response = await fetch("/getPathIndexMap");pathIndexMap = await response.json();
};/* 根據當前元素遞歸查找 他的parentNode 是否有 data-path 沒有就繼續 直到 查到 body 標簽結束 */
function findParentDataPath(element) {if (!element) return null;if (element.nodeType !== 1 || element.tagName == "body") return null; // 確保是元素節點if (element.hasAttribute("data-path")) {return element.getAttribute("data-path");}return findParentDataPath(element.parentNode);
}document.addEventListener("click", (e) => {if (isSelecting && selectedElement) {console.log("[VSCode跳轉插件] 回車鍵觸發跳轉");const dataIndex = selectedElement.getAttribute("data-path");const vscodePath = pathIndexMap[dataIndex];if (vscodePath) {fetch(`/jumpToVscode?path=${vscodePath}`);} else {/* 如果沒有vscodePath 即沒有找到data-path屬性 */const dataIndex = findParentDataPath(selectedElement);const vscodePath = pathIndexMap[dataIndex];if (vscodePath) {fetch(`/jumpToVscode?path=${vscodePath}`);}}console.log("[VSCode跳轉插件] vscodePath", vscodePath);isSelecting = false;selector.style.display = "none";selectedElement = null;}
});
// 監聽快捷鍵
document.addEventListener("keydown", (e) => {if (e.altKey && e.metaKey) {console.log("[VSCode跳轉插件] 選擇模式已激活");isSelecting = true;selector.style.display = "block";document.body.style.cursor = "pointer";init();}// 添加回車鍵觸發if (e.key === "Enter" && isSelecting && selectedElement) {console.log("[VSCode跳轉插件] 回車鍵觸發跳轉");const dataIndex = selectedElement.getAttribute("data-path");const vscodePath = pathIndexMap[dataIndex];if (vscodePath) {fetch(`/jumpToVscode?path=${vscodePath}`);}console.log("[VSCode跳轉插件] vscodePath", vscodePath);isSelecting = false;selector.style.display = "none";selectedElement = null;}
});document.addEventListener("keyup", (e) => {if (!e.altKey && !e.metaKey) {console.log("[VSCode跳轉插件] 選擇模式已關閉");isSelecting = false;selector.style.display = "none";selectedElement = null;}
});// 監聽鼠標移動
document.addEventListener("mousemove", (e) => {if (!isSelecting) return;const element = document.elementFromPoint(e.clientX, e.clientY);if (element && element !== selectedElement) {selectedElement = element;const rect = element.getBoundingClientRect();selector.style.left = rect.left + "px";selector.style.top = rect.top + "px";selector.style.width = rect.width + "px";selector.style.height = rect.height + "px";console.log("[VSCode跳轉插件] 當前選中元素:", element);}
});
// package.json 對應版本
{"name": "vite","private": true,"version": "0.0.0","type": "module","scripts": {"1": "node ./node/parser.js","2": "node ./node/index.ts","dev": "vite","build": "tsc -b && vite build","lint": "eslint .","preview": "vite preview"},"dependencies": {"@babel/traverse": "^7.28.3","@types/antd": "^0.12.32","antd": "^5.27.2","fs": "^0.0.1-security","path": "^0.12.7","react": "^19.1.1","react-dom": "^19.1.1","url": "^0.11.4"},"devDependencies": {"@babel/generator": "^7.28.3","@babel/parser": "^7.28.3","@eslint/js": "^9.33.0","@rollup/pluginutils": "^5.2.0","@types/node": "^24.3.0","@types/react": "^19.1.10","@types/react-dom": "^19.1.7","@vitejs/plugin-react": "^5.0.0","eslint": "^9.33.0","eslint-plugin-react-hooks": "^5.2.0","eslint-plugin-react-refresh": "^0.4.20","globals": "^16.3.0","typescript": "~5.8.3","typescript-eslint": "^8.39.1","vite": "^7.1.2"}
}
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import vitePluginReactLineColumn from "./plugin/vite-plugin-react-line-column.ts";
export default defineConfig({plugins: [react(), vitePluginReactLineColumn()],
});
實現思路
1. 首先我們可以先練習 怎么樣將我們的 jsx 代碼插入我們想要的一些屬性進去
// 1. 解析我們的代碼生成 AST
const ast = parser.parse(code, {sourceType: "module",plugins: ["jsx"],
});
// 遍歷 AST 有一個屬性 JSXOpeningElement 就是我們的 jsx 標簽(traverse as any).default(ast, {JSXOpeningElement(path: any) {const line = path?.node?.loc?.start?.line;const value = `${filePath}:${line}`;const index = `${Object.keys(filePathIndexMap)?.length || 0}`;filePathIndexMap[index] = value;const pathAttribute = {type: "JSXAttribute",name: { type: "JSXIdentifier", name: "data-path" },value: {type: "StringLiteral",value: index,},};// 檢查是否已經存在 path 屬性,如果不存在則添加const existingPathAttribute = path.node.attributes.find((attr: any) => {return (attr?.name &&attr?.name.type === "JSXIdentifier" &&attr?.name.name === "data-path");});if (!existingPathAttribute) {path.node.attributes.push(pathAttribute);}},});
// 生成的新代碼 再轉回去// 生成新代碼,設置 retainLines 為 true 避免生成不必要的轉義序列const { code: newCode } = (generate as any).default(ast, {retainLines: true,jsescOption: {minimal: true,},});
在
generate
函數中,我們傳入了一個配置對象,其中:
retainLines: true
盡量保留原始代碼的行號和格式,減少不必要的換行和格式化。
jsescOption: { minimal: true }
jsesc 是 @babel/generator 內部用于處理字符串轉義的工具,
minimal: true 表示只對必要的字符進行轉義,避免生成不必要的 Unicode 轉義序列。
通過這些配置,可以確保生成的代碼中不會出現亂碼的 Unicode 轉義序列。
請確保已經安裝了所需的 Babel 相關依賴,如果沒有安裝,可以使用以下命令進行安裝:
npm install @babel/parser @babel/traverse @babel/generator
2. 然后我們使用 vite 插件 hook 來進行我們數據處理
// 轉換代碼的 hookasync transform(code, id) {const filter = createFilter(/\.(js|jsx|ts|tsx)$/);if (!filter(id)) {return null;}const transformedCode = (await processFile(id, filePathIndexMap)) as any;return {code: transformedCode,map: null,};},
這里可以進行優化,就是已經獲取到 code 了 就不需要將這個 path(id)傳遞給這個函數,可以直接優化這個函數直接接受 code 就行,不需要再讀取文件
3. 使用 vite 插件 hook 來提供接口
1、 收集所有索引和路徑的映射接口2、 提供接口給一個路徑跳轉到 vscode
4. 實現 js 代碼注入
使用純 js 實現事件監聽和命令執行
- 監聽 快捷鍵 option + command 開啟我們的選擇模式 并調用接口獲取映射關系
- 監聽 鼠標移動 獲取當前元素寬、高設置給這個 createSelector 的樣式 讓他展示出來
- 監聽 鼠標點擊事件 如果選擇模式開啟了 切 選中元素 獲取這個元素的 data-path 屬性然后根據映射關系調用 vscode 跳轉接口 跳轉到對應的代碼即可