如何阅读'嵌套深'&'引用关系复杂'的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项目? 要不咱写个loader缓解一下~_第1张图片

三. 样式方案

     赋予标签属性之后我们就要思考如何获取它了, 显而易见我们这次要用属性选择器, 把所有标签属性有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项目? 要不咱写个loader缓解一下~_第2张图片

五. 正则'难以招架'的几种情况

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项目? 要不咱写个loader缓解一下~_第3张图片

七. 如何生成与解析react代码

     这时我们可以为其传入一个参数jsx:true:

  let astTree = esprima.parseModule(context, { jsx: true });
遍历这颗树

     由于树结构可能会非常深, 我们可以用工具函数estraverse来做遍历:

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

此时报错了, 一起欣赏下吧:

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

解决遍历问题

     我在网上找到了解决办法, 就是用专门处理jsxElement的循环插件yarn add estraverse-fb:

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

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

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

生成代码

     我平时常用的解析纯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项目? 要不咱写个loader缓解一下~_第6张图片

但此时问题肯定是出在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项目? 要不咱写个loader缓解一下~_第7张图片

配合recast确实可以把代码还原的不错, 但这就真的结束了么?

十. ts有话说!

     当我把开发的loader投入到实际项目时, 那真是大写的傻眼, 假设开发的代码如下:

import React from "react";

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

则会产生如下报错信息:

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

     也好理解, 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

     这次就是这样, 希望与你一起进步。