preface

In large projects, it’s not uncommon for QA to throw you a link that doesn’t work, but you have no idea where the page/component file is.

Wouldn’t it be nice if you could click on a component on the page and automatically jump to the corresponding file in VSCode and locate the corresponding line number?

The React-dev-inspector responds to this need.

It’s so easy to use, you’ll understand it in seconds after watching this GIF:

You can check it out at the preview site.

use

There are three simple steps:

  1. Build time:
    • I need to add onewebpack loaderLet’s go through the pre-compileASTNode, add file path, name and other related information to the DOM node.
    • Need to useDefinePluginInject the root path of the project runtime, then use it to concatenate the file path and open the corresponding file of VSCode.
  2. The runtimeThe React component needs to be wrapped in the outermost layerInspectorComponent, which is used to listen for shortcuts on the browser side, and pop up the mask layer of debug, which is used when clicking the mask layerfetchSend a request to the native service to open VSCode.
  3. The local service: Need to startreact-dev-utilsA middleware in the server, listening for a specific path, executes the instruction to open VSCode on the native server.

Here’s a quick breakdown of what these steps actually do.

Principle of simplified

Build time

First want to know if the browser end this component belongs to which files, so inevitably at build time went to traverse the code file, according to the structure of the code parsing generated AST, then hang up the current component on each component of DOM elements corresponding file location and line number, so in the development environment the resulting DOM elements like this:

<div
  data-inspector-line="11"
  data-inspector-column="4"
  data-inspector-relative-path="src/components/Slogan/Slogan.tsx"
  class="css-1f15bld-Description e1vquvfb0"
>
  <p
    data-inspector-line="44"
    data-inspector-column="10"
    data-inspector-relative-path="src/layouts/index.tsx"
  >
    Inspect react components and click will jump to local IDE to view component
    code.
  </p>
</div>
;
Copy the code

This allows you to enable debug mode when typing a shortcut key, and make DOM hover add a mask layer and display the component’s information:

This step through the WebPack loader to get the uncompiled JSX source code, and then with AST processing can be completed.

The runtime

Since you need to add hover events on the browser side and add a mask box element, it is inevitable to invade the runtime code. Here, you can reduce the intrusion as much as possible by wrapping an Inspector around the entire application.

import React from 'react'
import { Inspector } from 'react-dev-inspector'

const InspectorWrapper = process.env.NODE_ENV === 'development'
  ? Inspector
  : React.Fragment

export const Layout = () = > {
  // ...

  return (
    <InspectorWrapper
      keys={['control', 'shift', 'command', 'c/ / ']}default keys
      .  // Props see below
    >
     <Page />
    </InspectorWrapper>)}Copy the code

You can also customize your favorite shortcut keys to enable debug mode.

After debugging mode is enabled, hover over the component you want to debug, the mask box will appear, click again, automatically open the corresponding component file in VSCode, and jump to the corresponding row and column.

So the key point is that the jump actually sends a request to the server of the local machine with the help of fetch. The server executes script commands such as code SRC /Inspector/index.ts to open VSCode, which requires the help of the third step I said. Local services are started and middleware is introduced.

The local service

Remember create-react-app or vue-cli front-end projects that pop up a global mask and stack information when an error occurs, and click to redirect to VSCode’s corresponding file? The react-dev-inspector is implemented using the create-react-app react-dev-utils toolkit. (Yes, create-react-app creates a project with this service, so you don’t need to manually load it.)

The react-dev-utils package a piece of middleware for this function: errorOverlayMiddleware

In fact, the code is very simple, is listening to a special URL:

// launchEditorEndpoint.js
module.exports = "/__open-stack-frame-in-editor";
Copy the code
// errorOverlayMiddleware.js
const launchEditor = require("./launchEditor");
const launchEditorEndpoint = require("./launchEditorEndpoint");

module.exports = function createLaunchEditorMiddleware() {
  return function launchEditorMiddleware(req, res, next) {
    if (req.url.startsWith(launchEditorEndpoint)) {
      const lineNumber = parseInt(req.query.lineNumber, 10) | |1;
      const colNumber = parseInt(req.query.colNumber, 10) | |1;
      launchEditor(req.query.fileName, lineNumber, colNumber);
      res.end();
    } else{ next(); }}; };Copy the code

The launchEditor core method of opening the editor will be discussed in more detail in a moment, but can be skipped for now, as long as you know that you need to start the service.

