cover_image

亲测好用!揭秘这款前端开发提效神器

段娇娇 大转转FE
2025年01月21日 01:02
图片

在开发过程中,尤其是当我们面对一个陌生的模块或需要修改他人编写的项目时,往往会遇到理解代码逻辑困难、定位问题耗时等挑战,那今天推荐的这个工具可以帮助我们高效应对,短时间内快速定位到关键代码,辅助我们迅速搞清楚代码的结构与流程,从而节省我们在理解代码和定位问题上的时间。

工具介绍

code-inspector-plugin 是一款开源的基于 webpack/vite/rspack/nextjs/nuxt/umijs plugin 的提升开发效率的工具,点击页面上的 DOM,它能够自动打开你的 IDE 并将光标定位到 DOM 对应的源代码位置。

图片

优点

开发提效

只需点击页面上的 DOM 元素,IDE 自动打开并精准定位到对应的源码位置,让开发、调试效率成倍提升!

简单易用

无需侵入源码,仅需引入打包工具即可轻松启用,接入过程快如闪电。

适配性强

  • 全面兼容构建工具:支持 webpack、vite、rspack、rsbuild、esbuild、farm、nextjs、nuxt、umijs 等主流工具。
  • 多框架支持:完美适配 vue、react、preact、solid、qwik、svelte、astro 等前端框架。
  • 智能 IDE 识别:兼容 vscode、webstorm、atom、hbuilderX、PhpStorm、Pycharm、IntelliJ IDEA 等多款 IDE。
  • 环境自动识别:仅在开发环境中生效,不影响生产环境,安全又高效。

用它,开发工作从此快人一步!

使用

安装

npm install code-inspector-plugin -D

yarn add code-inspector-plugin -D

pnpm add code-inspector-plugin -D

配置

以 webpack 项目为例

const { codeInspectorPlugin } = require('code-inspector-plugin');

module.exports = () => ({
  plugins: [
    codeInspectorPlugin({
      bundler'webpack',
    }),
  ],
});

使用

目前使用 DOM 源码定位功能的方式有两种:

方式一(推荐)

按住快捷键(Mac 默认 Option + Shift,Windows Alt + Shift )可以查看控制台提示:

  1. 鼠标悬停时页面元素会被高亮遮罩,并显示信息。
  2. 点击元素,IDE 自动打开并定位到对应代码位置。

方式二

启用页面上的 代码审查开关按钮(需配置 showSwitch: true):

点击按钮切换模式:

  • 彩色开关:代码审查模式开启,操作同方式一。
  • 黑白开关:模式关闭

详见官网:https://inspector.fe-dev.cn/guide/start.html

实现原理

实现思路

  1. 参与源码编译:
  • 在使用如 Webpack、Vite 或 Rspack 等打包工具进行编译时,code-inspector-plugin 插件会加入编译流程。
  • 插件会对 Vue 或 JSX 代码进行 AST 分析,从中提取出 DOM 元素的源代码信息(包括文件路径、行号和列号),并将这些信息作为额外的属性添加到 DOM 元素中。
  1. 前端交互逻辑
  • 一旦编译完成,插件会将一些交互功能嵌入网页,监听用户点击事件。
  • 当用户点击某个 DOM 元素时,插件会从该元素的属性中提取文件路径、行号和列号,并通过 HTTP 请求将这些信息发送到后端服务器。
  1. 后台服务处理请求
  • 后端启动一个 Node.js 服务,接收前端发送的 HTTP 请求。
  • 接收到请求后,服务会根据文件路径、行号和列号打开相关的 IDE,并将光标定位到正确的位置。
  1. 自动打开 IDE 并定位源代码
  • 后端服务利用 Node.js 的 spawn 或 exec 方法,启动 IDE,并将其定位到相应的源代码位置,帮助开发者快速修复或查看代码。

源码解析

接下来,根据作者的实现思路,来看看代码是如何实现的吧!

参与源码编译

入口

