乐趣区

关于前端:如何阅读嵌套深引用关系复杂的reactts项目-要不咱写个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

     针对特定类型的文件, 管制只在 ’ 开发环境下 ’ 为元素标签注入 ’ 门路属性 ’, 并且它自身就很不便取得以后文件所属门路。

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

效果图

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

三. 款式计划

     赋予标签属性之后咱们就要思考如何获取它了, 不言而喻咱们这次要用 属性选择器 , 把所有标签属性有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")
         },

五. 正则 ’ 难以招架 ’ 的几种状况

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);
};

七. 如何生成与解析 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,
          },
        });
        }
      },
    });
  1. 筛选出 JSXOpeningElement 类型的元素
  2. node.attributes.push将要新增的属性放入元素的属性队列
  3. JSXIdentifier属性名类型
  4. Literal属性值类型

配合 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>;
}

则会产生如下报错信息:

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

     这次就是这样, 心愿与你一起提高。

退出移动版