南锋

南奔万里空,脱死锋镝余

cocosCreator全局错误监听及错误的解决方法

我们在使用cocosCreator开发游戏,如果游戏上线,出现了bug我们一般是不知道的,除了玩家反馈。而且算有玩家反馈,也不知道具体是哪里出现了问题。我这里是做了一个全局的错误监听,然后将错误日志上报到服务器,然后通过上报的错误日志来定位问题。甚至可以发现很多玩家没有反馈到的bug。

创建全局错误监听

这里注意,不同的错误类型监听方法不一样,而且只在web端生效,如果是原生端,这里的监听是不会上报的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
window.onerror = (message, source, lineno, colno, error) => {
if (lineno != lineno1 && colno1 != lineno && message != message1) {
HttpReq.Ins.reqDotting(error, { type: `5`, content: `捕获到全局错误:来源: ${source},行号: ${lineno},列号: ${colno}`, additional: `错误对象: ${error}` });
}
lineno1 = lineno;
colno1 = colno;
message1 = message;
return true;
};

window.addEventListener("unhandledrejection", (event) => {
// console.error("未处理的 Promise 异常:", event.reason);
let errorInfo = {
message: "",
stack: "",
};

if (event.reason instanceof Error) {
// 如果event.reason是一个Error对象,获取错误消息和堆栈信息
errorInfo.message = event.reason.message;
errorInfo.stack = event.reason.stack;
} else {
// 如果不是一个Error对象,直接使用toString()方法尝试获取有用的信息
errorInfo.message = event.reason.toString();
}
const errorInfoString = JSON.stringify(errorInfo);
HttpReq.Ins.reqDotting(ClientDottingName.error, { type: `3`, content: `未处理的 Promise 异常`, additional: `${errorInfoString}` });
});

window.addEventListener("error", (event) => {
const target = event.target as HTMLElement;
if (target && (target.tagName === "IMG" || target.tagName === "SCRIPT" || target.tagName === "LINK")) {
HttpReq.Ins.reqDotting(error, { type: `4`, content: `资源加载失败:`, additional: `${target.getAttribute("src") || target.getAttribute("href")}` });
}
}, true);

这里需要注意几点:

  1. 上报的接口要改成你自己的,我这里是随便写的一个
  2. 在捕获全局错误window.onerror时,一旦触发可能就是无限触发,会导致游戏卡死的那种,只有杀掉进程才能恢复。所以我做了一个判断,如果行号和列号和上一次相同,并且错误信息相同,则不再上报,减少服务器的压力。

查看问题及解决方法

查看上面的报错日志,这里以下面日志为例:

1
2
3
4
5
6
7
8
9
10
11
{"message":"Cannot read properties of null (reading 'length')","stack":"TypeError: Cannot read properties of null (reading 'length')\n    
at e.getChildByName (https://lengmo714.top/test/cocos-js/cc.7297c.js:1:383931)\n
at n.watchTheGame (https://lengmo714.top/test/assets/main/index.fef9f.js:317:57103)\n
at s.<anonymous> (https://lengmo714.top/test/assets/main/index.fef9f.js:221:6210)\n
at s (https://lengmo714.top/test/src/chunks/bundle.846d5.js:110:641)\n
at Generator.<anonymous> (https://lengmo714.top/test/src/chunks/bundle.846d5.js:110:1982)\n
at Generator.next (https://lengmo714.top/test/src/chunks/bundle.846d5.js:110:1004)\n
at e (https://lengmo714.top/test/src/chunks/bundle.846d5.js:108:166)\n
at u (https://lengmo714.top/test/src/chunks/bundle.846d5.js:108:3450)\n
at https://lengmo714.top/test/src/chunks/bundle.846d5.js:108:3509\n
at new Promise (<anonymous>)"}

上面的日志说明在调用 getChildByName 返回了 null,然后代码里尝试读取它的 length 属性,导致空指针异常。

解决方法

解决方法有很多,比如直接全局搜索wathcTheGame方法,看具体时哪里出错了,但是如果这个方法用的地方比较多,可能也判断不出具体的问题。
再或者打包时关闭代码混淆,本地测试可以用,但是上传到正式服不建议这么做。
推荐方法,利用SourceMap
在打包的时候,勾选择SourceMap,这样打包的时候给每个.js文件生成一个和源码映射的.map文件。如果是线上环境,不建议上传相关的.map文件。后面我们在排查错误的时候,可以通过这个.map文件来找我们出问题的源代码。

使用方法:

  1. 打包时勾选SourceMap
  2. 安装source-map
    使用下面命令行进行全局安装:
    1
    npm install source-map
  3. 创建一个 JS 脚本,比如叫 lookupSourceMap.js,内容如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const fs = require('fs');
    const sourceMap = require('source-map');

    async function lookup(mapFile, line, column) {
    const rawSourceMap = JSON.parse(fs.readFileSync(mapFile, 'utf-8'));
    const consumer = await new sourceMap.SourceMapConsumer(rawSourceMap);

    const pos = consumer.originalPositionFor({ line: line, column: column });
    console.log('原始源码位置:', pos);

    consumer.destroy();
    }

    const [,, mapFile, line, column] = process.argv;
    if (!mapFile || !line || !column) {
    console.error('用法: node lookupSourceMap.js path/to/file.js.map 行 列');
    process.exit(1);
    }

    lookup(mapFile, Number(line), Number(column));
    运行示例:
    1
    node lookupSourceMap.js ./cc.js.map 1 383931  // 后面分别是行号和列号
    输出示例:
    1
    2
    3
    4
    5
    6
    原始源码位置: {
    source: 'src/game/watchTheGame.ts',
    line: 120,
    column: 15,
    name: 'watchTheGame'
    }
    如下图所示:
    示例图
+