澳门新浦京娱乐场网站-www.146.net-新浦京娱乐场官网
做最好的网站

获得百度网盘大小文件真实下载地址,前端品质

前端性能与异常上报

2018/08/22 · 基础技术 · 性能

原文出处: counterxing   

原文出处: happylindz   

说明:本文章仅供学习交流使用 如有侵权 立刻删除 

1, HTTP 协议 和 AJAX
2, DOM 事件机制, 事件冒泡和事件捕获

来看看机智的前端童鞋怎么防盗

2016/07/12 · JavaScript · 4 评论 · HTML5

原文出处: VaJoy   

很多开发的童鞋都是只身混江湖、夜宿城中村,如果居住的地方安保欠缺,那么出门在外难免担心屋里的财产安全。

事实上世面上有很多高大上的防盗设备,但对于机智的前端童鞋来说,只要有一台附带摄像头的电脑,就可以简单地实现一个防盗监控系统~

纯 JS 的“防盗”能力很大程度借助于 H5 canvas 的力量,且非常有意思。如果你对 canvas 还不熟悉,可以先点这里阅读我的系列教程。

step1. 调用摄像头

我们需要先在浏览器上访问和调用摄像头,用来监控屋子里的一举一动。不同浏览器中调用摄像头的 API 都略有出入,在这里我们以 chrome 做示例:

JavaScript

<video width="640" height="480" autoplay></video> <script> var video = document.querySelector('video'); navigator.webkitGetUserMedia({ video: true }, success, error); function success(stream) { video.src = window.webkitURL.createObjectURL(stream); video.play(); } function error(err) { alert('video error: ' err) } </script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<video width="640" height="480" autoplay></video>
 
<script>
    var video = document.querySelector('video');
 
    navigator.webkitGetUserMedia({
                video: true
            }, success, error);
 
    function success(stream) {
        video.src = window.webkitURL.createObjectURL(stream);
        video.play();
    }
 
    function error(err) {
        alert('video error: ' err)
    }
</script>

运行页面后,浏览器出于安全性考虑,会询问是否允许当前页面访问你的摄像头设备,点击“允许”后便能直接在 <video> 上看到摄像头捕获到的画面了:

图片 1

step2. 捕获 video 帧画面

光是开着摄像头监视房间可没有任何意义,浏览器不会帮你对监控画面进行分析。所以这里我们得手动用脚本捕获 video 上的帧画面,用于在后续进行数据分析。

从这里开始咱们就要借助 canvas 力量了。在 Canvas入门(五)一文我们介绍过 ctx.drawImage() 方法,通过它可以捕获 video 帧画面并渲染到画布上。

我们需要创建一个画布,然后这么写:

JavaScript

<video width="640" height="480" autoplay></video> <canvas width="640" height="480"></canvas> <script> var video = document.querySelector('video'); var canvas = document.querySelector('canvas'); // video捕获摄像头画面 navigator.webkitGetUserMedia({ video: true }, success, error); function success(stream) { video.src = window.webkitURL.createObjectURL(stream); video.play(); } function error(err) { alert('video error: ' err) } //canvas var context = canvas.getContext('2d'); setTimeout(function(){ //把当前视频帧内容渲染到画布上 context.drawImage(video, 0, 0, 640, 480); }, 5000); </script>

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
<video width="640" height="480" autoplay></video>
<canvas width="640" height="480"></canvas>
 
<script>
    var video = document.querySelector('video');
    var canvas = document.querySelector('canvas');
 
    // video捕获摄像头画面
    navigator.webkitGetUserMedia({
                video: true
            }, success, error);
 
    function success(stream) {
        video.src = window.webkitURL.createObjectURL(stream);
        video.play();
    }
 
    function error(err) {
        alert('video error: ' err)
    }
 
    //canvas
    var context = canvas.getContext('2d');
 
    setTimeout(function(){
        //把当前视频帧内容渲染到画布上
        context.drawImage(video, 0, 0, 640, 480);
    }, 5000);
 
</script>

如上代码所示,5秒后把视频帧内容渲染到画布上(下方右图)

图片 2

step3. 对捕获的两个帧画面执行差异混合

在上面我们提到过,要有效地识别某个场景,需要对视频画面进行数据分析。

那么要怎么识别咱们的房子是否有人突然闯入了呢?答案很简单 —— 定时地捕获 video 画面,然后对比前后两帧内容是否存在较大变化。

我们先简单地写一个定时捕获的方法,并将捕获到的帧数据存起来:

JavaScript

//canvas var context = canvas.getContext('2d'); var preFrame, //前一帧 curFrame; //当前帧 //捕获并保存帧内容 function captureAndSaveFrame(){ console.log(context); preFrame = curFrame; context.drawImage(video, 0, 0, 640, 480); curFrame = canvas.toDataURL; //转为base64并保存 } //定时捕获 function timer(delta){ setTimeout(function(){ captureAndSaveFrame(); timer(delta) }, delta || 500); } timer();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    //canvas
    var context = canvas.getContext('2d');
    var preFrame,   //前一帧
        curFrame;   //当前帧
 
    //捕获并保存帧内容
    function captureAndSaveFrame(){ console.log(context);
        preFrame = curFrame;
        context.drawImage(video, 0, 0, 640, 480);
        curFrame = canvas.toDataURL;  //转为base64并保存
    }
 
    //定时捕获
    function timer(delta){
        setTimeout(function(){
            captureAndSaveFrame();
            timer(delta)
        }, delta || 500);
    }
 
    timer();

如上代码所示,画布会每隔500毫秒捕获并渲染一次 video 的帧内容(夭寿哇,做完这个动作不小心把饼干洒了一地。。。(“▔□▔)/)

图片 3

留意这里我们使用了 canvas.toDataURL 方法来保存帧画面。

接着就是数据分析处理了,我们可以通过对比前后捕获的帧画面来判断摄像头是否监控到变化,那么怎么做呢?

熟悉设计的同学肯定常常使用一个图层功能 —— 混合模式:

图片 4

当有两个图层时,对顶层图层设置“差值/Difference”的混合模式,可以一目了然地看到两个图层的差异:

图片 5

“图A”是我去年在公司楼下拍的照片,然后我把它稍微调亮了一点点,并在上面画了一个 X 和 O 得到“图B”。接着我把它们以“差值”模式混合在一起,得到了最右的这张图。

JavaScript

“差值”模式原理:要混合图层双方的RGB值中每个值分别进行比较,用高值减去低值作为合成后的颜色,通常用白色图层合成一图像时,可以得到负片效果的反相图像。用黑色的话不发生任何变化(黑色亮度最低,下层颜色减去最小颜色值0,结果和原来一样),而用白色会得到反相效果(下层颜色被减去,得到补值),其它颜色则基于它们的亮度水平

1
“差值”模式原理:要混合图层双方的RGB值中每个值分别进行比较,用高值减去低值作为合成后的颜色,通常用白色图层合成一图像时,可以得到负片效果的反相图像。用黑色的话不发生任何变化(黑色亮度最低,下层颜色减去最小颜色值0,结果和原来一样),而用白色会得到反相效果(下层颜色被减去,得到补值),其它颜色则基于它们的亮度水平

在CSS3中,已经有 blend-mode 特性来支持这个有趣的混合模式,不过我们发现,在主流浏览器上,canvas 的 globalCompositeOperation 接口也已经良好支持了图像混合模式:

于是我们再建多一个画布来展示前后两帧差异:

JavaScript

<video width="640" height="480" autoplay></video> <canvas width="640" height="480"></canvas> <canvas width="640" height="480"></canvas> <script> var video = document.querySelector('video'); var canvas = document.querySelectorAll('canvas')[0]; var canvasForDiff = document.querySelectorAll('canvas')[1]; // video捕获摄像头画面 navigator.webkitGetUserMedia({ video: true }, success, error); function success(stream) { video.src = window.URL.createObjectURL(stream); video.play(); } function error(err) { alert('video error: ' err) } //canvas var context = canvas.getContext('2d'), diffCtx = canvasForDiff.getContext('2d'); //将第二个画布混合模式设为“差异” diffCtx.globalCompositeOperation = 'difference'; var preFrame, //前一帧 curFrame; //当前帧 //捕获并保存帧内容 function captureAndSaveFrame(){ preFrame = curFrame; context.drawImage(video, 0, 0, 640, 480); curFrame = canvas.toDataURL(); //转为base64并保存 } //绘制base64图像到画布上 function drawImg(src, ctx){ ctx = ctx || diffCtx; var img = new Image(); img.src = src; ctx.drawImage(img, 0, 0, 640, 480); } //渲染前后两帧差异 function renderDiff(){ if(!preFrame || !curFrame) return; diffCtx.clearRect(0, 0, 640, 480); drawImg(preFrame); drawImg(curFrame); } //定时捕获 function timer(delta){ setTimeout(function(){ captureAndSaveFrame(); renderDiff(); timer(delta) }, delta || 500); } timer(); </script>

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<video width="640" height="480" autoplay></video>
<canvas width="640" height="480"></canvas>
<canvas width="640" height="480"></canvas>
 
<script>
    var video = document.querySelector('video');
    var canvas = document.querySelectorAll('canvas')[0];
    var canvasForDiff = document.querySelectorAll('canvas')[1];
 
    // video捕获摄像头画面
    navigator.webkitGetUserMedia({
                video: true
            }, success, error);
 
    function success(stream) {
        video.src = window.URL.createObjectURL(stream);
        video.play();
    }
 
    function error(err) {
        alert('video error: ' err)
    }
 
    //canvas
    var context = canvas.getContext('2d'),
        diffCtx = canvasForDiff.getContext('2d');
    //将第二个画布混合模式设为“差异”
    diffCtx.globalCompositeOperation = 'difference';
 
    var preFrame,   //前一帧
        curFrame;   //当前帧
 
    //捕获并保存帧内容
    function captureAndSaveFrame(){
        preFrame = curFrame;
        context.drawImage(video, 0, 0, 640, 480);
        curFrame = canvas.toDataURL();  //转为base64并保存
    }
 
    //绘制base64图像到画布上
    function drawImg(src, ctx){
        ctx = ctx || diffCtx;
        var img = new Image();
        img.src = src;
        ctx.drawImage(img, 0, 0, 640, 480);
    }
 
    //渲染前后两帧差异
    function renderDiff(){
        if(!preFrame || !curFrame) return;
        diffCtx.clearRect(0, 0, 640, 480);
        drawImg(preFrame);
        drawImg(curFrame);
    }
 
    //定时捕获
    function timer(delta){
        setTimeout(function(){
            captureAndSaveFrame();
            renderDiff();
            timer(delta)
        }, delta || 500);
    }
 
    timer();
 
</script>

