乐趣区

关于前端:Flutter-常见异常分析


前言

在上篇「Sentry 在百瓶的落地实际」中,笔者次要从计划选型 & 落地实际两个大的方面进行了论述,本篇文章咱们次要对 Sentry 在百瓶的落地实际中遇到的问题进行剖析。本文中次要剖析的问题次要包含以下几大类(Flutter SDK 版本为 1.22.6,Dart SDK 版本为 2.10.5):

  • NoSuchMethodError
  • Flutter 官网 bug(曾经修复)
  • StateError
  • NetworkError(DNS)

NoSuchMethodError

问题一

问题形容:

在进行 List、String 等类型数据判空解决时,间接应用 xxx.isNotEmpty,没有进行判断是否为 null,导致 NoSuchMethodError:The getter isNotEmpty was called null。

问题截图:


解决方案:

// 问题代码
if(timeEndList.isNotEmpty){...}
// 解决方案
static bool isNotNullOrEmpty<E>(Iterable<E> iterable) => iterable != null && iterable.isNotEmpty;

if (IterableUtils.isNotNullOrEmpty(timeEndList)){...}

在进行判空解决时,须要首先判断是否为 null,而后再应用 isNotEmpty 进行判断,防止这种类型谬误,思考到咱们在我的项目中会应用大量相似判断,所以咱们能够对同一类型的数据判断办法进行封装,防止每处应用都要再去写一遍。

问题二

问题形容:

这里是应用了 Future.wait 并发申请多个 API,并且在第二个 API 设置超时,因为第二个 API 申请超时,在后续解决响应时,没有解决空异样判断导致获取不到 code。

问题截图:


解决方案:

// 问题代码
if (res[1].code == HttpCode.ok) {...}

// 解决方案
if (res[1]?.code == HttpCode.ok) {...}

在应用了 Future.wait 并发申请多个 API,如果有设置超时解决,要思考到 API 申请超时失败的问题尽量避免这种问题产生。

问题三

问题形容:

当咱们须要获取到 与 Widget 上下文相关联的 RenderBox 尺寸或者地位时,产生谬误。

问题截图:


解决方案:

// 问题代码
if (IterableUtils.isNotNullOrEmpty(ctx.state.details) == true) {final RenderBox renderBox = ctx.state.detailsKey.currentContext.findRenderObject();
  final Offset postion = renderBox.localToGlobal(Offset.zero);
  ctx.dispatch(MallGoodsDetailActionCreator.setDetailsOffsetYAction(postion.dy));
}

// 解决方案
if (IterableUtils.isNotNullOrEmpty(ctx.state.details) == true) {WidgetsBinding.instance.addPostFrameCallback((_) {final RenderBox renderBox = ctx.state.detailsKey.currentContext.findRenderObject();
    final Offset postion = renderBox.localToGlobal(Offset.zero);
    ctx.dispatch(MallGoodsDetailActionCreator.setDetailsOffsetYAction(postion.dy));
  });
}

产生以上问题的起因是,上下文并没有与咱们的 state 进行关联,如果要防止这种状况产生,咱们能够在 Widget 渲染结束后再进行获取 RenderBox 尺寸或者地位。

Flutter 官网 bug(曾经修复)

问题形容:

在应用 NestedScrollView 组件时,因为 position.minScrollExtent 能够为空,在生产环境中运行会偶现 NoSuchMethodError nested_scroll_view.dart in _NestedScrollCoordinator.hasScrolledBody NoSuchMethodError: The method ‘>’ was called on null. Receiver: null Tried calling: >() 这个问题,目前官网曾经解决并且合并到 master 分支。

问题截图:

那么这个问题是如何产生的呢?用官网的原文来解释就是:

  1. scheduleAttachRootWidget 将调用 _firstBuild 并新建一个具备空像素的 _NestedScrollPosition;
  2. FocusManager 将安顿一个微工作;
  3. 实现 firstBuild 而后刷新 microTask,NestedScrollView 又 dirty 了;
  4. scheduleWarmUpFrame 将重建 dirty 节点并触发异样(_NestedScrollPosition 仅在布局后可用)。

解决方案:

