大家好,我是武傑
最近線上上遇到一個很有意思的問題, 以下是排查過程。
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 (
<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代碼
結合使用者的操作步驟, 在使用者進入結果頁的時候報錯, 此時常量一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線上問題節點提供了一大助力 。
作者簡介
崔武傑
■ C端及中台産研中心-用戶端研發部-前端團隊-C端組
■ 2018年加入汽車之家, 目前主要負責APP端的搜尋業務前端開發工作。
來源:微信公衆号:之家技術
出處:https://mp.weixin.qq.com/s/uB0oKqEUYz8znM3qezRs0g