vite 将react老项目中的没有兼容处理的写法转成兼容性写法

背景与目标

背景 :后端接口技改后,查询无数据时将返回 null 而非空对象,这可能导致前端老代码因未做兼容处理而出现运行时错误(如访问 null 的属性、对 null 进行解构等)。

目标 :针对指定文件 / 文件夹内的 JavaScript/TypeScript 代码(js/jsx/ts/tsx),自动添加可选链(?.)和逻辑表达式(||/&&),确保对对象、数组的操作在遇到 null/undefined 时不报错,兼容后端返回格式的变化。

使用和安装

npx -y @chagee/fix-optional babel-fix ./src 可以指定文件或者文件夹

技术方案

采用 Babel 插件转换 方案,通过 AST(抽象语法树)遍历代码,对关键节点进行自动修改,核心流程如下:

  1. 基于 Babel 插件机制:通过自定义 Babel 插件,在代码转译阶段拦截并处理 AST 节点。
  2. 节点钩子处理:针对可能引发错误的语法节点(成员访问、解构、赋值等),编写访问器钩子(Visitor),自动添加容错逻辑。
  3. TypeScript 兼容:处理 TypeScript 代码时,先剥离泛型等类型信息,确保转换逻辑专注于运行时代码。

术语解释优化

  1. LogicalExpression(逻辑表达式) 指包含逻辑运算符 ||(逻辑或)或 &&(逻辑与)的表达式,用于组合或判断多个条件。示例:a || [](若 a 为假值则返回空数组)、a && b(若 a 为真值则返回 b)。核心结构包含:

    1. left:左侧表达式
    2. right:右侧表达式
    3. operator:逻辑运算符(|| 或 &&)
  2. MemberExpression | OptionalMemberExpression(成员访问表达式 | 可选成员访问表达式) 用于访问对象成员的表达式,包含以下关键属性:

    1. Optional:标记是否为可选访问(即是否带 ?. 运算符)。
    2. Computed:标记是否为计算属性访问(如 a[b][c] 中 [b] 和 [c] 为计算属性)。
    3. Object:当前成员的父级对象(如 a.b.c 中,a.b 是 c 的 Object,a 是 b 的 Object)。
    4. Property:当前访问的属性(如 a.b.c 中,c 是最终的 Property)。
  3. VariableDeclarator(变量声明器) 用于变量声明与赋值的表达式,核心属性:

    1. id:赋值语句的左侧(被赋值的变量或解构模式)。
    2. init:赋值语句的右侧(赋值的初始值)。
  4. SpreadElement(扩展元素) 指使用 ... 语法的解构元素,用于将数组或对象的属性展开(如 [a, ...b] 或 {x, ...y})。

  5. UpdateExpression(更新表达式) 包含自增(++)或自减(--)运算符的表达式,用于修改变量的值(如 a++ 或 obj.prop--)。

  6. AssignmentExpression(赋值表达式) 用于变量或属性赋值的表达式(区别于 VariableDeclarator,后者是声明时的赋值),如 a.b = 1 或 x = y + z。

  7. CallExpression | OptionalCallExpression(函数调用表达式 | 可选函数调用表达式) 用于调用函数或方法的表达式,Optional 标记是否为可选调用(即是否带 ?.() 语法),如 fn() 或 obj.method?.()。

核心实现逻辑补充与优化

MemberExpression | OptionalMemberExpression(成员访问表达式)

问题 :深层属性访问可能因中间值为 undefined 或 null 报错(如 a = {}; a.b.c 会报错)。优化目标 :自动为深层成员访问添加可选链(?.),避免报错。

实现逻辑:

  • 递归遍历成员表达式的层级(如 a.b.c 拆解为 a → a.b → a.b.c)。
  • 对每个非顶层的成员访问添加 ?. 运算符(顶层不添加,避免 ?.a 这类无效语法)。
  • 对计算属性(如 a[b])转换为 a?.[b]。

示例:

css 复制代码
a.b.c        →  a?.b?.c  
a[b][c]      →  a?.[b]?.[c]  

VariableDeclarator(变量声明器)

问题 :数组或对象解构时,若右侧初始值为 undefined 或 null,会导致解构失败(如 const [x] = undefined 报错)。优化目标:为解构的初始值添加默认兜底,确保解构安全。

实现逻辑:

  • 判断左侧 id 是数组解构([x, y])还是对象解构({x, y})。
  • 若右侧 init 不是 LogicalExpression,则为其包裹 || [] 或 || {}(根据解构类型)。

示例:

