LOGO OA教程 ERP教程 模切知識(shí)交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

[轉(zhuǎn)帖]【推薦】前端調(diào)試神器,支持 Vue、React 等多框架!!!

liguoquan
2024年3月1日 16:14 本文熱度 881
:【推薦】前端調(diào)試神器,支持 Vue、React 等多框架!!!


背景及相關(guān)信息

不知道你是否遇到過(guò)產(chǎn)品或者測(cè)試給你一個(gè)頁(yè)面讓你改一點(diǎn)東西,你卻找不到頁(yè)面源代碼在哪里的場(chǎng)景?對(duì)于一些大型項(xiàng)目,文件數(shù)量多、文件層級(jí)深、代碼行數(shù)多,查找一個(gè)頁(yè)面上組件對(duì)應(yīng)的源代碼位置,往往需要花費(fèi)大量時(shí)間。

為了解決這個(gè)問(wèn)題,我開發(fā)了 code-inspector-plugin 插件,只需要點(diǎn)擊頁(yè)面上的元素,就能夠自動(dòng)打開 vscode 定位到源代碼。已經(jīng)在快手內(nèi)部30+項(xiàng)目中接入了使用,取得了不錯(cuò)的反響。效果如下圖所示:

點(diǎn)擊下述的 demo,也可以快速在線體驗(yàn)效果:

  • vue online demo[1]
  • react online demo[2]
  • preact online demo[3]
  • solid online demo[4]
  • 接入:想要使用的小伙伴,可以參考code-inspector-plugin接入文檔[5]接入使用
  • github 源碼:覺(jué)得插件好用的可以辛苦動(dòng)下小手幫作者 github 點(diǎn)個(gè) star:code-inspector[6]

code-inspector-plugin 的優(yōu)點(diǎn)

其實(shí) code-inspector-plugin 是之前我看到過(guò)一篇 react 點(diǎn)擊頁(yè)面元素定位源代碼的文章,受到啟發(fā)后實(shí)現(xiàn)的。但是相比而言, code-inspector-plugin 在支持場(chǎng)景的豐富性以及接入的便捷程度上,都得到了巨大的提升,具備以下優(yōu)勢(shì):

  • 支持的打包器更加廣泛:支持 webpack/vite/rspack 以及 umi 等一切基于上述三個(gè)打包器實(shí)現(xiàn)的打包工具
  • 支持的框架及場(chǎng)景更加廣泛:支持 vue2/vue3/react/preact/solid 框架以及 next/nuxt 等SSR場(chǎng)景(以及一切以 vue2/vue3/react/preact/solid 框架為基礎(chǔ)封裝的 SSR 場(chǎng)景),支持在微前端中使用。
  • 支持多種系統(tǒng)及 IDE:支持 Mac、Windows 和 Linux 系統(tǒng),支持 vscode、webstorm、atom、hbuilderX、IDEA、phpsotrm 等多種 IDE,也支持自定義 IDE 的支持
  • 接入更加簡(jiǎn)便,對(duì)代碼無(wú)侵入:無(wú)論是在什么項(xiàng)目中,只需要在 webpack/vite/rspack 的配置中添加 code-inspector-plugin 插件即可,不需要修改任何源代碼或者其他的配置
  • 自動(dòng)識(shí)別環(huán)境:插件內(nèi)部會(huì)針對(duì) webpack/vite/rspack 開發(fā)環(huán)境下的一些內(nèi)置信息,自動(dòng)識(shí)別環(huán)境,僅在開發(fā)環(huán)境下生效,不會(huì)影響生產(chǎn)環(huán)境

code-inspector-plugin 實(shí)現(xiàn)原理

