10

How to read the react+ts project with'deep nesting' &'complex reference relationship'? Or let's write a loader to ease it~

introduce

This article describes the whole process I went through to make this function, falling into the pit and crawling out continuously, which is more interesting than the result process, so I want to share it.

1. The project is too'complex', and I have to worry about finding a component

As the project gets bigger and bigger (cha), there will be a lot of deep code modules, for example, you see a'business card box' displayed on the page, but you may need to look for several minutes to find this'business card box' In which file did you write the code for this project, if you just received this project and it was not maintained by you in the previous years, then the process of finding the code will be painful, and React Developer Tools does not solve this problem well.

It should be clear that the so-called'complex' may just be the'poor' written by everyone's code, and the code structure design is unreasonable, such as excessive abstraction. Many people think that as long as the component code is continuously extracted, and the fewer comments, the better. Good code, in fact, it's only at a'rough level'. The code is written for people to see. The logic of the code is clear, and it is easy to read and easy to find the core function nodes. Software will degrade performance. After all, it is inevitable to generate new scopes. vue react than 0613dac89b53d5 to be too abstract.

One of the solutions I thought of here is this, to add react ' attribute to each element: (This time we take the 0613dac89b53ec + Ts project as an example)

  • For example, an exported button component, the code location is 'object/src/page/home/index.tsx'
  • Then we can write <button tipx='object/src/page/home/index.tsx'>button</button>
  • We can hover to show the path, or view the path information through the console
  • For example, tags such as img and input that cannot use the pseudo-element need to open the console to view

2. Scheme selection

Google Chrome plugin

Although it is easy to insert attributes for tags, it is impossible to read the development path where the plug-in is located. This solution can be ruled out.

vscode plugin

The folder where the development file is located can be read very well, but adding the path attribute will destroy the overall code structure, and it is not easy to deal with the user's initiative to delete some attributes and distinguish the development environment from the production environment. After all, we can't in the production environment. Will deal with it.

loader

For a specific type of file, the control only injects the'path attribute' into the element tag in the'development environment', and it is very convenient to get the path of the current file by itself.

This article is just a small functional plug-in. Although it did not solve the big problem, the thinking process is quite interesting.

Effect picture

When the mouse is selected and hovered on an element, the folder path of the element is displayed

image.png

Three. Style plan

After assigning the tag attribute, we have to think about how to get it. Obviously, we will use the attribute selector this time to retrieve all tags with the tag attribute of tipx , and then we will use the pseudo element befour or after to display the file address.

attr Do you remember?

This attribute is css code to obtain the dom tag attribute, and we can write it as follows:

[tipx]:hover[tipx]::after{
  content: attr(tipx);
  color: white;
  display: flex;
  position: relative;
  align-items: center;
  background-color: red;
  justify-content: center;
  opacity: .6;
  font-size: 16px;
  padding: 3px 7px;
  border-radius: 4px;
}

4. Scheme 1: loader with regular