效果如下(夭寿啊,做完这个动作我又把雪碧洒在键盘上了。。。(#--)/ )

图片 6

可以看到,当前后两帧差异不大时,第三个画布几乎是黑乎乎的一片,只有当摄像头捕获到动作了,第三个画布才有明显的高亮内容出现。

因此,我们只需要对第三个画布渲染后的图像进行像素分析——判断其高亮阈值是否达到某个指定预期:

JavaScript

var context = canvas.getContext('2d'), diffCtx = canvasForDiff.getContext('2d'); //将第二个画布混合模式设为“差异” diffCtx.globalCompositeOperation = 'difference'; var preFrame, //前一帧 curFrame; //当前帧 var diffFrame; //存放差异帧的imageData //捕获并保存帧内容 function captureAndSaveFrame(){ preFrame = curFrame; context.drawImage(video, 0, 0, 640, 480); curFrame = canvas.toDataURL(); //转为base64并保存 } //绘制base64图像到画布上 function drawImg(src, ctx){ ctx = ctx || diffCtx; var img = new Image(); img.src = src; ctx.drawImage(img, 0, 0, 640, 480); } //渲染前后两帧差异 function renderDiff(){ if(!preFrame || !curFrame) return; diffCtx.clearRect(0, 0, 640, 480); drawImg(preFrame); drawImg(curFrame); diffFrame = diffCtx.getImageData( 0, 0, 640, 480 ); //捕获差异帧的imageData对象 } //计算差异 function calcDiff(){ if(!diffFrame) return 0; var cache = arguments.callee, count = 0; cache.total = cache.total || 0; //整个画布都是白色时所有像素的值的总和 for (var i = 0, l = diffFrame.width * diffFrame.height * 4; i < l; i = 4) { count = diffFrame.data[i] diffFrame.data[i 1] diffFrame.data[i 2]; if(!cache.isLoopEver){ //只需在第一次循环里执行 cache.total = 255 * 3; //单个白色像素值 } } cache.isLoopEver = true; count *= 3; //亮度放大 //返回“差异画布高亮部分像素总值”占“画布全亮情况像素总值”的比例 return Number(count/cache.total).toFixed(2); } //定时捕获 function timer(delta){ setTimeout(function(){ captureAndSaveFrame(); renderDiff(); setTimeout(function(){ console.log(calcDiff()); }, 10); timer(delta) }, delta || 500); } timer();

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
    var context = canvas.getContext('2d'),
        diffCtx = canvasForDiff.getContext('2d');
    //将第二个画布混合模式设为“差异”
    diffCtx.globalCompositeOperation = 'difference';
 
    var preFrame,   //前一帧
        curFrame;   //当前帧
 
    var diffFrame;  //存放差异帧的imageData
 
    //捕获并保存帧内容
    function captureAndSaveFrame(){
        preFrame = curFrame;
        context.drawImage(video, 0, 0, 640, 480);
        curFrame = canvas.toDataURL();  //转为base64并保存
    }
 
    //绘制base64图像到画布上
    function drawImg(src, ctx){
        ctx = ctx || diffCtx;
        var img = new Image();
        img.src = src;
        ctx.drawImage(img, 0, 0, 640, 480);
    }
 
    //渲染前后两帧差异
    function renderDiff(){
        if(!preFrame || !curFrame) return;
        diffCtx.clearRect(0, 0, 640, 480);
        drawImg(preFrame);
        drawImg(curFrame);
        diffFrame = diffCtx.getImageData( 0, 0, 640, 480 );  //捕获差异帧的imageData对象
    }
 
    //计算差异
    function calcDiff(){
        if(!diffFrame) return 0;
        var cache = arguments.callee,
            count = 0;
        cache.total = cache.total || 0; //整个画布都是白色时所有像素的值的总和
        for (var i = 0, l = diffFrame.width * diffFrame.height * 4; i < l; i = 4) {
            count = diffFrame.data[i] diffFrame.data[i 1] diffFrame.data[i 2];
            if(!cache.isLoopEver){  //只需在第一次循环里执行
                cache.total = 255 * 3;   //单个白色像素值
            }
        }
        cache.isLoopEver = true;
        count *= 3;  //亮度放大
        //返回“差异画布高亮部分像素总值”占“画布全亮情况像素总值”的比例
        return Number(count/cache.total).toFixed(2);
    }
 
    //定时捕获
    function timer(delta){
        setTimeout(function(){
            captureAndSaveFrame();
            renderDiff();
            setTimeout(function(){
                console.log(calcDiff());
            }, 10);
 
            timer(delta)
        }, delta || 500);
    }
 
    timer();

注意这里我们使用了 count *= 3 来放大差异高亮像素的亮度值,不然得出的数值实在太小了。我们运行下页面(图片较大加载会有点慢)

图片 7

经过试(xia)验(bai),个人觉得如果 calcDiff() 返回的比值如果大于 0.20,那么就可以定性为“一间空屋子,突然有人闯进来”的情况了。

step4. 上报异常图片

当上述的计算发现有状况时,需要有某种途径通知我们。有钱有精力的话可以部署个邮件服务器,直接发邮件甚至短信通知到自己,but 本文走的吃吐少年路线,就不搞的那么高端了。

那么要如何简单地实现异常图片的上报呢?我暂且想到的是 —— 直接把问题图片发送到某个站点中去。

这里我们选择博客园的“日记”功能,它可以随意上传相关内容。

JavaScript

p.s.,其实这里原本是想直接把图片传到博客园相册上的,可惜POST请求的图片实体要求走 file 格式,即无法通过脚本更改文件的 input[type=file],转 Blob 再上传也没用,只好作罢。

1
p.s.,其实这里原本是想直接把图片传到博客园相册上的,可惜POST请求的图片实体要求走 file 格式,即无法通过脚本更改文件的 input[type=file],转 Blob 再上传也没用,只好作罢。

我们在管理后台创建日记时,通过 Fiddler 抓包可以看到其请求参数非常简单:

图片 8

从而可以直接构造一个请求:

JavaScript

//异常图片上传处理 function submit(){ //ajax 提交form $.ajax({ url : '', type : "POST", data : { '__VIEWSTATE': '', '__VIEWSTATEGENERATOR': '4773056F', 'Editor$Edit$txbTitle': '告警' Date.now(), 'Editor$Edit$EditorBody': '<img src="' curFrame '" />', 'Editor$Edit$lkbPost': '保存' }, success: function(){ console.log('submit done') } }); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    //异常图片上传处理
    function submit(){
 
        //ajax 提交form
        $.ajax({
            url : 'http://i.cnblogs.com/EditDiary.aspx?opt=1',
            type : "POST",
            data : {
                '__VIEWSTATE': '',
                '__VIEWSTATEGENERATOR': '4773056F',
                'Editor$Edit$txbTitle': '告警' Date.now(),
                'Editor$Edit$EditorBody': '<img src="' curFrame '" />',
                'Editor$Edit$lkbPost': '保存'
            },
            success: function(){
                console.log('submit done')
            }
        });
    }

当然如果请求页面跟博客园域名不同,是无法发送 cookie 导致请求跨域而失效,不过这个很好解决,直接修改 host 即可(怎么修改就不介绍了,自行百度吧)

我这边改完 host,通过  的地址访问页面,发现摄像头竟然失效了~

通过谷歌的文档可以得知,这是为了安全性考虑,非 HTTPS 的服务端请求都不能接入摄像头。不过解决办法也是有的,以 window 系统为例,打开 cmd 命令行面板并定位到 chrome 安装文件夹下,然后执行:

ZSH

chrome --unsafely-treat-insecure-origin-as-secure="" --user-data-dir=C:testprofile

1
chrome --unsafely-treat-insecure-origin-as-secure="http://i.cnblogs.com/h5monitor/final.html"  --user-data-dir=C:testprofile

此举将以沙箱模式打开一个独立的 chrome 进程,并对指定的站点去掉安全限制。注意咱们在新开的 chrome 中得重新登录博客园。

这时候便能正常访问摄像头了,我们对代码做下处理,当差异检测发现异常时,创建一份日记,最小间隔时间为5秒(不过后来发现没必要,因为博客园已经有做了时间限制,差不多10秒后才能发布新的日记)

JavaScript

//定时捕获 function timer(delta){ setTimeout(function(){ captureAndSaveFrame(); renderDiff(); if(calcDiff() > 0.2){ //监控到异常,发日志 submit() } timer(delta) }, delta || 500); } setTimeout(timer, 60000 * 10); //设定打开页面十分钟后才开始监控 //异常图片上传处理 function submit(){ var cache = arguments.callee, now = Date.now(); if(cache.reqTime && (now - cache.reqTime < 5000)) return; //日记创建最小间隔为5秒 cache.reqTime = now; //ajax 提交form $.ajax({ url : '', type : "POST", timeout : 5000, data : { '__VIEWSTATE': '', '__VIEWSTATEGENERATOR': '4773056F', 'Editor$Edit$txbTitle': '告警' Date.now(), 'Editor$Edit$EditorBody': '<img src="' curFrame '" />', 'Editor$Edit$lkbPost': '保存' }, success: function(){ console.log('submit done') }, error: function(err){ cache.reqTime = 0; console.log('error: ' err) } }); }

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
36
37
38
39
40
41
42
43
44
45
    //定时捕获
    function timer(delta){
        setTimeout(function(){
            captureAndSaveFrame();
            renderDiff();
            if(calcDiff() > 0.2){  //监控到异常,发日志
                submit()
            }
 
            timer(delta)
        }, delta || 500);
    }
 
    setTimeout(timer, 60000 * 10);  //设定打开页面十分钟后才开始监控
 
 
    //异常图片上传处理
    function submit(){
        var cache = arguments.callee,
            now = Date.now();
        if(cache.reqTime && (now - cache.reqTime < 5000)) return;  //日记创建最小间隔为5秒
 
        cache.reqTime = now;
 
        //ajax 提交form
        $.ajax({
            url : 'http://i.cnblogs.com/EditDiary.aspx?opt=1',
            type : "POST",
            timeout : 5000,
            data : {
                '__VIEWSTATE': '',
                '__VIEWSTATEGENERATOR': '4773056F',
                'Editor$Edit$txbTitle': '告警' Date.now(),
                'Editor$Edit$EditorBody': '<img src="' curFrame '" />',
                'Editor$Edit$lkbPost': '保存'
            },
            success: function(){
                console.log('submit done')
            },
            error: function(err){
                cache.reqTime = 0;
                console.log('error: ' err)
            }
        });
    }

执行效果:

图片 9

日记也是妥妥的出来了:

图片 10

点开就能看到异常的那张图片了:

图片 11

要留意的是,博客园对日记发布数量是有做每日额度限制来防刷的,达到限额的话会导致当天的随笔和文章也无法发布,所以得谨慎使用:

图片 12

不过这种形式仅能上报异常图片,暂时无法让我们及时收悉告警,有兴趣的童鞋可以试着再写个 chrome 插件,定时去拉取日记列表做判断,如果有新增日记则触发页面 alert。

另外我们当然希望能直接对闯入者进行警告,这块比较好办 —— 搞个警示的音频,在异常的时候触发播放即可:

JavaScript

//播放音频 function fireAlarm(){ audio.play() } //定时捕获 function timer(delta){ setTimeout(function(){ captureAndSaveFrame(); if(preFrame && curFrame){ renderDiff(); if(calcDiff() > 0.2){ //监控到异常 //发日记 submit(); //播放音频告警 fireAlarm(); } } timer(delta) }, delta || 500); } setTimeout(timer, 60000 * 10); //设定打开页面十分钟后才开始监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    //播放音频
    function fireAlarm(){
        audio.play()
    }
 
 
    //定时捕获
    function timer(delta){
        setTimeout(function(){
            captureAndSaveFrame();
            if(preFrame && curFrame){
                renderDiff();
                if(calcDiff() > 0.2){  //监控到异常
                    //发日记
                    submit();
                    //播放音频告警
                    fireAlarm();
                }
            }
            timer(delta)
        }, delta || 500);
    }
 
    setTimeout(timer, 60000 * 10);  //设定打开页面十分钟后才开始监控

最后说一下,本文代码均挂在我的github上,有兴趣的童鞋可以自助下载。共勉~

1 赞 4 收藏 4 评论

图片 13

概述

对于后台开发来说,记录日志是一种非常常见的开发习惯,通常我们会使用try...catch代码块来主动捕获错误、对于每次接口调用,也会记录下每次接口调用的时间消耗,以便我们监控服务器接口性能,进行问题排查。

刚进公司时,在进行Node.js的接口开发时,我不太习惯每次排查问题都要通过跳板机登上服务器看日志,后来慢慢习惯了这种方式。

举个例子:

JavaScript

/** * 获取列表数据 * @parma req, res */ exports.getList = async function (req, res) { //获取请求参数 const openId = req.session.userinfo.openId; logger.info(`获得百度网盘大小文件真实下载地址,前端品质与极度申报。handler getList, user openId is ${openId}`); try { // 拿到列表数据 const startTime = new Date().getTime(); let res = await ListService.getListFromDB(openId); logger.info(`handler getList, ListService.getListFromDB cost time ${new Date().getTime() - startDate}`); // 对数据处理,返回给前端 // ... } catch(error) { logger.error(`handler getList is error, ${JSON.stringify(error)}`); } };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 获取列表数据
* @parma req, res
*/
exports.getList = async function (req, res) {
    //获取请求参数
    const openId = req.session.userinfo.openId;
    logger.info(`handler getList, user openId is ${openId}`);
 
    try {
        // 拿到列表数据
        const startTime = new Date().getTime();
        let res = await ListService.getListFromDB(openId);
        logger.info(`handler getList, ListService.getListFromDB cost time ${new Date().getTime() - startDate}`);
        // 对数据处理,返回给前端
        // ...
    } catch(error) {
        logger.error(`handler getList is error, ${JSON.stringify(error)}`);
    }
};

以下代码经常会出现在用Node.js的接口中,在接口中会统计查询DB所耗时间、亦或是统计RPC服务调用所耗时间,以便监测性能瓶颈,对性能做优化;又或是对异常使用try ... catch主动捕获,以便随时对问题进行回溯、还原问题的场景,进行bug的修复。

而对于前端来说呢?可以看以下的场景。

最近在进行一个需求开发时,偶尔发现webgl渲染影像失败的情况,或者说影像会出现解析失败的情况,我们可能根本不知道哪张影像会解析或渲染失败;又或如最近开发的另外一个需求,我们会做一个关于webgl渲染时间的优化和影像预加载的需求,如果缺乏性能监控,该如何统计所做的渲染优化和影像预加载优化的优化比例,如何证明自己所做的事情具有价值呢?可能是通过测试同学的黑盒测试,对优化前后的时间进行录屏,分析从进入页面到影像渲染完成到底经过了多少帧图像。这样的数据,可能既不准确、又较为片面,设想测试同学并不是真正的用户,也无法还原真实的用户他们所处的网络环境。回过头来发现,我们的项目,虽然在服务端层面做好了日志和性能统计,但在前端对异常的监控和性能的统计。对于前端的性能与异常上报的可行性探索是有必要的。

前言

之前在对公司的前端代码脚本错误进行排查,试图降低 JS Error 的错误量,结合自己之前的经验对这方面内容进行了实践并总结,下面就此谈谈我对前端代码异常监控的一些见解。

本文大致围绕下面几点展开讨论:

  1. JS 处理异常的方式
  2. 上报方式
  3. 异常监控上报常见问题

【原创内容】转载请注明出处!

  • 网址组成(四部分)
    协议 http, https(https 是加密的 http)
    主机 g.cn zhihu.com之类的网址
    端口 HTTP 协议默认是 80,因此一般不用填写
    路径 下面的「/」和「/question/31838184」都是路径
    http://www.zhihu.com:80/
    https://www.zhihu.com/question/31838184

异常捕获

对于前端来说,我们需要的异常捕获无非为以下两种:

  • 接口调用情况;
  • 页面逻辑是否错误,例如,用户进入页面后页面显示白屏;

对于接口调用情况,在前端通常需要上报客户端相关参数,例如:用户OS与浏览器版本、请求参数(如页面ID);而对于页面逻辑是否错误问题,通常除了用户OS与浏览器版本外,需要的是报错的堆栈信息及具体报错位置。

JS 异常处理

对于 Javascript 而言,我们面对的仅仅只是异常,异常的出现不会直接导致 JS 引擎崩溃,最多只会使当前执行的任务终止。

  1. 当前代码块将作为一个任务压入任务队列中,JS 线程会不断地从任务队列中提取任务执行。
  2. 当任务执行过程中出现异常,且异常没有捕获处理,则会一直沿着调用栈一层层向外抛出,最终终止当前任务的执行。
  3. JS 线程会继续从任务队列中提取下一个任务继续执行。
JavaScript

&lt;script&gt; error console.log('永远不会执行'); &lt;/script&gt;
&lt;script&gt; console.log('我继续执行') &lt;/script&gt;

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5a707ba987416418324373-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5a707ba987416418324373-2">
2
</div>
<div class="crayon-num" data-line="crayon-5a707ba987416418324373-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5a707ba987416418324373-4">
4
</div>
<div class="crayon-num" data-line="crayon-5a707ba987416418324373-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5a707ba987416418324373-6">
6
</div>
<div class="crayon-num" data-line="crayon-5a707ba987416418324373-7">
7
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5a707ba987416418324373-1" class="crayon-line">
&lt;script&gt;
</div>
<div id="crayon-5a707ba987416418324373-2" class="crayon-line crayon-striped-line">
  error
</div>
<div id="crayon-5a707ba987416418324373-3" class="crayon-line">
  console.log('永远不会执行');
</div>
<div id="crayon-5a707ba987416418324373-4" class="crayon-line crayon-striped-line">
&lt;/script&gt;
</div>
<div id="crayon-5a707ba987416418324373-5" class="crayon-line">
&lt;script&gt;
</div>
<div id="crayon-5a707ba987416418324373-6" class="crayon-line crayon-striped-line">
  console.log('我继续执行')
</div>
<div id="crayon-5a707ba987416418324373-7" class="crayon-line">
&lt;/script&gt;
</div>
</div></td>
</tr>
</tbody>
</table>

图片 14

在对脚本错误进行上报之前,我们需要对异常进行处理,程序需要先感知到脚本错误的发生,然后再谈异常上报。

脚本错误一般分为两种:语法错误,运行时错误。

下面就谈谈几种异常监控的处理方式:

注:

异常捕获方法

try-catch 异常处理

try-catch 在我们的代码中经常见到,通过给代码块进行 try-catch 进行包装后,当代码块发生出错时 catch 将能捕捉到错误的信息,页面也将可以继续执行。

但是 try-catch 处理异常的能力有限,只能捕获捉到运行时非异步错误,对于语法错误和异步错误就显得无能为力,捕捉不到。

1 文件夹暂时不支持直接下载,可以进入文件夹,调用列表打包函数进行下载   downFilelist()  无任何参数

  • 电脑通信靠IP地址,IP地址记不住就发明了域名(domain name)
    然后电脑自动向DNS服务器(domain name server)查询域名
    对应的IP地址

  • 比如g.cn这样的网址,可以通过电脑的ping程序查出对应 IP 地址
    ➜ ping g.cn
    PING g.cn (74.125.69.160): 56 data bytes

全局捕获

可以通过全局监听异常来捕获,通过window.onerror或者addEventListener,看以下例子:

JavaScript

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) { console.log('errorMessage: ' errorMessage); // 异常信息 console.log('scriptURI: ' scriptURI); // 异常文件路径 console.log('lineNo: ' lineNo); // 异常行号 console.log('columnNo: ' columnNo); // 异常列号 console.log('error: ' error); // 异常堆栈信息 // ... // 异常上报 }; throw new Error('这是一个错误');

1
2
3
4
5
6
7
8
9
10
window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
  console.log('errorMessage: ' errorMessage); // 异常信息
  console.log('scriptURI: ' scriptURI); // 异常文件路径
  console.log('lineNo: ' lineNo); // 异常行号
  console.log('columnNo: ' columnNo); // 异常列号
  console.log('error: ' error); // 异常堆栈信息
  // ...
  // 异常上报
};
throw new Error('这是一个错误');

图片 15

通过window.onerror事件,可以得到具体的异常信息、异常文件的URL、异常的行号与列号及异常的堆栈信息,再捕获异常后,统一上报至我们的日志服务器。

亦或是,通过window.addEventListener方法来进行异常上报,道理同理:

JavaScript

window.addEventListener('error', function() { console.log(error); // ... // 异常上报 }); throw new Error('这是一个错误');

1
2
3
4
5
6
window.addEventListener('error', function() {
  console.log(error);
  // ...
  // 异常上报
});
throw new Error('这是一个错误');

图片 16

示例:运行时错误

JavaScript

try { error // 未定义变量 } catch(e) { console.log('我知道错误了'); console.log(e); }

1
2
3
4
5
6
try {
  error    // 未定义变量
} catch(e) {
  console.log('我知道错误了');
  console.log(e);
}

图片 17

然而对于语法错误和异步错误就捕捉不到了。

2 必须先进入自己的网盘 再下载   如果是别人的 要先保存到自己的网盘 再进行下载

try… catch

使用try... catch虽然能够较好地进行异常捕获,不至于使得页面由于一处错误挂掉,但try ... catch捕获方式显得过于臃肿,大多代码使用try ... catch包裹,影响代码可读性。

示例:语法错误

JavaScript

try { var error = 'error'; // 大写分号 } catch(e) { console.log('我感知不到错误'); console.log(e); }

1
2
3
4
5
6
try {
  var error = 'error';   // 大写分号
} catch(e) {
  console.log('我感知不到错误');
  console.log(e);
}

图片 18

一般语法错误在编辑器就会体现出来,常表现的错误信息为: Uncaught SyntaxError: Invalid or unexpected token xxx 这样。但是这种错误会直接抛出异常,常使程序崩溃,一般在编码时候容易观察得到。

代码开始

  • 端口是什么?
    一个比喻:
    用邮局互相写信的时候,ip相当于地址(也可以看做邮编,地址是域名)
    端口是收信人姓名(因为一个地址比如公司、家只有一个地址,但是却可能有很多收信人)
    端口就是一个标记收信人的数字。
    端口是一个 16 位的数字,所以范围是 0-65535(2**16)
    0000 0000 0000 0000
    1111 1111 1111 1111

常见问题

示例:异步错误

JavaScript

try { setTimeout(() => { error // 异步错误 }) } catch(e) { console.log('我感知不到错误'); console.log(e); }

1
2
3
4
5
6
7
8
try {
  setTimeout(() => {
    error        // 异步错误
  })
} catch(e) {
  console.log('我感知不到错误');
  console.log(e);
}

图片 19

除非你在 setTimeout 函数中再套上一层 try-catch,否则就无法感知到其错误,但这样代码写起来比较啰嗦。

var s5 = '' yunData.sign5;
var s1 = '' yunData.sign1;
var t1 = s(s5,s1);
var SIGN = base64Encode(t1);
SIGN = encodeURIComponent(SIGN);

HTTP协议

  • 一个传输协议,协议就是双方都遵守的规范。
    为什么叫超文本传输协议呢,因为收发的是文本信息。
    1,浏览器(客户端)按照规定的格式发送文本数据(请求)到服务器
    2,服务器解析请求,按照规定的格式返回文本数据到浏览器
    3,浏览器解析得到的数据,并做相应处理

  • 请求和返回是一样的数据格式,分为4部分:
    1,请求行或者响应行
    2,Header(请求的 Header 中 Host 字段是必须的,其他都是可选)
    3,rnrn(连续两个换行回车符,用来分隔Header和Body)
    4,Body(可选)

浏览器访问这个网址的时候
https://list.jd.com/list.html?cat=670,677,11303&go=0
实际上发的请求如下
请求的格式,注意大小写(这是一个不包含Body的请求):
原始数据如下
'GET /list.html?cat=670,677,11303&go=0 HTTP/1.1rnhost:list.jd.comrnrn'
打印出来如下
GET /list.html?cat=670,677,11303&go=0 HTTP/1.1
Host: list.jd.com
Gua: hello

gua=1

其中
1, GET 是请求方法(还有POST等,这就是个标志字符串而已)
2,/ 是请求的路径(这代表根路径)
3,HTTP/1.1 中,1.1是版本号,通用了20年

具体字符串是 'GET / HTTP/1.1rnhost:g.cnrnrn'

返回的数据如下
HTTP/1.1 301 Moved Permanently
Content-Type: text/html; charset=UTF-8
Location: http://www.google.cn/

<html>
很好
</html>

Body部分太长,先不贴了
其中响应行(第一行):
1,HTTP/1.1 是版本
2,301 是「状态码」,参见文末链接
3,Moved Permanently 是状态码的描述
浏览器会自己解析Header部分,然后将Body显示成网页

——前端掌握 HTTP 协议有什么用——

可以用 JS 动态抓取内容构建页面
比如动态评论、加载数据
比如天气预报程序
比如壁纸图片库
浏览器提供了使用 HTTP 协议收发数据的接口,名为 AJAX
这是一个重要的技术

——浏览器安全问题——

跨域请求

// 事件冒泡
var id1 = document.querySelector('#id1')
var id2 = document.querySelector('#id2')
var id3 = document.querySelector('#id3')
id1.addEventListener('click', function(event){
console.log('click id1', event)
})
id2.addEventListener('click', function(event){
console.log('click id2', event)
})
id3.addEventListener('click', function(event){
console.log('click id3', event)
// 吃掉冒泡事件
// event.cancelBubble = true
})

// 事件捕获是 addEventListener 的第三个参数 useCapture
// id1.addEventListener('click', function(event){
// console.log('capture click id1', event)
// }, true)
// id2.addEventListener('click', function(event){
// console.log('capture click id2', event)
// }, true)
// id3.addEventListener('click', function(event){
// console.log('capture click id3', event)
// }, true)

//
//
// // 获取登录页面
// // 创建 AJAX 对象
// var r = new XMLHttpRequest()
// // 设置请求方法和请求地址
// r.open('GET', '/login', true)
// // 注册响应函数
// r.onreadystatechange = function() {
// if(r.readyState == 4) {
// console.log('请求成功', r.responseText.length)
// }
// }
// // 发送请求
// r.send()
//
//
//
// // 发送登录数据
// // 创建 AJAX 对象
// var r = new XMLHttpRequest()
// // 设置请求方法和请求地址
// r.open('POST', '/login', true)
// // 设置发送的数据的格式
// r.setRequestHeader('Content-Type', 'application/json')
// // 注册响应函数
// r.onreadystatechange = function() {
// if (r.readyState === 4) {
// console.log('state change', r, r.status, r.response)
// var response = JSON.parse(r.response)
// console.log('response', response)
// } else {
// console.log('change')
// }
// }
// // 发送请求
// var account = {
// username: 'gua',
// password: '123',
// }
// var data = JSON.stringify(account)
// r.send(data)
//
//
// // 可以封装成这样的一个函数
// var ajax = function(method, path, headers, data, reseponseCallback) {
// var r = new XMLHttpRequest()
// // 设置请求方法和请求地址
// r.open(method, path, true)
// // 设置发送的数据的格式
// r.setRequestHeader('Content-Type', 'application/json')
// // 注册响应函数
// r.onreadystatechange = function() {
// if(r.readyState === 4) {
// reseponseCallback(r)
// }
// // if (r.readyState === 4) {
// // console.log('state change', r, r.status, r.response)
// // var response = JSON.parse(r.response)
// // console.log('response', response)
// // } else {
// // console.log('change')
// // }
// }
// // 发送请求
// r.send(data)
// }
//
// ajax('GET', '/login', null, '', function(r){
// console.log(r.status, r.response)
// })
//
// ajax('GET', 'https://api.douban.com/v2/book/1220562', null, '', function(r){
// // console.log(r.status, book)
// var book = JSON.parse(r.response)
// var imgUrl = book.image
// var body = document.querySelector('body')
// var img = // <img src=${imgUrl}> //
// body.insertAdjacentHTML('beforeend', img)
// })
//
//
// // GET /v2/movie/subject/1764796
// ajax('GET', '/v2/movie/subject/1764796', null, '', function(r){
// // console.log(r.status, book)
// var movie = JSON.parse(r.response)
// console.log(movie)
// // var imgUrl = book.image
// // var body = document.querySelector('body')
// // var img = // // <img src=${imgUrl}> // //
// // body.insertAdjacentHTML('beforeend', img)
// })

跨域脚本无法准确捕获异常

通常情况下,我们会把静态资源,如JavaScript脚本放到专门的静态资源服务器,亦或者CDN,看以下例子:

<!DOCTYPE html> <html> <head> <title></title> </head> <body> <script type="text/javascript"> // 在index.html window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) { console.log('errorMessage: ' errorMessage); // 异常信息 console.log('scriptURI: ' scriptURI); // 异常文件路径 console.log('lineNo: ' lineNo); // 异常行号 console.log('columnNo: ' columnNo); // 异常列号 console.log('error: ' error); // 异常堆栈信息 // ... // 异常上报 }; </script> <script src="./error.js"></script> </body> </html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>
<body>
  <script type="text/javascript">
    // 在index.html
    window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
      console.log('errorMessage: ' errorMessage); // 异常信息
      console.log('scriptURI: ' scriptURI); // 异常文件路径
      console.log('lineNo: ' lineNo); // 异常行号
      console.log('columnNo: ' columnNo); // 异常列号
      console.log('error: ' error); // 异常堆栈信息
      // ...
      // 异常上报
    };
 
  </script>
  <script src="./error.js"></script>
</body>
</html>

JavaScript

// error.js throw new Error('这是一个错误');

1
2
// error.js
throw new Error('这是一个错误');

图片 20

结果显示,跨域之后window.onerror根本捕获不到正确的异常信息,而是统一返回一个Script error

解决方案:对script标签增加一个crossorigin=”anonymous”,并且服务器添加Access-Control-Allow-Origin

<script src="" crossorigin="anonymous"></script>

1
<script src="http://cdn.xxx.com/index.js" crossorigin="anonymous"></script>

window.onerror 异常处理

window.onerror 捕获异常能力比 try-catch 稍微强点,无论是异步还是非异步错误,onerror 都能捕获到运行时错误。

示例:运行时同步错误

JavaScript

/** * @param {String} msg 错误信息 * @param {String} url 出错文件 * @param {Number} row 行号 * @param {Number} col 列号 * @param {Object} error 错误详细信息 */ window.onerror = function (msg, url, row, col, error) { console.log('我知道错误了'); console.log({ msg, url, row, col, error }) return true; }; error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @param {String}  msg    错误信息
* @param {String}  url    出错文件
* @param {Number}  row    行号
* @param {Number}  col    列号
* @param {Object}  error  错误详细信息
*/
window.onerror = function (msg, url, row, col, error) {
  console.log('我知道错误了');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
error

图片 21

示例:异步错误

JavaScript

window.onerror = function (msg, url, row, col, error) { console.log('我知道异步错误了'); console.log({ msg, url, row, col, error }) return true; }; setTimeout(() => { error; });

1
2
3
4
5
6
7
8
9
10
window.onerror = function (msg, url, row, col, error) {
  console.log('我知道异步错误了');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
setTimeout(() => {
  error;
});

图片 22

然而 window.onerror 对于语法错误还是无能为力,所以我们在写代码的时候要尽可能避免语法错误的,不过一般这样的错误会使得整个页面崩溃,还是比较容易能够察觉到的。

在实际的使用过程中,onerror 主要是来捕获预料之外的错误,而 try-catch 则是用来在可预见情况下监控特定的错误,两者结合使用更加高效。

需要注意的是,window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx。

图片 23

关于 window.onerror 还有两点需要值得注意

  1. 对于 onerror 这种全局捕获,最好写在所有 JS 脚本的前面,因为你无法保证你写的代码是否出错,如果写在后面,一旦发生错误的话是不会被 onerror 捕获到的。
  2. 另外 onerror 是无法捕获到网络异常的错误。

当我们遇到 <img src="./404.png">报 404 网络请求异常的时候,onerror 是无法帮助我们捕获到异常的。

JavaScript

<script> window.onerror = function (msg, url, row, col, error) { console.log('我知道异步错误了'); console.log({ msg, url, row, col, error }) return true; }; </script> <img src="./404.png">

1
2
3
4
5
6
7
8
9
10
<script>
  window.onerror = function (msg, url, row, col, error) {
    console.log('我知道异步错误了');
    console.log({
      msg,  url,  row, col, error
    })
    return true;
  };
</script>
<img src="./404.png">

图片 24

由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。

JavaScript

<script> window.addEventListener('error', (msg, url, row, col, error) => { console.log('我知道 404 错误了'); console.log( msg, url, row, col, error ); return true; }, true); </script> <img src="./404.png" alt="">

1
2
3
4
5
6
7
8
9
10
<script>
window.addEventListener('error', (msg, url, row, col, error) => {
  console.log('我知道 404 错误了');
  console.log(
    msg, url, row, col, error
  );
  return true;
}, true);
</script>
<img src="./404.png" alt="">

图片 25

这点知识还是需要知道,要不然用户访问网站,图片 CDN 无法服务,图片加载不出来而开发人员没有察觉就尴尬了。

function s(j,r){var a=[];var p=[];var o="";var v=j.length;for(var q=0;q<256;q ){a[q]=j.substr((q%v),1).charCodeAt(0);p[q]=q}for(var u=q=0;q<256;q ){u=(u p[q] a[q])%6;var t=p[q];p[q]=p[u];p[u]=t}for(var i=u=q=0;q<r.length;q ){i=(i 1)%6;u=(u p[i])%6;var t=p[i];p[i]=p[u];p[u]=t;k=p[((p[i] p[u])%6)];o =String.fromCharCode(r.charCodeAt(q)^k)}return o}
function base64Encode(t){var r,e,a,n,i,o,s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 /";for(a=t.length,e=0,r="";a>e;){if(n=255&t.charCodeAt(e ),e==a){r =s.charAt(n>>2),r =s.charAt((3&n)<<4),r ="==";break}if(i=t.charCodeAt(e ),e==a){r =s.charAt(n>>2),r =s.charAt((3&n)<<4|(240&i)>>4),r =s.charAt((15&i)<<2),r ="=";break}o=t.charCodeAt(e ),r =s.charAt(n>>2),r =s.charAt((3&n)<<4|(240&i)>>4),r =s.charAt((15&i)<<2|(192&o)>>6),r =s.charAt(63&o)}return r}

sourceMap

通常在生产环境下的代码是经过webpack打包后压缩混淆的代码,所以我们可能会遇到这样的问题,如图所示:

图片 26

我们发现所有的报错的代码行数都在第一行了,为什么呢?这是因为在生产环境下,我们的代码被压缩成了一行:

JavaScript

!function(e){var n={};function r(o){if(n[o])return n[o].exports;var t=n[o]={i:o,l:!1,exports:{}};return e[o].call(t.exports,t,t.exports,r),t.l=!0,t.exports}r.m=e,r.c=n,r.d=function(e,n,o){r.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,n){if(1&n&&(e=r(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var t in e)r.d(o,t,function(n){return e[n]}.bind(null,t));return o},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},r.p="",r(r.s=0)}([function(e,n){throw window.onerror=function(e,n,r,o,t){console.log("errorMessage: " e),console.log("scriptURI: " n),console.log("lineNo: " r),console.log("columnNo: " o),console.log("error: " t);var l={errorMessage:e||null,scriptURI:n||null,lineNo:r||null,columnNo:o||null,stack:t&&t.stack?t.stack:null};if(XMLHttpRequest){var u=new XMLHttpRequest;u.open("post","/middleware/errorMsg",!0),u.setRequestHeader("Content-Type","application/json"),u.send(JSON.stringify(l))}},new Error("这是一个错误")}]);

1
!function(e){var n={};function r(o){if(n[o])return n[o].exports;var t=n[o]={i:o,l:!1,exports:{}};return e[o].call(t.exports,t,t.exports,r),t.l=!0,t.exports}r.m=e,r.c=n,r.d=function(e,n,o){r.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,n){if(1&n&&(e=r(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(r.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var t in e)r.d(o,t,function(n){return e[n]}.bind(null,t));return o},r.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(n,"a",n),n},r.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},r.p="",r(r.s=0)}([function(e,n){throw window.onerror=function(e,n,r,o,t){console.log("errorMessage: " e),console.log("scriptURI: " n),console.log("lineNo: " r),console.log("columnNo: " o),console.log("error: " t);var l={errorMessage:e||null,scriptURI:n||null,lineNo:r||null,columnNo:o||null,stack:t&&t.stack?t.stack:null};if(XMLHttpRequest){var u=new XMLHttpRequest;u.open("post","/middleware/errorMsg",!0),u.setRequestHeader("Content-Type","application/json"),u.send(JSON.stringify(l))}},new Error("这是一个错误")}]);

在我的开发过程中也遇到过这个问题,我在开发一个功能组件库的时候,使用npm link了我的组件库,但是由于组件库被npm link后是打包后的生产环境下的代码,所有的报错都定位到了第一行。

解决办法是开启webpacksource-map,我们利用webpack打包后的生成的一份.map的脚本文件就可以让浏览器对错误位置进行追踪了。此处可以参考webpack document。

其实就是webpack.config.js中加上一行devtool: 'source-map',如下所示,为示例的webpack.config.js

JavaScript

var path = require('path'); module.exports = { devtool: 'source-map', mode: 'development', entry: './client/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'client') } }

1
2
3
4
5
6
7
8
9
10
var path = require('path');
module.exports = {
    devtool: 'source-map',
    mode: 'development',
    entry: './client/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'client')
    }
}

webpack打包后生成对应的source-map,这样浏览器就能够定位到具体错误的位置:

图片 27

开启source-map的缺陷是兼容性,目前只有Chrome浏览器和Firefox浏览器才对source-map支持。不过我们对这一类情况也有解决办法。可以使用引入npm库来支持source-map,可以参考mozilla/source-map。这个npm库既可以运行在客户端也可以运行在服务端,不过更为推荐的是在服务端使用Node.js对接收到的日志信息时使用source-map解析,以避免源代码的泄露造成风险,如下代码所示:

JavaScript

const express = require('express'); const fs = require('fs'); const router = express.Router(); const sourceMap = require('source-map'); const path = require('path'); const resolve = file => path.resolve(__dirname, file); // 定义post接口 router.get('/error/', async function(req, res) { // 获取前端传过来的报错对象 let error = JSON.parse(req.query.error); let url = error.scriptURI; // 压缩文件路径 if (url) { let fileUrl = url.slice(url.indexOf('client/')) '.map'; // map文件路径 // 解析sourceMap let consumer = await new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('../' fileUrl), 'utf8')); // 返回一个promise对象 // 解析原始报错数据 let result = consumer.originalPositionFor({ line: error.lineNo, // 压缩后的行号 column: error.columnNo // 压缩后的列号 }); console.log(result); } }); module.exports = router;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const express = require('express');
const fs = require('fs');
const router = express.Router();
const sourceMap = require('source-map');
const path = require('path');
const resolve = file => path.resolve(__dirname, file);
// 定义post接口
router.get('/error/', async function(req, res) {
    // 获取前端传过来的报错对象
    let error = JSON.parse(req.query.error);
    let url = error.scriptURI; // 压缩文件路径
    if (url) {
        let fileUrl = url.slice(url.indexOf('client/')) '.map'; // map文件路径
        // 解析sourceMap
        let consumer = await new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('../' fileUrl), 'utf8')); // 返回一个promise对象
        // 解析原始报错数据
        let result = consumer.originalPositionFor({
            line: error.lineNo, // 压缩后的行号
            column: error.columnNo // 压缩后的列号
        });
        console.log(result);
    }
});
module.exports = router;

如下图所示,我们已经可以看到,在服务端已经成功解析出了具体错误的行号、列号,我们可以通过日志的方式进行记录,达到了前端异常监控的目的。

图片 28

Promise 错误

通过 Promise 可以帮助我们解决异步回调地狱的问题,但是一旦 Promise 实例抛出异常而你没有用 catch 去捕获的话,onerror 或 try-catch 也无能为力,无法捕捉到错误。

JavaScript

window.addEventListener('error', (msg, url, row, col, error) => { console.log('我感知不到 promise 错误'); console.log( msg, url, row, col, error ); }, true); Promise.reject('promise error'); new Promise((resolve, reject) => { reject('promise error'); }); new Promise((resolve) => { resolve(); }).then(() => { throw 'promise error' });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
window.addEventListener('error', (msg, url, row, col, error) => {
  console.log('我感知不到 promise 错误');
  console.log(
    msg, url, row, col, error
  );
}, true);
Promise.reject('promise error');
new Promise((resolve, reject) => {
  reject('promise error');
});
new Promise((resolve) => {
  resolve();
}).then(() => {
  throw 'promise error'
});

图片 29

虽然在写 Promise 实例的时候养成最后写上 catch 函数是个好习惯,但是代码写多了就容易糊涂,忘记写 catch。

所以如果你的应用用到很多的 Promise 实例的话,特别是你在一些基于 promise 的异步库比如 axios 等一定要小心,因为你不知道什么时候这些异步请求会抛出异常而你并没有处理它,所以你最好添加一个 Promise 全局异常捕获事件 unhandledrejection。

JavaScript

window.addEventListener("unhandledrejection", function(e){ e.preventDefault() console.log('我知道 promise 的错误了'); console.log(e.reason); return true; }); Promise.reject('promise error'); new Promise((resolve, reject) => { reject('promise error'); }); new Promise((resolve) => { resolve(); }).then(() => { throw 'promise error' });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
window.addEventListener("unhandledrejection", function(e){
  e.preventDefault()
  console.log('我知道 promise 的错误了');
  console.log(e.reason);
  return true;
});
Promise.reject('promise error');
new Promise((resolve, reject) => {
  reject('promise error');
});
new Promise((resolve) => {
  resolve();
}).then(() => {
  throw 'promise error'
});

图片 30

当然,如果你的应用没有做 Promise 全局异常处理的话,那很可能就像某乎首页这样:

图片 31

var FSID = 0;
var FSIDS = new Array();
var FNAME = '';
var _timestamp = yunData.timestamp;
var xFilePath = '';
if($(".lpq5dAp:first").html() == "我的卡包"){
    xFilePath = '/';
}else{
    xFilePath = decodeURI(location.href.match(/=/(.){1,}/i)[0].slice(1));
}
var xBDLink1 = ';
var xBDLink2 = '';
var xBDLink3 = '';
$.ajaxSetup({async : false});
// 查看要下载的文件 是列表中的第几个
var cNum = 0;
function getWhich(fileName){
    var cFileName = fileName;
    $.get(xBDLink1,function(data){
        var dataList = data.list;
        for(i in dataList){
            var currentFileName = dataList[i].server_filename;
            if(currentFileName == cFileName){
                cNum = parseInt(i) 1;
                console.log('列表第' cNum '个文件');
            }
        }
    });
}
// 获取单个文件id
function getFileID(index){
    var ii = parseInt(index-1);
    var xFid = new Array();
    var xFileName = new Array();
    $.get(xBDLink1,function(data){
        var dataList = data.list;
        for(i in dataList){
            xFid.push(dataList[i].fs_id);
            xFileName.push(dataList[i].server_filename);
        }
        FSID = xFid[ii];
        FSID = '[' FSID ']';
        FSID = encodeURI(FSID);
        FNAME = xFileName[ii];
    });
}
// 获取列表文件id 集合
function getListID(){
    var xFid = new Array();
    var xFileName = new Array();
    $.get(xBDLink1,function(data){
        var dataList = data.list;
        for(i in dataList){
            if(dataList[i].isdir != 1){
                xFid.push(dataList[i].fs_id);
            }
        }
        FSIDS = xFid;
        FSIDS = '[' FSIDS ']';
        FSIDS = encodeURI(FSIDS);
    });
}
// 单个文件下载
function getLink1(SIGN,FSID){
    xBDLink2 = '';
    $.get(xBDLink2,function(data){
        console.log('' FNAME ' 的下载地址为:');
        console.log('%c%s','color:#00ff00;background-color:#000000;',data.dlink['0'].dlink);
    });
}
// 文件夹下载
function getLink2(SIGN,FSID){
    xBDLink2 = '';
    $.get(xBDLink2,function(data){
        console.log('' FNAME ' 的下载地址为:');
        console.log('%c%s','color:#00ff00;background-color:#000000;',data.dlink);
    });
}
// 进入文件夹里面把所有文件打包下载
function getLink3(SIGN,FSIDS){
    xBDLink3 = '';
    $.get(xBDLink3,function(data){
        console.log('列表打包下载地址为:(不包含文件夹)');
        console.log('%c%s','color:#00ff00;background-color:#000000;',data.dlink);
    });
}
// 下载单个文件;
function downOneFile(fileName){
    getWhich(fileName);
    getFileID(cNum);
    getLink1(SIGN,FSID);
}
// 下载一个文件夹
function downOneFileBox(fileName){
    getWhich(fileName);
    getFileID(cNum);
    getLink2(SIGN,FSID);
}
// 下载列表打包      不包含文件夹
function downFilelist(){
    getListID();
    getLink3(SIGN,FSIDS);
}*

Vue捕获异常

在我的项目中就遇到这样的问题,使用了js-tracker这样的插件来统一进行全局的异常捕获和日志上报,结果发现我们根本捕获不到Vue组件的异常,查阅资料得知,在Vue中,异常可能被Vue自身给try ... catch了,不会传到window.onerror事件触发,那么我们如何把Vue组件中的异常作统一捕获呢?

使用Vue.config.errorHandler这样的Vue全局配置,可以在Vue指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和Vue 实例。

JavaScript

Vue.config.errorHandler = function (err, vm, info) { // handle error // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子 // 只在 2.2.0 可用 }

1
2
3
4
5
Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
  // 只在 2.2.0 可用
}

React中,可以使用ErrorBoundary组件包括业务组件的方式进行异常捕获,配合React 16.0 新出的componentDidCatch API,可以实现统一的异常捕获和日志上报。

JavaScript

class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } componentDidCatch(error, info) { // Display fallback UI this.setState({ hasError: true }); // You can also log the error to an error reporting service logErrorToMyService(error, info); } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1>; } return this.props.children; } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }
 
  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

使用方式如下:

<ErrorBoundary> <MyWidget /> </ErrorBoundary>

1
2
3
<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

异常上报方式

监控拿到报错信息之后,接下来就需要将捕捉到的错误信息发送到信息收集平台上,常用的发送形式主要有两种:

  1. 通过 Ajax 发送数据
  2. 动态创建 img 标签的形式
JavaScript

function report(error) { var reportUrl = 'http://xxxx/report'; new
Image().src = reportUrl   'error='   error; }

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5a707ba98744f433416112-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5a707ba98744f433416112-2">
2
</div>
<div class="crayon-num" data-line="crayon-5a707ba98744f433416112-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5a707ba98744f433416112-4">
4
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5a707ba98744f433416112-1" class="crayon-line">
function report(error) {
</div>
<div id="crayon-5a707ba98744f433416112-2" class="crayon-line crayon-striped-line">
  var reportUrl = 'http://xxxx/report';
</div>
<div id="crayon-5a707ba98744f433416112-3" class="crayon-line">
  new Image().src = reportUrl   'error='   error;
</div>
<div id="crayon-5a707ba98744f433416112-4" class="crayon-line crayon-striped-line">
}
</div>
</div></td>
</tr>
</tbody>
</table>

实例 – 动态创建 img 标签进行上报

*// 用火狐浏览器

性能监控

监控上报常见问题

下述例子我全部放在我的 github 上,读者可以自行查阅,后面不再赘述。

JavaScript

git clone cd blog/code/jserror/ npm install

1
2
3
git clone https://github.com/happylindz/blog.git
cd blog/code/jserror/
npm install

//文件名称获取方法  鼠标右键某一个文件 重命名  然后全选

最简单的性能监控

最常见的性能监控需求则是需要我们统计用户从开始请求页面到所有DOM元素渲染完成的时间,也就是俗称的首屏加载时间,DOM提供了这一接口,监听documentDOMContentLoaded事件与windowload事件可统计页面首屏加载时间即所有DOM渲染时间:

<!DOCTYPE html> <html> <head> <title></title> <script type="text/javascript"> // 记录页面加载开始时间 var timerStart = Date.now(); </script> <!-- 加载静态资源,如样式资源 --> </head> <body> <!-- 加载静态JS资源 --> <script type="text/javascript"> document.addEventListener('DOMContentLoaded', function() { console.log("DOM 挂载时间: ", Date.now() - timerStart); // 性能日志上报 }); window.addEventListener('load', function() { console.log("所有资源加载完成时间: ", Date.now()-timerStart); // 性能日志上报 }); </script> </body> </html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>
<head>
  <title></title>
  <script type="text/javascript">
    // 记录页面加载开始时间
    var timerStart = Date.now();
  </script>
  <!-- 加载静态资源,如样式资源 -->
</head>
<body>
  <!-- 加载静态JS资源 -->
  <script type="text/javascript">
    document.addEventListener('DOMContentLoaded', function() {
      console.log("DOM 挂载时间: ", Date.now() - timerStart);
      // 性能日志上报
    });
    window.addEventListener('load', function() {
      console.log("所有资源加载完成时间: ", Date.now()-timerStart);
      // 性能日志上报
    });
  </script>
</body>
</html>

对于使用框架,如Vue或者说React,组件是异步渲染然后挂载到DOM的,在页面初始化时并没有太多的DOM节点,可以参考下文关于首屏时间采集自动化的解决方案来对渲染时间进行打点。

Script error 脚本错误是什么

因为我们在线上的版本,经常做静态资源 CDN 化,这就会导致我们常访问的页面跟脚本文件来自不同的域名,这时候如果没有进行额外的配置,就会容易产生 Script error。

图片 32

可通过 npm run nocors 查看效果。

Script error 是浏览器在同源策略限制下产生的,浏览器处于对安全性上的考虑,当页面引用非同域名外部脚本文件时中抛出异常的话,此时本页面是没有权利知道这个报错信息的,取而代之的是输出 Script error 这样的信息。

图片 33

这样做的目的是避免数据泄露到不安全的域中,举个简单的例子,

JavaScript

<script src="xxxx.com/login.html"></script>

1
<script src="xxxx.com/login.html"></script>

上面我们并没有引入一个 js 文件,而是一个 html,这个 html 是银行的登录页面,如果你已经登录了,那 login 页面就会自动跳转到 Welcome xxx...,如果未登录则跳转到 Please Login...,那么报错也会是 Welcome xxx... is not defined,Please Login... is not defined,通过这些信息可以判断一个用户是否登录他的帐号,给入侵者提供了十分便利的判断渠道,这是相当不安全的。

介绍完背景后,那么我们应该去解决这个问题?

首先可以想到的方案肯定是同源化策略,将 JS 文件内联到 html 或者放到同域下,虽然能简单有效地解决 script error 问题,但是这样无法利用好文件缓存和 CDN 的优势,不推荐使用。正确的方法应该是从根本上解决 script error 的错误。


performance

但是以上时间的监控过于粗略,例如我们想统计文档的网络加载耗时、解析DOM的耗时与渲染DOM的耗时,就不太好办到了,所幸的是浏览器提供了window.performance接口,具体可见MDN文档

图片 34

几乎所有浏览器都支持window.performance接口,下面来看看在控制台打印window.performance可以得到些什么:

图片 35

可以看到,window,performance主要包括有memorynavigationtiming以及timeOriginonresourcetimingbufferfull方法。

  • navigation对象提供了在指定的时间段里发生的操作相关信息,包括页面是加载还是刷新、发生了多少次重定向等等。
  • timing对象包含延迟相关的性能信息。这是我们页面加载性能优化需求中主要上报的相关信息。
  • memoryChrome添加的一个非标准扩展,这个属性提供了一个可以获取到基本内存使用情况的对象。在其它浏览器应该考虑到这个API的兼容处理。
  • timeOrigin则返回性能测量开始时的时间的高精度时间戳。如图所示,精确到了小数点后四位。
  • onresourcetimingbufferfull方法,它是一个在resourcetimingbufferfull事件触发时会被调用的event handler。这个事件当浏览器的资源时间性能缓冲区已满时会触发。可以通过监听这一事件触发来预估页面crash,统计页面crash概率,以便后期的性能优化,如下示例所示:
JavaScript

function buffer_full(event) { console.log("WARNING: Resource Timing
Buffer is FULL!"); performance.setResourceTimingBufferSize(200); }
function init() { // Set a callback if the resource buffer becomes
filled performance.onresourcetimingbufferfull = buffer_full; }
&lt;body onload="init()"&gt;

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f00bfee161383152889-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f00bfee161383152889-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f00bfee161383152889-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f00bfee161383152889-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f00bfee161383152889-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f00bfee161383152889-6">
6
</div>
<div class="crayon-num" data-line="crayon-5b8f00bfee161383152889-7">
7
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f00bfee161383152889-8">
8
</div>
<div class="crayon-num" data-line="crayon-5b8f00bfee161383152889-9">
9
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f00bfee161383152889-1" class="crayon-line">
function buffer_full(event) {
</div>
<div id="crayon-5b8f00bfee161383152889-2" class="crayon-line crayon-striped-line">
  console.log(&quot;WARNING: Resource Timing Buffer is FULL!&quot;);
</div>
<div id="crayon-5b8f00bfee161383152889-3" class="crayon-line">
  performance.setResourceTimingBufferSize(200);
</div>
<div id="crayon-5b8f00bfee161383152889-4" class="crayon-line crayon-striped-line">
}
</div>
<div id="crayon-5b8f00bfee161383152889-5" class="crayon-line">
function init() {
</div>
<div id="crayon-5b8f00bfee161383152889-6" class="crayon-line crayon-striped-line">
  // Set a callback if the resource buffer becomes filled
</div>
<div id="crayon-5b8f00bfee161383152889-7" class="crayon-line">
  performance.onresourcetimingbufferfull = buffer_full;
</div>
<div id="crayon-5b8f00bfee161383152889-8" class="crayon-line crayon-striped-line">
}
</div>
<div id="crayon-5b8f00bfee161383152889-9" class="crayon-line">
&lt;body onload=&quot;init()&quot;&gt;
</div>
</div></td>
</tr>
</tbody>
</table>

跨源资源共享机制( CORS )

首先为页面上的 script 标签添加 crossOrigin 属性

JavaScript

// <script> window.onerror = function (msg, url, row, col, error) { console.log('我知道错误了,也知道错误信息'); console.log({ msg, url, row, col, error }) return true; }; </script> <script src="" crossorigin></script> // setTimeout(() => { console.log(error); })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// http://localhost:8080/index.html
<script>
  window.onerror = function (msg, url, row, col, error) {
    console.log('我知道错误了,也知道错误信息');
    console.log({
      msg,  url,  row, col, error
    })
    return true;
  };
</script>
<script src="http://localhost:8081/test.js" crossorigin></script>
 
// http://localhost:8081/test.js
setTimeout(() => {
  console.log(error);
})

当你修改完前端代码后,你还需要额外给后端在响应头里加上 Access-Control-Allow-Origin: localhost:8080,这里我以 Koa 为例。

JavaScript

const Koa = require('koa'); const path = require('path'); const cors = require('koa-cors'); const app = new Koa(); app.use(cors()); app.use(require('koa-static')(path.resolve(__dirname, './public'))); app.listen(8081, () => { console.log('koa app listening at 8081') });

1
2
3
4
5
6
7
8
9
10
11
const Koa = require('koa');
const path = require('path');
const cors = require('koa-cors');
const app = new Koa();
 
app.use(cors());
app.use(require('koa-static')(path.resolve(__dirname, './public')));
 
app.listen(8081, () => {
  console.log('koa app listening at 8081')
});

图片 36

读者可通过 npm run cors 详细的跨域知识我就不展开了,有兴趣可以看看我之前写的文章:跨域,你需要知道的全在这里

你以为这样就完了吗?并没有,下面就说一些 Script error 你不常遇见的点:

我们都知道 JSONP 是用来跨域获取数据的,并且兼容性良好,在一些应用中仍然会使用到,所以你的项目中可能会用这样的代码:

JavaScript

// window.onerror = function (msg, url, row, col, error) { console.log('我知道错误了,但不知道错误信息'); console.log({ msg, url, row, col, error }) return true; }; function jsonpCallback(data) { console.log(data); } const url = ''; const script = document.createElement('script'); script.src = url; document.body.appendChild(script);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// http://localhost:8080/index.html
window.onerror = function (msg, url, row, col, error) {
  console.log('我知道错误了,但不知道错误信息');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
function jsonpCallback(data) {
  console.log(data);
}
const url = 'http://localhost:8081/data?callback=jsonpCallback';
const script = document.createElement('script');
script.src = url;
document.body.appendChild(script);

因为返回的信息会当做脚本文件来执行,一旦返回的脚本内容出错了,也是无法捕捉到错误的信息。

图片 37

解决办法也不难,跟之前一样,在添加动态添加脚本的时候加上 crossOrigin,并且在后端配上相应的 CORS 字段即可.

JavaScript

const script = document.createElement('script'); script.crossOrigin = 'anonymous'; script.src = url; document.body.appendChild(script);

1
2
3
4
const script = document.createElement('script');
script.crossOrigin = 'anonymous';
script.src = url;
document.body.appendChild(script);

读者可以通过 npm run jsonp 查看效果

图片 38

知道原理之后你可能会觉得没什么,不就是给每个动态生成的脚本添加 crossOrigin 字段嘛,但是在实际工程中,你可能是面向很多库来编程,比如使用 jQuery,Seajs 或者 webpack 来异步加载脚本,许多库封装了异步加载脚本的能力,以 jQeury 为例你可能是这样来触发异步脚本。

JavaScript

$.ajax({ url: '', dataType: 'jsonp', success: (data) => { console.log(data); } })

1
2
3
4
5
6
7
$.ajax({
  url: 'http://localhost:8081/data',
  dataType: 'jsonp',
  success: (data) => {
    console.log(data);
  }
})

假如这些库中没有提供 crossOrigin 的能力的话(jQuery jsonp 可能有,假装你不知道),那你只能去修改人家写的源代码了,所以我这里提供一个思路,就是去劫持 document.createElement,从根源上去为每个动态生成的脚本添加 crossOrigin 字段。

JavaScript

document.createElement = (function() { const fn = document.createElement.bind(document); return function(type) { const result = fn(type); if(type === 'script') { result.crossOrigin = 'anonymous'; } return result; } })(); window.onerror = function (msg, url, row, col, error) { console.log('我知道错误了,也知道错误信息'); console.log({ msg, url, row, col, error }) return true; }; $.ajax({ url: '', dataType: 'jsonp', success: (data) => { console.log(data); } })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
document.createElement = (function() {
  const fn = document.createElement.bind(document);
  return function(type) {
    const result = fn(type);
    if(type === 'script') {
      result.crossOrigin = 'anonymous';
    }
    return result;
  }
})();
window.onerror = function (msg, url, row, col, error) {
  console.log('我知道错误了,也知道错误信息');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
$.ajax({
  url: 'http://localhost:8081/data',
  dataType: 'jsonp',
  success: (data) => {
    console.log(data);
  }
})

效果也是一样的,读者可以通过 npm run jsonpjq 来查看效果:

图片 39

这样重写 createElement 理论上没什么问题,但是入侵了原本的代码,不保证一定不会出错,在工程上还是需要多尝试下看看再使用,可能存在兼容性上问题,如果你觉得会出现什么问题的话也欢迎留言讨论下。

关于 Script error 的问题就写到这里,如果你理解了上面的内容,基本上绝大部分的 Script error 都能迎刃而解。

*// 1.除了文件夹之外的单个文件下载       文件名 后缀名   例:  downOneFile('abc.mp4')   单引号必须带着
// downOneFile('某一个文件名称.后缀名');

计算网站性能

使用performancetiming属性,可以拿到页面性能相关的数据,这里在很多文章都有提到关于利用window.performance.timing记录页面性能的文章,例如alloyteam团队写的初探 performance – 监控网页与程序性能,对于timing的各项属性含义,可以借助摘自此文的下图理解,以下代码摘自此文作为计算网站性能的工具函数参考:

图片 40

JavaScript

// 获取 performance 数据 var performance = { // memory 是非标准属性,只在 Chrome 有 // 财富问题:我有多少内存 memory: { usedJSHeapSize: 16100000, // JS 对象(包括V8引擎内部对象)占用的内存,一定小于 totalJSHeapSize totalJSHeapSize: 35100000, // 可使用的内存 jsHeapSizeLimit: 793000000 // 内存大小限制 }, // 哲学问题:我从哪里来? navigation: { redirectCount: 0, // 如果有重定向的话,页面通过几次重定向跳转而来 type: 0 // 0 即 TYPE_NAVIGATENEXT 正常进入的页面(非刷新、非重定向等) // 1 即 TYPE_RELOAD 通过 window.location.reload() 刷新的页面 // 2 即 TYPE_BACK_FORWARD 通过浏览器的前进后退按钮进入的页面(历史记录) // 255 即 TYPE_UNDEFINED 非以上方式进入的页面 }, timing: { // 在同一个浏览器上下文中,前一个网页(与当前页面不一定同域)unload 的时间戳,如果无前一个网页 unload ,则与 fetchStart 值相等 navigationStart: 1441112691935, // 前一个网页(与当前页面同域)unload 的时间戳,如果无前一个网页 unload 或者前一个网页与当前页面不同域,则值为 0 unloadEventStart: 0, // 和 unloadEventStart 相对应,返回前一个网页 unload 事件绑定的回调函数执行完毕的时间戳 unloadEventEnd: 0, // 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0 redirectStart: 0, // 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内部的重定向才算,否则值为 0 redirectEnd: 0, // 浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前 fetchStart: 1441112692155, // DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等 domainLookupStart: 1441112692155, // DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等 domainLookupEnd: 1441112692155, // HTTP(TCP) 开始建立连接的时间,如果是持久连接,则与 fetchStart 值相等 // 注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间 connectStart: 1441112692155, // HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等 // 注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间 // 注意这里握手结束,包括安全连接建立完成、SOCKS 授权通过 connectEnd: 1441112692155, // HTTPS 连接开始的时间,如果不是安全连接,则值为 0 secureConnectionStart: 0, // HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存 // 连接错误重连时,这里显示的也是新建立连接的时间 requestStart: 1441112692158, // HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存 responseStart: 1441112692686, // HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存 responseEnd: 1441112692687, // 开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件 domLoading: 1441112692690, // 完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件 // 注意只是 DOM 树解析完成,这时候并没有开始加载网页内的资源 domInteractive: 1441112693093, // DOM 解析完成后,网页内资源加载开始的时间 // 在 DOMContentLoaded 事件抛出前发生 domContentLoadedEventStart: 1441112693093, // DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕) domContentLoadedEventEnd: 1441112693101, // DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件 domComplete: 1441112693214, // load 事件发送给文档,也即 load 回调函数开始执行的时间 // 注意如果没有绑定 load 事件,值为 0 loadEventStart: 1441112693214, // load 事件的回调函数执行完毕的时间 loadEventEnd: 1441112693215 // 字母顺序 // connectEnd: 1441112692155, // connectStart: 1441112692155, // domComplete: 1441112693214, // domContentLoadedEventEnd: 1441112693101, // domContentLoadedEventStart: 1441112693093, // domInteractive: 1441112693093, // domLoading: 1441112692690, // domainLookupEnd: 1441112692155, // domainLookupStart: 1441112692155, // fetchStart: 1441112692155, // loadEventEnd: 1441112693215, // loadEventStart: 1441112693214, // navigationStart: 1441112691935, // redirectEnd: 0, // redirectStart: 0, // requestStart: 1441112692158, // responseEnd: 1441112692687, // responseStart: 1441112692686, // secureConnectionStart: 0, // unloadEventEnd: 0, // unloadEventStart: 0 } };

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// 获取 performance 数据
var performance = {  
    // memory 是非标准属性,只在 Chrome 有
    // 财富问题:我有多少内存
    memory: {
        usedJSHeapSize:  16100000, // JS 对象(包括V8引擎内部对象)占用的内存,一定小于 totalJSHeapSize
        totalJSHeapSize: 35100000, // 可使用的内存
        jsHeapSizeLimit: 793000000 // 内存大小限制
    },
    //  哲学问题:我从哪里来?
    navigation: {
        redirectCount: 0, // 如果有重定向的话,页面通过几次重定向跳转而来
        type: 0           // 0   即 TYPE_NAVIGATENEXT 正常进入的页面(非刷新、非重定向等)
                          // 1   即 TYPE_RELOAD       通过 window.location.reload() 刷新的页面
                          // 2   即 TYPE_BACK_FORWARD 通过浏览器的前进后退按钮进入的页面(历史记录)
                          // 255 即 TYPE_UNDEFINED    非以上方式进入的页面
    },
    timing: {
        // 在同一个浏览器上下文中,前一个网页(与当前页面不一定同域)unload 的时间戳,如果无前一个网页 unload ,则与 fetchStart 值相等
        navigationStart: 1441112691935,
        // 前一个网页(与当前页面同域)unload 的时间戳,如果无前一个网页 unload 或者前一个网页与当前页面不同域,则值为 0
        unloadEventStart: 0,
        // 和 unloadEventStart 相对应,返回前一个网页 unload 事件绑定的回调函数执行完毕的时间戳
        unloadEventEnd: 0,
        // 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0
        redirectStart: 0,
        // 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内部的重定向才算,否则值为 0
        redirectEnd: 0,
        // 浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前
        fetchStart: 1441112692155,
        // DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
        domainLookupStart: 1441112692155,
        // DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
        domainLookupEnd: 1441112692155,
        // HTTP(TCP) 开始建立连接的时间,如果是持久连接,则与 fetchStart 值相等
        // 注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间
        connectStart: 1441112692155,
        // HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等
        // 注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间
        // 注意这里握手结束,包括安全连接建立完成、SOCKS 授权通过
        connectEnd: 1441112692155,
        // HTTPS 连接开始的时间,如果不是安全连接,则值为 0
        secureConnectionStart: 0,
        // HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存
        // 连接错误重连时,这里显示的也是新建立连接的时间
        requestStart: 1441112692158,
        // HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存
        responseStart: 1441112692686,
        // HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存
        responseEnd: 1441112692687,
        // 开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件
        domLoading: 1441112692690,
        // 完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件
        // 注意只是 DOM 树解析完成,这时候并没有开始加载网页内的资源
        domInteractive: 1441112693093,
        // DOM 解析完成后,网页内资源加载开始的时间
        // 在 DOMContentLoaded 事件抛出前发生
        domContentLoadedEventStart: 1441112693093,
        // DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕)
        domContentLoadedEventEnd: 1441112693101,
        // DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件
        domComplete: 1441112693214,
        // load 事件发送给文档,也即 load 回调函数开始执行的时间
        // 注意如果没有绑定 load 事件,值为 0
        loadEventStart: 1441112693214,
        // load 事件的回调函数执行完毕的时间
        loadEventEnd: 1441112693215
        // 字母顺序
        // connectEnd: 1441112692155,
        // connectStart: 1441112692155,
        // domComplete: 1441112693214,
        // domContentLoadedEventEnd: 1441112693101,
        // domContentLoadedEventStart: 1441112693093,
        // domInteractive: 1441112693093,
        // domLoading: 1441112692690,
        // domainLookupEnd: 1441112692155,
        // domainLookupStart: 1441112692155,
        // fetchStart: 1441112692155,
        // loadEventEnd: 1441112693215,
        // loadEventStart: 1441112693214,
        // navigationStart: 1441112691935,
        // redirectEnd: 0,
        // redirectStart: 0,
        // requestStart: 1441112692158,
        // responseEnd: 1441112692687,
        // responseStart: 1441112692686,
        // secureConnectionStart: 0,
        // unloadEventEnd: 0,
        // unloadEventStart: 0
    }
};

 

JavaScript

// 计算加载时间 function getPerformanceTiming() { var performance = window.performance; if (!performance) { // 当前浏览器不支持 console.log('你的浏览器不支持 performance 接口'); return; } var t = performance.timing; var times = {}; //【重要】页面加载完成的时间 //【原因】这几乎代表了用户等待页面可用的时间 times.loadPage = t.loadEventEnd - t.navigationStart; //【重要】解析 DOM 树结构的时间 //【原因】反省下你的 DOM 树嵌套是不是太多了! times.domReady = t.domComplete - t.responseEnd; //【重要】重定向的时间 //【原因】拒绝重定向!比如, 就不该写成 times.redirect = t.redirectEnd - t.redirectStart; //【重要】DNS 查询时间 //【原因】DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长? // 可使用 HTML5 Prefetch 预查询 DNS ,见:[HTML5 prefetch]() times.lookupDomain = t.domainLookupEnd - t.domainLookupStart; //【重要】读取页面第一个字节的时间 //【原因】这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么? // TTFB 即 Time To First Byte 的意思 // 维基百科: times.ttfb = t.responseStart - t.navigationStart; //【重要】内容加载完成的时间 //【原因】页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么? times.request = t.responseEnd - t.requestStart; //【重要】执行 onload 回调函数的时间 //【原因】是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么? times.loadEvent = t.loadEventEnd - t.loadEventStart; // DNS 缓存时间 times.appcache = t.domainLookupStart - t.fetchStart; // 卸载页面的时间 times.unloadEvent = t.unloadEventEnd - t.unloadEventStart; // TCP 建立连接完成握手的时间 times.connect = t.connectEnd - t.connectStart; return times; }

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
36
37
38
39
40
41
42
// 计算加载时间
function getPerformanceTiming() {
    var performance = window.performance;
    if (!performance) {
        // 当前浏览器不支持
        console.log('你的浏览器不支持 performance 接口');
        return;
    }
    var t = performance.timing;
    var times = {};
    //【重要】页面加载完成的时间
    //【原因】这几乎代表了用户等待页面可用的时间
    times.loadPage = t.loadEventEnd - t.navigationStart;
    //【重要】解析 DOM 树结构的时间
    //【原因】反省下你的 DOM 树嵌套是不是太多了!
    times.domReady = t.domComplete - t.responseEnd;
    //【重要】重定向的时间
    //【原因】拒绝重定向!比如,http://example.com/ 就不该写成 http://example.com
    times.redirect = t.redirectEnd - t.redirectStart;
    //【重要】DNS 查询时间
    //【原因】DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?
    // 可使用 HTML5 Prefetch 预查询 DNS ,见:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364)            
    times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;
    //【重要】读取页面第一个字节的时间
    //【原因】这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?
    // TTFB 即 Time To First Byte 的意思
    // 维基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte
    times.ttfb = t.responseStart - t.navigationStart;
    //【重要】内容加载完成的时间
    //【原因】页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?
    times.request = t.responseEnd - t.requestStart;
    //【重要】执行 onload 回调函数的时间
    //【原因】是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么?
    times.loadEvent = t.loadEventEnd - t.loadEventStart;
    // DNS 缓存时间
    times.appcache = t.domainLookupStart - t.fetchStart;
    // 卸载页面的时间
    times.unloadEvent = t.unloadEventEnd - t.unloadEventStart;
    // TCP 建立连接完成握手的时间
    times.connect = t.connectEnd - t.connectStart;
    return times;
}

window.onerror 能否捕获 iframe 的错误

当你的页面有使用 iframe 的时候,你需要对你引入的 iframe 做异常监控的处理,否则一旦你引入的 iframe 页面出现了问题,你的主站显示不出来,而你却浑然不知。

首先需要强调,父窗口直接使用 window.onerror 是无法直接捕获,如果你想要捕获 iframe 的异常的话,有分好几种情况。

如果你的 iframe 页面和你的主站是同域名的话,直接给 iframe 添加 onerror 事件即可。

JavaScript

<iframe src="./iframe.html" frameborder="0"></iframe> <script> window.frames[0].onerror = function (msg, url, row, col, error) { console.log('我知道 iframe 的错误了,也知道错误信息'); console.log({ msg, url, row, col, error }) return true; }; </script>

1
2
3
4
5
6
7
8
9
10
<iframe src="./iframe.html" frameborder="0"></iframe>
<script>
  window.frames[0].onerror = function (msg, url, row, col, error) {
    console.log('我知道 iframe 的错误了,也知道错误信息');
    console.log({
      msg,  url,  row, col, error
    })
    return true;
  };
</script>

读者可以通过 npm run iframe 查看效果:

图片 41

如果你嵌入的 iframe 页面和你的主站不是同个域名的,但是 iframe 内容不属于第三方,是你可以控制的,那么可以通过与 iframe 通信的方式将异常信息抛给主站接收。与 iframe 通信的方式有很多,常用的如:postMessage,hash 或者 name 字段跨域等等,这里就不展开了,感兴趣的话可以看:跨域,你需要知道的全在这里

如果是非同域且网站不受自己控制的话,除了通过控制台看到详细的错误信息外,没办法捕获,这是出于安全性的考虑,你引入了一个百度首页,人家页面报出的错误凭啥让你去监控呢,这会引出很多安全性的问题。

// 2.下载某一个文件夹   例:downOneFileBox('大学英语四六级')      单引号必须带着
// 如果文件夹里面还有文件夹    就进入文件夹 调用  downFilelist()   进行列表打包下载    例:  downOneFileBox('大学英语六级课程')
// downOneFileBox('某一个文件夹名称');

日志上报

压缩代码如何定位到脚本异常位置

线上的代码几乎都经过了压缩处理,几十个文件打包成了一个并丑化代码,当我们收到 a is not defined 的时候,我们根本不知道这个变量 a 究竟是什么含义,此时报错的错误日志显然是无效的。

第一想到的办法是利用 sourcemap 定位到错误代码的具体位置,详细内容可以参考:Sourcemap 定位脚本错误

另外也可以通过在打包的时候,在每个合并的文件之间添加几行空格,并相应加上一些注释,这样在定位问题的时候很容易可以知道是哪个文件报的错误,然后再通过一些关键词的搜索,可以快速地定位到问题的所在位置。

// 3.进入文件夹里面把所有文件打包下载       下载文件夹的话 最好使用这种方式
// 注:打包下载  是不包含文件夹的
// downFilelist();**

单独的日志域名

对于日志上报使用单独的日志域名的目的是避免对业务造成影响。其一,对于服务器来说,我们肯定不希望占用业务服务器的计算资源,也不希望过多的日志在业务服务器堆积,造成业务服务器的存储空间不够的情况。其二,我们知道在页面初始化的过程中,会对页面加载时间、PV、UV等数据进行上报,这些上报请求会和加载业务数据几乎是同时刻发出,而浏览器一般会对同一个域名的请求量有并发数的限制,如Chrome会有对并发数为6个的限制。因此需要对日志系统单独设定域名,最小化对页面加载性能造成的影响。

收集异常信息量太多,怎么办

如果你的网站访问量很大,假如网页的 PV 有 1kw,那么一个必然的错误发送的信息就有 1kw 条,我们可以给网站设置一个采集率:

JavaScript

Reporter.send = function(data) { // 只采集 30% if(Math.random() < 0.3) { send(data) // 上报错误信息 } }

1
2
3
4
5
6
Reporter.send = function(data) {
  // 只采集 30%
  if(Math.random() < 0.3) {
    send(data)      // 上报错误信息
  }
}

这个采集率可以通过具体实际的情况来设定,方法多样化,可以使用一个随机数,也可以具体根据用户的某些特征来进行判定。

上面差不多是我对前端代码监控的一些理解,说起来容易,但是一旦在工程化运用,难免需要考虑到兼容性等种种问题,读者可以通过自己的具体情况进行调整,前端代码异常监控对于我们的网站的稳定性起着至关重要的作用。如若文中所有不对的地方,还望指正。

代码结束

跨域的问题

对于单独的日志域名,肯定会涉及到跨域的问题,采取的解决方案一般有以下两种:

  • 一种是构造空的Image对象的方式,其原因是请求图片并不涉及到跨域的问题;
JavaScript

var url = 'xxx'; new Image().src = url;

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f00bfee170123843269-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f00bfee170123843269-2">
2
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f00bfee170123843269-1" class="crayon-line">
var url = 'xxx';
</div>
<div id="crayon-5b8f00bfee170123843269-2" class="crayon-line crayon-striped-line">
new Image().src = url;
</div>
</div></td>
</tr>
</tbody>
</table>
  • 利用Ajax上报日志,必须对日志服务器接口开启跨域请求头部Access-Control-Allow-Origin:*,这里Ajax就并不强制使用GET请求了,即可克服URL长度限制的问题。
JavaScript

if (XMLHttpRequest) { var xhr = new XMLHttpRequest();
xhr.open('post', 'https://log.xxx.com', true); //
上报给node中间层处理 xhr.setRequestHeader('Content-Type',
'application/json'); // 设置请求头
xhr.send(JSON.stringify(errorObj)); // 发送参数 }

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f00bfee174544186263-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f00bfee174544186263-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f00bfee174544186263-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f00bfee174544186263-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f00bfee174544186263-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f00bfee174544186263-6">
6
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f00bfee174544186263-1" class="crayon-line">
if (XMLHttpRequest) {
</div>
<div id="crayon-5b8f00bfee174544186263-2" class="crayon-line crayon-striped-line">
  var xhr = new XMLHttpRequest();
</div>
<div id="crayon-5b8f00bfee174544186263-3" class="crayon-line">
  xhr.open('post', 'https://log.xxx.com', true); // 上报给node中间层处理
</div>
<div id="crayon-5b8f00bfee174544186263-4" class="crayon-line crayon-striped-line">
  xhr.setRequestHeader('Content-Type', 'application/json'); // 设置请求头
</div>
<div id="crayon-5b8f00bfee174544186263-5" class="crayon-line">
  xhr.send(JSON.stringify(errorObj)); // 发送参数
</div>
<div id="crayon-5b8f00bfee174544186263-6" class="crayon-line crayon-striped-line">
}
</div>
</div></td>
</tr>
</tbody>
</table>

在我的项目中使用的是第一种的方式,也就是构造空的Image对象,但是我们知道对于GET请求会有长度的限制,需要确保的是请求的长度不会超过阈值。

参考文章

  • 脚本错误量极致优化-监控上报与Script error
  • 前端代码异常日志收集与监控
  • 前端魔法堂——异常不仅仅是try/catch

    1 赞 2 收藏 评论

 

省去响应主体

对于我们上报日志,其实对于客户端来说,并不需要考虑上报的结果,甚至对于上报失败,我们也不需要在前端做任何交互,所以上报来说,其实使用HEAD请求就够了,接口返回空的结果,最大地减少上报日志造成的资源浪费。

用火狐浏览器

合并上报

类似于雪碧图的思想,如果我们的应用需要上报的日志数量很多,那么有必要合并日志进行统一的上报。

解决方案可以是尝试在用户离开页面或者组件销毁时发送一个异步的POST请求来进行上报,但是尝试在卸载(unload)文档之前向web服务器发送数据。保证在文档卸载期间发送数据一直是一个困难。因为用户代理通常会忽略在卸载事件处理器中产生的异步XMLHttpRequest,因为此时已经会跳转到下一个页面。所以这里是必须设置为同步的XMLHttpRequest请求吗?

JavaScript

window.addEventListener('unload', logData, false); function logData() { var client = new XMLHttpRequest(); client.open("POST", "/log", false); // 第三个参数表明是同步的 xhr client.setRequestHeader("Content-Type", "text/plain;charset=UTF-8"); client.send(analyticsData); }

1
2
3
4
5
6
7
8
window.addEventListener('unload', logData, false);
 
function logData() {
    var client = new XMLHttpRequest();
    client.open("POST", "/log", false); // 第三个参数表明是同步的 xhr
    client.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
    client.send(analyticsData);
}

使用同步的方式势必会对用户体验造成影响,甚至会让用户感受到浏览器卡死感觉,对于产品而言,体验非常不好,通过查阅MDN文档,可以使用sendBeacon()方法,将会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:使它可靠,异步并且不会影响下一页面的加载。此外,代码实际上还要比其他技术简单!

下面的例子展示了一个理论上的统计代码模式——通过使用sendBeacon()方法向服务器发送数据。

JavaScript

window.addEventListener('unload', logData, false); function logData() { navigator.sendBeacon("/log", analyticsData); }

1
2
3
4
5
window.addEventListener('unload', logData, false);
 
function logData() {
    navigator.sendBeacon("/log", analyticsData);
}

打开你要下载的视频页面  等页面加载完成 **然后**打开控制台 F12 控制台**

小结

作为前端开发者而言,要对产品保持敬畏之心,时刻保持对性能追求极致,对异常不可容忍的态度。前端的性能监控与异常上报显得尤为重要。

代码难免有问题,对于异常可以使用window.onerror或者addEventListener的方式添加全局的异常捕获侦听函数,但可能使用这种方式无法正确捕获到错误:对于跨域的脚本,需要对script标签增加一个crossorigin=”anonymous”;对于生产环境打包的代码,无法正确定位到异常产生的行数,可以使用source-map来解决;而对于使用框架的情况,需要在框架统一的异常捕获处埋点。

而对于性能的监控,所幸的是浏览器提供了window.performance API,通过这个API,很便捷地获取到当前页面性能相关的数据。

而这些异常和性能数据如何上报呢?一般说来,为了避免对业务产生的影响,会单独建立日志服务器和日志域名,但对于不同的域名,又会产生跨域的问题。我们可以通过构造空的Image对象来解决,亦或是通过设定跨域请求头部Access-Control-Allow-Origin:*来解决。此外,如果上报的性能和日志数据高频触发,则可以在页面unload时统一上报,而unload时的异步请求又可能会被浏览器所忽略,且不能改为同步请求。此时navigator.sendBeacon API可算帮了我们大忙,它可用于通过HTTP将少量数据异步传输到Web服务器。而忽略页面unload时的影响。

1 赞 1 收藏 评论

图片 42

复制代码开始到代码结束里面的所有内容 到控制台  选择3种下载方式中的一种即可 

去掉 downXXX() 前面的 '//'  直接回车 即可看到下载地址     不用的记得把前面的 '//' 加上

 

**如果觉得下载慢的话  可以配合 IDM  接近满速下载**   

链接: 密码: 8mpv

 

如果遇到报错  试试以下几种方法 :

1、清除缓存强制刷新页面

2、看看 console.log(111) 能否正常打印  如果不能的话 就新建标签页 打开百度盘

3、关闭浏览器 清除缓存 打开浏览器 打开百度盘

如果以上方式都不能正常下载 可以给我留言

再次强调 以上内容仅供交流学习 

本文由澳门新浦京娱乐场网站发布于新浦京娱乐场官网,转载请注明出处:获得百度网盘大小文件真实下载地址,前端品质