css 复制代码
// 数组解构const [a, b] = xxx  →  const [a, b] = xxx || []  

// 对象解构const {a, b} = xxx  →  const {a, b} = xxx || {}  

SpreadElement(扩展元素)

问题 :扩展运算符后的变量若为 undefined 或 null,会导致解构报错(如 [...undefined] 报错)。优化目标:为扩展元素添加默认兜底,确保其为有效数组或对象。

实现逻辑:

  • 根据父级上下文判断是数组扩展(如 [...b])还是对象扩展(如 {...b})。
  • 若扩展的变量(argument)不是 LogicalExpression,则包裹为 ...(b || []) 或 ...(b || {})。

示例:

css 复制代码
// 数组扩展[a, ...b]      →  [a, ...(b || [])]  

// 对象扩展{a, ...b}      →  {a, ...(b || {})}  

UpdateExpression(更新表达式)

问题 :自增 / 自减操作的目标若为 undefined 或 null,会报错(如 a.b.c++ 中 a.b 为 null 时)。优化目标:确保更新操作仅在目标有效时执行,避免报错。

实现逻辑:

  • 提取更新表达式的目标(如 a.b.c++ 中的 a.b.c)。
  • 构建逻辑表达式:目标.toString() && 原更新表达式(利用 toString() 验证目标是否有效)。
  • 标记右侧更新表达式为 "已处理",避免被其他钩子重复修改。
  • 左侧表达式会被 MemberExpression 钩子处理,自动添加可选链。

示例:

css 复制代码
a.b.c++  →  a?.b?.c?.toString() && a.b.c++  

AssignmentExpression(赋值表达式)

问题 :对不存在的深层属性赋值会报错(如 a.b.c = 1 中 a.b 为 undefined 时)。优化目标:确保赋值操作仅在父级属性有效时执行。

实现逻辑:

  • 提取赋值左侧的表达式(如 a.b.c = 1 中的 a.b.c),并移除最后一层属性(得到 a.b)。
  • 构建逻辑表达式:父级表达式 && 原赋值表达式(确保父级存在时才执行赋值)。
  • 标记右侧赋值表达式为 "已处理",避免重复修改。
  • 左侧父级表达式会被 MemberExpression 钩子处理,自动添加可选链。

示例:

css 复制代码
a.b.c = 1  →  a?.b && a.b.c = 1  

CallExpression | OptionalCallExpression(函数调用表达式)

问题 :调用 undefined 或 null 的方法(如数组方法)会报错;部分方法(如 concat)的参数若为非数组可能导致异常。优化目标:确保方法调用的主体和参数有效。

实现逻辑:

  • 方法主体处理 :若调用的是数组方法(如 map、filter、concat),为主体添加 || [] 兜底(如 a.b.filter() → (a.b || []).filter())。
  • 参数处理 :对 concat 等需要数组参数的方法,为其第一个参数添加 || [] 兜底(如 a.b.concat(c) → (a.b || []).concat(c || []))。
  • 可选调用转换 :将普通调用转换为可选调用(如 a.b() → a.b?.()),避免主体为 null/undefined 时的报错。

示例:

scss 复制代码
a.b.filter(x => x>0)  →  (a.b || []).filter(x => x>0)  
a.b.concat(c)         →  (a.b || []).concat(c || [])  
obj.method()          →  obj.method?.()  

核心实现图

代码仓库

github

参考文档

Babel 插件手册

相关推荐
Java后端的Ai之路4 分钟前
01-React基础教程
开发语言·前端·python·react.js·前端框架
流水白开11 分钟前
React的Virtual DOM、Diff算法和Fiber
前端·react.js
光影少年2 小时前
如果 React 组件的属性没有传值,它的默认值是什么?
前端·javascript·react.js
Fate_I_C3 小时前
Capacitor 应用鸿蒙化实战:用 hionic 把 React 应用跑在 OpenHarmony 上
react.js·华为·harmonyos
liangshanbo12154 小时前
面试题:React.memo 和 useMemo 的区别
前端·react.js·前端框架
太子釢1 天前
React 函数组件与 Hook 实践指南
前端·react.js
liangshanbo12151 天前
面试题:React 中 Lane 和 Scheduler 是怎么配合的?
前端·react.js·前端框架
liangshanbo12151 天前
React面试题:Hooks 为什么不能在分支和循环里面写?
前端·javascript·react.js
FungLeo1 天前
成为全栈·Next.js 网站前台篇·响应式、可访问性与错误状态:内容站不能只验桌面首页
react.js·响应式设计·next.js·移动端适配·成为全栈·web 可访问性