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

大家好,我是武杰

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


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/facebook/metro

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 依赖关系等, 业务代码经过一系列解析, 转换等措施, 最终生成打包代码。


►业务原代码

// App.js
import React from "react";
import { StyleSheet, Text, View } from "react-native";
export default class bundletest extends React.Component {
  render() {
    return (
      
        
          hello word
        
      
    );
  }
}
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,[{


文件路径

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



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


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

分析文件中相关代码

常一c中关于t.length代码

常一d中关于t.length代码

结合用户的操作步骤, 在用户进入结果页的时候报错, 此时常量一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


►相关文件

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

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


►相关文件

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

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

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


总结

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


作者简介

崔武杰

C端及中台产研中心-客户端研发部-前端团队-C端组

2018年加入汽车之家, 目前主要负责APP端的搜索业务前端开发工作。

来源:微信公众号:之家技术

出处:https://mp.weixin.qq.com/s/uB0oKqEUYz8znM3qezRs0g

展开阅读全文

页面更新:2024-05-18

标签:代码   常量   模块   原代码   特征   定义   错误   关系   文件   数据

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top