当前位置:首页 > 技术文章 > 服务接口一旦异常,你的前端页面就直接崩溃了?

服务接口一旦异常,你的前端页面就直接崩溃了?

go1231个月前 (10-17)技术文章86

在 JavaScript 开发中,细节处理不当往往会导致意想不到的运行时错误,甚至让应用崩溃。可能你昨天上完线还没问题,第二天突然一大堆人艾特你,你就说你慌不慌。来吧,咱们来捋一下怎么做才能让你的代码更健壮,即使后端数据出问题了咱前端也能稳得一批。

本文转载于稀土掘金技术社区,作者:CoderLiu

原文链接:https://juejin.cn/post/7388022210856222732


前言

在 JavaScript 开发中,细节处理不当往往会导致意想不到的运行时错误,甚至让应用崩溃。可能你昨天上完线还没问题,第二天突然一大堆人艾特你,你就说你慌不慌。

来吧,咱们来捋一下怎么做才能让你的代码更健壮,即使后端数据出问题了咱前端也能稳得一批。

解构失败报错

不做任何处理直接将后端接口数据进行解构

const handleData = (data)=> {
  const { user } = data;
  const { id, name } = user;
}
handleData({})

VM244:3 Uncaught TypeError: Cannot destructure property 'id' of 'user' as it is undefined.

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象(装箱)。由于 undefined 、null 无法转为对象,所以对它们进行解构赋值时就会报错。

所以当 data 为 undefined 、null 时候,上述代码就会报错。

第二种情况,虽然给了默认值,但是依然会报错

const handleData = (data)=> {
  const { user = {} } = data;
  const { id, name } = user;
}
handleData({user: null})

ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。

所以当 props.data 为 null,那么 const { name, age } = null 就会报错!

good:

const handleData = (data)=> {
  const { user } = data;
  const { id, name } = user || {};
}
handleData({user: null})

数组方法调用报错


从接口拿回来的数据直接用当成数组来用

const handleData = (data)=> {
  const { userList } = data;
  const newList = userList.map((item)=> item.name)
}
handleData({userList: null})
handleData({userList: 123})

VM394:3 Uncaught TypeError: userList.map is not a function

那么问题来了,如果 userList 不符合预期,不是数组时必然就报错了,所以最好判断一下

good:

const handleData = (data)=> {
  const { userList } = data;
  if(Array.isArray(userList)){
    const newList = userList.map((item)=> item.name)
  }
}
handleData({userList: 123})

遍历对象数组报错

遍历对象数组时也要注意 null 或 undefined 的情况

const handleData = (data)=> {
  const { userList } = data;
  const newList = userList.map((item)=> item.name)
}
handleData({userList: [ null, undefined ]})

VM547:3 Uncaught TypeError: Cannot read properties of null (reading 'name')

一旦数组中某项值是 undefined 或 null,那么 item.name 必定报错,可能又白屏了。

good:

const handleData = (data)=> {
  const { userList } = data;
  const newList = userList.map((item)=> item?.name)
}
handleData({userList: [null]})

但是如果是这种情况就不good了

const handleData = (data)=> {
  const { userList } = data;
  const newList = userList.map((item)=> `用户id是${item?.id},用户名字是${item?.name},用户年龄是${item?.age}岁了`);
}
handleData({userList: [null]})

? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编译后的代码size增大。

good:

const handleData = (data)=> {
  const { userList } = data;
  const newList = userList.map((item)=> {
    const { id, name, age } = item || {};
    return `用户id是${id},用户名字是${name},用户年龄是${age}岁了`
  });
}
handleData({userList: [null]})

当可选链操作符较多的情况时无论是性能还是可读性都明显上面这种方式更好。

复习一下装箱

大家可以思考一下,以下代码会不会报错

const handleData = (data)=> {
  const { userList } = data;
  const newList = userList.map((item)=> item.name)
}
handleData({userList: ['', 123]})

是不会报错的,因为在 JavaScript 中,当你在一些基本类型上直接访问属性时这些类型会被自动临时转换成它们对应的对象类型。这种转换称为“装箱”(boxing)。例如:

  • ('').name

    空字符串被临时转换成一个字符串对象。由于没有名为 name 的属性,所以它返回 undefined,但不会报错。

    let str = "hello";
    console.log(str.length); // 5

    在这里,str.length 实际上是在字符串对象上调用的,而不是直接在基本类型字符串上。JavaScript 引擎在幕后将字符串 "hello" 装箱为 String 对象,因此可以访问 length 属性。

  • (123).name

    数字 123 被临时转换成一个数字对象。由于没有名为 name 的属性,所以它返回 undefined,但不会报错。

    let num = 123;
    console.log(num.toFixed(2)); // "123.00"

    num.toFixed(2) 调用了数字对象的 toFixed 方法。JavaScript 将数字 123 装箱为 Number 对象。

  • (null).name

    null 是一个特殊的基本类型,当尝试访问其属性时会报错,因为 null 不能被装箱为对象。

    try {
      const name = (null).name; // TypeError: Cannot read property 'name' of null
    } catch (error) {
      console.error(error);
    }
  • (undefined).name

    undefined 也不能被装箱为对象。

    try {
      const name = (undefined).name; // TypeError: Cannot read property 'name' of undefined
    } catch (error) {
      console.error(error);
    }