根据 bundler配置的打包工具参数(Vite、Webpack、Rspack、Esbuild)加载相应插件,并通过 .env.local 文件和 CODE_INSPECTOR 环境变量控制是否启用。

import { ViteCodeInspectorPlugin } from 'vite-code-inspector-plugin';
import WebpackCodeInspectorPlugin from 'webpack-code-inspector-plugin';
import { EsbuildCodeInspectorPlugin } from 'esbuild-code-inspector-plugin';

export function CodeInspectorPlugin(options: CodeInspectorPluginOptions): any {
  // 没有 bundler 参数,报错
  if (!options?.bundler) {
    console.log(
      chalk.red(
        'Please specify the bundler in the options of code-inspector-plugin.'
      )
    );
    return;
  }
  
  ...
  
  if (options.bundler === 'webpack' || options.bundler === 'rspack') {
    // 使用 webpack 插件
    return new WebpackCodeInspectorPlugin(params);
  } else if (options.bundler === 'esbuild') {
    return EsbuildCodeInspectorPlugin(params);
  } else {
    // 使用 vite 插件
    return ViteCodeInspectorPlugin(params);
  }
}

export const codeInspectorPlugin = CodeInspectorPlugin;

插件内部逻辑

以 webpack-plugin 为例,在 Webpack 构建中通过自定义 loader.jsinject-loader.js 来注入源代码信息:

applyLoader:添加自定义 loader 和 inject-loader 到 Webpack 配置。

WebpackCodeInspectorPlugin:检查是否处于开发环境,在开发模式下注册 loader, 这里判断了仅在开发环境下生效,有效降低用户接入的心智。

    const applyLoader = (options: LoaderOptions, compiler: any) => {
      if (!isFirstLoad) {
        return;
      }
      isFirstLoad = false;
      // 适配 webpack 各个版本
      const _compiler = compiler?.compiler || compiler;
      const module = _compiler?.options?.module;
      const rules = module?.rules || module?.loaders || [];
      rules.push(
        {
          test: options?.match ?? /.(vue|jsx|tsx|js|ts|mjs|mts)$/,
          exclude/node_modules/,
          use: [
            {
              loader: path.resolve(compatibleDirname, `./loader.js`),
              options,
            },
          ],
          ...(options.enforcePre === false ? {} : { enforce'pre' }), // 默认指定当前插件优先执行
        },
        {
          ...(options?.injectTo
            ? { resource: options?.injectTo }
            : {
                test/.(jsx|tsx|js|ts|mjs|mts)$/,
                exclude/node_modules/,
              }),
          use: [
            {
              loader: path.resolve(compatibleDirname, `./inject-loader.js`),
              options,
            },
          ],
          enforce'post',
        }
      );
    }

    class WebpackCodeInspectorPlugin {
      options: Options;

      constructor(options: Options) {
        this.options = options;
      }

      apply(compiler) {
      
        // 自定义 dev 环境判断
        let isDev: boolean;
        if (typeof this.options?.dev === 'function') {
          isDev = this.options?.dev();
        } else {
          isDev = this.options?.dev;
        }

        if (isDev === false) {
          return;
        }

        // 仅在开发环境下使用
        if (
          !isDev &&
          compiler?.options?.mode !== 'development' &&
          process.env.NODE_ENV !== 'development'
        ) {
          return;
        }
        ...

        applyLoader({ ...this.options, record }, compiler);
      }
    }

    export default WebpackCodeInspectorPlugin;

两个 loader 内部逻辑

第一个 loader

处理不同类型的文件(Vue、JSX、Svelte),使用 transformCode 方法根据文件类型对内容进行转换。transformCode 会调用不同的转换函数(transformVuetransformJsxtransformSvelte)处理不同的语法。


