找回密码
 立即注册
首页 资源区 代码 前端 TypeError 错误永久消失术

前端 TypeError 错误永久消失术

列蜜瘘 2025-6-4 20:04:55
作者:来自 vivo 互联网大前端团队-  Sun Maobin
通过开发 Babel 插件,打包时自动为代码添加 可选链运算符(?.),从而有效避免 TypeError 的发生。
一、背景介绍

在 JS 中当获取引用对象为空值的属性时,程序会立即终止运行并报错:TypeError: Cannot read properties of ...
在 ECMAScript 2020 新增的 **可选链运算符(?.)**,当属性值不存在时返回 undefined,从而可有效避免该错误的发生。
  1. let a
  2. a.b.c.d // Uncaught TypeError: Cannot read properties of undefined (reading 'b')
  3. a?.b?.c?.d // undefined
复制代码
本文将分享如何借助这一特性开发 Babel 插件,自动为代码添加 ?.,从而根治系统中的 TypeError 错误。
二、项目痛点


  • 维护中的代码可能存在 TypeError 隐患,数量大维护成本高,比如:存在大量直接取值操作:a.b.c.d
  • 在新代码中使用 ?. 书写起来太繁琐,同时也导致源码不易阅读,比如:a?.b?.c?.d
因此,如果我们只要在打包环节自动为代码添加 ?.,就可以很好解决这些问题。
三、解决思路

开发 Babel 插件 在打包时进行代码转换:

  • 将存在隐患的操作符 . 或 [] 转换为 ?.
  • 将臃肿的短路表达式 && 转换为 ?.
  1. // in
  2. a.b.c.d
  3. a['b']['c']['d']
  4. a && a.b && a.b.c && a.b.c.d
  5. // out
  6. a?.b?.c?.d
复制代码
四、目标价值

通用于任何基于 Babel 的 JS 项目,在源码 0 改动的情况下,彻底消灭 TypeError 错误。
五、功能实现

5.1 Babel 插件核心


  • 识别代码中可能存在 TypeError 的风险操作:属性获取 和 方法调用
  • 支持自定义 Babel 参数配置,includes 或 excludes 代码转换规则
  • 短路表达式 && 自动优化
  1. import { declare } from '@babel/helper-plugin-utils';
  2. import * as t from '@babel/types';
  3. export default declare((api, options) => {
  4.   // 仅支持 Babel7
  5.   api.assertVersion(7);
  6.   return {
  7.     // Babel 插件名称
  8.     name: 'babel-plugin-auto-optional-chaining',
  9.     visitor: {
  10.       /**
  11.        * 通过 Babel AST 解析语法后,仅针对以下数据类型做处理
  12.        * - MemberExpression:a.b 或 a['b']
  13.        * - CallExpression:a.b() 或 a['b']()
  14.        * - OptionalMemberExpression:a?.b 或 a?.['b']
  15.        * - OptionalCallExpression:a.b?.() 或 a.['b']?.()
  16.        */
  17.       'MemberExpression|CallExpression|OptionalMemberExpression|OptionalCallExpression'(path) {
  18.         // 避免重复处理
  19.         if (path.node.extra.hasAoc) return;
  20.         // isValidPath:通过 Babel 配置参数决定是否处理该节点
  21.         const isMeCe = path.isMemberExpression() || path.isCallExpression();
  22.         if (isMeCe && !isValidPath(path, options)) return;
  23.         // 属性获取
  24.         // shortCircuitOptimized:&& 短路表达式优化后再做替换处理
  25.         if (path.isMemberExpression() || path.isOptionalMemberExpression()) {
  26.           const ome = t.OptionalMemberExpression(path.node.object, path.node.property, path.node.computed, true);
  27.           if (!shortCircuitOptimized(path, ome)) {
  28.             path.replaceWith(ome);
  29.           };
  30.         };
  31.         // 方法掉用
  32.         // shortCircuitOptimized:&& 短路表达式优化后再做替换处理
  33.         if (path.isCallExpression() || path.isOptionalCallExpression()) {
  34.           const oce = t.OptionalCallExpression(path.node.callee, path.node.arguments, false);
  35.           if (!shortCircuitOptimized(path, oce)) {
  36.             path.replaceWith(oce);
  37.           };
  38.         };
  39.         // 添加已处理标记
  40.         path.node.extra.hasAoc = true;
  41.       }
  42.     }
  43.   };
  44. });
复制代码
5.2 Babel 参数配置