JavaScript 中的基本类型包括:

string
number
boolean
symbol
bigint
null
undefined

对应的对象类型是:

String
Number
Boolean
Symbol
BigInt

装箱的工作原理:

当你访问基本类型的属性或方法时,JavaScript 会自动将基本类型装箱为其对应的对象类型。这个临时的对象允许你访问属性和方法,但它是短暂的,一旦属性或方法访问完成,这个对象就会被销毁。

需要注意的是,null 和 undefined 没有对应的对象类型,不能被装箱。所以访问它们的属性或方法会直接报错!所以时刻警惕 null 和 undefined 这俩坑。

使用对象方法时报错

同理,只要变量能被转成对象,就可以使用对象的方法,但是 undefined 和 null 无法转换成对象。对其使用对象方法时就会报错。

const handleData = (data)=> {
  const { user } = data;
  const newList = Object.entries(user);
}
handleData({user: null});

VM601:3 Uncaught TypeError: Cannot convert undefined or null to object

下面这两种优化方式都可

good:

const handleData = (data)=> {
  const { user } = data;
  const newList = Object.entries(user || {})
}
handleData({user: null})

good:

/**
 * 判断给定值的类型或获取给定值的类型名称。
 *
 * @param {*} val - 要判断类型的值。
 * @param {string} [type] - 可选,指定的类型名称,用于检查 val 是否属于该类型。
 * @returns {string|boolean} - 如果提供了 type 参数,返回一个布尔值表示 val 是* 否属于该类型;如果没有提供 type 参数,返回 val 的类型名称(小写)。
 *
 * @example
 * // 获取类型名称
 * console.log(judgeDataType(123)); // 输出 'number'
 * console.log(judgeDataType([])); // 输出 'array'
 *
 * @example
 * // 判断是否为指定类型
 * console.log(judgeDataType(123, 'number')); // 输出 true
 * console.log(judgeDataType([], 'array')); // 输出 true
 */
function judgeDataType(val, type) {
  const dataType = Object.prototype.toString.call(val).slice(8, -1).toLowerCase();
  return type ? dataType === type : dataType;
}

const handleData = (data)=> {
  const { user } = data;
  // 判断是否为对象
  if(judgeDataType({}, "object")){
    const newList = Object.entries(user || {})
  }
}
handleData({user: null})

async/await 报错未捕获

这个也是比较容易犯且低级的错误

import React, { useState } from 'react';

const List = () => {
  const [loading, setLoading] = useState(false);
  const getData = async () => {
    setLoading(true);
    const res = await fetchListData();
    setLoading(false);
  }
}

如果 fetchListData() 执行报错,页面就会一直在加载中,所以一定要捕获一下。

good:

const List = () => {
  const [loading, setLoading] = useState(false);
  const getData = async () => {
    setLoading(true);
    try {
      const res = await queryData();
      setLoading(false);
    } catch (error) {
      setLoading(false);
    }
  }
}

当然如果觉得这种方式不优雅,用 await-to-js 库或者其他方式都可以,记得捕获就行。

JSON.parse报错

如果传入的不是一个有效的可被解析的 JSON 字符串就会报错啦。

const handleData = (data)=> {
  const { userStr } = data;
  const user = JSON.parse(userStr);
}
handleData({userStr: 'fdfsfsdd'})


16:06:57.521 VM857:1 Uncaught SyntaxError: Unexpected token 'd', "fdfsfsdd" is not valid JSON

这里没必要去判断一个字符串是否为有效的 JSON 字符串,只要利用 trycatch 来捕获错误即可。

good:

const handleData = (data)=> {
  const { userStr } = data;
  try {
    const user = JSON.parse(userStr);
  } catch (error) {
    console.error('不是一个有效的JSON字符串')
  }
}
handleData({userStr: 'fdfsfsdd'})

动态导入模块失败报错

动态导入某些模块时,也要注意可能会报错

const loadModule = async () => {
    const module = await import('./dynamicModule.js');
    module.doSomething();
}

如果导入的模块存在语法错误、网络或者跨域问题、文件不存在、循环依赖、甚至文件非常大导致内存不足、模块内的运行时错误等都有可能阻塞后续代码执行。

good:

const loadModule = async () => {
  try {
    const module = await import('./dynamicModule.js');
    module.doSomething();
  } catch (error) {
    console.error('Failed to load module:', error);
  }
}

