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

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

介绍

     本文讲述了我为做出这个功能所经历的全过程, 不断的掉进坑里又不断地爬出来, 相比于结果过程更有趣, 所以才想把它分享出来。

一. 项目太'复杂', 找个组件都发愁

     随着项目越做越大(cha), 会多出不少很深的代码模块, 比如你看到页面上显示的一个'名片框', 但你可能需要找好几分钟才能找到这个'名片框'的代码写在了哪个文件里, 如果这个项目你只是接收过来, 前几年不是你在维护, 那么寻找代码这个过程会很痛苦, React Developer Tools也并没有很好的解决这个问题。

     要明确一点所谓的'复杂'可能只是大家代码写的'差', 代码结构设计的不合理, 比如过分抽象, 很多人认为只要不断的抽出组件代码, 并且注释越少越好, 这样写的就是好代码, 其实这只是处于'比较初级的水平', 代码是写给人看的将代码写的逻辑清晰, 并且容易读懂容易找到核心的功能节点才是好代码, 往往过分的抽离出小组件会使性能下降, 毕竟难免要生成新的作用域, 很多人写react比写vue更容易过分抽象。

     这里我想到的解决方案之一是这样的, 为每个元素添加一个'地址'属性: (本次以react + Ts 项目为例)

  • 比如某个导出的 button组件, 代码所在位置'object/src/page/home/index.tsx'
  • 则我们就可以这样写 <button tipx='object/src/page/home/index.tsx'>按钮</button>
  • 我们可以悬停展示路径, 也可以通过控制台查看路径信息
  • 比如img、input这种无法使用伪元素的标签需要打开控制台查看

二. 方案选择

谷歌浏览器插件

     这个虽然很容易为标签插入属性, 但是无法读取到插件所在的开发路径, 这个方案可以排除了。

vscode 插件

     可以很好的读取到开发文件所在的文件夹, 但是添加路径属性的话会破坏整体的代码结构, 并且不好处理用户主动删掉某些属性以及区分开发环境与生产环境, 毕竟生产环境我们可不会做处理。

loader

     针对特定类型的文件, 控制只在'开发环境下'为元素标签注入'路径属性', 并且它本身就很方便获得当前文件所属路径。

     本篇也只是做了个小功能插件, 虽然没解决大问题, 但是思考过程还挺有意思的。

效果图

当鼠标选停放在元素上, 则展示出该元素的文件夹路径

image.png

三. 样式方案

     赋予标签属性之后我们就要思考如何获取它了, 显而易见我们这次要用属性选择器, 把所有标签属性有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配正则

     简单粗暴的方式那肯定非 正则 莫属, 匹配出所有的开始标签, 比如<div替换成<div tipx='xxxxxx' , 这里要注意我们不用向自定义的组件上放属性, 要把属性放在原生标签上。

  // 大概就是这个意思, 列举出所有的原生标签名
  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")
         },

image.png

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

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

上述情况会被正则误判成真实标签, 但其实不应该修改这个字符串。

2:名称重复
<divss>自定义标签名<divss>

此类标签几率小, 但是有几率出现重名的情况

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

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

我们不好判断外层是单引号还是双引号

4:styled-components

这个技术的书写方式使我们没法拆分出来, 比如下面的写法:

import styled from "styled-components";

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

六. 方案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);
};

image.png

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

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

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

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

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

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

image.png

解决遍历问题

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

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

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

可以正常循环:
image.png

生成代码

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

然后就又报错了:
image.png

但此时问题肯定是出在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属性值类型

image.png

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

十. ts有话说!

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

import React from "react";

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

则会产生如下报错信息:

image.png

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

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

自信自律, 终身学习.

5.5k 声望
6.8k 粉丝
0 条评论
推荐阅读
第九期:前端九条启发分享
下图是一个常见的列表, 点击列表里的详情按钮会跳到详情页, 那么也许我们在详情页修改了数据状态, 此时可能需要把修改后的状态直接传给列表页从而本地直接更新列表, 这样就不用发送新的api请求与后端交互了。

lulu_up5阅读 167

手把手教你写一份优质的前端技术简历
不知不觉一年一度的秋招又来了,你收获了哪些大厂的面试邀约,又拿了多少offer呢?你身边是不是有挺多人技术比你差,但是却拿到了很多大厂的offer呢?其实,要想面试拿offer,首先要过得了简历那一关。如果一份简...

tonychen152阅读 17.7k评论 5

封面图
正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青56阅读 8.4k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy48阅读 6.8k评论 12

从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
总结截止到本章 “从零搭建 Node.js 企业级 Web 服务器” 主题共计 16 章内容就更新完毕了,回顾第零章曾写道:搭建一个 Node.js 企业级 Web 服务器并非难事,只是必须做好几个关键事项这几件必须做好的关键事项就...

乌柏木75阅读 7k评论 16

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs42阅读 6.8k评论 12

封面图
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
分层规范从本章起,正式进入企业级 Web 服务器核心内容。通常,一块完整的业务逻辑是由视图层、控制层、服务层、模型层共同定义与实现的,如下图:从上至下,抽象层次逐渐加深。从下至上,业务细节逐渐清晰。视图...

乌柏木45阅读 8.4k评论 6

自信自律, 终身学习.

5.5k 声望
6.8k 粉丝
宣传栏