当前位置: 主页 > 前端开发

前端解析json数组-前端怎么解析json

发布时间:2023-02-09 09:07   浏览次数:次   作者:佚名

前言

如何快速定位线上bug是大多数开发者都会遇到的问题

web-see[1]前端监控解决方案,提供前端录屏+定位源码,让bug无处遁形

这是前端监控的第二篇文章。 本文介绍如何实现错误恢复功能。 第一篇从0到1搭建前端监控平台,面试必备亮点项目(开源)[2] 没看过的朋友,建议先了解一下

最终效果

在监控后台,通过报错信息列表可以查看具体报错的源码,以及报错时的录屏回放

效果演示:

录屏记录了用户的所有操作,红线代表鼠标的移动轨迹

找到源代码

前端项目发布上线时,一般都会对代码进行压缩、混淆,甚至加密。 在线代码报错时,很难定位到具体源码

SourceMap完美解决了代码逆向解题的问题。 项目打包时,除了生成最终的XXX.js文件外,还会额外生成一个XXX.js.map文件

.map文件包含原始代码及其映射信息,可用于破译错误信息的源代码

源映射文件

先了解SourceMap的基本内容

例如app.a2a3ceec.js的代码如下:

var add=function(x, y){return x+y;};
//# sourceMappingURL=app.a2a3ceec.js.map
复制代码

其中sourceMappingURL用于描述文件对应的map文件

对应的app.a2a3ceec.js.map代码如下:

{
  version : 3// SourceMap标准版本,最新的为3
  file: "js/app.a2a3ceec.js"// 转换后的文件名
  sourceRoot : ""// 转换前的文件所在目录,如果与转换前的文件在同一目录,该项为空
  sources: [ // 转换前的文件,该项是一个数组,表示可能存在多个文件合并
    "webpack://web-see-demo/./src/App.vue",
    "webpack://web-see-demo/./src/main.js"
  ], 
  names: [], // 转换前的所有变量名和属性名
  sourcesContent: [ // 原始文件内容
    "const add = (x,y) => {\n  return x+y;\n}"
  ],
  // 所有映射点
  mappings: "AAAA,IAAM,GAAG,GAAG,UAAC,CAAQ,EAAC,CAAQ;IAC5B,OAAO,CAAC,GAAC,CAAC,CAAC;AACb,CAAC,CAAA"
}
复制代码

其中sources和sourcesContent是关键字段,在下面的还原例子中会用到

source-map-js库

代码还原,这里主要使用source-map-js[3]库,下面介绍如何使用

前端解析json数组_js json数组解析_前端怎么解析json

示例代码:

import sourceMap from 'source-map-js';

/**
* findCodeBySourceMap用于获取map文件对应的源代码
* @param { string } fileName .map文件名称
* @param { number } line 发生错误的行号
* @param { number } column 发生错误的列号
* @param { function } 回调函数,返回对应的源码
*/

const findCodeBySourceMap = async ({ fileName, line, column }, callback) => {
  // loadSourceMap 用于获取服务器上 .map 的文件内容
  let sourceData = await loadSourceMap(fileName);
  let { sourcesContent, sources } = sourceData;
  // SourceMapConsumer实例表示一个已解析的源映射
  // 可以通过在生成的源中给它一个文件位置来查询有关原始文件位置的信息
  let consumer = await new sourceMap.SourceMapConsumer(sourceData);
  // 输入错误的发生行和列,可以得到源码对应原始文件、行和列信息
  let result = consumer.originalPositionFor({
    lineNumber(line),
    columnNumber(column)
  });
  // 从sourcesContent得到具体的源码信息
  let code = sourcesContent[sources.indexOf(result.source)];
  ……
  callback(code)
复制代码

本节代码仓库[4]

源图恢复过程:

1、从服务器获取指定.map的文件内容

2.new一个SourceMapConsumer实例,代表一个解析后的source map,给它一个文件位置,可以查询原始文件位置的信息

3.输入错误所在的行列,即可得到原文件名、行列信息对应的源码

4.从源文件的sourcesContent字段获取对应的源代码信息

接下来的重点就变成了:如何获取出错的原始文件名、行列信息

错误堆栈解析器库

通过第一篇的介绍,我们知道捕获错误的方式有很多种

比如error事件,unhandledrejection事件,vue中的Vue.config.errorHander前端解析json数组,react中的componentDidCatch

为了消除浏览器之间的差异,使用error-stack-parser[5]库提取给定错误的原始文件名、行列信息

示例代码:

import ErrorStackParser from 'error-stack-parser';

ErrorStackParser.parse(new Error('BOOM'));

// 返回值 StackFrame 堆栈列表
[
    StackFrame({functionName: 'foo', args: [], fileName: 'path/to/file.js', lineNumber: 35, columnNumber: 79, isNative: false, isEval: false}),
    StackFrame({functionName: 'Bar', fileName: 'https://cdn.somewherefast.com/utils.min.js', lineNumber: 1, columnNumber: 832, isNative: false, isEval: false, isConstructor: true}),
    StackFrame(... and so on ...)
]
复制代码

这里简单介绍一下JS栈列表

堆栈示例:

function c() {
  try {
    var bar = baz;
    throw new Error()
  } catch (e) {
    console.log(e.stack);
  }
}
function b() {
  c();
}
function a() {
  b();
}
a();
复制代码

上述代码中,c函数执行时会报错,调用栈为a -> b -> c,如下图所示:

前端解析json数组_js json数组解析_前端怎么解析json

堆栈.png

一般我们只需要定位c函数的栈信息,所以在使用error-stack-parser库时,只取StackFrame数组中的第一个元素

js json数组解析_前端解析json数组_前端怎么解析json

最终代码:

import ErrorStackParser from 'error-stack-parser';

// 取StackFrame数组中的第一个元素
let stackFrame = ErrorStackParser.parse(error)[0];
// 获取对应的原始文件名、行和列信息,并上报
let { fileName, columnNumber, lineNumber } = stackFrame;
复制代码

演示

下载web-see-demo[6]安装运行

1)点击js报错按钮,执行HomeView.vue文件中的codeErr方法