This is a middleware designed for Express. The before provided in the devServer option of WebPack can also be easily connected to this middleware. If your project does not use Express, you can just rewrite it using this middleware. Just listen on the interface to get information about the file and call the core method launchEditor.

Once these steps are complete, the plug-in is plugged in, and fetch(‘/__open-stack-frame-in-editor? FileName = / Users/admin/app/SRC/Title. The TSX ‘) to test whether the react – dev – utils service open success.

Injection absolute path

Note that in the previous request fileName= is prefixed with the absolute path, and only relative paths such as SRC/title. TSX will be saved on the DOM node. Concatenate the relative path on the component to get the full path so VSCode can open smoothly.

This requires writing the startup path to the browser environment with DefinePlugin:

new DefinePlugin({
  "process.env.PWD": JSON.stringfy(process.env.PWD),
});
Copy the code

At this point, the complete plug-in integration, simplified version of the principle of the end of the analysis.

Key source

After reading the simplification principle above, you can almost write a similar plug-in, but the implementation details may be different. Here is not a complete analysis of the source code, look at the source code is worth paying attention to some of the details.

How do I bury a point on an element

On the browser side, you can find the corresponding path of the node in VSCode. The key is the buried point at compile time. Webpack loader takes the code string and returns the string after you process it, which is perfect for adding new attributes to the element. All we need to do is take advantage of the entire AST capabilities in Babel:

export default function inspectorLoader(
  this: webpack.loader.LoaderContext,
  source: string
) {
  const { rootContext: rootPath, resourcePath: filePath } = this;

  const ast: Node = parse(source);

  traverse(ast, {
    enter(path: NodePath<Node>) {
      if (path.type === "JSXOpeningElement") {
        doJSXOpeningElement(path.node asJSXOpeningElement, { relativePath }); }}});const { code } = generate(ast);

  return code
}
Copy the code

Parse -> traverse -> Generate (JSXOpeningElement, JSXOpeningElement);

const doJSXOpeningElement: NodeHandler<
  JSXOpeningElement,
  { relativePath: string }
> = (node, option) = > {
  const { stop } = doJSXPathName(node.name)
  if (stop) return { stop }

  const { relativePath } = option

  // Write the line number
  const lineAttr = jsxAttribute(
    jsxIdentifier('data-inspector-line'),
    stringLiteral(node.loc.start.line.toString()),
  )

  // Write the column number
  const columnAttr = jsxAttribute(
    jsxIdentifier('data-inspector-column'),
    stringLiteral(node.loc.start.column.toString()),
  )

  // Write the relative path of the component
  const relativePathAttr = jsxAttribute(
    jsxIdentifier('data-inspector-relative-path'),
    stringLiteral(relativePath),
  )

  // Add these attributes to the element
  node.attributes.push(lineAttr, columnAttr, relativePathAttr)

  return { result: node }
}
Copy the code

Get component name

When the mouse hover over a DOM node at runtime, only the DOM element is retrieved. How to retrieve the component name? The React element is named __reactInternalInstance, and the fiber node reference is attached to the DOM.

