您现在的位置是:首页 >学无止境 >【原理分析】React 通过页面直接打开vscode并定位到具体源代码网站首页学无止境

【原理分析】React 通过页面直接打开vscode并定位到具体源代码

穷酸的小明 2025-02-27 00:01:04
简介【原理分析】React 通过页面直接打开vscode并定位到具体源代码

效果如下在这里插入图片描述

在这里插入图片描述

原理

低于react19以下react官方会在每个fiber节点都生成一个ReactFilerXxxx属性

在这里插入图片描述

可以看到里面属性有debugSource就会定位到具体文件 我们只需要使用vscode的协议open即可
例如vscode://file/xxx/xxx

上图可以看到还有_debugOwner也就是上级元素,结构也是个fiberNode,我们就可以一直往上层找将所有的父级也一并找出来。

伪代码如下

  const findParentNodeFileAttr = (node: HTMLElement) => {
    const list: FiberNode[] = [];
    let _node: HTMLElement | null = node;
    let attr = findReactFiberAttr(_node);
    while (
      !attr &&
      _node &&
      _node.parentElement !== body &&
      _node !== body &&
      !rootDom.contains(_node) &&
      rootDom !== _node
    ) {
      _node = _node.parentElement;
      if (_node) {
        attr = findReactFiberAttr(_node);
      }
    }
    if (attr) list.push(attr);
    while (attr?._debugOwner) {
      list.push(attr._debugOwner);
      attr = attr._debugOwner;
    }

    (window as any).react_find_attr_list = list;
    return { list, attr, _node, currentNode: node };
  };
  
  
  const findParentNodeFileAttr = (node: HTMLElement) => {
    const list: FiberNode[] = [];
    let _node: HTMLElement | null = node;
    let attr = findReactFiberAttr(_node);
    while (
      !attr &&
      _node &&
      _node.parentElement !== body
    ) {
      _node = _node.parentElement;
      if (_node) {
        attr = findReactFiberAttr(_node);
      }
    }
    if (attr) list.push(attr);
    while (attr?._debugOwner) {
      list.push(attr._debugOwner);
      attr = attr._debugOwner;
    }

    (window as any).react_find_attr_list = list;
    return { list, attr, _node, currentNode: node };
  };

react19.x中_debugSource属性被删除了

issue如下:

https://github.com/facebook/react/issues/31981

迫使开发者需要使用插件的方式来对每个dom进行源码位置定位,我们需要借助babel来生成ast,并且在每个元素加入一个源码路径的属性例如source-file-path

例如vite插件的写法如下

import type { Plugin } from 'vite';
import { NodePath, transformSync, types } from '@babel/core';
import type { JSXOpeningElement } from '@babel/types';
export default function reactSourcePlugin(): Plugin {
  return {
    enforce: 'pre',
    name: 'vite-plugin-react-source',
    transform(code: string, id: string) {
      if (!id.match(/.[jt]sx$/)) return null;

      const result = transformSync(code, {
        filename: id,
        presets: [['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'],
        plugins: [
          function sourceAttributePlugin() {
            return {
              name: 'source-attribute',
              visitor: {
                JSXOpeningElement(path: NodePath<JSXOpeningElement>) {
                  const loc = path.node.loc;
                  if (!loc) return;

                  path.node.attributes.push(
                    types.jsxAttribute(
                      types.jsxIdentifier('source-file-path'),
                      types.stringLiteral(`${id}:${loc.start.line}`),
                    ),
                  );
                },
              },
            };
          },
        ],
        ast: true,
        sourceMaps: true,
        configFile: false,
        babelrc: false,
      });

      if (!result?.code) return null;

      return {
        code: result.code,
        map: result.map,
      };
    },
  };
}

webpack则可以使用loader的方式,代码类似

import type { LoaderContext } from 'webpack';
import { transformSync, types } from '@babel/core';
import type { NodePath } from '@babel/core';
import type { JSXOpeningElement } from '@babel/types';
import type { RawSourceMap } from 'source-map';

function transformCode(code: string, id: string) {
  const result = transformSync(code, {
    filename: id,
    presets: [['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'],
    plugins: [
      function sourceAttributePlugin() {
        return {
          name: 'source-attribute',
          visitor: {
            JSXOpeningElement(path: NodePath<JSXOpeningElement>) {
              const loc = path.node.loc;
              if (!loc) return;

              path.node.attributes.push(
                types.jsxAttribute(
                  types.jsxIdentifier('source-file-path'),
                  types.stringLiteral(`${id}:${loc.start.line}`),
                ),
              );
            },
          },
        };
      },
    ],
    ast: true,
    sourceMaps: true,
    configFile: false,
    babelrc: false,
  });

  if (!result?.code) return null;

  return {
    code: result.code,
    map: result.map as RawSourceMap | undefined,
  };
}

export default function loader(this: LoaderContext<any>, source: string) {
  const callback = this.async();
  const filename = this.resourcePath;

  try {
    const result = transformCode(source, filename);
    if (!result) {
      callback(null, source);
      return;
    }

    callback(null, result.code, result.map);
  } catch (err) {
    callback(err as Error);
  }
}

使用方法如下

{
    test: /.(ts|tsx|js|jsx)$/,
    exclude: /node_modules/,
    use: [
      {
        loader: 'babel-loader',
        options: {
          presets: [
            '@babel/preset-env',
            '@babel/preset-react',
            '@babel/preset-typescript'
          ]
        }
      },{
        loader: require.resolve('react-find/webpack/webpack-react-source-loader')
      }
    ]
  }

最后

欢迎使用npm包,支持react17.x 18.x 19.x,nextjs,欢迎多多交流。

https://www.npmjs.com/package/react-find

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。