支持 includes 和 excludes 两个参数,决定自动处理的代码 ?. 的策略。

  • includes - 仅处理指定代码片段
  • excludes - 排除指定代码片段不做处理
  1. // includes 列表,支持正则
  2. const isIncludePath = (path, includes: []) => {
  3.   return includes.some(item => {
  4.     let op = path.hub.file.code.substring(path.node.start, path.node.end);
  5.     return new RegExp(`^${item}$`).test(op);
  6.   })
  7. };
  8. // excludes 列表,支持正则
  9. const isExcludePath = (path, excludes: []) => {
  10.   // 忽略:excludes 列表,支持正则
  11.   return excludes.some(item => {
  12.     let op = path.hub.file.code.substring(path.node.start, path.node.end);
  13.     return new RegExp(`^${item}$`).test(op);
  14.   })
  15. };
  16. // 校验配置参数
  17. const isValidPath = (path, {includes, excludes}) => {
  18.   // 如果配置了 includes,仅处理 includes 匹配的节点
  19.   if (includes?.length) {
  20.     return isIncludePath(path, includes);
  21.   }
  22.   // 如果配置了 excludes,则不处理 excludes 匹配的节点
  23.   if (includes?.length) {
  24.     return !isExcludePath(path, includes);
  25.   }
  26.   // 默认全部处理
  27.   return true;
  28. }
复制代码
5.3 短路表达式优化

支持添加参数 optimizer=false 关闭优化
  1. const shortCircuitOptimized = (path, replaceNode) => {
  2.   // 支持添加参数 optimizer=false 关闭优化
  3.   if (options.optimizer === false) return false;
  4.   const pc = path.container;
  5.   // 判断是否逻辑操作 &&
  6.   if (pc.type !== 'LogicalExpression') return false;
  7.   // 只处理 a && a.b 中的 a.b
  8.   if (pc.type === 'LogicalExpression' && path.key === 'left') return false;
  9.   // 递归寻找上一级是否逻辑表达式,即:a && a.b && a.b.c
  10.   const pp = path.parentPath;
  11.   if (pp.isLogicalExpression() && path.parent.operator === '&&'){
  12.     let ln = pp.node.left;
  13.     let rn = pp.node.right?.object ?? pp.node.right?.callee ?? {};
  14.     const isTypeId = type => 'Identifier' === type;
  15.     const isValidType = type => [
  16.       'MemberExpression',
  17.       'OptionalMemberExpression',
  18.       'CallExpression',
  19.       'OptionalCallExpression'
  20.     ].includes(type);
  21.     const isEqName = (a, b) => {
  22.       if ((a?.name ?? b?.name) === undefined) return false;
  23.       return a?.name === b?.name;
  24.     };
  25.     // 递归处理并替换
  26.     // 如:a && a.b && a.b.c ==> a?.b && a.b.c ==> a?.b?.c
  27.     const getObj = (n, r = '') => {
  28.       const reObj = obj => {
  29.           r = r ? `${obj.name}.${r}` : obj.name;
  30.       };
  31.       isTypeId(n.property?.type) && reObj(n.property);
  32.       isTypeId(n.object?.type) && reObj(n.object);
  33.       isTypeId(n.callee?.type) && reObj(n.callee);
  34.       if (isValidType(n.object?.type)) {
  35.         return getObj(n.object, r);
  36.       };
  37.       if (isValidType(n.callee?.type)) {
  38.         return getObj(n.callee, r);
  39.       };
  40.       return r;
  41.     };
  42.     // eg:a && a.b
  43.     if (isTypeId(ln.type) && isTypeId(rn.type)) {
  44.       if (isEqName(ln, rn)) {
  45.         return pp.replaceWith(replaceNode);
  46.       }
  47.     };
  48.     // eg:a && a.b | a && a.b.c...
  49.     if (isTypeId(ln.type) && isValidType(rn.type)) {
  50.       const rnObj = getObj(rn);
  51.       if (rnObj.startsWith(ln.name)) {
  52.         return pp.replaceWith(replaceNode);
  53.       }
  54.     };
  55.     // eg:a.b && a.b.c | a.b && a.b.c...
  56.     // 注意:a.b.c && a.b.d 不会被转换
  57.     if (isValidType(ln.type) && isValidType(rn.type)) {
  58.       const lnObj = getObj(ln);
  59.       const rnObj = getObj(rn);
  60.       if (rnObj.startsWith(lnObj)) {
  61.         return pp.replaceWith(replaceNode);
  62.       }
  63.     };
  64.   };
  65.   return false;
  66. };
复制代码
六、插件应用

