最近在线上遇到一个很有意思的问题, 以下是排查过程。
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 打包分为三个阶段。
该阶段用于解析模块文件的路径。从入口文件开始,寻找依赖模块的文件路径,构建一张所有模块的图,它的具体顶层执行位置在 IncrementalBundler.js 文件的 buildGraph() 方法
该阶段用于转义文件至目标平台能够理解的代码, Metro 使用 Babel 作为转义工具。
序列化阶段会把各个模块按照一定顺序组合到单个或者多个 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 个相关的文件
分析常量一文件和常量二文件对应关系
发现只有常一 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 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 线上问题节点提供了一大助力 。
评论