The simple and rude way must be the regularity, which matches all the start tags, such as replacing <div <div tipx='xxxxxx' . It should be noted here that we don't need to put attributes on the custom component, but put the attributes on the native label.

  // 大概就是这个意思, 列举出所有的原生标签名
  context = context.replace(
    /\<(div|span|p|ul|li|i|a")/g,
    `<$1 tipx='${this.resourcePath}'`
  );

We create react project from scratch and set loader :

  1. npx create-react-app show_path --template typescript , ts has a pit in the back to appreciate it slowly.
  2. yarn eject exposed configuration.
  3. In config build folder loaders/loader.js .

    module.exports = function (context) {
      // .... 稍后在此大(lang)展(bei)身(bu)手(kan)
      context = context.replace(
     /\<(div|span|p|ul|li|i|a")/g,
     `<$1 tipx='${this.resourcePath}'`
      );
      return context
    };
    
  4. Open the show_path/config/webpack.config.js file, about line 557, and add the following code:

         {
           test: /\.(tsx)$/,
           use: [
             require.resolve("./loaders/loader.js")
         },

image.png

Five. Several situations of regular "difficult to parry"

1: div string
const str = "<div>是古代程序员, 经常使用的标签"

The above situation will be misjudged as a real label by regular rules, but this string should not be modified in fact.

2: Duplicate name
<divss>自定义标签名<divss>

The probability of this type of label is small, but there is a chance that the same name may appear

3: single quotation mark double quotation mark
const str = "<div>标签外层已经有双引号</div>"

// 替换后报错
const str = "<div tipx="xxx/xx/xx/x.index">标签外层已经有双引号</div>"

It’s hard for us to judge whether the outer layer is single or double quotation marks

4:styled-components

The way this technology is written makes it impossible for us to separate it, such as the following writing:

import styled from "styled-components";

export default function Home() {
  const MyDiv = styled.div`
    border: 1px solid red;
  `;
  return <MyDiv>123</MyDiv>
}

6. Scheme 2: AST tree & get current file path

Finally reached the main task. Parsing the code into a tree structure can be more comfortable for analysis. The more useful plug-ins for converting AST trees are esprima and recast . We can divide the steps into three parts, code to tree structure, Loop through the tree structure, and tree structure to code.

The current file path webpack has been injected into the loader, this.resourcePath can be retrieved, but it will be a global path, that is, the full path of the computer from the root directory to the current directory. If necessary, we can split it and display it.

We loader.js , and reported an error during the "first step" parsing, because it did not recognize the grammar of jsx

const esprima = require("esprima");

module.exports = function (context, map, meta) {
  let astTree = esprima.parseModule(context);
  console.log(astTree);
  this.callback(null, context, map, meta);
};

image.png

7. How to generate and parse react code

At this time, we can pass in a parameter jsx:true :

  let astTree = esprima.parseModule(context, { jsx: true });
Traverse this tree

Since the tree structure may be very deep, we can use the utility function estraverse to traverse:

    estraverse.traverse(astTree, {
      enter(node) {
        console.log(node);
      },
    });

An error was reported at this time, let's enjoy it together:

image.png

Solve the traversal problem

I found a solution on the Internet, which is to use the loop plug-in yarn add estraverse-fb that specializes in jsxElement:

// 替换前
const estraverse = require("estraverse");

// 替换后
const estraverse = require("estraverse-fb");

It can loop normally:
image.png

Generate code

The tool function I usually use to parse pure js code has escodegen :

const esprima = require("esprima");
const estraverse = require("estraverse-fb");
const escodegen = require("escodegen");

module.exports = function (context, map, meta) {
  let astTree = esprima.parseModule(context, { jsx: true });
  estraverse.traverse(astTree, {
    enter(node) {}
  });
  // 此处将AST树转成js代码
  context = escodegen.generate(astTree);
  this.callback(null, context, map, meta);
};

Then it reported an error again:
image.png

But at this time the problem must be the step of restoring the AST tree to jscode. I searched for escodegen and did not find a configuration that can solve the current problem. At that time, I had to look for other plug-ins.

8. recast

recast is also a very useful AST conversion library, recast official website address , but it does not have its own useful traversal method, use it as follows:

const recast = require("recast");

module.exports = function (context, map, meta) {
    // 1: 生成树
    const ast = recast.parse(context);
    // 2: 转换树
    const out = recast.print(ast).code;
    context = out;
  this.callback(null, context, map, meta);
};

Then we reluctantly cut love and only take its tree-to-code function:

// 替换前
 context = escodegen.generate(astTree);

// 替换后
 context = recast.print(astTree).code;

9. Find the target & assign attributes

The front and back processes have been opened. Now it is necessary to assign attributes to the tags. Let's look directly at the writing I summarized here:

    const path = this.resourcePath;
    estraverse.traverse(astTree, {
      enter(node) {
        if (node.type === "JSXOpeningElement") {
        node.attributes.push({
          type: "JSXAttribute",
          name: {
            type: "JSXIdentifier",
            name: "tipx",
          },
          value: {
            type: "Literal",
            value: path,
          },
        });
        }
      },
    });
  1. Filter out JSXOpeningElement type 0613dac89b5bb8
  2. node.attributes.push Put the new attributes into the attribute queue of the element
  3. JSXIdentifier Attribute name type
  4. Literal Attribute value type

image.png

recast with 0613dac89b5c72 can indeed restore the code well, but is this really over?

Ten. ts has something to say!

When I put the developed loader into the actual project, it was really dumbfounded in capitalization, assuming the developed code is as follows:

import React from "react";

export default function Home() {
  interface C {
    name: string;
  }
  const c: C = {
    name: "金毛",
  };
  return <div title="title">home 页面</div>;
}

The following error message will be generated:

image.png

It's interface understand that 0613dac89b5d1b cannot be used at will, because this is the syntax ts js . The first thing I thought of was ts-loader and tried ts-loader first, and then we parsed the compiled code, but it didn't work.

esprima cannot directly read ts grammar, ts-loader cannot parse jsx very well, and the parsed code cannot match the various codes we wrote to parse the AST tree. I was in a quagmire at the time. At this time, it is omnipotent. The babel-loader bravely stood up!

Eleven. babel changed the cut

We put it at the top to execute:

{
   test: /\.(tsx)$/,
   use: [
       require.resolve("./loaders/loader.js"),
       {
        loader: require.resolve("babel-loader"),
          options: {
            presets: [[require.resolve("babel-preset-react-app")]],
          },
        },
       ],
},

At that time, I gave myself a 4.6s palm, and finally passed, but it can't end like this, because the file has been babel , so in theory, we can remove the special processing jsx

// 之前的
const estraverse = require("estraverse-fb");

// 现在的
const estraverse = require("estraverse");


// 之前的
let astTree = esprima.parseModule(context, { jsx: true });

// 现在的
let astTree = esprima.parseModule(context);
The loop is no longer jsx, and the loop body has to be changed
// 之前的
  estraverse.traverse(astTree, {
    enter(node) {
      if (node.type === "JSXOpeningElement") {
        node.attributes.push({
           type: "JSXAttribute",
           name: {
             type: "JSXIdentifier",
             name: "tipx",
           },
           value: {
             type: "Literal",
             value: path,
           },
         });
      }
    },
  });

// 现在的
  estraverse.traverse(astTree, {
    enter(node) {
      if (node.type === "ObjectExpression") {
        node.properties.push({
          type: "Property",
          key: { type: "Identifier", name: "tipx" },
          computed: false,
          value: {
            type: "Literal",
            value: path,
            raw: '""',
          },
          kind: "init",
          method: false,
          shorthand: false,
        });
      }
    },
  });

Starting our project at this time can already parse the ts language, but...there is another problem in the actual project!

12. Errors in actual development

According to the way I configured above, I put it into the official project intact, and an error was reported. I will just say that the cause of the error is that package.json needs to specify the type babel

  "babel": {
    "presets": [
      "react-app"
    ]
  },

Here is my version of babel

    "@babel/core": "7.12.3",
    "babel-loader": "8.1.0",
    "babel-plugin-named-asset-import": "^0.3.7",
    "babel-preset-react-app": "^10.0.0",

Do you think there are no bugs?

Thirteen. I really need a try to debut!

It's really that some grammar still has problems. It may be necessary to carry out a unique configuration based on the characteristics of each project, but there are only 3 pages of code in the hundred pages and a strange error was reported. In the end, I chose to use try catch wrap the whole process, and this is also the case. The most rigorous approach, after all, just an auxiliary plug-in should not affect the main process.

14. Complete code

const esprima = require('esprima');
const estraverse = require('estraverse');
const recast = require('recast');
module.exports = function (context, map, meta) {
  const path = this.resourcePath;
  let astTree = '';
  try {
    astTree = esprima.parseModule(context);
    estraverse.traverse(astTree, {
      enter(node) {
        if (node.type === 'ObjectExpression') {
          node.properties.push({
            type: 'Property',
            key: { type: 'Identifier', name: 'tipx' },
            computed: false,
            value: {
              type: 'Literal',
              value: path,
              raw: '""',
            },
            kind: 'init',
            method: false,
            shorthand: false,
          });
        }
      },
    });
    context = recast.print(astTree).code;
  } catch (error) {
    console.log('>>>>>>>>错误');
  }
  return context;
};

Configuration

        {
          test: /\.(tsx)$/,
          use: [
            require.resolve("./loaders/loader.js"),
            {
              loader: require.resolve("babel-loader"),
              options: {
                presets: [[require.resolve("babel-preset-react-app")]],
              },
            },
          ],
        },

15. My harvest?

Although the final code is not long, the process is really bumpy. I keep trying various libraries, and if I want to solve the problem, I have to dig out what these libraries do. Just this once made me understand the compilation aspect. Have a better understanding.

The entire component can only mark the location of the component code, but it cannot indicate the file location of its parent. You also need to open the console to view the tipx attribute of his parent tag, but at least when a small component is out The problem, it happens that the naming of this small component is not standardized, and the set is a bit deep, and we are not familiar with the code, then try to use this loader find him.

end

This is the case this time, I hope to make progress with you.


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者