背景与目标
背景 :后端接口技改后,查询无数据时将返回 null
而非空对象,这可能导致前端老代码因未做兼容处理而出现运行时错误(如访问 null
的属性、对 null
进行解构等)。
目标 :针对指定文件 / 文件夹内的 JavaScript/TypeScript 代码(js
/jsx
/ts
/tsx
),自动添加可选链(?.
)和逻辑表达式(||
/&&
),确保对对象、数组的操作在遇到 null
/undefined
时不报错,兼容后端返回格式的变化。
使用和安装
npx -y @chagee/fix-optional babel-fix ./src 可以指定文件或者文件夹
技术方案
采用 Babel 插件转换 方案,通过 AST(抽象语法树)遍历代码,对关键节点进行自动修改,核心流程如下:
- 基于 Babel 插件机制:通过自定义 Babel 插件,在代码转译阶段拦截并处理 AST 节点。
- 节点钩子处理:针对可能引发错误的语法节点(成员访问、解构、赋值等),编写访问器钩子(Visitor),自动添加容错逻辑。
- TypeScript 兼容:处理 TypeScript 代码时,先剥离泛型等类型信息,确保转换逻辑专注于运行时代码。

术语解释优化
-
LogicalExpression(逻辑表达式) 指包含逻辑运算符
||
(逻辑或)或&&
(逻辑与)的表达式,用于组合或判断多个条件。示例:a || []
(若a
为假值则返回空数组)、a && b
(若a
为真值则返回b
)。核心结构包含:left
:左侧表达式right
:右侧表达式operator
:逻辑运算符(||
或&&
)
-
MemberExpression | OptionalMemberExpression(成员访问表达式 | 可选成员访问表达式) 用于访问对象成员的表达式,包含以下关键属性:
Optional
:标记是否为可选访问(即是否带?.
运算符)。Computed
:标记是否为计算属性访问(如a[b][c]
中[b]
和[c]
为计算属性)。Object
:当前成员的父级对象(如a.b.c
中,a.b
是c
的Object
,a
是b
的Object
)。Property
:当前访问的属性(如a.b.c
中,c
是最终的Property
)。
-
VariableDeclarator(变量声明器) 用于变量声明与赋值的表达式,核心属性:
id
:赋值语句的左侧(被赋值的变量或解构模式)。init
:赋值语句的右侧(赋值的初始值)。
-
SpreadElement(扩展元素) 指使用
...
语法的解构元素,用于将数组或对象的属性展开(如[a, ...b]
或{x, ...y}
)。 -
UpdateExpression(更新表达式) 包含自增(
++
)或自减(--
)运算符的表达式,用于修改变量的值(如a++
或obj.prop--
)。 -
AssignmentExpression(赋值表达式) 用于变量或属性赋值的表达式(区别于
VariableDeclarator
,后者是声明时的赋值),如a.b = 1
或x = y + z
。 -
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?.()
核心实现图
