从十万行代码定位undefined is not an object (evaluating 't.length')

avatar
前端工程师 @木狐

最近在线上遇到一个很有意思的问题, 以下是排查过程。

1. 问题现象

中间页进入结果页的时候, 点击某一个搜索词页面直接白屏, 如下gif动画:

图片

2 排查过程

2.1 分析初因

由于问题不稳定复现, 所以定位不到具体代码位置, 公司技术运营平台查到该用户的报错如下, 从日志来看与原代码也毫无关联

TypeError: undefined is not an object (evaluating 't.length')
This error is located at:
    in w 
    in H
    in RCTView
    in Unknown
    in RCTView
    in Unknown
    in Unknown
    in RCTScrollContentView
    in RCTScrollView
    in B
    in ScrollView
    in Unknown

找到线上用户对应的包代码下载, 这是什么🤔, 搜索t.length关键词,包含t.length文件行有283个,包含“w”文件涉及500+, 包含“H”文件涉及50+, 瞬间蒙圈

图片

尝试着找了几个包含t.length代码行,也没有任何逻辑可言, 排查思路陷入了僵局...... , 晚上下班回到家满脑子都是t.length的问题, 为此还特意发了个微信朋友圈纪念了下😂

图片

第二天继续查问题原因, 既然从代码报错没法直接找到对应代码, 想着是不是可以转换下思路, 了解react-native 原代码到jsbundle生成到底发生了什么正着梳理, 也许会有奇效。

TypeError: undefined is not an object (evaluating 't.length')
This error is located at:
    in w 
    in H

in w, in H 中的w, H 指向的是哪些具体的业务代码, 接下来, 决定从打包压缩着手分析

2.2 react-native 打包

 经过查阅资料,我们了解到metro是构建 jsbundle 包及提供开发服务的工具,默认被集成在 react-native 命令行工具内,可以在这里找到其开发服务集成源码。metro 打包分为三个阶段。

  • Resolution (解析)

该阶段用于解析模块文件的路径。从入口文件开始,寻找依赖模块的文件路径,构建一张所有模块的图,它的具体顶层执行位置在 IncrementalBundler.js 文件的 buildGraph() 方法

  • Transformation (转换)

该阶段用于转义文件至目标平台能够理解的代码, Metro 使用 Babel 作为转义工具。

  • Serialization (序列化)

序列化阶段会把各个模块按照一定顺序组合到单个或者多个 jsbundle。

相关链接:

github.com/react-nativ…

react-native 使用 metro 打包之后的 bundle 大致分为四层

  • var 声明层: 对当前运行环境, bundle 启动时间,以及进程相关信息;

  • poyfill 层: !(function(r){}) , 定义了对 define(__d)、 require(__r)、clear(__c) 的支持,以及 module(react-native 及第三方 dependences 依赖的 module) 的加载逻辑;

  • 模块定义层: __d 定义的代码块,包括 RN 框架源码 js 部分、自定义 js 代码部分、图片资源信息,供 require 引入使用

  • require层:  r 定义的代码块,找到 d 定义的代码块 并执行

  • 模块定义层:  __d 代码块就是开发所对应业务代码, 只需要分析模块定义层里代码关系即可。

通过了解知道 _d()有三个参数,分别是对应 factory 函数、 moduleId 、 module 依赖关系等, 业务代码经过一系列解析, 转换等措施, 最终生成打包代码。

业务原代码

import React from "react";
import { StyleSheetTextView } from "react-native";
export default class bundletest extends React.Component {
  render() {
    return (
      <React.Fragment>
        <View style={styles.body}>
          <Text style={styles.text}>hello word</Text>
        </View>
      </React.Fragment>
    );
  }
}
const styles = StyleSheet.create({
  body: {
    backgroundColor"white",
    flex1,
    justifyContent"center",
    alignItems"center",
  },
  text: {
    textAlign"center",
    color"red",
  },
});

中间过程解析/转义-Babel 转义