下面我們重點(diǎn)解析一下 code-inspector-plugin 的實(shí)現(xiàn)原理,插件的整體功能可以簡(jiǎn)單拆解為以下幾部分:

  • 參與源碼編譯:打包工具(webpack/vite/rspack)編譯時(shí),code-inspector-plugin 插件會(huì)參與編譯過(guò)程,對(duì)于 vue/jsx 語(yǔ)法會(huì)進(jìn)行 ast 解析,獲取到 dom 部分的源代碼所在的 文件路徑、行、列 信息,并將這些信息作為 dom 上的 attribute 額外添加進(jìn)去。
  • 運(yùn)行時(shí)交互代碼:編譯完成后,插件會(huì)向網(wǎng)頁(yè)中注入監(jiān)聽按鍵定位源代碼的交互邏輯,當(dāng)用戶點(diǎn)擊定位 dom 時(shí),能夠獲取 dom 的 attribute 上的 文件路徑、行、列 信息,將信息發(fā)送一個(gè) http 請(qǐng)求給后臺(tái)
  • 啟動(dòng)一個(gè) node server 服務(wù):在后臺(tái)啟動(dòng)一個(gè) node server 服務(wù),用于接收上一步發(fā)送過(guò)來(lái)的 http 請(qǐng)求
  • 識(shí)別并打開 IDE:node server 收到請(qǐng)求后,根據(jù)請(qǐng)求帶過(guò)來(lái)的 文件路徑、行、列 信息,使用 node 的 spawn 或者 exec 子進(jìn)程打開 IDE,并將鼠標(biāo)定位到 IDE 對(duì)應(yīng)的位置

編譯 vue/jsx 源代碼

要參與源代碼的編譯過(guò)程,對(duì)于 vite 項(xiàng)目,我們可以通過(guò) vite 插件的 transform 函數(shù)入口中實(shí)現(xiàn);對(duì)于 webpack/rspack 項(xiàng)目,可以實(shí)現(xiàn)一個(gè) loader 實(shí)現(xiàn)。不同的打包工具只是對(duì)應(yīng)的入口不同,而對(duì)于 vue/jsx 語(yǔ)法的編譯和解析過(guò)程都是公用的。

編譯 vue 語(yǔ)法

對(duì)于 vue 語(yǔ)法的編譯,我們可以使用 vue 內(nèi)置的包 @vue/compiler-dom 實(shí)現(xiàn),以及通過(guò) magic-string 包來(lái)向 ast 注入額外的信息,簡(jiǎn)化的代碼如下:

import { parse, transform } from '@vue/compiler-dom';
import MagicString from 'magic-string';

// content 是由 vite transform 函數(shù)或者 webpack/rspack loader 傳過(guò)來(lái)的源代碼
const s = new MagicString(content);

// vue/react 部分內(nèi)置元素添加 attrs 可能報(bào)錯(cuò),不處理
const escapeTags = [
  'style',
  'script',
  'template',
  'transition',
  'keepalive',
  'keep-alive',
  'component',
  'slot',
  'teleport',
  'transition-group',
  'transitiongroup',
  'suspense',
  "fragment"
];