// 问题代码
bool get hasScrolledBody {for (final _NestedScrollPosition position in _innerPositions) {assert(position.minScrollExtent != null && position.pixels != null);
    if (position.pixels > position.minScrollExtent) {return true;}
  }
  return false;
}

// 解决方案
bool get hasScrolledBody {for (final _NestedScrollPosition position in _innerPositions) {if (!position.hasContentDimensions || !position.hasPixels) {continue;} else if (position.pixels > position.minScrollExtent) {return true;}
  }
  return false;
}

StateError

问题形容:

当咱们在应用 list.firstWhere 的时候,通常会引发 Bad State: No element 这类问题。

问题截图:


解决方案:

// 问题代码
Map<String, String> getInitialSkuById(String skuId, List<Map<String, dynamic>> skuList) {final Map<String, String> selectedKeyValue = <String, String>{};
  final Map<String, dynamic> selectedSku =
      skuList.firstWhere((Map<String, dynamic> skuItem) => skuItem['id'] == skuId);

  if (selectedSku['stockNum'] > 0) {selectedSku.forEach((String k, dynamic v) {if (k.contains('keyStr')) {selectedKeyValue[k] = v;
      }
    });
  }

  return selectedKeyValue;
}

// 解决方案
Map<String, String> getInitialSkuById(String skuId, List<Map<String, dynamic>> skuList) {final Map<String, String> selectedKeyValue = <String, String>{};
  final Map<String, dynamic> selectedSku = skuList.firstWhere((Map<String, dynamic> skuItem) => skuItem['id'] == skuId,
    orElse: null,
  );

  if (selectedSku != null && selectedSku['stockNum'] > 0) {selectedSku.forEach((String k, dynamic v) {if (k.contains('keyStr')) {selectedKeyValue[k] = v;
      }
    });
  }
  return selectedKeyValue;
}

在咱们应用 list.firstWhere 的时候,通常有匹配不到条件的时候,这个时候就十分有必要应用 orElse 来进行解决这种状况。

上面的代码依据条件筛选为 ‘green’ 的后果值,如果没有的话就返回 ‘No matching color found’,后果输入为:No matching color found。

final List<String> list = <String>['red', 'yellow', 'pink', 'blue'];
final String item = list.firstWhere((String element) => element == 'green',
  orElse: () => 'No matching color found',);
print(item); // // No matching color found

如果没有写 orElse 的状况下会抛出异样: Unhandled exception: Bad state: No element。当然如果在 Null safety 版本下,能够间接应用 firstWhereOrNull 办法来进行解决。
上面咱们来比照一下 firstWhere 和 firstWhereOrNull 的源码:

 E firstWhere(bool test(E element), {E orElse()?}) {for (E element in this) {if (test(element)) return element;
  }
  if (orElse != null) return orElse();
  throw IterableElementError.noElement();}

T? firstWhereOrNull(bool Function(T element) test) {for (var element in this) {if (test(element)) return element;
  }
  return null;
}

firstWhere 会首先进行匹配符合条件的后果,如果没有匹配到,再进行解决 orElse,如果没有 orElse,就会抛出异样;firstWhereOrNull 就简略的多了,如果没有匹配到符合条件的值,就会间接返回 null。

NetworkError(DNS)

网络谬误是导致网络申请失败的谬误条件,每个网络谬误都有一个类型,它是一个字符串,每个网络谬误都有一个阶段,它形容了谬误产生在哪个阶段:

  1. dns:DNS 解析过程中产生的谬误;
  2. connection:平安连贯建设期间产生的谬误;
  3. application:申请和响应传输过程中产生的谬误;

问题形容:

在客户端向服务单发动网络申请时,都会通过 DNS 解析的过程,个别状况下都是基于 DNS 协定向运营商 Local DNS 发动解析申请的传统形式,然而这种状况下可能会呈现域名劫持和跨网拜访的问题,造成域名解析异样。

解决方案:

那么,如果咱们的 App 在发动网络申请的时候,发现 DNS 解析失败,咱们应该怎么办?当然咱们能够接入阿里云云解析 DNS 服务或者腾讯挪动解析 HTTP DNS 等服务来更加无效的保障 App、小程序失常拜访。

