写点什么

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

  • 2023-07-14
    北京
  • 本文字数:5997 字

    阅读完需:约 20 分钟

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

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。


相关链接:


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 中的代码, 进行比对分析, 发现原业务中的代码组件会生成如下代码特征:「红框中标注的代码片段」



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



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



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


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

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

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

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

TypeError: t.map is not a functionThis 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 线上问题节点提供了一大助力 。



用户头像

前端技术创新 体验优化 分享经验 共同进步 2018-11-25 加入

通过技术的创新和优化,为用户创造更好的使用体验,并与更多的前端开发者分享我们的经验和成果。我们欢迎对前端开发感兴趣的朋友加入我们的团队,一同探讨技术,共同进步。

评论

发布
暂无评论
从十万行代码定位undefined is not an object (evaluating 't.length')_汽车之家客户端前端团队_InfoQ写作社区