if (fileType === 'vue') {
  // vue template 處理
  const ast = parse(content, {
    comments: true,
  });

  transform(ast, {
    nodeTransforms: [
      ((node: TemplateChildNode) => {
        // node.type === 1 說(shuō)明是元素(排除掉 text、comment 等)
        if (
          !node.loc.source.includes('data-insp-path') &&
          node.type === 1 &&
          escapeTags.indexOf(node.tag.toLowerCase()) === -1
        
) {
          // 向 dom 上添加一個(gè)帶有 filepath/row/column 的屬性
          const insertPosition =
            node.loc.start.offset + node.tag.length + 1;
          const { line, column } = node.loc.start;
          // filePath 也是 vite transform 函數(shù)或者 webpack/rspack loader 傳過(guò)來(lái)的
          const addition = ` data-insp-path="${filePath}:${line}:${column}:${
            node.tag
          }"${node.props.length ? ' ' : ''}`;

          s.prependLeft(insertPosition, addition);
        }
      }
as NodeTransform,
    ],
  });

  return s.toString();
}

編譯 tsx 代碼

對(duì)于 tsx 語(yǔ)法的編譯和解析使用 babel 實(shí)現(xiàn),并且需要引入一些 babel 相關(guān)的包,完成對(duì)于 ts、vueJsx 等場(chǎng)景的兼容,簡(jiǎn)化的代碼如下:

import MagicString from 'magic-string';
import type { TemplateChildNode, NodeTransform } from '@vue/compiler-dom';
import vueJsxPlugin from '@vue/babel-plugin-jsx';
import { parse as babelParse, traverse as babelTraverse } from '@babel/core';
import tsPlugin from '@babel/plugin-transform-typescript';
import importMetaPlugin from '@babel/plugin-syntax-import-meta';
import proposalDecorators from '@babel/plugin-proposal-decorators';

// content 是由 vite transform 函數(shù)或者 webpack/rspack loader 傳過(guò)來(lái)的源代碼
const s = new MagicString(content);

// vue/react 部分內(nèi)置元素添加 attrs 可能報(bào)錯(cuò),不處理
const escapeTags = [
  'style',
  'script',
  'template',
  'transition',
  'keepalive',
  'keep-alive',
  'component',
  'slot',
  'teleport',
  'transition-group',
  'transitiongroup',
  'suspense',
  "fragment"
];

if (fileType === 'jsx') {
  // jsx 處理
  const ast = babelParse(content, {
    babelrc: false,
    comments: true,
    configFile: false,
    plugins: [
      importMetaPlugin,
      [vueJsxPlugin, {}],
      [tsPlugin, { isTSX: true, allowExtensions: true }],
      [proposalDecorators, { legacy: true }],
    ],
  });

  babelTraverse(ast, {
    enter({ node }: any) {
      if (
        node.type === 'JSXElement' &&
        escapeTags.indexOf(
          (node?.openingElement?.name?.name || '').toLowerCase()
        ) === -1 &&
        node?.openingElement?.name?.name
      ) {
        if (
          node.openingElement.attributes.some(
            (attr: any) =>
              attr.type !== 'JSXSpreadAttribute' &&
              attr.name.name === 'data-insp-path'
          )
        ) {
          return;
        }

        // 向 dom 上添加一個(gè)帶有 filepath/row/column 的屬性
        const insertPosition =
          node.openingElement.end -
          (node.openingElement.selfClosing ? 2 : 1);
        const { line, column } = node.loc.start;
        // filePath 也是 vite transform 函數(shù)或者 webpack/rspack loader 傳過(guò)來(lái)的
        const addition = ` data-insp-path="${filePath}:${line}:${column + 1}:${
          node.openingElement.name.name
        }
"${node.openingElement.attributes.length ? ' ' : ''}`
;

        s.prependLeft(insertPosition, addition);
      }
    },
  });
  return s.toString();
}

上面 vue/jsx 編譯完成后,其實(shí)相當(dāng)于在源代碼基礎(chǔ)上為每個(gè) dom 注入了一個(gè) data-insp-path 屬性,最終元素到頁(yè)面上,對(duì)應(yīng)的 dom 就會(huì)添加一個(gè)這樣的屬性,如下圖所示:

運(yùn)行時(shí)交互注入

code-inspector-plugin 插件的交互功能主要包含監(jiān)聽兩部分:

  • 監(jiān)聽組合鍵按住時(shí),鼠標(biāo)在 dom 上移動(dòng)時(shí)會(huì)出現(xiàn) DOM 遮罩層信息
  • 點(diǎn)擊遮罩層會(huì)獲取 DOM attribute 上的源代碼信息,向后臺(tái)發(fā)送一個(gè)請(qǐng)求

這部分功能的實(shí)現(xiàn)上難度不大,就是基礎(chǔ)的 html+js+css,為了保證 js 邏輯和 css 樣式不會(huì)影響到宿主頁(yè)面,我采用了 web component 組件的方式來(lái)封裝了這部分邏輯(基于 lit 實(shí)現(xiàn)的 web component)。具體的實(shí)現(xiàn)細(xì)節(jié)將不多講了,源碼位于 packages/core/src/client/index.ts[7] 文件中。

為了簡(jiǎn)化用戶的使用,不需要用戶手動(dòng)向頁(yè)面中添加交互邏輯的組件,我通過(guò) webpack/vite/rspack 插件,在 development 環(huán)境下將 web component 組件注入到頁(yè)面中。

本地的 Node Server 服務(wù)

Node Server 同樣是插件在 webpack/vite/rspack 開始編譯的時(shí)候啟動(dòng)的,用于監(jiān)聽用戶發(fā)送 http 請(qǐng)求。

我們?cè)O(shè)置了一個(gè)默認(rèn)的端口 6666,為了防止端口沖突,我們需要使用 portFinder 繼續(xù)向下尋找一個(gè)可用的接口去啟動(dòng)服務(wù):