API 兼容性问题报错

fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

低版本 Node 不支持 fetch,需要更高兼容性的场景使用 axios 等更好。

其他包括小程序开发,web开发等也同理,如果使用了某些不支持的 es 新特性或者较新版本的平台才支持的api也会导致直接报错,使用时做好判断或直接用兼容性更好的写法。

框架在编译时已经帮我们解决了大部分的兼容性问题,但是有些场景还需额外注意。

内存溢出崩溃

滥用内存缓存可能会导致内存溢出

const cache = {};

function addToCache(key, value) {
  cache[key] = value;
  // 没有清理机制,缓存会无限增长
}

避免闭包持有大对象的引用

function createClosure() {
  const largeData = new Array(1000000).fill('x');

  return function() {
    console.log(largeData.length);
  };
}

const closure = createClosure();
// largeData 现在被闭包引用会一直存活在内存中,即使不再直接使用

closure = null; // 手动解除引用

记得清除定时器和事件监听器

// React
useEffect(() => {
  const timeoutId = setTimeout(() => {
    // 一些操作
  }, 1000);

  return () => clearTimeout(timeoutId);
}, []);
function setupHandler() {
  const largeData = new Array(1000000).fill('x');
  const handler = function() {
    console.log(largeData.length);
  };

  document.getElementById('myButton').addEventListener('click', handler);

  return function cleanup() {
    document.getElementById('myButton').removeEventListener('click', handler);
  };
}

const cleanup = setupHandler();
// 在适当的时候调用
// cleanup();

还有深度递归,JSON.parse() 解析超大数据等都可能会对内存造成压力。

总结

以上列举了js在运行时可能会发生错误而导致的应用崩溃的一些边界情况,这些都是在开发时不那么容易察觉,eslint等静态检查工具也无能为力的场景,当然如果用typescript的话还是可以帮助我们避免大部分坑的,如果不用 ts 的话就不可避免的需要考虑这些情况才能写出健壮的代码。

边界场景的容错一定要做,原则上不信任任何外部输入数据的存在性和类型,历史经验告诉我们,不做容错出错只是早晚的事。

帮别人review代码的时候也可以参考以上清单,如果大家还有补充欢迎讨论,最后祝各位大佬没有bug。


声明:本站所有内容均为自动采集而来,如有侵权,请联系删除

相关文章

Redis连环五十二问!看谁顶得住?

Redis连环五十二问!看谁顶得住?

基本 1.说说什么是Redis? Redis是一种基于键...

用 PHP 处理 10 亿行数据!

用 PHP 处理 10 亿行数据!

今天,我将带大家一起走进“挑衅十亿行“数据的世界。当然,这个事情是依据GitHub上的一个“十亿行挑衅”(1brc)运动而来,现在正在进行,如果你没有听说过,可查看Gunnar Morlings 的 1brc 存储库。https://github.com/gunnarmorling/1brc我之所以...

2024 年的最佳 PHP 框架

2024 年的最佳 PHP 框架

在本文中,我们将预测在 2024 年持续风行的最佳 PHP 框架。我们首先将看看PHP框架是什么,什么时候该斟酌应用PHP框架,以及应用PHP框架的重要长处都是什么。我还会介绍最合适初学者的 PHP 框架以及用于 Web 开发的最佳框架。什么是PHP框架?     &...

一文读懂多家厂商的大模型训练、推理、部署策略

一文读懂多家厂商的大模型训练、推理、部署策略

4 月 20 日,第 102 期源创会在武汉胜利举行。本期邀请来自武汉人工智能研讨院、华为、MindSpore、京东云、Gitee AI 的人工智能专家,环绕【大模型竞技与性能优化】主题发表演讲。接下来就一起看看本期运动的出色瞬间吧!大合影 get ✅披萨和礼物不能少!接下来进入主题演讲回想环节。可...

请立刻停止编写 Dockerfiles 并使用 docker init

请立刻停止编写 Dockerfiles 并使用 docker init

您是那种认为编写 Dockerfile 和 docker-compose.yml 文件很苦楚的人之一吗?我承认,我就是其中之一。我总是想知道我是否遵守了 Dockerfile、 docker-compose 文件的最佳编写实践,我畏惧在不知不觉中引入了安全破绽。但是现在,我不必再担忧这个问题了,感激...

服务器为什么大多用 Linux 而不是 Windows ?

服务器为什么大多用 Linux 而不是 Windows ?

前几天在知乎看到一个话题很有意思,且很有讨论意义。“服务器为什么大多用 Linux”,除了开源、好用等原因,回答也代表了各种不同人需求和看法,摘取一些分享给大家,也欢迎留言讨论。来自知乎好友“熊大你又骗俺”的回答首先在20年前,windows server+iis+asp+access 的方案,还是...