codeErr的源码是:

前端怎么解析json_前端解析json数组_js json数组解析

代码错误.png

2)Vue.config.errorHander中捕获的错误信息为:

前端解析json数组_js json数组解析_前端怎么解析json

长度.png

3)ErrorStackParser.parse解析出来的stackFrame为:

js json数组解析_前端怎么解析json_前端解析json数组

堆栈框架.png

4)consumer.originalPositionFor恢复后的结果为:

前端解析json数组_js json数组解析_前端怎么解析json

结果.png

5)最终源码:

前端解析json数组_前端怎么解析json_js json数组解析

code.png 过程总结

js json数组解析_前端怎么解析json_前端解析json数组

源图.png

js json数组解析_前端解析json数组_前端怎么解析json

如上图所示,总结定位源码的过程:

1.在项目中引入监控SDK,打包后发布js文件到服务器

2.将.map文件放到指定地址统一存放

3.在线代码报错时,使用error-stack-parser获取具体的原始文件名,行列信息,并报错

4.使用source-map从.map文件中获取对应的源码并显示

前端录屏

web-see监控通过rrweb[7]提供前端录屏功能

网络使用

先介绍下如何在vue中使用

录制示例:

import { record } from 'rrweb';
// events存储录屏信息
let events = [];
// record 用于记录 `DOM` 中的所有变更
rrweb.record({
  emit(event, isCheckout) {
    // isCheckout 是一个标识,告诉你重新制作了快照
    if (isCheckout) {
      events.push([]);
    }
    events.push(event);
  },
  recordCanvastrue// 记录 canvas 内容
  checkoutEveryNms10 * 1000// 每10s重新制作快照
  checkoutEveryNth200// 每 200 个 event 重新制作快照
});
复制代码

播放示例:

<template>
  <div ref='player'>
  div>
template>
<script>
import rrwebPlayer from 'rrweb-player';
import 'rrweb-player/dist/style.css';
export default {
   mounted() {
     // 将记录的变更按照对应的时间一一重放
     new rrwebPlayer(
        {
          targetthis.$refs.player, // 回放所需要的HTML元素
          data: { events }
        },
        {
          UNSAFE_replayCanvastrue // 回放 canvas 内容
        }
     )
   }
}
script
>
复制代码

rrweb原理分析

rrweb主要由三个库组成:rrweb、rrweb-player和rrweb-snapshot:

1)rrweb:提供两种方法:record和replay; record方法用于记录页面DOM的变化,replay方法支持根据时间戳恢复DOM的变化

2)rrweb-player:基于svelte模板实现,为rrweb提供一个播放GUI工具,支持暂停、倍速播放、拖动时间轴等功能。调用rrweb提供的replay等方法在内部

3)rrweb-snapshot:包括快照和重建两个特性,快照用于将DOM序列化为增量快照,重建负责将增量快照恢复为DOM

rrweb的整体流程:

1)rrweb在录制的时候,会先做一个首屏DOM快照,遍历整个页面的DOM树,转换成JSON结构数据,使用增量快照处理方式,通过mutationObserver获取DOM增量变化,并同步转换成JSON数据存储

2)整个录制过程会生成一个唯一的id来确定增量数据对应的DOM节点,通过时间戳保证播放顺序。

3)播放时会创建一个iframe作为事件播放的容器,重构首屏DOM快照。 在遍历 JSON 时,会根据序列化后的节点数据构建实际的 DOM 节点

js json数组解析_前端怎么解析json_前端解析json数组

4)rrweb可以监控的用户行为包括:鼠标移动、鼠标交互、页面滚动、窗口变化、用户输入等,可以通过添加相应的监控事件来实现

压缩数据

如果一直录屏,数据量巨大

根据实测,记录时间为10s,数据大小约为8M(页面复杂度不同,用户操作频率不同,导致大小不同)

如果不对数据进行压缩,直接传输到后端,面对大量的用户,需要非常高的带宽来支撑。 还好rrweb官方提供了数据压缩功能[8]

基于packFn的单数据压缩,录制时可作为packFn传入

rrweb.record({
  emit(event) {},
  packFn: rrweb.pack,
});
复制代码

