大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群

背景及相关信息

不知道你是否遇到过产品或者测试给你一个页面让你改一点东西,你却找不到页面源代码在哪里的场景?对于一些大型项目,文件数量多、文件层级深、代码行数多,查找一个页面上组件对应的源代码位置,往往需要花费大量时间。

为了解决这个问题,我开发了 code-inspector-plugin 插件,只需要点击页面上的元素,就能够自动打开 vscode 定位到源代码。已经在快手内部30+项目中接入了使用,取得了不错的反响。效果如下图所示:

562438b034fd70f1f08e4b07f21f45bf.gif

code-inspector.gif

点击下述的 demo,也可以快速在线体验效果:

  • vue online demo[1]

  • react online demo[2]

  • preact online demo[3]

  • solid online demo[4]

  • 接入:想要使用的小伙伴,可以参考code-inspector-plugin接入文档[5]接入使用

  • github 源码:觉得插件好用的可以辛苦动下小手帮作者 github 点个 star:code-inspector[6]

code-inspector-plugin 的优点

其实 code-inspector-plugin 是之前我看到过一篇 react 点击页面元素定位源代码的文章,受到启发后实现的。但是相比而言, code-inspector-plugin 在支持场景的丰富性以及接入的便捷程度上,都得到了巨大的提升,具备以下优势:

  • 支持的打包器更加广泛:支持 webpack/vite/rspack 以及 umi 等一切基于上述三个打包器实现的打包工具

  • 支持的框架及场景更加广泛:支持 vue2/vue3/react/preact/solid 框架以及 next/nuxt 等SSR场景(以及一切以 vue2/vue3/react/preact/solid 框架为基础封装的 SSR 场景),支持在微前端中使用。

  • 支持多种系统及 IDE:支持 Mac、Windows 和 Linux 系统,支持 vscode、webstorm、atom、hbuilderX、IDEA、phpsotrm 等多种 IDE,也支持自定义 IDE 的支持

  • 接入更加简便,对代码无侵入:无论是在什么项目中,只需要在 webpack/vite/rspack 的配置中添加 code-inspector-plugin 插件即可,不需要修改任何源代码或者其他的配置

  • 自动识别环境:插件内部会针对 webpack/vite/rspack 开发环境下的一些内置信息,自动识别环境,仅在开发环境下生效,不会影响生产环境

code-inspector-plugin 实现原理

下面我们重点解析一下 code-inspector-plugin 的实现原理,插件的整体功能可以简单拆解为以下几部分:

  • 参与源码编译:打包工具(webpack/vite/rspack)编译时,code-inspector-plugin 插件会参与编译过程,对于 vue/jsx 语法会进行 ast 解析,获取到 dom 部分的源代码所在的 文件路径、行、列 信息,并将这些信息作为 dom 上的 attribute 额外添加进去。

  • 运行时交互代码:编译完成后,插件会向网页中注入监听按键定位源代码的交互逻辑,当用户点击定位 dom 时,能够获取 dom 的 attribute 上的 文件路径、行、列 信息,将信息发送一个 http 请求给后台

  • 启动一个 node server 服务:在后台启动一个 node server 服务,用于接收上一步发送过来的 http 请求

  • 识别并打开 IDE:node server 收到请求后,根据请求带过来的 文件路径、行、列 信息,使用 node 的 spawn 或者 exec 子进程打开 IDE,并将鼠标定位到 IDE 对应的位置

编译 vue/jsx 源代码

要参与源代码的编译过程,对于 vite 项目,我们可以通过 vite 插件的 transform 函数入口中实现;对于 webpack/rspack 项目,可以实现一个 loader 实现。不同的打包工具只是对应的入口不同,而对于 vue/jsx 语法的编译和解析过程都是公用的。

编译 vue 语法

对于 vue 语法的编译,我们可以使用 vue 内置的包 @vue/compiler-dom 实现,以及通过 magic-string 包来向 ast 注入额外的信息,简化的代码如下:

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

// content 是由 vite transform 函数或者 webpack/rspack loader 传过来的源代码
const s = new MagicString(content);

// vue/react 部分内置元素添加 attrs 可能报错,不处理
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 说明是元素(排除掉 text、comment 等)
        if (
          !node.loc.source.includes('data-insp-path') &&
          node.type === 1 &&
          escapeTags.indexOf(node.tag.toLowerCase()) === -1
        ) {
          // 向 dom 上添加一个带有 filepath/row/column 的属性
          const insertPosition =
            node.loc.start.offset + node.tag.length + 1;
          const { line, column } = node.loc.start;
          // filePath 也是 vite transform 函数或者 webpack/rspack loader 传过来的
          const addition = ` data-insp-path="${filePath}:${line}:${column}:${
            node.tag
          }"${node.props.length ? ' ' : ''}`;

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

  return s.toString();
}
编译 tsx 代码

对于 tsx 语法的编译和解析使用 babel 实现,并且需要引入一些 babel 相关的包,完成对于 ts、vueJsx 等场景的兼容,简化的代码如下:

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 函数或者 webpack/rspack loader 传过来的源代码
const s = new MagicString(content);

// vue/react 部分内置元素添加 attrs 可能报错,不处理
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 上添加一个带有 filepath/row/column 的属性
        const insertPosition =
          node.openingElement.end -
          (node.openingElement.selfClosing ? 2 : 1);
        const { line, column } = node.loc.start;
        // filePath 也是 vite transform 函数或者 webpack/rspack loader 传过来的
        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 编译完成后,其实相当于在源代码基础上为每个 dom 注入了一个 data-insp-path 属性,最终元素到页面上,对应的 dom 就会添加一个这样的属性,如下图所示:

a4ca14d35a52d8a30088966104a26421.png
image.png

