react-scanner 原理揭秘:AST 解析与 JSX 组件扫描是如何工作的
【免费下载链接】react-scannerExtract React components and props usage from code.项目地址: https://gitcode.com/gh_mirrors/re/react-scanner
如果你想快速摸清一个 React 项目里组件到底被用了多少次、每个 prop 的使用频率如何,react-scanner 是一个非常趁手的静态分析工具。它并不运行你的代码,而是从源码层面解析出组件与 props 的使用情况,输出一份结构化的 JSON 报告。这篇文章将带你深入它的内部实现,一步步拆解AST 解析与JSX 组件扫描的完整工作流程,让即使是新手也能看懂这套"扫描魔法"背后的原理。
先认识 react-scanner:静态分析的思路
react-scanner 的核心能力是Extract React components and props usage from code(从代码中提取 React 组件和 props 的使用情况),它天然支持 TypeScript。整个过程不依赖浏览器、不执行 JSX,而是通过**抽象语法树(AST)**来"读"代码。
它的工作可以概括为三步流水线:
| 步骤 | 做什么 | 对应源码 |
|---|---|---|
| ① 爬取文件 | 按 glob 规则找出所有待扫描的源文件 | src/run.js |
| ② 解析扫描 | 把代码解析成 AST,识别组件与 props | src/scan.js |
| ③ 处理报告 | 用处理器把原始报告加工成统计结果 | src/processors/ |
整个入口很简单:无论是命令行还是编程式调用,最终都会走进 src/scanner.js 的run方法,先做配置校验,再触发扫描流程。
第一步:如何精准爬取目标文件
扫描开始前,react-scanner 要先确定"扫哪些文件"。在 src/run.js 中,它借助fdir这个高性能目录爬取库,配合 glob 规则过滤出目标文件:
const files = new fdir() .glob(...globs) .exclude(getExcludeFn(config.exclude)) .withFullPaths() .crawl(crawlFrom) .sync();默认的 glob 是**/!(*.test|*.spec).@(js|ts)?(x),也就是自动跳过测试文件,只扫描.js、.jsx、.ts、.tsx。exclude配置则可以在 src/utils.js 的getExcludeFn中看到:既支持字符串/正则数组,也支持自定义函数,灵活排除node_modules之类的目录。
第二步:AST 解析,让机器"看懂"代码
这是最核心的一环。机器没法直接理解字符串形式的 JSX,所以需要先把它解析成抽象语法树。react-scanner 选择的解析器是@typescript-eslint/typescript-estree——它能把 TypeScript 和 JSX 代码都转换成标准的 ESTree 节点树。
在 src/scan.js 中可以看到解析选项:
const parseOptions = { loc: true, // 记录每个节点的行列位置 jsx: true, // 开启 JSX 语法支持 };loc: true非常重要,它让报告能够精确到某个组件出现在哪个文件的第几行第几列,这为后续的代码审计提供了精确坐标。
第三步:JSX 组件扫描,遍历 AST 找答案
解析出 AST 之后,react-scanner 使用astray这个轻量遍历库,只对两类节点感兴趣:
ImportDeclaration:记录 import 信息(来源模块、本地别名)JSXOpeningElement:发现组件被使用的位置
这部分逻辑在 src/scan.js 中。每当遇到一个 JSX 开始标签,getComponentNameFromAST(src/scan.js)就会解析出它的完整名称:
JSXIdentifier:普通名称,如ButtonJSXMemberExpression:成员表达式,如Footer.Content.Legal,会递归拼接成带点的完整路径
同时,importsMap(src/scan.js)维护着"本地名 → 导入来源"的映射。这样即使你写了import { Link as BasisLink } from "basis",扫描器也能准确还原出它实际指向的组件名,而不是被别名迷惑。
属性提取:JSXAttribute 与展开属性
组件实例上的 props 是怎么被记录的?看 src/scan.js 的getInstanceInfo,它会遍历node.attributes:
JSXAttribute:普通属性,比如<Text margin="4">,属性名和值都会被提取JSXSpreadAttribute:{...props}形式的展开属性,无法静态确定具体键值,所以标记propsSpread: true
每个使用实例最终会记录下:导入信息、props 键值对、是否展开、以及文件位置。
深入 getPropValue:字面量与表达式
一个有趣的细节是 prop 值如何被"翻译"成报告内容,这由getPropValue(src/scan.js)完成:
| AST 节点类型 | 报告中的值 |
|---|---|
Literal(字符串/数字/布尔) | 字面量本身,如"4" |
JSXExpressionContainer内的字面量 | 表达式的实际值 |
| 其他表达式(变量、函数等) | AST 类型名,如(Identifier) |
所以静态分析有其天然边界:传的是变量时,它只能告诉你"这里传了一个 Identifier 类型的表达式"。不过你可以通过配置getPropValue自定义处理,把表达式还原成真实代码,比如在 README.md 中就有用escodegen生成表达式源码的例子。
报告结构:一棵嵌套的组件树
扫描完成后,所有实例会聚合进一个嵌套的 report 对象,数据结构大致如下:
组件名 └── instances[] 每次使用实例 ├── importInfo 导入来源信息 ├── props prop 名 → 值 ├── propsSpread 是否使用了展开 └── location 文件与行列位置子组件(如Footer.Content)会嵌套在父组件之下,而forEachComponent(src/utils.js)提供了递归遍历这棵树的辅助方法,方便处理器逐层访问。
处理器:把原始报告变成有用统计
原始报告是"过程数据",直接看比较繁琐,所以 react-scanner 内置了三个处理器(src/processors/processors.json):
count-components:统计每个组件被使用的次数,输出{ "Text": 17 },见 count-components.jscount-components-and-props(默认):统计组件实例数 + 各 prop 的使用频次,见 count-components-and-props.jsraw-report:原样输出原始 JSON 报告,见 raw-report.js
处理器还可以自由组合、串联执行(支持异步函数),例如先把统计结果算出来,再通过自定义处理器把数据发给你的监控系统,这正是设计系统团队做组件健康度评估的常用玩法。
写在最后:静态扫描的适用场景
理解了 AST 解析与 JSX 组件扫描的原理,你就能明白 react-scanner 的价值所在:
- ✅ 统计设计系统组件的使用热度,决定哪些组件值得继续维护
- ✅ 追踪某个 prop 的取值分布,为废弃旧用法提供数据支撑
- ✅ 在大型代码库中快速盘点组件引用关系,无需手动搜索
由于是纯静态分析,它不会执行你的业务代码,速度快且无副作用;而 AST 方案又比正则匹配更可靠,能正确处理别名、展开属性、嵌套子组件等复杂情况。如果你正在为组件库的"家底"发愁,不妨 clone 下来动手跑一跑,亲眼看看这份报告是如何一步步生成的。🚀
【免费下载链接】react-scannerExtract React components and props usage from code.项目地址: https://gitcode.com/gh_mirrors/re/react-scanner
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考