import http from 'http';
import portFinder from 'portfinder';
import path from 'path';
import launchEditor from './launch-editor';

const DefaultPort = 6666;

export function startServer(callback: (port: number) => any, editor?: Editor{
  const server = http.createServer((req: any, res: any) => {
    // 收到請(qǐng)求喚醒vscode
    const params = new URLSearchParams(req.url.slice(1));
    const file = params.get('file'as string;
    const line = Number(params.get('line'));
    const column = Number(params.get('column'));
    res.writeHead(200, {
      'Access-Control-Allow-Origin''*',
      'Access-Control-Allow-Methods''*',
      'Access-Control-Allow-Headers''*',
      'Access-Control-Allow-Private-Network''true',
    });
    res.end('ok');
    launchEditor(file, line, column, editor);
  });

  // 尋找可用接口
  portFinder.getPort({ port: DefaultPort }, (err: Error, port: number) => {
    if (err) {
      throw err;
    }
    server.listen(port, () => {
      callback(port);
    });
  });
}

識(shí)別并打開 IDE

Node Server 接收到了請(qǐng)求后,需要打開用戶的 IDE 并定位到源代碼,這一步是如何實(shí)現(xiàn)的呢?

市面上大多數(shù)的 IDE,多支持通過(guò) {IDE路徑} -g {path}:{line}:{column} 的終端命令,打開 IDE 并將鼠標(biāo)光標(biāo)定位到指定的位置,部分 IDE 還支持在全局安裝命令行工具簡(jiǎn)化使用。以 vscode 為例,有兩種方式:

  1. 在終端通過(guò) vscode 應(yīng)用路徑直接打開應(yīng)用
  2. 通過(guò)安裝 vscode 提供的命令行工具,在終端通過(guò) code 指令喚醒,launching-from-the-command-line[8]

這里我們采用了第二種方式,通過(guò) node 的 spwan 或者 exec 啟動(dòng)一個(gè)子進(jìn)程,執(zhí)行 code -g 文件路徑:行:列 就能打開 vscode 并定位到對(duì)應(yīng)的文件路徑、行、列位置,簡(jiǎn)化代碼如下:

function launchEditor(
  fileName: string,
  lineNumber: unknown,
  colNumber: unknown,
  _editor?: Editor
{
  // others code....

  let [editor, ...args] = guessEditor(_editor);

  // others code....

  _childProcess = child_process.spawn(editor, args, { stdio: 'inherit' });
}

除了如何打開 IDE 的問(wèn)題,另一個(gè)要解決的問(wèn)題是,如果用戶設(shè)備上安裝了多種 IDE,我們要打開哪個(gè) IDE?

這個(gè)功能我們是基于 react-devtools[9] 的源碼實(shí)現(xiàn)了,它會(huì)去匹配用戶當(dāng)前設(shè)備上正在運(yùn)行的進(jìn)程,在 IDE 列表中匹配打開。在此基礎(chǔ)上我們優(yōu)化并豐富了這部分的功能,支持了以下特性:

  • 優(yōu)化了 IDE 的匹配順序:因?yàn)閷?duì)于 web 項(xiàng)目,大多數(shù)開發(fā)者使用的 IDE 是 vscode 或者 webstorm,所以我們會(huì)優(yōu)先匹配這兩個(gè) IDE
  • 支持用戶指定 IDE:支持用戶通過(guò)在 .env.local 文件中指定聲明要打開的 IDE,除了內(nèi)置支持識(shí)別的 IDE 外,用戶也可以用 IDE 可執(zhí)行路徑方式指定(意味著支持所有 IDE)

代碼架構(gòu)設(shè)計(jì)

上面的實(shí)現(xiàn)原理中,我們講述了 code-inspector-plugin 插件的核心內(nèi)容,除了這部分之外,還想分享下我們?cè)诖a可維護(hù)性和用戶使用體驗(yàn)方面所做的努力。

