天天看點

從十萬行代碼定位undefined is not an object (evaluating 't.length')

作者:閃念基因

大家好,我是武傑

最近線上上遇到一個很有意思的問題, 以下是排查過程。

1.問題現象

中間頁進入結果頁的時候, 點選某一個搜尋詞頁面直接白屏, 如下gif動畫:

從十萬行代碼定位undefined is not an object (evaluating 't.length')

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')

簡直是

從十萬行代碼定位undefined is not an object (evaluating 't.length')

嘗試着找了幾個包含t.length代碼行,也沒有任何邏輯可言, 排查思路陷入了僵局...... , 晚上下班回到家滿腦子都是t.length的問題, 為此還特意發了個微信朋友圈紀念了下

從十萬行代碼定位undefined is not an object (evaluating '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 (
      <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 't.length')

元件轉換為最終代碼的過程中, 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 't.length')

從上述27個檔案中搜尋t.length 最終篩選出8個檔案

從十萬行代碼定位undefined is not an object (evaluating 't.length')

jsbundle代碼中搜尋 e.default=H 特性, 共有4個檔案代碼

從十萬行代碼定位undefined is not an object (evaluating 't.length')

經過比對發現, 根據 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 't.length')
從十萬行代碼定位undefined is not an object (evaluating 't.length')

檔案路徑

  • 常一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 't.length')
  • 常二a ./src/Views/LunTan/LoadComp.js
  • 常二b ./src/Views/MiddleV3/HeaderComponent.js

►分析常量一檔案和常量二檔案對應關系

從十萬行代碼定位undefined is not an object (evaluating 't.length')

發現隻有常一c,常一d檔案與特征二的檔案相關

分析檔案中相關代碼

常一c中關于t.length代碼

從十萬行代碼定位undefined is not an object (evaluating 't.length')

常一d中關于t.length代碼

從十萬行代碼定位undefined is not an object (evaluating '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條包含有特征的資料

從十萬行代碼定位undefined is not an object (evaluating 't.length')

31條包含特征的資料中其中有7條資料有t.map

從十萬行代碼定位undefined is not an object (evaluating 't.length')

►相關檔案

  • 常一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 't.length')

►相關檔案

  • 常二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 't.length')

結合操作使用者的操作行為,以及接口請求實時日志, 常一f中代碼會被執行, 至此問題檔案定位, 和我們僞造的錯誤js檔案一緻。

通過僞造js錯誤, 我們在測試環境中根據上報的錯誤日志, 驗證了提取特征資料是可用的。

總結

經過分析react-native 原代碼到jsbundle打包過程以及jsbundle壓縮代碼, 總結提取出一種的業務代碼元件特征資料 .default)(w,[{ 。且在測試環境中進行了驗證, 為我們日常定位RN線上問題節點提供了一大助力 。

作者簡介

崔武傑

■ C端及中台産研中心-用戶端研發部-前端團隊-C端組

■ 2018年加入汽車之家, 目前主要負責APP端的搜尋業務前端開發工作。

來源:微信公衆号:之家技術

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

下一篇: Day2-棧

繼續閱讀