共计 7307 个字符,预计需要花费 19 分钟才能阅读完成。
如何浏览 ’ 嵌套深 ’&’ 援用关系简单 ’ 的 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
:
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 = "<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 树的插件有 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 <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
这次就是这样, 心愿与你一起提高。