如何阅读'嵌套深'&'引用关系复杂'的react+ts项目?|如何阅读'嵌套深'&'引用关系复杂'的react+ts项目? 要不咱写个loader缓解一下~
如何阅读'嵌套深'&'引用关系复杂'的react+ts项目? 要不咱写个loader缓解一下~
介绍
???? 本文讲述了我为做出这个功能所经历的全过程, 不断的掉进坑里又不断地爬出来, 相比于结果过程更有趣, 所以才想把它分享出来。
一. 项目太'复杂', 找个组件都发愁
???? 随着项目越做越大(cha), 会多出不少很深的代码模块, 比如你看到页面上显示的一个'名片框', 但你可能需要找好几分钟才能找到这个'名片框'的代码写在了哪个文件里, 如果这个项目你只是接收过来, 前几年不是你在维护, 那么寻找代码这个过程会很痛苦, React Developer Tools
也并没有很好的解决这个问题。
???? 要明确一点所谓的'复杂'可能只是大家代码写的'差', 代码结构设计的不合理, 比如过分抽象, 很多人认为只要不断的抽出组件代码, 并且注释越少越好, 这样写的就是好代码, 其实这只是处于'比较初级的水平', 代码是写给人看的将代码写的逻辑清晰, 并且容易读懂容易找到核心的功能节点才是好代码, 往往过分的抽离出小组件会使性能下降, 毕竟难免要生成新的作用域, 很多人写react
比写vue
更容易过分抽象。
???? 这里我想到的解决方案之一是这样的, 为每个元素添加一个'地址'属性: (本次以react
+ Ts
项目为例)
- 比如某个导出的
button
组件, 代码所在位置'object/src/page/home/index.tsx'
- 则我们就可以这样写
- 我们可以悬停展示路径, 也可以通过控制台查看路径信息
- 比如img、input这种无法使用
伪元素
的标签需要打开控制台查看
谷歌浏览器插件 ???? 这个虽然很容易为标签插入属性, 但是无法读取到插件所在的开发路径, 这个方案可以排除了。
vscode 插件 ???? 可以很好的读取到开发文件所在的文件夹, 但是添加路径属性的话会破坏整体的代码结构, 并且不好处理用户主动删掉某些属性以及区分开发环境与生产环境, 毕竟生产环境我们可不会做处理。
loader 【如何阅读'嵌套深'&'引用关系复杂'的react+ts项目?|如何阅读'嵌套深'&'引用关系复杂'的react+ts项目? 要不咱写个loader缓解一下~】???? 针对特定类型的文件, 控制只在'开发环境下'为元素标签注入'路径属性', 并且它本身就很方便获得当前文件所属路径。
???? 本篇也只是做了个小功能插件, 虽然没解决大问题, 但是思考过程还挺有意思的。
效果图 当鼠标选停放在元素上, 则展示出该元素的文件夹路径
文章图片
三. 样式方案
???? 赋予标签属性之后我们就要思考如何获取它了, 显而易见我们这次要用
属性选择器
, 把所有标签属性有tipx
的标签全部检索出来, 然后我们通过伪元素befour
或者after
来展示这个文件地址。attr
你还记得不?
???? 这个属性是css
代码用来获取dom
标签属性的, 而我们就可以有如下的写法:[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;
}
四. 方案1: loader配正则
???? 简单粗暴的方式那肯定非
正则
莫属, 匹配出所有的开始标签, 比如替换成 , 这里要注意我们不用向自定义的组件上放属性, 要把属性放在原生标签上。
// 大概就是这个意思, 列举出所有的原生标签名
context = context.replace(
/\<(div|span|p|ul|li|i|a")/g,
`<$1 tipx='${this.resourcePath}'`
);
???? 我们从头创建react
项目并设置loader
:
npx create-react-app show_path --template typescript
, ts在后面有坑慢慢欣赏。
yarn eject
暴露配置。
- 在
config
文件夹下建立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
};
- 打开
show_path/config/webpack.config.js
文件, 大概第557行, 添加如下代码:
{
test: /\.(tsx)$/,
use: [
require.resolve("./loaders/loader.js")
},
文章图片
五. 正则'难以招架'的几种情况
1:div字符串
const str = "是古代程序员, 经常使用的标签"
上述情况会被正则误判成真实标签, 但其实不应该修改这个字符串。
2:名称重复
自定义标签名
此类标签几率小, 但是有几率出现重名的情况
3:单引号双引号
const str = "标签外层已经有双引号"// 替换后报错
const str = "标签外层已经有双引号"
我们不好判断外层是单引号还是双引号
4:styled-components
这个技术的书写方式使我们没法拆分出来, 比如下面的写法:
import styled from "styled-components";
export default function Home() {
const MyDiv = styled.div`
border: 1px solid red;
`;
return 123
}
六. 方案2: AST树 & 获取当前文件路径
???? 终于到达主线任务了, 将代码解析成树结构就可以更舒服的分析了, 比较好用的转换AST树的插件有esprima
和recast
, 我们可以把步骤差分成三部分, code转树结构
、循环遍历树结构
、树结构转code
。
???? 当前文件路径webpack
已经注入了loader里面, this.resourcePath
就可以取到, 但它会是一个全局路径, 也就是从根目录一直到当前目录的电脑完整路径, 有需要的话我们可以进行一下拆分展示。
???? 我们为loader.js
写入代码,进行 "第一步" 解析的时候报错了, 原因是它不认识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);
};
文章图片
七. 如何生成与解析react
代码
???? 这时我们可以为其传入一个参数jsx:true
:
let astTree = esprima.parseModule(context, { jsx: true });
遍历这颗树
???? 由于树结构可能会非常深, 我们可以用工具函数estraverse
来做遍历:
estraverse.traverse(astTree, {
enter(node) {
console.log(node);
},
});
此时报错了, 一起欣赏下吧:
文章图片
解决遍历问题
???? 我在网上找到了解决办法, 就是用专门处理jsxElement的循环插件yarn add estraverse-fb
:
// 替换前
const estraverse = require("estraverse");
// 替换后
const estraverse = require("estraverse-fb");
可以正常循环:
文章图片
生成代码
???? 我平时常用的解析纯js代码的工具函数登场了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);
};
然后就又报错了:
文章图片
但此时问题肯定是出在AST树还原成jscode这一步了, 搜索了escodegen
的各种配置并没有找到可以解决当前问题的配置, 当时也只好去寻找其他插件了。
八. recast
???? recast
也是一款很好用的AST转换库, recast官网地址, 但他没有自带好用的遍历方法, 使用方式如下:
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);
};
那我们忍痛割爱只取它的树转code功能:
// 替换前
context = escodegen.generate(astTree);
// 替换后
context = recast.print(astTree).code;
九. 找到目标 & 赋予属性
???? 前后流程都打通了现在需要对标签赋予属性了, 这里直接看我总结的写法吧:
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,
},
});
}
},
});
- 筛选出
JSXOpeningElement
类型的元素
node.attributes.push
将要新增的属性放入元素的属性队列
JSXIdentifier
属性名类型
Literal
属性值类型
文章图片
配合recast
确实可以把代码还原的不错, 但这就真的结束了么?
十. ts有话说!
???? 当我把开发的loader
投入到实际项目时, 那真是大写的傻眼, 假设开发的代码如下:
import React from "react";
export default function Home() {
interface C {
name: string;
}
const c: C = {
name: "金毛",
};
return home 页面;
}
则会产生如下报错信息:
文章图片
???? 也好理解, interface
不能随意使用, 因为这是ts
的语法咱们js
不认识, 我第一时间想到的是ts-loader
并且尝试了让ts-loader
先编译, 然后我们解析它编译过的代码, 但是果然行不通。
???? esprima
这边无法直接读懂ts语法
, ts-loader
无法很好的解析jsx
并且解析后的代码无法与我们之前写的各种解析AST树的代码相配合, 我当时一度陷入'泥潭', 这个时候万能的babel-loader
勇敢的站了出来!
十一. babel
改变了切
???? 我们把它放在最前面执行:
{
test: /\.(tsx)$/,
use: [
require.resolve("./loaders/loader.js"),
{
loader: require.resolve("babel-loader"),
options: {
presets: [[require.resolve("babel-preset-react-app")]],
},
},
],
},
???? 当时给自己鼓了4.6s的掌, 终于通过了, 但是不能就这样结束了, 由于文件已经被babel
处理过了, 所以理论上我们之前针对jsx
的特殊处理都可以去掉了:
// 之前的
const estraverse = require("estraverse-fb");
// 现在的
const estraverse = require("estraverse");
// 之前的
let astTree = esprima.parseModule(context, { jsx: true });
// 现在的
let astTree = esprima.parseModule(context);
循环的已经不是jsx了, 循环体里面也要大改
// 之前的
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,
});
}
},
});
此时启动我们的项目就已经可以解析ts
语言了, 但是...投入实际项目里又又又出问题了!
十二. 实际开发时的错误
???? 按照我上面配置的方式原封不动的放入正式项目, 竟然报错了, 我就直接说吧错误原因是package.json
里面需要为babel
指定类型:
"babel": {
"presets": [
"react-app"
]
},
这里再附上我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",
你以为这就没bug了?
十三. 竟然真需要try登场!
???? 真的是一些语法仍然有问题, 可能需要结合每个项目的特点进行一个独特的配置, 但是进百页代码只有3页报了奇怪的错, 最后还是选择使用try catch
包裹住了整个过程, 这样也是最严谨的做法, 毕竟只是个辅助插件不应影响主体流程的进行。
十四. 完整代码
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;
};
配置
{
test: /\.(tsx)$/,
use: [
require.resolve("./loaders/loader.js"),
{
loader: require.resolve("babel-loader"),
options: {
presets: [[require.resolve("babel-preset-react-app")]],
},
},
],
},
十五. 我的收获?
???? 虽然最终的代码并不长, 但是过程真的是挺坎坷的, 不断的尝试各种库, 并且要想解决问题就要挖一挖这些库到底做了什么, 就这样一次就使我对编译方面有了更好的理解。
???? 整个组件只能标出组件代码所在的位置, 并不能很好的指出其父级所在的文件位置, 还需要打开控制台查看他父级标签的tipx
属性, 但至少当某个小小的组件出问题, 恰好这个小组件的命名不规范,且套还有点深, 而且我们还不熟悉代码, 那就试试使用这个loader
找出他吧。
end
???? 这次就是这样, 希望与你一起进步。
推荐阅读
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 考研英语阅读终极解决方案——阅读理解如何巧拿高分
- Ⅴ爱阅读,亲子互动——打卡第178天
- 如何寻找情感问答App的分析切入点
- 上班后阅读开始变成一件奢侈的事
- mybatisplus如何在xml的连表查询中使用queryWrapper
- mybatisplus|mybatisplus where QueryWrapper加括号嵌套查询方式
- MybatisPlus使用queryWrapper如何实现复杂查询
- 历史教学书籍
- 如何在Mac中的文件选择框中打开系统隐藏文件夹
- 漫画初学者如何学习漫画背景的透视画法(这篇教程请收藏好了!)