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
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
:
npx create-react-app show_path --template typescript
, ts has a pit in the back to appreciate it slowly.yarn eject
exposed configuration.In
config
build folderloaders/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 };
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") },
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);
};
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:
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:
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:
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,
},
});
}
},
});
- Filter out
JSXOpeningElement
type 0613dac89b5bb8 node.attributes.push
Put the new attributes into the attribute queue of the elementJSXIdentifier
Attribute name typeLiteral
Attribute value type
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:
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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。