开发中,难免有些错误逻辑没有想到,线上各种复杂的环境,可能导致程序错误,或者报错,但是打包后的代码都是混淆压缩的,很难定位到发生错误的位置,如何根据线上的错误代码快速定位到源呢。
比如下面这段错误代码
import React, { useEffect } from 'react';
import styles from './index.less';
import ErrorBoundary from '@/components/ErrorBoundary';
function IndexPage() {
useEffect(() => {
setTimeout(() => {
console.log(a) // 这儿错误代码
}, 2000)
}, [])
return (
<div>
<h1 className={styles.title}>Page index</h1>
</div>
);
}
export default function App() {
return (
<ErrorBoundary>
<IndexPage />
</ErrorBoundary>
)
}
打包后线上的报错位置很难定位到源码是哪儿有问题,我平时在项目中经常遇到这样的问题,每次排查起来都比较困难,要如何解决这个问题呢
webpack打包的时候的可以开启source-map,打包出来会有一个.js.map文件,该文件可以映射我们打包后的文件
- 使用ErrorBoundary收集react错误
- 调用TraceKit获取发生错误的行数、调用栈级错误信息
- TraceKit获取的错误信息传递给服务端
- 服务端根据上传的错误信息调用source-map插件,获取源码报错的位置
使用ErrorBoundary收集react错误
// ErrorBoundary组件
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
}
render() {
if (this.state.hasError) {
return <h1>出错了!</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary
//
import React, { useEffect } from 'react';
import styles from './index.less';
import ErrorBoundary from '@/components/ErrorBoundary';
function IndexPage() {
useEffect(() => {
setTimeout(() => {
console.log(a)
}, 2000)
}, [])
return (
<div>
<h1 className={styles.title}>Page index</h1>
</div>
);
}
export default function App() {
return (
<ErrorBoundary>
<IndexPage />
</ErrorBoundary>
)
}
如果组件发生错误,会除非这个getDerivedStateFromError这个函数,componentDidCatch收集错误信息和调用栈等,文字功底不哈,详情可看官网这两个函数的解释。
TraceKit获取发生错误的行数、调用栈级错误信息,上报错误
import React from "react";
import TraceKit from 'tracekit'; // 引入tracekit
TraceKit.report.subscribe((error) => {
const { message = "", stack } = error || {};
console.log("stack", stack[0])
const obj = {
message,
stack: {
column: stack[0].column,
line: stack[0].line,
func: stack[0].func,
url: stack[0].url
}
};
console.log("调用栈信息", obj)
// 上传错误信息
const response = fetch("http://localhost:3001/errorUp", {
method: "POST",
body: JSON.stringify(obj),
headers: {
"Content-Type": "application/json",
},
}).then(res => {
console.log("上传成功")
})
})
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 调用TraceKit收集错误的的信息
TraceKit.report(error)
}
render() {
if (this.state.hasError) {
return <h1>出错了!</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary
在componentDidCatch中调用TraceKit.report(),把报错信息传给TraceKit解析,解析出报错的行数、报错的文件路径、报错了的列、级报错的函数,,并把报错信息传给服务器
服务端解析
服务端根据上传的错误信息,调用source-map插件解析map文件,找到源码中对应的错误信息位置
const express = require('express')
const cors = require('cors')
const SourceMap = require('source-map');
const multiparty = require("multiparty") // 处理form-data请求
const path = require('path')
const fse = require("fs-extra")
const fs =require('fs')
const app = express()
// 解析 url-encoded格式的表单数据
app.use(express.urlencoded({ extended: false }));
// 解析json格式的表单数据
app.use(express.json());
app.use(cors())
app.post('/errorUp', async (req, res) => {
const urlParams = req.body;
console.log('urlParams', req.body);
const stack = urlParams.stack;
// 获取文件名
const fileName = path.basename(stack.url);
// 获取对应的map文件
const filePath = path.join(__dirname, fileName + '.map');
console.log("filePath",filePath)
const readFile = function (filePath) {
return new Promise((resolve, reject) => {
fs.readFile(filePath, { encoding: 'utf-8' }, (err, data) => {
if (err) {
console.log('readFileErr', err)
return reject(err);
}
resolve(JSON.parse(data));
})
})
}
async function searchSource({ filePath, line, column }) {
const rawSourceMap = await readFile(filePath);
const consumer = await new SourceMap.SourceMapConsumer(rawSourceMap);
const res = consumer.originalPositionFor({ line, column })
consumer.destroy();
return res;
}
let sourceMapParseResult = '';
try {
// 解析sourceMap结果
sourceMapParseResult = await searchSource({ filePath, line: stack.line, column: stack.column });
} catch (err) {
sourceMapParseResult = err;
}
console.log('解析结果', sourceMapParseResult)
res.send("ok")
})
app.listen(3001, () => {
console.log("serve3001 服务端启动")
})
找到对应源码中的报错的位置,在/src/pages/index.tsx文件中第7行处18的一列,刚好于源码对应
总结
主要用到了tracekit解析错误的调用栈信息,在使用source-map插件根据.map文件和tracekit解析的错误信息找到源码中发生错误的位置,关键用到tracekit、source-map插件和.map文件