export default async function WebpackCodeInspectorLoader(content: string{
  // jsx 语法
  const isJSX =
    isJsTypeFile(filePath) ||
    (filePath.endsWith('.vue') &&
      jsxParamList.some((param) => params.get(param) !== null));
  if (isJSX) {
    return transformCode({ content, filePath, fileType'jsx', escapeTags });
  }

  // vue jsx
  const isJsxWithScript =
    filePath.endsWith('.vue') &&
    (params.get('lang') === 'tsx' || params.get('lang') === 'jsx');
  if (isJsxWithScript) {
    const { descriptor } = parseSFC(content, {
      sourceMapfalse,
    });
    // 处理 <script> 标签内容
    // 注意:.vue 允许同时存在 <script> 和 <script setup>
    const scripts = [
      descriptor.script?.content,
      descriptor.scriptSetup?.content,
    ];
    for (const script of scripts) {
      if (!script) continue;
      const newScript = transformCode({
        content: script,
        filePath,
        fileType'jsx',
        escapeTags,
      });
      content = content.replace(script, newScript);
    }
    return content;
  }

  // vue
  const isVue =
    filePath.endsWith('.vue') &&
    params.get('type') !== 'style' &&
    params.get('type') !== 'script' &&
    params.get('raw') === null;
  if (isVue) {
    return transformCode({ content, filePath, fileType'vue', escapeTags });
  }

  // svelte
  const isSvelte = filePath.endsWith('.svelte');
  if (isSvelte) {
    return transformCode({ content, filePath, fileType'svelte', escapeTags });
  }

  return content;
}

transformCode 函数根据 fileType 调用对应的转换函数 (transformVue, transformJsx, transformSvelte)。

import { transformJsx } from './transform-jsx';
import { transformSvelte } from './transform-svelte';
import { transformVue } from './transform-vue';

export function transformCode(params: TransformCodeParams{
  const { content, filePath, fileType, escapeTags = [] } = params;
  const finalEscapeTags = [
    ...CodeInspectorEscapeTags,
    ...escapeTags,
  ];
  try {
    if (fileType === 'vue') {
      return transformVue(content, filePath, finalEscapeTags);
    } else if (fileType === 'jsx') {
      return transformJsx(content, filePath, finalEscapeTags);
    } else if (fileType === 'svelte') { 
      return transformSvelte(content, filePath, finalEscapeTags);
    } else {
      return content;
    }
  } catch (error) {
    return content;
  }
}

以编译 vue 语法为例(transformVue 方法) 使用 vue 内置的包 @vue/compiler-dom的 parse 将模板转换为 AST, transform 是在 AST 上进行加工,以及通过 magic-string 包来向 AST 中注入:路径名称(data-insp-path) =  路径:行:列:html 标签。

import MagicString from 'magic-string';
import type {
  TemplateChildNode,
  NodeTransform,
  ElementNode,
from '@vue/compiler-dom';

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

export function transformVue(
  content: string,
  filePath: string,
  escapeTags: EscapeTags
{
  const s = new MagicString(content);

  // parse 方法解析vue 模版为ast 对象
  const ast = parse(content, {
    commentstrue,
  });
  
  ...

  if (pugMap.has(filePath) && templateNode) {
    ...
  } else {
    transform(ast, {
      nodeTransforms: [
        ((node: TemplateChildNode) => {
          if (
            !node.loc.source.includes(PathName) &&
            node.type === VueElementType &&
            !isEscapeTags(escapeTags, node.tag)
          ) {
            // 向 dom 上添加一个带有 filepath/row/column 的属性
            const insertPosition = node.loc.start.offset + node.tag.length + 1;
            const { line, column } = node.loc.start;
            // 规则: 注入的路径名称data-insp-path = 路径:行:列:html标签
            const addition = ${PathName}="${filePath}:${line}:${column}:${
              node.tag
            }
"${node.props.length ? ' ' : ''}`
;

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

  return s.toString();
}

上面 vue/jsx 编译完成后,其实相当于在源代码基础上为每个 dom 注入了一个 data-insp-path 属性,最终元素到页面上,对应的 dom 就会添加一个这样的属性,如下图所示:

图片
第二个 loader(inject-loader)

启用 node 服务监听打开 vscode,把页面交互代码注入到入口文件里面。

通过该 loader,使用 web component 组件在开发环境注入到页面中,简化用户的使用,不需要用户手动向页面中添加交互逻辑的组件。

import { normalizePath, getCodeWithWebComponent } from 'code-inspector-core';

export default async function WebpackCodeInjectLoader(
      content: string,
      source: any,
      meta: any
    
{
      this.async();
      this.cacheable && this.cacheable(true);
      const filePath = normalizePath(this.resourcePath); // 当前文件的绝对路径
      const options = this.query;

      // start server and inject client code to entry file
      content = await getCodeWithWebComponent(options, filePath, content, options.record);

      this.callback(null, content, source, meta);
    }

getCodeWithWebComponent 方法内部核心调用了 getWebComponentCode,将打包的自定义组件代码 client.umd.js 注入到入口 js 文件中,在加载入口 js 时立即执行,注入到页面中。

import { startServer } from './server';

let compatibleDirname = '';

if (typeof __dirname !== 'undefined') {
  compatibleDirname = __dirname;
else {
  compatibleDirname = dirname(fileURLToPath(import.meta.url));
}

// 这个路径是根据打包后来的,client.umd.js 是啥,后面说
export const clientJsPath = path.resolve(compatibleDirname, './client.umd.js');
const jsClientCode = fs.readFileSync(clientJsPath, 'utf-8');

export function getInjectedCode(options: CodeOptions, port: number{
  let code = `'use client';`;
  code += getEliminateWarningCode();
  if (options?.hideDomPathAttr) {
    code += getHidePathAttrCode();
  }
  code += getWebComponentCode(options, port);
  return `/* eslint-disable */\n` + code.replace(/\n/g'');
}

export function getWebComponentCode(options: CodeOptions, port: number{
  const {
    hotKeys = ['shiftKey''altKey'],
    showSwitch = false,
    hideConsole = false,
    autoToggle = true,
    behavior = {},
    ip = false,
  } = options || ({} as CodeOptions);
  const { locate = true, copy = false } = behavior;
  return `
;(function (){
  if (typeof window !== 'undefined') {
    if (!document.documentElement.querySelector('code-inspector-component')) {
      var script = document.createElement('script');
      script.setAttribute('type', 'text/javascript');
      script.textContent = ${`${jsClientCode}`};
  
      var inspector = document.createElement('code-inspector-component');
      inspector.port = ${port};
      inspector.hotKeys = '${(hotKeys ? hotKeys : [])?.join(',')}';
      inspector.showSwitch = !!${showSwitch};
      inspector.autoToggle = !!${autoToggle};
      inspector.hideConsole = !!${hideConsole};
      inspector.locate = !!${locate};
      inspector.copy = ${typeof copy === 'string' ? `'${copy}'` : !!copy};
      inspector.ip = '${getIP(ip)}';
      document.documentElement.append(inspector); // 将自定义组件插入到页面上
    }
  }
})();
`
;
}

export async function getCodeWithWebComponent(
  options: CodeOptions,
  file: string,
  code: string,
  record: RecordInfo
{
  // start server
  await startServer(options, record);

  recordEntry(record, file);

  // 判断js文件,且是入口js文件,注入代码
  if (
    (isJsTypeFile(file) && getFilePathWithoutExt(file) === record.entry) ||
    file === AstroToolbarFile
  ) {
      ...
    } else {
      code = `${injectCode};${code}`;
    }
  }
  return code;
}

经过这一步,把自定义组件代码 注入到了 app.js 中,在页面加载之后,我们可以看到页面上 被注入了 shadow dom 自定义组件

图片
图片

client.umd.js 是谁打包后的产物呢?

通过这个 vite 配置看出,它指定了入口文件为 src/client/index.ts,输出文件名为 client,并将库的全局变量名设置为 vueInspectorClient

import { defineConfig } from 'vite';
import { terser } from 'rollup-plugin-terser';

// https://vitejs.dev/config/
export default defineConfig({
  build: {
    lib: {
      entry: ['src/client/index.ts'],
      formats: ['umd'],
      fileName'client',
      name'vueInspectorClient',
    },
    minifytrue,
    emptyOutDirfalse,
    target: ['node8''es2015'],
  },
  plugins: [
    // @ts-ignore
    terser({
      format: {
        commentsfalse
      }
    })
  ]
});

接下来我们再来看src/client/index.ts的代码逻辑。

前端交互逻辑

code-inspector-plugin 插件的交互功能主要包含监听两部分:

  • 监听组合键按住时,鼠标在 dom 上移动时会出现 DOM 遮罩层信息
  • 点击遮罩层会获取 DOM attribute 上的源代码信息,向后台发送一个请求
图片

自定义组件

实现了一个基于 LitElement 的开发者工具组件,主要功能是通过快捷键和鼠标交互快速定位 DOM 元素,并支持在代码编辑器(如 VS Code)中打开对应的源文件。核心功能包括:

  • 热键功能:组件监听用户按下特定的热键(如 shiftKey, altKey),并在按下热键时启用代码检查功能。

  • 元素定位:当鼠标悬停在页面元素上时,组件会显示该元素的详细信息,如路径、行号、列号等。如果用户点击该元素,会打开 VSCode 或者复制代码路径到剪贴板。

  • UI 控制:有一个开关按钮,用来开启和关闭功能。开关的位置支持拖动,用户可以自由移动它。

  • 样式和事件处理:组件在显示时会加入遮罩层,阻止元素选择(userSelect: 'none'),并通过 HTTP 请求或者 img 请求方式来通知本地服务端打开 VSCode。

import { LitElement, css, html } from 'lit';

export class CodeInspectorComponent extends LitElement {
  @property()
  hotKeys: string = 'shiftKey,altKey';
  @property()
  port: number = DefaultPort;
  ...

  isTracking = (e: any) => {
    return (
      this.hotKeys && this.hotKeys.split(',').every((key) => e[key.trim()])
    );
  };

  // 渲染遮罩层
  renderCover = (target: HTMLElement) => {
    // 设置 target 的位置
    const { top, right, bottom, left } = target.getBoundingClientRect();
    this.position = {
      top,
      right,
      bottom,
      left,
      ...
    };
    ...
    
    // 增加鼠标光标样式
    this.addGlobalCursorStyle();
    // 防止 select
    if (!this.preUserSelect) {
      this.preUserSelect = getComputedStyle(document.body).userSelect;
    }
    document.body.style.userSelect = 'none';
    // 获取元素信息
    let paths = target.getAttribute(PathName) || (target as CodeInspectorHtmlElement)[PathName] || '';
   
    if (!paths && target.getAttribute('data-astro-source-file')) {
      paths = `${target.getAttribute(
        'data-astro-source-file'
      )}
:${target.getAttribute(
        'data-astro-source-loc'
      )}
:${target.tagName.toLowerCase()}`
;
    }
    const segments = paths.split(':');
    const name = segments[segments.length - 1];
    const column = Number(segments[segments.length - 2]);
    const line = Number(segments[segments.length - 3]);
    const path = segments.slice(0, segments.length - 3).join(':');
    this.element = { name, path, line, column };
    this.show = true;
  };

  removeCover = () => {
    ...
  };

  sendXHR = () => {
    const file = encodeURIComponent(this.element.path);
    const url = `http://${this.ip}:${this.port}/?file=${file}&line=${this.element.line}&column=${this.element.column}`;
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.send();
    xhr.addEventListener('error', () => {
      this.sendType = 'img';
      this.sendImg();
    });
  };

  // 通过img方式发送请求,防止类似企业微信侧边栏等内置浏览器拦截逻辑
  sendImg = () => {
    const file = encodeURIComponent(this.element.path);
    const url = `http://${this.ip}:${this.port}/?file=${file}&line=${this.element.line}&column=${this.element.column}`;
    const img = document.createElement('img');
    img.src = url;
  };

  // 请求本地服务端,打开vscode
  trackCode = () => {
    if (this.locate) {
      if (this.sendType === 'xhr') {
        this.sendXHR();
      } else {
        this.sendImg();
      }
    }
    if (this.copy) {
      ...
    }
  };

  copyToClipboard(text: string) {
    ...
  }

  // 移动按钮
  moveSwitch = (e: MouseEvent) => {
    ...
  };

  handleMouseup = () => {
    this.hoverSwitch = false;
  };

  // 鼠标移动渲染遮罩层位置
  handleMouseMove = (e: MouseEvent) => {
    ...
  };

  // 鼠标点击唤醒遮罩层
  handleMouseClick = (e: any) => {
    if (this.isTracking(e) || this.open) {
      if (this.show) {
        e.stopPropagation();
        e.preventDefault();
        // 唤醒 vscode
        this.trackCode();
        // 清除遮罩层
        this.removeCover();
        if (this.autoToggle) {
          this.open = false;
        }
      }
    }
  };

  // disabled 无法触发 click 事件
  handlePointerDown = (e: any) => {
    ...
  };

  // 监听键盘抬起,清除遮罩层
  handleKeyUp = (e: any) => {
    if (!this.isTracking(e) && !this.open) {
      this.removeCover();
    }
  };

  // 记录鼠标按下时初始位置
  recordMousePosition = (e: MouseEvent) => {
    this.mousePosition = {
      baseXthis.inspectorSwitchRef.offsetLeft,
      baseYthis.inspectorSwitchRef.offsetTop,
      moveX: e.pageX,
      moveY: e.pageY,
    };
    this.dragging = true;
    e.preventDefault();
  };

  // 结束拖拽
  handleMouseUp = () => {
    this.dragging = false;
  };

  // 切换开关
  switch = (e: Event) => {
    if (!this.moved) {
      this.open = !this.open;
      e.preventDefault();
      e.stopPropagation();
    }
    this.moved = false;
  };

  protected firstUpdated(): void {
    if (!this.hideConsole) {
      this.printTip();
    }
    
    ...
    window.addEventListener('click'this.handleMouseClick, true);
    window.addEventListener('pointerdown'this.handlePointerDown, true);
    document.addEventListener('keyup'this.handleKeyUp);
    document.addEventListener('mouseleave'this.removeCover);
    document.addEventListener('mouseup'this.handleMouseUp);
    this.inspectorSwitchRef.addEventListener(
      'mousedown',
      this.recordMousePosition
    );
    this.inspectorSwitchRef.addEventListener('click'this.switch);
  }

  disconnectedCallback(): void {
    window.removeEventListener('mousemove'this.handleMouseMove);
    window.removeEventListener('mousemove'this.moveSwitch);
    ...
  }

  render() {
    const containerPosition = {
      displaythis.show ? 'block' : 'none',
      top`${this.position.top - this.position.margin.top}px`,
      left`${this.position.left - this.position.margin.left}px`,
      ...
    };
    ...
    return html`
      <div
        class="code-inspector-container"
        id="code-inspector-container"
        style=
${styleMap(containerPosition)}
      >

      ...
        <div
          id="element-info"
          class="element-info 
${this.infoClassName.vertical}${this
            .infoClassName.horizon}
"
          style=
${styleMap({ width: this.infoWidth })}
        >

          <div class="element-info-content">
            <div class="name-line">
              <div class="element-name">
                <span class="element-title">&lt;
${this.element.name}&gt;</span>
                <span class="element-tip">click to open IDE</span>
              </div>
            </div>
            <div class="path-line">
${this.element.path}</div>
          </div>
        </div>
      </div>
      <div
        id="inspector-switch"
        class="inspector-switch 
${this.open
          ? 'active-inspector-switch'
          : ''}
${this.moved ? 'move-inspector-switch' : ''}"
        style=
${styleMap({ display: this.showSwitch ? 'flex' : 'none' })}
      >

        
${this.open
          ? html`
              <svg
                t="1677801709811"
                class="icon"
                viewBox="0 0 1024 1024"
                version="1.1"
                xmlns="http://www.w3.org/2000/svg"
                p-id="1110"
                xmlns:xlink="http://www.w3.org/1999/xlink"
                width="1em"
                height="1em"
              >

              ...
            </svg>`
}

      </div>
    `
;
  }
  ...

后台服务处理请求

实现了一个 HTTP 服务器,通过接收到的请求打开指定的代码文件并跳转到某行某列。具体功能如下:

createServer:创建一个 HTTP 服务器,监听请求,解析请求中的 file(文件路径)、line(行号)、column(列号)参数,打开文件并跳转到指定位置。

startServer:检查是否已有端口信息(record.port)。如果没有,它会调用 createServer 来获取一个可用端口并启动服务器。

// 启动本地接口,访问时唤起vscode
import http from 'http';
import portFinder from 'portfinder';
import launchEditor from './launch-editor';

export function createServer(callback: (port: number) => anyoptions?: CodeOptions{
  const server = http.createServer((req: any, res: any) => {
    // 收到请求唤醒vscode
    const params = new URLSearchParams(req.url.slice(1));
    const file = decodeURIComponent(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');
    // 调用 hooks
    options?.hooks?.afterInspectRequest?.(options, { file, line, column });
    // 打开 IDE
    launchEditor(file, line, column, options?.editor, options?.openIn, options?.pathFormat);
    
  });

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

export async function startServer(options: CodeOptions, record: RecordInfo{
  if (!record.port) {
    if (!record.findPort) {
      record.findPort = new Promise((resolve) => {
        createServer((port: number) => {
          resolve(port);
        }, options);
      });
    }
    record.port = await record.findPort;
  }
}

自动打开 IDE 并定位源代码

这里源码就不展示了,感兴趣的可以自己去看看源码。

源码核心解决了 2 个问题:

  1. 如何打开用户的 IDE 并定位到源代码?

默认方式:通过安装 通过 node 的 spwan 或者 exec 启动一个子进程,执行 code -g 文件路径:行:列 打开 vscode 并定位到对应的文件路径、行、列位置。

自定义编辑器路径方式: 通过 {IDE路径} -g {path}:{line}:{column}打开并定位编辑器。

  1. 用户系统安装了多种 IDE,如何智能地启动适合的代码编辑器?

匹配用户当前设备上正在运行的进程,在 IDE 列表中匹配打开,同时针对 web 项目,设置了优先匹配 vscode、 webstorm。

使用中遇到的问题

1、按住快捷键点击页面发送请求了,但是 vscode 没被打开

要将 code 命令添加到 PATH 中,以便可以从终端启动 VS Code

解决:cmd+shfit+p 选择 shell 命令: 在 path 中安装 code 命令

图片

2、开发环境 code-inspector-plugin 在安卓低端机上会报 globalThis is not defined, 这个文件报的 append-code-${port}.js

解决:找源码作者兼容处理了一下,升级最新包即可。

3、默认 enforcePre 为 true 导致 eslint-plugin 校验错误

图片

解决:设置 enforcePre:false。

  1. 移动端项目使用开关方式,体验不佳

解决:使用最新包即可,已推源码作者 增加了移动端的交互体验,目前已支持开关位置、拖拽;在移动端开发环境,「长按」可以展示对应的组件,「点击」能打开对应的 vscode 组件。

参考

官网:https://inspector.fe-dev.cn/guide/start.html

掘金原文:https://juejin.cn/post/7326002010084311079

github 源码:https://github.com/zh-lx/code-inspector


想了解更多转转公司的业务实践,点击关注下方的公众号吧!


前端 · 目录
上一篇ESlint 过去、现在、未来下一篇圈复杂度在转转前端质量体系中的应用
继续滑动看下一个
大转转FE
向上滑动看下一个