分包提升可維護(hù)性

上述核心內(nèi)容的實(shí)現(xiàn),絕大部分是與 vite/webpack/rspack 等打包器無(wú)關(guān)的,打包器插件只是作為代碼編譯和交互代碼注入的入口承載。所以我們采用 monorepo 架構(gòu),將核心代碼都提取到了 core 中,monorepo 的包如下:

📦packages
┣ 📂code-inspector-plugin --------------------------   入口包
┣ 📂core ----------------------------------------  核心代碼處理
┣ 📂vite-plugin ---------------------------------  vite 插件
┗ 📂webpack-plugin ---------------------------   webpack 插件

其中,vite-plugin 和 webpack-plugin 分別作為 vite 和 webpack 的入口。(rspack 由于在插件系統(tǒng)的設(shè)計(jì)上完全支持了 webpack,所以可以直接使用 webpack-plugin 作為 rspack 的入口,如果后面二者出現(xiàn)差異,會(huì)考慮再分一個(gè) rspack-plugin 的包)。

同時(shí)為了降低用戶在多種打包器中的接入心智,我們使用 code-inspector-plugin 將 vite/webpack/rspack 等不同項(xiàng)目的插件進(jìn)行了整合作為唯一入口,用戶只需要通過(guò) bundler 參數(shù)指定項(xiàng)目的打包器即可,其他配置完全一致。

降低用戶接入本

在降低用戶成本方面,我們主要做了兩件事情:

  1. 為了讓用戶不需要修改任何的源代碼,我們對(duì)于頁(yè)面交互的代碼,直接通過(guò)插件注入,不需要用戶手動(dòng)引入任何的組件,對(duì)用戶代碼無(wú)任何侵入。
  2. 對(duì)于 webpack 和 rspack 的項(xiàng)目,像啟動(dòng) node 服務(wù)這種邏輯是在插件中實(shí)現(xiàn)的,而參與源代碼的編譯需要在 loader 中實(shí)現(xiàn)。雖然讓用戶同時(shí)接入一個(gè) plugin 和一個(gè) loader 成本也沒(méi)有那么高,但是為了最大程度降低用戶接入成本,我們插件會(huì)在 webpack/rspack 編譯前,自動(dòng)將 loader 添加到 module.rules 中,用戶只需要接入一個(gè) plugin 即可,免去了 loader 的接入成本。

原文地址:https://juejin.cn/post/7326002010084311079



該文章在 2024/3/1 16:14:54 編輯過(guò)
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點(diǎn)晴ERP是一款針對(duì)中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國(guó)內(nèi)大量中小企業(yè)的青睞。
點(diǎn)晴PMS碼頭管理系統(tǒng)主要針對(duì)港口碼頭集裝箱與散貨日常運(yùn)作、調(diào)度、堆場(chǎng)、車隊(duì)、財(cái)務(wù)費(fèi)用、相關(guān)報(bào)表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點(diǎn),圍繞調(diào)度、堆場(chǎng)作業(yè)而開發(fā)的。集技術(shù)的先進(jìn)性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點(diǎn)晴WMS倉(cāng)儲(chǔ)管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購(gòu)管理,倉(cāng)儲(chǔ)管理,倉(cāng)庫(kù)管理,保質(zhì)期管理,貨位管理,庫(kù)位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號(hào)管理軟件。
點(diǎn)晴免費(fèi)OA是一款軟件和通用服務(wù)都免費(fèi),不限功能、不限時(shí)間、不限用戶的免費(fèi)OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved

黄频国产免费高清视频,久久不卡精品中文字幕一区,激情五月天AV电影在线观看,欧美国产韩国日本一区二区
午夜福利免费体检区 | 中文一区二区在线播放 | 中文字幕精品亚洲一区 | 天天视频在线观看免费专区 | 一本一道DVD在线观看免费视频 | 亚洲夜夜性夜综合久久 |