rn打离线包往往会遇到一个问题,代码报错的行号是bundle文件中的行号,而且变量名也是编译过的,这样代码的阅读性就会下降,甚至根本找不到原因。而rn自己对离线sourcemap的支持非常不好(感觉很不科学啊,online的根本不需要sourcemap啊),甚至依赖包根本不能用。
所以我总结了一下离线sourcemap在android项目中的应用。
1.首先你要把sourcemap文件打包到android项目中,差不多像这样
2.其次,你的rn项目要依赖react-native-fs包,install出来以后把把下面android包中rnfs文件夹拷到你自己的android项目中当做你自己的操作类。大概像这样
3.往你项目的packageManager中添加rnfspackage,并在app启动的时候创建sourcemap文件在本地,像这样
4.在rn中创建错误收集handler
import
ErrorUtils
from
'ErrorUtils';
import {
alert }
from
'react-native';
import {
initSourceMaps,
getStackTrace }
from
'./SourceMapHandler';
import {
nativeCallModuleWrapper }
from
'./NativeModuleWrappers/NativeCallModuleWrapper';
const
setupErrorHandler =
async (
sourceMapBundleName)
=> {
await
initSourceMaps({
sourceMapBundle:
sourceMapBundleName,
collapseInLine:
true });
await (
async
function
initUncaughtErrorHandler() {
const
defaultGlobalHandler =
ErrorUtils.
getGlobalHandler();
ErrorUtils.
setGlobalHandler(
async (
error,
isFatal)
=> {
try {
if (!
__DEV__) {
error.
stack =
await
getStackTrace(
error);
}
nativeCallModuleWrapper.
reportCrash(
error.
message,
error.
stack);
}
catch (
ex) {
alert(
`${
ex.
message
}----Unable to setup global handler----${
ex.
stack
}`);
}
if (
__DEV__ &&
defaultGlobalHandler) {
defaultGlobalHandler(
error,
isFatal);
}
});
}());
};
export default
setupErrorHandler;
5.创建错误收集handler需要用到的方法
import
RNFS
from
'react-native-fs';
import
SourceMap
from
"source-map";
import
StackTrace
from
"stacktrace-js";
import
ErrorStackParser
from
"error-stack-parser";
let
sourceMapper =
undefined;
let
options =
undefined;
/**
* Init Source mapper with options
* Required:
*
@param
{String} [opts.sourceMapBundle] - source map bundle, for example "main.jsbundle.map"
* Optional:
*
@param
{String} [opts.projectPath] - project path to remove from files path
*
@param
{Boolean} [opts.collapseInLine] — Will collapse all stack trace in one line, otherwise return lines array
*/
export
const
initSourceMaps =
async
opts
=> {
if (!
opts || !
opts.
sourceMapBundle) {
throw
new
Error(
'Please specify sourceMapBundle option parameter');
}
options =
opts;
};
export
const
getStackTrace =
async
error
=> {
if (!
options) {
throw
new
Error(
'Please firstly call initSourceMaps with options');
}
if (!
sourceMapper) {
sourceMapper =
await
createSourceMapper();
}
try {
const
minStackTrace =
await
fromError(
error);
const
stackTrace =
minStackTrace.
map(
row
=> {
const
mapped =
sourceMapper(
row);
const
source =
mapped.
source ||
"";
const
fileName =
options.
projectPath ?
source.
split(
options.
projectPath)
.pop() :
source;
const
functionName =
mapped.
name ||
"unknown";
return {
fileName,
functionName,
lineNumber:
mapped.
line,
columnNumber:
mapped.
column,
position:
`${
functionName
}@${
fileName
}:${
mapped.
line
}:${
mapped.
column
}`
};
});
return
options.
collapseInLine ?
stackTrace.
map(
i
=>
i.
position).
join(
'\n') :
stackTrace;
}
catch (
error) {
alert(
error.
message);
throw
error;
}
};
function
_filtered(
stackframes,
filter) {
if (typeof
filter ===
'function') {
return
stackframes.
filter(
filter);
}
return
stackframes;
}
function
fromError(
error,
opts){
var
_options = {
filter:
function(
stackframe) {
// Filter out stackframes for this library by default
return (
stackframe.
functionName ||
'').
indexOf(
'StackTrace$$') ===
-1 &&
(
stackframe.
functionName ||
'').
indexOf(
'ErrorStackParser$$') ===
-1 &&
(
stackframe.
functionName ||
'').
indexOf(
'StackTraceGPS$$') ===
-1 &&
(
stackframe.
functionName ||
'').
indexOf(
'StackGenerator$$') ===
-1;
},
sourceCache: {}
};
opts =
_merge(
_options,
opts);
//var gps = new StackTraceGPS(opts);
return
new
Promise(
function(
resolve) {
var
stackframes =
_filtered(
ErrorStackParser.
parse(
error),
opts.
filter);
resolve(
Promise.
all(
stackframes.
map(
function(
sf) {
return
new
Promise(
function(
resolve) {
//function resolveOriginal() {
resolve(
sf);
//}
//gps.pinpoint(sf).then(resolve, resolveOriginal)['catch'](resolveOriginal);
});
})));
}.
bind(
this));
}
function
_merge(
first,
second) {
var
target = {};
[
first,
second].
forEach(
function(
obj) {
for (
var
prop in
obj) {
if (
obj.
hasOwnProperty(
prop)) {
target[
prop] =
obj[
prop];
}
}
return
target;
});
return
target;
}
const
createSourceMapper =
async ()
=> {
RNFS.MainBundlePath="/storage/emulated/0/Android/data/com.test/cache"
const path =
`${
RNFS.
MainBundlePath
}/${
options.
sourceMapBundle
}`;
try {
const
fileExists =
await
RNFS.
exists(path);
if (!
fileExists) {
throw
new
Error(
__DEV__ ?
'Unable to read source maps in DEV mode' :
`Unable to read source maps, possibly invalid sourceMapBundle file, please check that it exists here: ${
RNFS.
MainBundlePath
}/${
options.
sourceMapBundle
}`
);
}
const
mapContents =
await
RNFS.
readFile(path,
'utf8');
const
sourceMaps =
JSON.
parse(
mapContents);
const
mapConsumer =
new SourceMap.
SourceMapConsumer(
sourceMaps);
return
sourceMapper =
row
=> {
return
mapConsumer.
originalPositionFor({
line:
row.
lineNumber,
column:
row.
columnNumber,
});
};
}
catch (
error) {
throw
error;
}
};
一定要设置好RNFS.MainBundlePath到你创建sourcemap文件的位置。