运行时交互注入

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

  • 监听组合键按住时,鼠标在 dom 上移动时会出现 DOM 遮罩层信息

  • 点击遮罩层会获取 DOM attribute 上的源代码信息,向后台发送一个请求

这部分功能的实现上难度不大,就是基础的 html+js+css,为了保证 js 逻辑和 css 样式不会影响到宿主页面,我采用了 web component 组件的方式来封装了这部分逻辑(基于 lit 实现的 web component)。具体的实现细节将不多讲了,源码位于 packages/core/src/client/index.ts[7] 文件中。

为了简化用户的使用,不需要用户手动向页面中添加交互逻辑的组件,我通过 webpack/vite/rspack 插件,在 development 环境下将 web component 组件注入到页面中。

本地的 Node Server 服务

Node Server 同样是插件在 webpack/vite/rspack 开始编译的时候启动的,用于监听用户发送 http 请求。

我们设置了一个默认的端口 6666,为了防止端口冲突,我们需要使用 portFinder 继续向下寻找一个可用的接口去启动服务:

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) => {
    // 收到请求唤醒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);
    });
  });
}

识别并打开 IDE

Node Server 接收到了请求后,需要打开用户的 IDE 并定位到源代码,这一步是如何实现的呢?

市面上大多数的 IDE,多支持通过 {IDE路径} -g {path}:{line}:{column} 的终端命令,打开 IDE 并将鼠标光标定位到指定的位置,部分 IDE 还支持在全局安装命令行工具简化使用。以 vscode 为例,有两种方式:

  1. 在终端通过 vscode 应用路径直接打开应用

  2. 通过安装 vscode 提供的命令行工具,在终端通过 code 指令唤醒,launching-from-the-command-line[8]

这里我们采用了第二种方式,通过 node 的 spwan 或者 exec 启动一个子进程,执行 code -g 文件路径:行:列 就能打开 vscode 并定位到对应的文件路径、行、列位置,简化代码如下:

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 的问题,另一个要解决的问题是,如果用户设备上安装了多种 IDE,我们要打开哪个 IDE?

这个功能我们是基于 react-devtools[9] 的源码实现了,它会去匹配用户当前设备上正在运行的进程,在 IDE 列表中匹配打开。在此基础上我们优化并丰富了这部分的功能,支持了以下特性:

  • 优化了 IDE 的匹配顺序:因为对于 web 项目,大多数开发者使用的 IDE 是 vscode 或者 webstorm,所以我们会优先匹配这两个 IDE

  • 支持用户指定 IDE:支持用户通过在 .env.local 文件中指定声明要打开的 IDE,除了内置支持识别的 IDE 外,用户也可以用 IDE 可执行路径方式指定(意味着支持所有 IDE)

代码架构设计

上面的实现原理中,我们讲述了 code-inspector-plugin 插件的核心内容,除了这部分之外,还想分享下我们在代码可维护性和用户使用体验方面所做的努力。

分包提升可维护性

上述核心内容的实现,绝大部分是与 vite/webpack/rspack 等打包器无关的,打包器插件只是作为代码编译和交互代码注入的入口承载。所以我们采用 monorepo 架构,将核心代码都提取到了 core 中,monorepo 的包如下:

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

其中,vite-pluginwebpack-plugin 分别作为 vite 和 webpack 的入口。(rspack 由于在插件系统的设计上完全支持了 webpack,所以可以直接使用 webpack-plugin 作为 rspack 的入口,如果后面二者出现差异,会考虑再分一个 rspack-plugin 的包)。

同时为了降低用户在多种打包器中的接入心智,我们使用 code-inspector-pluginvite/webpack/rspack 等不同项目的插件进行了整合作为唯一入口,用户只需要通过 bundler 参数指定项目的打包器即可,其他配置完全一致。

降低用户接入成本

在降低用户成本方面,我们主要做了两件事情:

  1. 为了让用户不需要修改任何的源代码,我们对于页面交互的代码,直接通过插件注入,不需要用户手动引入任何的组件,对用户代码无任何侵入。

  2. 对于 webpack 和 rspack 的项目,像启动 node 服务这种逻辑是在插件中实现的,而参与源代码的编译需要在 loader 中实现。虽然让用户同时接入一个 plugin 和一个 loader 成本也没有那么高,但是为了最大程度降低用户接入成本,我们插件会在 webpack/rspack 编译前,自动将 loader 添加到 module.rules 中,用户只需要接入一个 plugin 即可,免去了 loader 的接入成本。

最后

感谢大家此文的阅读,觉得有帮助的小伙伴可以在自己的项目中接入插件进行体验,有任何的使用建议或者问题欢迎在 https://github.com/zh-lx/code-inspector 进行反馈!

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

作者:快手基础平台前端 o翔哥o

Node 社群

 
 

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

28f6b5d20e9b750d32487c04041baca5.png

“分享、点赞、在看” 支持一下

参考资料

[1]

vue online demo: https://stackblitz.com/edit/vitejs-vite-4pseos?file=vite.config.ts

[2]

react online demo: https://stackblitz.com/edit/vitejs-vite-svtwrr?file=vite.config.ts

[3]

preact online demo: https://stackblitz.com/edit/vitejs-vite-iyawbf?file=vite.config.ts

[4]

solid online demo: https://stackblitz.com/edit/solidjs-templates-6u76jn?file=vite.config.ts

[5]

code-inspector-plugin接入文档: https://inspector.fe-dev.cn/

[6]

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

[7]

packages/core/src/client/index.ts: https://github.com/zh-lx/code-inspector/blob/main/packages/core/src/client/index.ts

[8]

launching-from-the-command-line: https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line

[9]

react-devtools: https://github.com/facebook/react/blob/29fbf6f62625c4262035f931681c7b7822ca9843/packages/react-devtools-core/src/editor.js

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