__d(function (g, r, i, a, m, e, d) {
  Object.defineProperty(e, "__esModule", {
    valuetrue
  });
  e.default = undefined;
  var _classCallCheck2 = r(d[0])(r(d[1]));
  var _createClass2 = r(d[0])(r(d[2]));
  var _inherits2 = r(d[0])(r(d[3]));
  var _possibleConstructorReturn2 = r(d[0])(r(d[4]));
  var _getPrototypeOf2 = r(d[0])(r(d[5]));
  var _react = r(d[0])(r(d[6]));
  var _reactNative = r(d[7]);
  function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = (0, _getPrototypeOf2.default)(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2.default)(this).constructor; result = Reflect.construct(SuperargumentsNewTarget); } else { result = Super.apply(thisarguments); } return (0, _possibleConstructorReturn2.default)(this, result); }; }
  function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.constructreturn falseif (Reflect.construct.shamreturn falseif (typeof Proxy === "function"return truetry { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (_e10) { return false; } }
  var bundletest = function (_React$Component) {
    (0, _inherits2.default)(bundletest, _React$Component);
    var _super = _createSuper(bundletest);
    function bundletest() {
      (0, _classCallCheck2.default)(this, bundletest);
      return _super.apply(thisarguments);
    }
    (0, _createClass2.default)(bundletest, [{
      key"render",
      valuefunction render() {
        return _react.default.createElement(_react.default.Fragmentnull, _react.default.createElement(_reactNative.View, {
          style: styles.body
        }, _react.default.createElement(_reactNative.Text, {
          style: styles.text
        }, "Hello, word")));
      }
    }]);
    return bundletest;
  }(_react.default.Component);
  e.default = bundletest;

最终生成的代码

__d(function(g, r, i, a, m, e, d) {
 var t = r(d[0]);
 Object.defineProperty(e, "__esModule", {
  value: !0
 }), e.default = void 0;
 var n = t(r(d[1])),
  l = t(r(d[2])),
  u = t(r(d[3])),
  o = t(r(d[4])),
  c = t(r(d[5])),
  f = t(r(d[6])),
  s = r(d[7]);


 function y() {
  if ("undefined" == typeof Reflect || !Reflect.constructreturn !1;
  if (Reflect.construct.shamreturn !1;
  if ("function" == typeof Proxyreturn !0;
  try {
   return Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function() {})), !0
  } catch (t) {
   return !1
  }
 }
 var v = (function(t) {
  (0, u.default)(b, t);
  var v, p, x = (v = b, p = y(), function() {
   var t, n = (0, c.default)(v);
   if (p) {
    var l = (0, c.default)(this)
     .constructor;
    t = Reflect.construct(n, arguments, l)
   } else t = n.apply(thisarguments);
   return (0, o.default)(this, t)
  });


  function b() {
   var t;
   (0, n.default)(this, b);
   for (var l = arguments.length, u = new Array(l), o = 0; o < l; o++) u[o] = arguments[o];
   return (t = x.call.apply(x, [this].concat(u)))
    .constructorName = 'bundletest', t
  }
  return (0, l.default)(b, [{
   key"render",
   valuefunction() {
    return f.default.createElement(f.default.Fragmentnull, f.default.createElement(s.View, {
     style: h.body
    }, f.default.createElement(s.Text, {
     style: h.text
    }, "hello word")))
   }
  }]), b
 })(r(d[8])
  .AHComponent);
 e.default = v;
 var h = s.StyleSheet.create({
  body: {
   backgroundColor"white",
   flex1,
   justifyContent"center",
   alignItems"center"
  },
  text: {
   textAlign"center",
   color"red"
  }
 })
}, "98c67a34b7a27a4e8ff1001bbc74a19f", ["68ecc7c5e070bf8f811a1f8e3b20e728""1b20a73cb5d4b73954dd587cbdab4855""7dad6d37d3929ceeb9ff64ac1515757b""1aa3fd5f6d386370a716a50aa3ebcc18""896613709e549c3b0b6037429eb23014""5e6c26349e041a98cc1727a3bc82f4ef""41fe1dc6e15d848f867b0cf953c50e53""1c16f2955ff5bbfcadfecfcbd249780f""26eaf122cbd63e32408eba8da33e6b56"]);

3 提取共性特征

根据RN打包压缩的过程, 找到业务原代码和jsbundle中的代码, 进行比对分析, 发现原业务中的代码组件会生成如下代码特征:「红框中标注的代码片段」

图片

组件转换为最终代码的过程中, class 会转化为一个变量然后通过e.default 赋值导出, 并且在该函数变量内部会有一个函数, 函数内是代码里的周期函数, 以及内部自定义函数等数据, 通过return形式返回. 基于此我们提取了两个共同特性. 称之为特征数据一, 特征数据二。

1. e.default = v; // 特征数据一
2. return (0, l.default)(b, [{; // 特征数据二

接下来, 我们分别根据提取的特性数据一、二 在代码压缩包中进行查找。

3.1 特征数据一:分析 e.default = v

 从报错信息中根据特征数据一对应两个常量常量一: e.default=w;常量二: e.default=H;

TypeError: undefined is not an object (evaluating 't.length')
This error is located at:
    in w -> 对应的是  -> e.default=w
    in H -> 对应的是  -> e.default=H
    in RCTView
    in Unknown
    in RCTView
    in Unknown
    in Unknown
    in RCTScrollContentView 与 FlatList 有关系
    in RCTScrollView
    in B
    in ScrollView

根据两个常量分别搜索对应的文件

我们从jsbundle代码中搜索 e.default=w 特性, 共有27个文件代码

图片

从上述27个文件中搜索t.length 最终筛选出8个文件

图片

jsbundle代码中搜索 e.default=H  特性, 共有4个文件代码

640.png

经过比对发现, 根据 e.default=w  和 e.default=H 最终筛选出的文件, 发现两者文件没有任何关联关系。

3.2 特征数据二:分析 return (0, l.default)(b, [{

从报错信息中根据特征数据二对应两个常量

常量一: .default)(w,[{

常量二: .default)(H,[{

TypeError: undefined is not an object (evaluating 't.length')
This error is located at:
    in w -> 对应的是  -> .default)(w,[{
    in H -> 对应的是  -> .default)(H,[{
    in RCTView
    in Unknown
    in RCTView
    in Unknown
    in Unknown
    in RCTScrollContentView 与 FlatList 有关系
    in RCTScrollView
    in B
    in ScrollView

根据两个常量分别搜索对应的文件

常量一: .default)(w,[{

图片

图片

文件路径

  • 常一a ./src/Components/Common/Basic/SRNLabelWithAvatar/index.js
  • 常一b ./src/Components/Common/Basic/SRNBanner/index.js
  • 常一c ./src/Components/Shared/Middle/Components/PageModle/SRNNewSearchHistoryV2.js
  • 常一d ./src/Components/Shared/Middle/Components/PageModle/SRNNewGuessYouLike.js
  • 常一e ./src/AdComponents/AdZhaoCheSeriesButton.js
  • 常一f ./src/AdComponents/AdZhaoCheSeriesButtonnew.js
  • 常一g ./src/Views/NewZongHe/index.js

常量二: .default)(H,[{ 存在2个相关的文件

图片

  • 常二a ./src/Views/LunTan/LoadComp.js

  • 常二b ./src/Views/MiddleV3/HeaderComponent.js

分析常量一文件和常量二文件对应关系

图片

发现只有常一c,常一d文件与特征二的文件相关

分析文件中相关代码

常一c中关于t.length代码

图片

常一d中关于t.length代码2

640 (1).png

结合用户的操作步骤, 在用户进入结果页的时候报错, 此时常量一c中代码会被执行, 至此问题文件定位.

基于提取的两个特征数据, 根据jsbundle找对应的原代码,发现特征数据一没有关联, 特征数据二关联到了实际报错的代码文件, 为了验证特征数据二的准确性, 通过本地构造一个.map的js执行错误, 发布到测试环境, 更新APP, 进行测试验证, 特征数据是否可以用作常规的报错排查手段, 用来定位具体原代码文件。

4 特征数据方法可用性验证

本地构造一个.map的js执行错误, 将代码发布到测试环境, 更新APP, 引发RN白屏崩溃, 进行测试验证. 特征数据二return (0, l.default)(b, [{;

4.1 公司技术平台中抓取到的错误信息

TypeError: t.map is not a function
This error is located at:
    in S -> .default)(S,[{
    in RCTView
    in Unknown
    in k
    in RCTView
    in Unknown
    in Unknown
    in c
    in RCTScrollContentView
    in RCTScrollView
    ...

4.2 根据代码报错获取报错常量

常量一: .default)(S,[{

常量二: .default)(k,[{

查找定位错误文件

常量一: .default)(S,[{

压缩代码中共有 31条包含有特征的数据

图片

31条包含特征的数据中其中有7条数据有t.map

图片

►相关文件

  • 常一a ./src/Components/Common/Basic/SRNDropdown/index.js
  • 常一b ./src/Components/NewShared/ZBlock/ZhaoChe/series.js
  • 常一c ./src/Components/NewShared/MBlock/MultiPurpose/multi_intention_spec.js
  • 常一d ./src/Components/NewShared/MBlock/Author_multi/bigCard.js
  • 常一e ./src/Components/NewShared/MBlock/Author_multi/column.js
  • 常一f ./src/Components/NewShared/MBlock/AllDealer2.1/index.js
  • 常一g ./src/Views/Prezonghe/index.js

常量二: .default)(k,[{

压缩代码中共有 10条包含有特征的数据

图片

►相关文件

  • 常二a ./src/Components/NewShared/ZBlock/XiaomiQa.js
  • 常二b ./src/Components/NewShared/MBlock/Vehicle_figure/headNew.js
  • 常二c ./src/Components/NewShared/MBlock/chenjin/header/Header.js
  • 常二d ./src/Components/NewShared/MBlock/CarSeries/maintainance/index.js
  • 常二e ./src/Components/NewShared/MBlock/MultiCar/Common/KeepRate.js
  • 常二f ./src/FeedCard/card90032.js
  • 常二g ./src/FeedCard/card90020.js
  • 常二h ./src/Views/NewZongHe/LoadComp.js
  • 常二i ./src/Views/AllSeries/SeriesItem.js
  • 常二j ./src/Views/ZhaoChe/index.js

►常量一文件和常量二文件对应关系

图片

结合操作用户的操作行为,以及接口请求实时日志, 常一f中代码会被执行, 至此问题文件定位, 和我们伪造的错误js文件一致。

通过伪造js错误, 我们在测试环境中根据上报的错误日志, 验证了提取特征数据是可用的。

总结

经过分析react-native 原代码到jsbundle打包过程以及jsbundle压缩代码, 总结提取出一种的业务代码组件特征数据 .default)(w,[{ 。且在测试环境中进行了验证, 为我们日常定位RN线上问题节点提供了一大助力 。

图片