回归正题,我们该如何在程序异常发生时捕捉并进行对应的处理呢?在Javascript中,我们通常有以下两种异常捕获机制。
基本的try…catch语句
function errFunc() {
// eslint-disable-next-line no-undef
error;
}
function catchError() {
try {
this.errFunc();
} catch (error) {
console.log(error);
}
}
catchError()
复制代码
能捕捉到的异常,必须是线程执行已经进入 try catch 但 try catch 未执行完的时候抛出来的,以下都是无法被捕获到的情形。
异步任务抛出的异常(执行时try catch已经从执行完了)
promise(异常内部捕获到了,并未往上抛异常,使用catch处理)
语法错误(代码运行前,在编译时就检查出来了的错误)
优点:能够较好地进行异常捕获,不至于使得页面由于一处错误挂掉
缺点:显得过于臃肿,大多代码使用try ... catch
包裹,影响代码可读性。
面试官:请用一句话描述 try catch 能捕获到哪些 JS 异常
全局异常监听window.onerror
window.onerror
最大的好处就是同步任务、异步任务都可捕获,可以得到具体的异常信息、异常文件的URL、异常的行号与列号及异常的堆栈信息,再捕获异常后,统一上报至我们的日志服务器,而且可以全局监听,代码看起来也简洁很多。
此方法有一定的浏览器兼容性
跨域脚本无法准确捕获异常,跨域之后window.onerror
捕获不到正确的异常信息,而是统一返回一个Script error
,可通过在<script>
使用crossorigin
属性来规避这个问题
image.png
window.addEventListener(‘error’, function() {
console.log(error);
// …
// 异常上报
});
throw new Error(‘这是一个错误’);
复制代码
Promise内部异常
前文已经提到,onerror
以及 try-catch
也无法捕获Promise实例抛出的异常,只能最后在 catch 函数上处理,但是代码写多了就容易糊涂,忘记写 catch。
如果你的应用用到很多的 Promise 实例的话,特别是在一些基于 promise 的异步库比如 axios 等一定要小心,因为你不知道什么时候这些异步请求会抛出异常而你并没有处理它,所以最好添加一个 Promise 全局异常捕获事件 unhandledrejection
。
window.addEventListener(“unhandledrejection”, e => {
console.log(‘unhandledrejection’,e)
});
复制代码
vue工程异常
window.onerror
并不能捕获.vue文件发生的获取,Vue 2.2.0以上的版本中增加了一个errorHandle
,使用Vue.config.errorHandler
这样的Vue全局配置,可以在Vue指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和Vue 实例。
//main.js
import { createApp } from “vue”;
import App from “./App.vue”;
let app = createApp(App);
app.config.errorHandler = function(e) {
console.log(e);
//错误上报…
};
app.mount(“#app”);
复制代码
Vue项目JS脚本错误捕获
综上,可以将几种方式有效结合起来,笔者这里是在vue-cli框架中做的处理,其余类似:
import { createApp } from “vue”;
import App from “./App.vue”;
let app = createApp(App);
window.addEventListener(
“error”,
(e) => {
console.log(e);
//TODO:上报逻辑
return true;
},
true
);
// 处理未捕获的异常,主要是promise内部异常,统一抛给 onerror
window.addEventListener(“unhandledrejection”, (e) => {
throw e.reason;
});
// 框架异常统一捕获
app.config.errorHandler = function(err, vm, info) {
//TODO:上报逻辑
console.log(err, vm, info);
};
app.mount(“#app”);
复制代码
sourcemap
生产环境下所有的报错的代码行数都在第一行了,为什么呢?
通常在该环境下的代码是经过webpack打包后压缩混淆的代码,否则源代码泄漏易造成安全问题,在生产环境下,我们的代码被压缩成了一行。而保留了sourcemap文件就可以利用webpack打包后的生成的一份.map的脚本文件就可以让浏览器对错误位置进行追踪了,但这种做法并不可取,更为推荐的是在服务端使用Node.js对接收到的日志信息时使用source-map解析,以避免源代码的泄露造成风险
image.png
vue.config.js
配置里通过属性productionSourceMap: true
可以控制webpack是否生成map文件
webpack自定义插件实现sourcemap自动上传
为了我们每一次构建服务端能拿到最新的map文件,我们编写一个插件让webpack在打包完成后触发一个钩子实现文件上传,在vue.config.js
中进行配置
调整 webpack 配置
//vue.config.js
let SourceMapUploader = require(“./source-map-upload”);
module.exports = {
configureWebpack: {
resolve: {
alias: {
“@”: resolve(“src”),
},
},
plugins: [
new SourceMapUploader({url: “http://localhost:3000/upload”})
],
}
// chainWebpack: (config) => {},
}
复制代码
//source-map-upload.js
const fs = require(“fs”);
const http = require(“http”);
const path = require(“path”);
class SourceMapUploader {
constructor(options) {
this.options = options;
}
/**
* 用到了hooks,done表示在打包完成之后
* status.compilation.outputOptions就是打包的dist文件
*/
apply(compiler) {
if (process.env.NODE_ENV == “production”) {
compiler.hooks.done.tap(“sourcemap-uploader”, async (status) => {
// console.log(status.compilation.outputOptions.path);
// 读取目录下的map后缀的文件
let dir = path.join(status.compilation.outputOptions.path, “/js/”);
let chunks = fs.readdirSync(dir);
let map_file = chunks.filter((item) => {
return item.match(/.js.map$/) !== null;
});
// 上传sourcemap
while (map_file.length > 0) {
let file = map_file.shift();
await this.upload(this.options.url, path.join(dir, file));
}
});
}
}
//调用upload接口,上传文件
upload(url, file) {
return new Promise((resolve) => {
let req = http.request(${url}?name=${path.basename(file)}
, {
method: “POST”,
headers: {
“Content-Type”: “application/octet-stream”,
Connection: “keep-alive”,
},
});
let fileStream = fs.createReadStream(file);
fileStream.pipe(req, { end: false });
fileStream.on(“end”, function() {
req.end();
resolve();
});
});
}
}
module.exports = SourceMapUploader;
复制代码
错误上报
两种方式:
img标签 这种方式无需加载任何通讯库,而且页面是无需刷新的,相当于get请求,没有跨域问题。缺点是有url长度限制,但一般来讲足够使用了。
ajax 与正常的接口请求无异,可以用post
这里采用第一种,通过动态创建一个img,浏览器就会向服务器发送get请求。将需要上报的错误数据放在url中,利用这种方式就可以将错误上报到服务器了。
确定上报的内容,应该包含异常位置(行号,列号),异常信息,在错误堆栈中包含了绝大多数调试有关的信息,我们通讯的时候只能以字符串方式传输,我们需要将对象进行序列化处理。
将异常数据从属性中解构出来,存入一个JSON对象
将JSON对象转换为字符串
将字符串转换为Base64
后端接收到信息后进行对应的反向操作,就可以在日志中记录。
1621581164(1).png
function uploadErr({ lineno, colno, error: { stack }, message, filename }) {
let str = window.btoa(
JSON.stringify({
lineno,
colno,
error: { stack },
message,
filename,
})
);
let front_ip = “http://localhost:3000/error”;
new Image().src = ${front_ip}?info=${str}
;
}
复制代码
后端服务
用koa搭一个简单后台服务,代码比较简单,按功能拆开来讲
上传文件接口
文件流写入:
router.post(“/upload”, async (ctx) => {
const stream = ctx.req;
const filename = ctx.query.name;
let dir = path.join(__dirname, “source-map”);
//判断source文件夹是否存在
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
let target = path.join(dir, filename);
const ws = fs.createWriteStream(target);
stream.pipe(ws);
});
复制代码
错误日志
使用log4js
记录我们的错误日志,这个也是非常流行的日志插件了,直接贴代码。
log4js-node
const path = require(‘path’)
const log4js = require(‘log4js’);
log4js.configure({
appenders: {
info: {
type: “dateFile”,
filename: path.join(__dirname, ‘logs’, ‘info’, ‘info’),
pattern: “yyyy-MM-dd.log”,
encoding: ‘utf-8’,
alwaysIncludePattern: true,
},
error: {// 错误日志
type: ‘dateFile’,
filename: path.join(__dirname, ‘logs’, ‘error’, ‘error’),
pattern: ‘yyyy-MM-dd.log’,
encoding: ‘utf-8’,
alwaysIncludePattern: true
}
},
categories: {
default: { appenders: [‘info’], level: ‘info’ },
info: { appenders: [‘info’], level: ‘info’ },
error: { appenders: [‘error’], level: ‘error’ }
}
});
/**
* 错误日志记录方式
* @param {*} content 日志输出内容
*/
function logError(content) {
const log = log4js.getLogger(“error”);
log.error(content)
}
/**
* 日志记录方式
* @param {*} content 日志输出内容
*/
function logInfo(content) {
const log = log4js.getLogger(“info”);
log.info(content)
}
module.exports = {
logError,
logInfo
}
复制代码
错误解析
这个接口就是对上报的错误信息进行解析,得到错误堆栈对象 上面我们已经拿到colno为2319,lineno为1,接下来需要安装一个插件帮助我们找到对应压缩前的代码位置。
npm install source-map -S
复制代码
先读取对应的map文件(按filename对应),然后只需传入压缩后的报错行号列号即可,就会返回压缩前的错误信息。打个比喻:简单地说相当于一本书的目录,我们根据目录可以快速找到某一部分内容的页数
router.get(“/error”, async (ctx) => {
const errInfo = ctx.query.info;
// 转码 反序列化
let obj = JSON.parse(Buffer.from(errInfo, “base64”).toString(“utf-8”));
let fileUrl = obj.filename.split(“/”).pop() + “.map”; // map文件路径
// 解析sourceMap
// 1.sourcemap文件的文件流,我们已经上传
// 2.文件编码格式
let consumer = await new sourceMap.SourceMapConsumer(
fs.readFileSync(path.join(__dirname, “source-map/” + fileUrl), “utf8”)
);
// 解析原始报错数据
let result = consumer.originalPositionFor({
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后
整理面试题,不是让大家去只刷面试题,而是熟悉目前实际面试中常见的考察方式和知识点,做到心中有数,也可以用来自查及完善知识体系。
《前端基础面试题》,《前端校招面试题精编解析大全》,《前端面试题宝典》,《前端面试题:常用算法》PDF完整版点击这里免费领取
整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
[外链图片转存中…(img-ywKIljCp-1712072057155)]
[外链图片转存中…(img-rhYax20K-1712072057156)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-vgRqAlQy-1712072057156)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后
整理面试题,不是让大家去只刷面试题,而是熟悉目前实际面试中常见的考察方式和知识点,做到心中有数,也可以用来自查及完善知识体系。
《前端基础面试题》,《前端校招面试题精编解析大全》,《前端面试题宝典》,《前端面试题:常用算法》PDF完整版点击这里免费领取
[外链图片转存中…(img-u1xDotZ0-1712072057156)]
[外链图片转存中…(img-sB6Fz40O-1712072057156)]
[外链图片转存中…(img-ckRYZdIg-1712072057157)]