播放时需要传入 rrweb.unpack 为 unpackFn

const replayer = new rrweb.Replayer(events, {
unpackFn: rrweb.unpack,
});
复制代码

不过官方的压缩方式是对每个事件数据单独进行压缩,压缩率并不高。根据实测,压缩率在70%左右。 比如原来8M的数据,压缩后大约是2.4M。

官方建议是一次性对多个事件进行批量压缩,这样压缩效果更好

web-see内部使用了pako.js[9]和js-base64[10]的组合,实测压缩率超过85%,原始8M数据压缩后约1.2M

压缩代码示例:

import pako from 'pako';
import { Base64 } from 'js-base64';

// 压缩
export function zip(data{
  if (!data) return data;
  // 判断数据是否需要转为JSON
  const dataJson = typeof data !== 'string' && typeof data !== 'number' ? JSON.stringify(data) : data;
  // 使用Base64.encode处理字符编码,兼容中文
  const str = Base64.encode(dataJson);
  let binaryString = pako.gzip(str);
  let arr = Array.from(binaryString);
  let s = '';
  arr.forEach((item) => {
    s += String.fromCharCode(item);
  });
  return Base64.btoa(s);
}
复制代码

解压代码示例:

import { Base64 } from 'js-base64';
import pako from 'pako';

// 解压
export function unzip(b64Data) {
let strData = Base64.atob(b64Data);
let charData = strData.split('').map(function (x) {
return x.charCodeAt(0);
});
let binData = new Uint8Array(charData);
let data = pako.ungzip(binData);
// ↓切片处理数据,防止内存溢出报错↓
let str = '';
const chunk = 8 * 1024;
let i;
for (i = 0; i < data.length / chunk; i++) {
str += String.fromCharCode.apply(null, data.slice(i * chunk, (i + 1) * chunk));
}
str += String.fromCharCode.apply(null, data.slice(i * chunk));
// ↑切片处理数据,防止内存溢出报错↑
const unzipStr = Base64.decode(str);
let result = '';
// 对象或数组进行JSON转换
try {
result = JSON.parse(unzipStr);
} catch (error) {
if (/Unexpected token o in JSON at position 0/.test(error)) {
// 如果没有转换成功,代表值为基本数据,直接赋值
result = unzipStr;
}
}
return result;
}
复制代码

何时上报录屏数据

一般关心的是页面报错时用户做了什么操作,所以目前只向服务器报错前10s的录屏

报错时如何只上报录屏信息?

1)在窗口上设置hasError和recordScreenId变量,hasError用于判断某个时间码是否报错; recordScreenId 用于记录录屏的id

2)当页面发出报错需要上报时,判断是否开启了屏幕录制,如果开启则将hasError设置为true,将window上的recordScreenId存储在本次上报信息的data中

3) rrweb 将重新创建快照的频率设置为 10s。 每次重置录屏,判断hasError是否为真(即这段时间是否出错),如果是,则上报本次录屏信息并重置 录屏信息和recordScreenId,作为下次使用录屏

js json数组解析_前端怎么解析json_前端解析json数组

4)后台报错列表,从这个报错的数据中取出recordScreenId播放录屏

录屏代码示例:

handleScreen() {
try {
// 存储录屏信息
let events = [];
record({
emit(event, isCheckout) {
if (isCheckout) {
// 此段时间内发生错误,上报录屏信息
if (_support.hasError) {
let recordScreenId = _support.recordScreenId;
// 重置recordScreenId,作为下次使用
_support.recordScreenId = generateUUID();
transportData.send({
type: EVENTTYPES.RECORDSCREEN,
recordScreenId,
time: getTimestamp(),
status: STATUS_CODE.OK,
events: zip(events)
});
events = [];
_support.hasError = false;
} else {
// 不上报,清空录屏
events = [];
_support.recordScreenId = generateUUID();
}
}
events.push(event);
},
recordCanvas: true,
// 默认每10s重新制作快照
checkoutEveryNms: 1000 * options.recordScreentime
});
复制代码

遗留问题前端解析json数组,在线解决

根据canvas官方配置,经验证rrweb还是不支持canvas录制。 比如使用echarts画图时,图文区录屏显示空白

官方配置[11]如下:

前端解析json数组_前端怎么解析json_js json数组解析

画布.png

测试demo[12]如下:

前端怎么解析json_前端解析json数组_js json数组解析

电子图表.png

录屏回放,图形区一片空白:

前端怎么解析json_前端解析json数组_js json数组解析

画布.gif

有研究过这方面的大佬指导一下,问题出在哪里,谢谢

总结

前端录屏+定位源码是目前比较流行的错误恢复方式,对于快速定位线上bug大有裨益

这两篇文章只是前端监控的入门介绍,还有很多可以深挖的地方。 欢迎小伙伴们多多讨论交流

最后推荐一篇阿里前端监控负责人的专题演讲:《大前端时代前端监控最佳实践》[13],了解前端的天花板有多高监测群岛

天冷了,别忘了穿秋裤

前端怎么解析json_前端解析json数组_js json数组解析

关于这篇文章