0
点赞
收藏
分享

微信扫一扫

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

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

1. 问题现象

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

从十万行代码定位undefined is not an object (evaluating

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+, 瞬间蒙圈

从十万行代码定位undefined is not an object (evaluating

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

从十万行代码定位undefined is not an object (evaluating

第二天继续查问题原因, 既然从代码报错没法直接找到对应代码, 想着是不是可以转换下思路, 了解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。

相关链接:

<https://github.com/react-native-community/cli/blob/e89f296b1f1b27da23ffb77e3c8fc5bc2f4942ee/packages/cli-plugin-metro/src/commands/start/runServer.ts#L9>

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 { StyleSheet, Text, View } 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",
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  text: {
    textAlign: "center",
    color: "red",
  },
});

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

__d(function (g, r, i, a, m, e, d) {
  Object.defineProperty(e, "__esModule", {
    value: true
  });
  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(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2.default)(this, result); }; }
  function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { 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(this, arguments);
    }
    (0, _createClass2.default)(bundletest, [{
      key: "render",
      value: function render() {
        return _react.default.createElement(_react.default.Fragment, null, _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.construct) return !1;
  if (Reflect.construct.sham) return !1;
  if ("function" == typeof Proxy) return !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(this, arguments);
   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",
   value: function() {
    return f.default.createElement(f.default.Fragment, null, 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",
   flex: 1,
   justifyContent: "center",
   alignItems: "center"
  },
  text: {
   textAlign: "center",
   color: "red"
  }
 })
}, "98c67a34b7a27a4e8ff1001bbc74a19f", ["68ecc7c5e070bf8f811a1f8e3b20e728", "1b20a73cb5d4b73954dd587cbdab4855", "7dad6d37d3929ceeb9ff64ac1515757b", "1aa3fd5f6d386370a716a50aa3ebcc18", "896613709e549c3b0b6037429eb23014", "5e6c26349e041a98cc1727a3bc82f4ef", "41fe1dc6e15d848f867b0cf953c50e53", "1c16f2955ff5bbfcadfecfcbd249780f", "26eaf122cbd63e32408eba8da33e6b56"]);

3 提取共性特征

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

从十万行代码定位undefined is not an object (evaluating

组件转换为最终代码的过程中, 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个文件代码

从十万行代码定位undefined is not an object (evaluating

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

从十万行代码定位undefined is not an object (evaluating

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

从十万行代码定位undefined is not an object (evaluating

经过比对发现, 根据 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,\[{

从十万行代码定位undefined is not an object (evaluating

从十万行代码定位undefined is not an object (evaluating

文件路径

  • 常一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个相关的文件

从十万行代码定位undefined is not an object (evaluating

  • 常二a ./src/Views/LunTan/LoadComp.js
  • 常二b ./src/Views/MiddleV3/HeaderComponent.js
分析常量一文件和常量二文件对应关系

从十万行代码定位undefined is not an object (evaluating

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

分析文件中相关代码

常一c中关于t.length代码

从十万行代码定位undefined is not an object (evaluating

常一d中关于t.length代码

从十万行代码定位undefined is not an object (evaluating

结合用户的操作步骤, 在用户进入结果页的时候报错, 此时常量一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条包含有特征的数据

从十万行代码定位undefined is not an object (evaluating

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

从十万行代码定位undefined is not an object (evaluating

►相关文件

  • 常一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条包含有特征的数据

从十万行代码定位undefined is not an object (evaluating

►相关文件

  • 常二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

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

从十万行代码定位undefined is not an object (evaluating

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

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

总结

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

从十万行代码定位undefined is not an object (evaluating

举报

相关推荐

0 条评论