近期在熟悉怎样处理前端异常,在客户端跑的h5代码,如果遇到体量大的客户群(几百w,几千w),对前端js进行异常监控就变得很重要了,因为测试并不能完整的捕捉到某些情况和场景下的异常,其中包括了接口返回信息缺失、固定操作下的js报错等,我的思路大概是这样:
1、前端对异常进行收集
2、上报到node server
3、通过 log4js进行日志记录
当然,如果想体验更好,也可以通过node server 将异常入库(结合mongodb),在后台通过数据结合类
chart 组件通过图形化展示异常信息更为直观。
一、前端异常收集。
前端的异常收集常用的两种方式:
1、try catch
try,catch能够知道出错的信息,并且也有堆栈信息可以知道在哪个文件第几行第几列发生错误。
但是try,catch的方案有2个缺点:
没法捕捉try,catch块,当前代码块有语法错误,JS解释器压根都不会执行当前这个代码块,所以也就没办法被catch住;
没法捕捉到全局的错误事件,也即是只有try,catch的块里边运行出错才会被你捕捉到,这里的块你要理解成一个函数块。
2、window.onerror
可以捕捉语法错误,也可以捕捉运行时错误;
可以拿到出错的信息,堆栈,出错的文件、行号、列号;
只要在当前页面执行的js脚本出错都会捕捉到,例如:浏览器插件的javascript、或者flash抛出的异常等。
跨域的资源需要特殊头部支持。
需要注意的是:
1、window.onerror能捕捉到语法错误,但是语法出错的代码块不能跟window.onerror在同一个块
2、对于跨域的JS资源,window.onerror拿不到详细的信息,需要往资源的请求添加额外的头部。
window.onerror = (msg, url, line, col, error) => {
//没有URL不上报!上报也不知道错误
if (msg != "Script error." && !url) {
return true;
}
setTimeout(() => {
var data = {};
//不一定所有浏览器都支持col参数
col = col || (window.event && window.event.errorCharacter) || 0;
data.url = url;
data.line = line;
data.col = col;
if (!!error && !!error.stack) {
//如果浏览器有堆栈信息
//直接使用
data.msg = error.stack.toString();
} else if (!!arguments.callee) {
//尝试通过callee拿堆栈信息
var ext = [];
var f = arguments.callee.caller, c = 3;
//这里只拿三层堆栈信息
while (f && (--c > 0)) {
ext.push(f.toString());
if (f === f.caller) {
break;//如果有环
}
f = f.caller;
}
ext = ext.join(",");
data.msg = ext;
}
//把data上报到后台!
//这里可以做日志上报
$.ajax({})
}, 0);
return false;
};这里我们在后面返回false,让控制台也能把错误打印出来,至此,前端异常收集完成!
二、上报到node server
服务端要做的,就是提供上传数据的接口,让错误数据能保存下来,很简单,我们加到express路由中就可以了:
/* 写入前端日志 */
router.post('/restapi/reportErrInfo', (req, res, next) => {
let logParams = req.query;
let logUrl = logParams.url;
let sendState = false;
if (autoUrl(logUrl)) {
logUtil.logh5Error(req, logParams);
sendState = true;
}
return res.json({
data: {
responseCode: '0',
responseMsg: sendState ? 'success!' : 'failed!'
}
})
});这里主要是通过接口写入日志的操作, 这是我通过log4js封装的方法,也就是下面这段:
logUtil.logh5Error(req, logParams);
另外需要重点说明两点:
1、日志上报使用post方式,更安全
2、为了防止恶意请求(csrc等),需要在接口处对信息进行鉴权处理,方法很多,常用的比如通过对比cookie,或者前端传token的方式
三、通过 log4js进行日志记录
nodeJS自带的console.log已经可以打印出日志了,为了让日志看起来没那么糟,我打算对日子进行改造(将常规日志response和错误日志error分开),具体实现如下:
1、新建log配置文件 logConfig.js
let path = require('path');
//日志根目录
let isDevEnv = (process.env.NODE_ENV == 'development' || process.env.NODE_ENV == 'FAT') ? true : false;
let baseLogPath = isDevEnv ? path.resolve(__dirname, '../logs') : '/home/reslogs’;
//错误日志目录
let errorPath = isDevEnv ? "/error" : ‘/home/errlogs';
//错误日志文件名
let errorFileName = "error";
//错误日志输出完整路径
let errorLogPath = baseLogPath + errorPath + "/" + errorFileName;
//响应日志目录
let responsePath = isDevEnv ? "/response" : '';
//响应日志文件名
let responseFileName = "response";
//响应日志输出完整路径
let responseLogPath = baseLogPath + responsePath + "/" + responseFileName;
module.exports = {
"appenders":
[
//错误日志 默认按小时数记录
{
"category": "errorLogger", //logger名称
"type": "dateFile", //日志类型
"filename": errorLogPath, //日志输出位置
"alwaysIncludePattern": true, //是否总是有后缀名
"pattern": "-yyyy-MM-dd.log", //后缀,每天创建一个新的日志文件
"path": errorPath //自定义属性,错误日志的根目录
},
//响应日志 响应日志默认按天记录
{
"category": "resLogger",
"type": "dateFile",
"filename": responseLogPath,
"alwaysIncludePattern": true,
"pattern": "-yyyy-MM-dd.log", //后缀,每天创建一个新的日志文件
"path": responsePath
}
],
"levels": //设置logger名称对应的的日志等级
{
"errorLogger": "ERROR",
"resLogger": "ALL"
},
"baseLogPath": baseLogPath //logs根目录
}这里是log4js的配置文件,记录日志类型,保存文件格式以及路径等信息
2、增加 logUtil.js ,代码如下:
let log4js = require('log4js');
let fs = require('fs');
import logConfig from 'config/logConfig';
import _ from 'lodash';
//加载配置文件
log4js.configure(logConfig);
let errorLogger = log4js.getLogger('errorLogger');
let resLogger = log4js.getLogger('resLogger');
let logUtil = {
initPath() {
if (logConfig.baseLogPath) {
confirmPath(logConfig.baseLogPath)
//根据不同的logType创建不同的文件目录
for (let i = 0; i < logConfig.appenders.length; i++) {
if (logConfig.appenders[i].path) {
confirmPath(logConfig.baseLogPath + logConfig.appenders[i].path);
}
}
}
},
logh5Error(req, error, resTime) {
if (req) {
errorLogger.error(formatError(req, error, 'h5', resTime));
}
},
logError(error, req, resTime) {
if (error) {
if (typeof (error) == "string") {
errorLogger.error('***** node server error *****', error);
} else {
errorLogger.error(formatError(req, error, 'node', resTime));
}
}
},
logResponse(ctx, resTime) {
if (ctx) {
resLogger.info(formatRes(ctx, resTime));
}
},
info(key, info) {
if (key) {
resLogger.info(key, info);
}
}
};
let confirmPath = function (pathStr) {
if (!fs.existsSync(pathStr)) {
fs.mkdirSync(pathStr);
// console.log('createPath: ' + pathStr);
}
}
//格式化响应日志
let formatRes = function (req, resTime) {
let logText = new String();
//响应日志开始
logText += "\n" + "*************** response log start ***************" + "\n";
//添加请求日志
logText += formatReqLog(req, resTime);
//响应状态码
logText += "response status: " + req.status + "\n";
//响应内容
logText += "response body: " + "\n" + JSON.stringify(req.body) + "\n";
//响应日志结束
logText += "*************** response log end ***************" + "\n";
return logText;
}
//格式化错误日志
let formatError = function (req = {}, error = {}, type = 'node server', resTime = 0) {
let logText = new String();
let err = type === 'h5' ? req.query : error;
//错误信息开始
logText += "\n" + "*************** " + type + " error log start ***************" + "\n";
//添加请求日志
if (!_.isEmpty(req)) {
logText += formatReqLog(req);
}
if (type === 'h5') {
//用户信息
if (err.userInfo) {
logText += "request user info: " + err.userInfo + "\n";
}
// 客户端渠道信息
if (err.pageParams) {
logText += "request client channel info: " + err.pageParams + "\n";
}
// 客户端设备信息
if (err.clientInfo) {
logText += "request mobile info: " + err.clientInfo + "\n";
}
//报错位置
logText += "err line: " + err.line + ", col: " + err.col + "\n";
//错误信息
logText += "err message: " + err.msg + "\n";
//错误页面
logText += "err url: " + err.url + "\n";
} else { // node server
//错误名称
logText += "err name: " + error.name + "\n";
//错误信息
logText += "err message: " + error.message + "\n";
//错误详情
logText += "err stack: " + error.stack + "\n";
}
//错误信息结束
logText += "*************** " + type + " error log end ***************" + "\n";
return logText;
};
//格式化请求日志
let formatReqLog = function (req) {
let logText = new String();
let method = req.method;
// 访问路径
logText += "request url: " + req.url + "\n";
//访问方法
logText += "request method: " + method + "\n";
//客户端ip
logText += "request client ip: " + req.ip + "\n";
return logText;
}
module.exports = logUtil;此处主要是创建日志输出目录,对日志格式进行重新编辑,并约定了前端和node端错误的格式
前端错误调用:logh5Error node端错误调用:logError
这里回到node接口调用时执行的方法,就能看明白了,
logUtil.logh5Error(req, logParams);
在app.js中对log4js进行初始化,主要代码:
//加载中间件
app.use(log4js.connectLogger(logger, {
level: 'auto',
format: ':method :url HTTP/:http-version :status [:res[content-length]]bytes :remote-addr :referrer :user-agent'
}));// 初始化日志目录 logUtil.initPath();
会在服务启动时在项目生成文件夹

来看看报错的效果吧:

参考文档:
1、前端代码异常监控