上面咱们来一起回顾一下 DNS 相干的常识:

  • 什么是 DNS
  • 域名分层构造
  • DNS 分层构造
  • DNS 解析过程

DNS

DNS 是域名零碎 (Domain Name System) 的缩写,是因特网的一项外围服务,它作为能够将域名和 IP 地址相互映射的一个分布式数据库,可能使人更不便的去拜访互联网,而不必去记住可能被机器读取的 IP 数串。

域名分层构造

因为因特网的用户数量过多,所有因特网在命名时采纳的是档次树状构造的命名办法。
任何一个连贯在因特网上的主机或路由器,都有一个惟一的层次结构(域名)。
域名能够划分为各个子域,子域还能够持续划分为子域的子域,这样就造成了顶级域名、主域名、子域名等。

  1. “.com” 是顶级域名;
  2. “baiping.com” 是主域名(也可称托管一级域名),次要指企页名;
  3. “example.baiping.com” 是子域名(也可称为托管二级域名);
  4. “www.example.baiping.com” 是子域名的子域(也可称为托管三级域名)。

DNS 分层构造

域名是分层构造,域名 DNS 服务器也是对应的层级构造。有了域名构造,还须要有域名 DNS 服务器去解析域名,且是须要由遍布全世界的域名 DNS 服务器去解析,域名 DNS 服务器实际上就是装有域名零碎的主机。

DNS 解析过程

DNS 查问的后果通常会在本地域名服务器中进行缓存,如果本地域名服务器中有缓存的状况下,则会跳过如下 DNS 查问步骤,很快返回解析后果。本地域名服务器没有缓存的状况下,DNS 查问所需的 8 个步骤:

  1. 用户在 Web 浏览器中输出 “example.com”,则由本地域名服务器开始进行递归查问。
  2. 本地域名服务器采纳迭代查问的办法,向根域名服务器进行查问;
  3. 根域名服务器通知本地域名服务器,下一步应该查问的顶级域名服务器 .com TLD(顶级域名服务器)的 IP 地址;
  4. 本地域名服务器向顶级域名服务器 .com TLD 进行查问;
  5. .com TLD 服务器通知本地域名服务器,下一步查问 example.com 权威域名服务器的 IP 地址;
  6. 本地域名服务器向 example.com 权威域名服务器发送查问;
  7. example.com 权威域名服务器通知本地域名服务器所查问的主机 IP 地址;
  8. 本地域名服务器最初把查问的 IP 地址响应给 Web 浏览器。一旦 DNS 查问的 8 个步骤返回了 example.com 的 IP 地址,浏览器就可能收回对网页的申请;
  9. 浏览器向 IP 地址收回 HTTP 申请;
  10. 该 IP 处的 Web 服务器返回要在浏览器中出现的网页。

名词解释:

  1. DNS Resolve:指本地域名服务器,它是 DNS 查找中的第一站,是负责解决收回初始申请的 DNS 服务器。运营商 ISP 调配的 DNS、谷歌 8.8.8.8 等都属于 DNS Resolver;
  2. Root Server:指根域名服务器,当本地域名服务器在本地查问不到解析后果时,则第一步会向它进行查问,并获取顶级域名服务器的 IP 地址;
  3. 递归查问:是指 DNS 服务器在收到用户发动的申请时,必须向用户返回一个精确的查问后果。如果 DNS 服务器本地没有存储与之对应的信息,则该服务器须要询问其余服务器,并将返回的查问构造提交给用户;
  4. 迭代查问:是指 DNS 服务器在收到用户发动的申请时,并不间接回复查问后果,而是通知另一台 DNS 服务器的地址,用户再向这台 DNS 服务器提交申请,这样顺次重复,直到返回查问后果。

总结

以上四种异样是咱们在编写代码初期常常遇到的问题,通过对以上四种异样的剖析,咱们能够失去一些经验总结,在后续的开发中,咱们能够依据这些总结,进行改良,以便更好的解决问题。

更多精彩请关注咱们的公众号「百瓶技术」,有不定期福利呦!

退出移动版