/** * https://stackoverflow.com/questions/29321742/react-getting-a-component-from-a-dom-element-for-debugging */
export const getElementFiber = (element: HTMLElement): Fiber | null= > {
  const fiberKey = Object.keys(element).find(
    key= > key.startsWith('__reactInternalInstance$'),if (fiberKey) {
    return element[fiberKey] as Fiber
  }

  return null
}
Copy the code

Since fiber might correspond to a normal DOM element such as div, rather than a component fiber, we would expect to look up the nearest component node and display its name (using the displayName or name attribute here). Because Fiber is a linked list, you can recursively look up the return property until you find the first node that matches your expectations.

The recursive search for fiber’s return is similar to the recursive search for the parentNode property in a DOM node and the recursive search for the parentNode.

// There are some component names that are masked by the re
export const debugToolNameRegex = / ^ (. *? \.Provider|.*? \.Consumer|Anonymous|Trigger|Tooltip|_.*|[a-z].*)$/;

export constgetSuitableFiber = (baseFiber? : Fiber): Fiber |null= > {
  let fiber = baseFiber
  
  while (fiber) {
    // The while loop recurses upwards to find the component that the displayName matches
    constname = fiber.type? .displayName ?? fiber.type? .nameif(name && ! debugToolNameRegex.test(name)) {return fiber
    }
	// If no return node is found, continue to search for the return node
    fiber = fiber.return
  }

  return null
}
Copy the code

The type attribute on Fiber corresponds to the function you’re writing in the case of a functional component, and the class attribute in the case of a class component. Take the displayName or name attribute above:

export constgetFiberName = (fiber? : Fiber): string |undefined= > {
  constfiberType = getSuitableFiber(fiber)? .typelet displayName: string | undefined

  // The displayName property is not guaranteed to be a string.
  // It's only safe to use for our purposes if it's a string.
  // github.com/facebook/react-devtools/issues/803
  //
  / / https://github.com/facebook/react/blob/v17.0.0/packages/react-devtools-shared/src/utils.js#L90-L112
  if (typeoffiberType? .displayName ==='string') {
    displayName = fiberType.displayName
  } else if (typeoffiberType? .name ==='string') {
    displayName = fiberType.name
  }

  return displayName
}
Copy the code

Service redirect principle of VSCode

Although react-dev-utils is simply an interface to execute code filepath when you fetch, it is actually a very clever implementation of multiple editors compatibility.

How do I “guess” which editor a user is using? This implementation defines a list of process names that correspond to the start instruction:

const COMMON_EDITORS_OSX = {
  '/Applications/Atom.app/Contents/MacOS/Atom': 'atom'.'/Applications/Visual Studio Code.app/Contents/MacOS/Electron': 'code'. }Copy the code

Then on macOS and Linux, execute ps x command to list the process name, and map the corresponding open editor instruction through the process name. Such as you have a process/Applications/Visual Studio Code. The app/Contents/MacOS/Electron, that means you are using is VSCode, just to get the Code this instruction.

Then call the child_process module to execute the command:

child_process.spawn("code", pathInfo, { stdio: "inherit" });
Copy the code

LaunchEditor source code address

Detailed Access Tutorial

Just make a few changes to the WebPack configuration at build time, add a global variable, and introduce a Loader.

const { DefinePlugin } = require('webpack');

{
  module: {
    rules: [{test: /\.(jsx|js)$/,
        use: [
          {
            loader: 'babel-loader'.options: {
              presets: ['es2015'.'react'],}},// Note that this loader Babel is executed before compiling
          {
            loader: 'react-dev-inspector/plugins/webpack/inspector-loader'.options: { exclude: [resolve(__dirname, 'Directories you want to exclude']},},],},},plugins: [
    new DefinePlugin({
      'process.env.PWD': JSON.stringify(process.env.PWD),
    }),
  ]
}
Copy the code

If your project is built by yourself rather than crA, you may not have the errorOverlayMiddleware middleware service enabled in your project. You can enable it in devServer of WebPack:

import createErrorOverlayMiddleware from 'react-dev-utils/errorOverlayMiddleware'

{
  devServer: {
    before(app) {
      app.use(createErrorOverlayMiddleware())
    }
  }
}
Copy the code

Also, make sure that your command line itself can open the VSCode editor with the code command. If this is not configured, follow these steps:

1. Open VSCode first.

2, Use Command + Shift + p (note that Windows use CTRL + Shift + P) then search for code and select install ‘code’ command in path.

Finally, access in the React project outermost layer:

import React from 'react'
import { Inspector } from 'react-dev-inspector'

const InspectorWrapper = process.env.NODE_ENV === 'development'
  ? Inspector
  : React.Fragment

export const Layout = () = > {
  // ...

  return (
    <InspectorWrapper
      keys={['control', 'shift', 'command', 'c/ / ']}default keys
      .  // Props see below
    >
     <Page />
    </InspectorWrapper>)}Copy the code

conclusion

Having a debugger like this is especially important during the development and maintenance of large projects, and even a good memory can’t keep up with the growing number of components… With this plug-in, we can jump from one component to another, which saves us a lot of time.

In the process of reading the source code of this plug-in, we can also see that to do something to improve the overall efficiency of the project, we often need to have a comprehensive understanding of the runtime, build time, Node side of a lot of knowledge, endless learning.

reading

Performance optimization for modern Web application architectures is the ultimate art of incremental refinement.

What did I learn from writing React at work?

What did I learn from writing React at work? Performance Optimization

Reveal the nature of front-end routing in depth, handwritten mini-Router

React-redux 100 lines of code to explore how

Thank you for attention

This article was first published in the public number “front-end from advanced to hospital”, click to receive the word advanced route, front-end algorithm zero-based selection of questions, I will often share some interesting things in work and life, and make friends and chat.