配置 babel.config.js 文件。
支持3个配置项:

  • includes - 仅处理指定代码片段(优先级高于 excludes)
  • excludes - 排除指定代码片段不做处理
  • optimizer - 如果设置为 false 则关闭优化短路表达式 &&
  1. module.exports = {
  2.   plugins: [
  3.     ['babel-plugin-auto-optional-chaining', {
  4.       excludes: [
  5.         'new .*',       // eg:new a.b() 不能转为 new a.b?.()
  6.         'process.env.*' // 固定短语通过.链接,不做处理
  7.       ],
  8.       // includes: [],
  9.       // optimizer: false
  10.     }]
  11.   ]
  12. }
复制代码
七、不足之处

自动为代码添加 ?. 可能会导致打包后文件体积略微增加,从而影响页面访问速度。
八、相关插件


对于不支持 可选链运算符 (?.) 的浏览器或者版本(如:Chrome a?.b// 第2步:考虑兼容性,使用 @babel/plugin-transform-optional-chaining 再做反向降级a?.b ==> a === null || a === void 0 ? void 0 : a.b;[/code]九、插件测试

以下是一些测试用例仅供参考,使用 babel-plugin-tester 进行测试。
Input 输入用例
  1. // 第1步:考虑健壮性,使用本文插件将代码自动转为可选链
  2. a.b ===> a?.b
  3. // 第2步:考虑兼容性,使用 @babel/plugin-transform-optional-chaining 再做反向降级
  4. a?.b ==> a === null || a === void 0 ? void 0 : a.b;
复制代码
Out 结果输出:
  1. // 常规操作
  2. const x = a.b.c.d
  3. const y = a['b']['c'].d
  4. const z = a.b[c.d].e
  5. if(a.b.c.d){}
  6. switch (a.b.c.d){}
  7. // 特殊操作
  8. (a).b // 括号运算
  9. const w = +a.b.c // 一元运算
  10. // 方法调用
  11. a.b.c.d()
  12. a().b
  13. a.b().c
  14. a.b(c.d).e
  15. fn(a.b.c.d)
  16. fn(a.b, 1)
  17. fn(...a)
  18. fn.a(...b).c(...d)
  19. // 短路表达式优化
  20. // optional member
  21. a && a.b
  22. a && a.b && a.b.c
  23. a.b && a.b.c && a.b.c.d
  24. this.a && this.a.b
  25. this.a.b && this.a.b.c && this.a.b.c.d
  26. this['a'] && this['a'].b
  27. this['a'] && this['a']['b'] && this['a']['b']['c']
  28. this['a'] && this['a'].b && this['a'].b['c']
  29. // optional method
  30. a && a.b()
  31. a && a.b().c
  32. a.b && a.b.c()
  33. a && a.b && a.b.c()
  34. // assign expression
  35. let a = a && a.b
  36. let b = a && a.b && a.b.c && a.b.c.d
  37. let c = a && a.b && a.b.c()
  38. // self is optional chaining
  39. a && a?.b
  40. a && a.b && a?.b?.c
  41. a && a?.b && a?.b?.c
  42. a && a?.b() && a?.b()?.c
  43. // function args
  44. fn(a && a.b)
  45. fn(a && a.b && a.b.c)
  46. // only did option chaining
  47. a.b && b.c
  48. a.b && a.c.d
  49. a.b && a.b.c && a.c.d
  50. a.b.c && a.b.d
  51. a.b.c && a.b
  52. a.b.c.d && a.b.c.e
  53. // not handle
  54. a && b
  55. a && b && c
  56. a || b
  57. a || b || true
  58. // 忽略赋值操作
  59. x.a = 1
  60. x.a.c = 2
  61. // 忽略算术运算
  62. a.b++
  63. ++a.b
  64. a.b--
  65. --a.b
  66. // 忽略指派赋值运算
  67. a.b += 1
  68. a.b -= 1
  69. // 忽略 in/of
  70. for (a in b.c.d);
  71. for (bar of b.c.d);
  72. // 忽略 new 操作符
  73. new a.b()
  74. new a.b.c()
  75. new a.b.c.d()
  76. new a().b
  77. new a.b().c.d
  78. // 配置忽略项
  79. process.env.a
  80. process.env.a.b.c
  81. // 忽略 ?. 本身
  82. a?.b
  83. a?.b?.c?.d
复制代码
十、写在最后

本文通过介绍如何开发一个 Babel 插件,在打包时自动为代码添加 **可选链运算符(?.)**,从而有效避免 JS 项目 TypeError 的发生。
希望这个思路能够有效的提升大家项目的健壮性和稳定性。
十一、参考资料


  • tc39/proposal-optional-chaining
  • 可选链运算符(?.)
  • Babel插件手册
  • Babel's optional chaining AST spec
  • ESTree
  • eslint/no-unsafe-optional-chaining

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册