如何阅读'嵌套深'&'引用关系复杂'的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缓解一下~】???? 针对特定类型的文件, 控制只在'开发环境下'为元素标签注入'路径属性', 并且它本身就很方便获得当前文件所属路径。
???? 本篇也只是做了个小功能插件, 虽然没解决大问题, 但是思考过程还挺有意思的。
效果图 当鼠标选停放在元素上, 则展示出该元素的文件夹路径
如何阅读'嵌套深'&'引用关系复杂'的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:
  1. npx create-react-app show_path --template typescript, ts在后面有坑慢慢欣赏。
  2. yarn eject 暴露配置。
  3. 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 };

  4. 打开show_path/config/webpack.config.js文件, 大概第557行, 添加如下代码:
    { test: /\.(tsx)$/, use: [ require.resolve("./loaders/loader.js") },

如何阅读'嵌套深'&'引用关系复杂'的react+ts项目?|如何阅读'嵌套深'&'引用关系复杂'的react+ts项目? 要不咱写个loader缓解一下~
文章图片

五. 正则'难以招架'的几种情况
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树的插件有esprimarecast, 我们可以把步骤差分成三部分, 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+ts项目?|如何阅读'嵌套深'&'引用关系复杂'的react+ts项目? 要不咱写个loader缓解一下~
文章图片

七. 如何生成与解析react代码
???? 这时我们可以为其传入一个参数jsx:true:
let astTree = esprima.parseModule(context, { jsx: true });

遍历这颗树 ???? 由于树结构可能会非常深, 我们可以用工具函数estraverse来做遍历:
estraverse.traverse(astTree, { enter(node) { console.log(node); }, });

此时报错了, 一起欣赏下吧:
如何阅读'嵌套深'&'引用关系复杂'的react+ts项目?|如何阅读'嵌套深'&'引用关系复杂'的react+ts项目? 要不咱写个loader缓解一下~
文章图片

解决遍历问题 ???? 我在网上找到了解决办法, 就是用专门处理jsxElement的循环插件yarn add estraverse-fb:
// 替换前 const estraverse = require("estraverse"); // 替换后 const estraverse = require("estraverse-fb");

可以正常循环:
如何阅读'嵌套深'&'引用关系复杂'的react+ts项目?|如何阅读'嵌套深'&'引用关系复杂'的react+ts项目? 要不咱写个loader缓解一下~
文章图片

生成代码 ???? 我平时常用的解析纯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); };

然后就又报错了:
如何阅读'嵌套深'&'引用关系复杂'的react+ts项目?|如何阅读'嵌套深'&'引用关系复杂'的react+ts项目? 要不咱写个loader缓解一下~
文章图片

但此时问题肯定是出在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, }, }); } }, });

  1. 筛选出JSXOpeningElement类型的元素
  2. node.attributes.push将要新增的属性放入元素的属性队列
  3. JSXIdentifier属性名类型
  4. Literal属性值类型
如何阅读'嵌套深'&'引用关系复杂'的react+ts项目?|如何阅读'嵌套深'&'引用关系复杂'的react+ts项目? 要不咱写个loader缓解一下~
文章图片

配合recast确实可以把代码还原的不错, 但这就真的结束了么?
十. ts有话说!
???? 当我把开发的loader投入到实际项目时, 那真是大写的傻眼, 假设开发的代码如下:
import React from "react"; export default function Home() { interface C { name: string; } const c: C = { name: "金毛", }; return home 页面; }

则会产生如下报错信息:
如何阅读'嵌套深'&'引用关系复杂'的react+ts项目?|如何阅读'嵌套深'&'引用关系复杂'的react+ts项目? 要不咱写个loader缓解一下~
文章图片

???? 也好理解, 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
???? 这次就是这样, 希望与你一起进步。

    推荐阅读