前语

常常会苦恼,平常做的项目很普通,没啥亮点;面试中也经常会被问到:做过哪些亮点项目吗?

前端监控便是一个很有亮点的项目,各个大厂都有自己的内部完结,没有监控的项目好比是在裸奔

文章分成以下六部分来介绍:

  • 自研监控渠道处理了哪些痛点,完结了什么亮点功用?

  • 相比sentry等监控计划,自研监控的优势有哪些?

  • 前端监控的规划计划、监控的目的

  • 数据的搜集办法:过错信息、功用数据、用户行为、加载资源、个性化目标等

  • 规划开发一个完好的监控SDK

  • 监控后台过错复原演示示例

痛点

某⼀天用户:xx商品无法下单!
⼜⼀天运营:xx广告在手机端打开不了!

我们反应的bug,怎样都复现不出来,为难的要死!

怎么记载项目的过错,并将过错复原出来,这是监控渠道要处理的痛点之一

过错复原

web-see 监控供给三种过错复原办法:定位源码、播映录屏、记载用户行为

定位源码

项目犯错,要是能定位到源码就好了,可线上的项目都是打包后的代码,也不能把 .map 文件放到线上

监控渠道经过 source-map 能够完结该功用

最终效果:

从0到1树立前端监控渠道,面试必备的亮点项目

播映录屏

大都场景下,定位到详细的源码,就能够定位bug,但假如是用户做了反常操作,或许是在某些杂乱操作下才呈现的bug,只是经过定位源码,仍是不能复原过错

要是能把用户的操作都录制下来,然后经过回放来复原过错就好了

监控渠道经过 rrweb 能够完结该功用

最终效果:

从0到1树立前端监控渠道,面试必备的亮点项目

回放的录屏中,记载了用户的一切操作,红色的线代表了鼠标的移动轨迹

前端录屏确实是件很帅的事情,可是不能走极端,假如把用户的一切操作都录制下来,是没有意义的

我们更重视的是,页面报错的时候用户做了哪些操作,所以监控渠道只把报错前10s的视频保存下来(单次录屏时长也能够自定义)

记载用户行为

经过 定位源码 + 播映录屏 这套组合,复原过错应该够用了,一起监控渠道也供给了 记载用户行为 这种办法

假设用户做了许多操作,操作的距离超越了单次录屏时长,录制的视频可能是不完好的,此刻能够凭借用户行为来剖析用户的操作,协助复现bug

最终效果:

从0到1树立前端监控渠道,面试必备的亮点项目

用户行为列表记载了:鼠标点击、接口调用、资源加载、页面路由改变、代码报错等信息

经过 定位源码、播映录屏、记载用户行为 这三板斧,处理了复现bug的痛点

自研监控的优势

为什么不直接用sentry私有化布置,而选择自研前端监控?

这是优先要考虑的问题,sentry作为前端监控的职业标杆,有许多能够学习的地方

相比sentry,自研监控渠道的优势在于:

1、能够将公司的SDK统一成一个,包含但不限于:监控SDK、埋点SDK、录屏SDK、广告SDK等

2、供给了更多的过错复原办法,一起过错信息能够和埋点信息联动,便可拿到更细致的用户行为栈,更快的排查线上过错

3、监控自定义的个性化目标:如 long task、memory页面内存、首屏加载时刻等。过多的长使命会形成页面丢帧、卡顿;过大的内存可能会形成低端机器的卡死、溃散

4、统计资源缓存率,来判别项目的缓存战略是否合理,提高缓存率能够减少服务器压力,也能够提高页面的打开速度

5、供给了采样对比+ 轮询修正机制的白屏检测计划,用于检测页面是否一向处于白屏状况,让开发者知道页面什么时候白了,详细完结见 前端白屏的检测计划,处理你的线上之忧

规划思路

一个完好的前端监控渠道包含三个部分:数据搜集与上报、数据剖析和存储、数据展现

从0到1树立前端监控渠道,面试必备的亮点项目

监控目的

从0到1树立前端监控渠道,面试必备的亮点项目

反常剖析

依照 5W1H 规律来剖析前端反常,需求知道以下信息

  1. What,发⽣了什么过错:JS过错、异步过错、资源加载、接口过错等
  2. When,呈现的时刻段,如时刻戳
  3. Who,影响了多少用户,包含报错事情数、IP
  4. Where,呈现的页面是哪些,包含页面、对应的设备信息
  5. Why,过错的原因是为什么,包含过错仓库、⾏列、SourceMap、反常录屏
  6. How,怎么定位复原问题,怎么反常报警,防止相似的过错产生

过错数据搜集

过错信息是最根底也是最重要的数据,过错信息首要分为下面几类:

  • JS 代码运转过错、语法过错等
  • 异步过错等
  • 静态资源加载过错
  • 接口恳求报错

过错捕获办法

1)try/catch

只能捕获代码惯例的运转过错,语法过错和异步过错不能捕获到

示例:

// 示例1:惯例运转时过错,能够捕获 ✅
 try {
   let a = undefined;
   if (a.length) {
     console.log('111');
   }
 } catch (e) {
   console.log('捕获到反常:', e);
}
// 示例2:语法过错,不能捕获 ❌  
try {
  const notdefined,
} catch(e) {
  console.log('捕获不到反常:', 'Uncaught SyntaxError');
}
// 示例3:异步过错,不能捕获 ❌
try {
  setTimeout(() => {
    console.log(notdefined);
  }, 0)
} catch(e) {
  console.log('捕获不到反常:', 'Uncaught ReferenceError');
}

2) window.onerror

window.onerror 能够捕获惯例过错、异步过错,但不能捕获资源过错

/**
* @param { string } message 过错信息
* @param { string } source 产生过错的脚本URL
* @param { number } lineno 产生过错的行号
* @param { number } colno 产生过错的列号
* @param { object } error Error目标
*/
window.onerror = function(message, source, lineno, colno, error) {
   console.log('捕获到的过错信息是:', message, source, lineno, colno, error )
}

示例:

window.onerror = function(message, source, lineno, colno, error) {
  console.log("捕获到的过错信息是:", message, source, lineno, colno, error);
};
// 示例1:惯例运转时过错,能够捕获 ✅
console.log(notdefined);
// 示例2:语法过错,不能捕获 ❌
const notdefined;
// 示例3:异步过错,能够捕获 ✅
setTimeout(() => {
  console.log(notdefined);
}, 0);
// 示例4:资源过错,不能捕获 ❌
let script = document.createElement("script");
script.type = "text/javascript";
script.src = "https://www.test.com/index.js";
document.body.appendChild(script);

3) window.addEventListener

当静态资源加载失利时,会触发 error 事情, 此刻 window.onerror 不能捕获到

示例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<script>
  window.addEventListener('error', (error) => {
    console.log('捕获到反常:', error);
  }, true)
</script>
<!-- 图片、script、css加载过错,都能被捕获 ✅ -->
<img src="https://test.cn/.png">
<script src="https://test.cn/.js"></script>
<link href="https://test.cn/.css" rel="stylesheet" />
<script>
  // new Image过错,不能捕获 ❌
  // new Image运用的比较少,能够自己独自处理
  new Image().src = 'https://test.cn/.png'
</script>
</html>

4)Promise过错

Promise中抛出的过错,无法被 window.onerror、try/catch、 error 事情捕获到,可经过 unhandledrejection 事情来处理

示例:

try {
  new Promise((resolve, reject) => {
    JSON.parse("");
    resolve();
  });
} catch (err) {
  // try/catch 不能捕获Promise中过错 ❌
  console.error("in try catch", err);
}
// error事情 不能捕获Promise中过错 ❌
window.addEventListener(
  "error",
  error => {
    console.log("捕获到反常:", error);
  },
  true
);
// window.onerror 不能捕获Promise中过错 ❌
window.onerror = function(message, source, lineno, colno, error) {
  console.log("捕获到反常:", { message, source, lineno, colno, error });
};
// unhandledrejection 能够捕获Promise中的过错 ✅
window.addEventListener("unhandledrejection", function(e) {
  console.log("捕获到反常", e);
  // preventDefault阻挠传达,不会在控制台打印
  e.preventDefault();
});

Vue 过错

Vue项目中,window.onerror 和 error 事情不能捕获到惯例的代码过错

反常代码:

export default {
  created() {
    let a = null;
    if(a.length > 1) {
        // ...
    }
  }
};

main.js中增加捕获代码:

window.addEventListener('error', (error) => {
  console.log('error', error);
});
window.onerror = function (msg, url, line, col, error) {
  console.log('onerror', msg, url, line, col, error);
};

控制台会报错,可是 window.onerror 和 error 不能捕获到

从0到1树立前端监控渠道,面试必备的亮点项目

vue 经过Vue.config.errorHander 来捕获反常:

Vue.config.errorHandler = (err, vm, info) => {
    console.log('进来啦~', err);
}

控制台打印:

从0到1树立前端监控渠道,面试必备的亮点项目

errorHandler源码剖析

src/core/util目录下,有一个error.js文件

function globalHandleError (err, vm, info) {
  // 获取大局装备,判别是否设置处理函数,默认undefined
  // 装备config.errorHandler办法
  if (config.errorHandler) {
    try {
      // 履行 errorHandler
      return config.errorHandler.call(null, err, vm, info)
    } catch (e) {
      // 假如开发者在errorHandler函数中,手动抛出相同过错信息throw err,判别err信息是否持平,防止log两次
      if (e !== err) {
        logError(e, null, 'config.errorHandler')
      }
    }
  }
  // 没有装备,惯例输出
  logError(err, vm, info)
}
function logError (err, vm, info) {
  if (process.env.NODE_ENV !== 'production') {
    warn(`Error in ${info}: "${err.toString()}"`, vm)
  }
  /* istanbul ignore else */
  if ((inBrowser || inWeex) && typeof console !== 'undefined') {
    console.error(err)
  } else {
    throw err
  }
}

经过源码明白了,vue 运用 try/catch 来捕获惯例代码的报错,被捕获的过错会经过 console.error 输出而防止运用溃散

能够在 Vue.config.errorHandler 中将捕获的过错上报

Vue.config.errorHandler = function (err, vm, info) {
  // handleError办法用来处理过错并上报
  handleError(err);
}

React 过错

从 react16 开端,官方供给了 ErrorBoundary 过错鸿沟的功用,被该组件包裹的子组件,render 函数报错时会触发离当时组件最近父组件的ErrorBoundary

生产环境,一旦被 ErrorBoundary 捕获的过错,也不会触发大局的 window.onerror 和 error 事情

父组件代码:

import React from 'react';
import Child from './Child.js';
// window.onerror 不能捕获render函数的过错 ❌
window.onerror = function (err, msg, c, l) {
  console.log('err', err, msg);
};
// error 不能render函数的过错 ❌
window.addEventListener( 'error', (error) => {
    console.log('捕获到反常:', error);
  },true
);
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError(error) {
    // 更新 state 使下一次烘托能够显现降级后的 UI
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    // componentDidCatch 能够捕获render函数的过错 
    console.log(error, errorInfo)
    // 相同能够将过错日志上报给服务器
    reportError(error, errorInfo);
  }
  render() {
    if (this.state.hasError) {
      // 自定义降级后的 UI 并烘托
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}
function Parent() {
  return (
    <div>
      父组件
      <ErrorBoundary>
        <Child />
      </ErrorBoundary>
    </div>
  );
}
export default Parent;

子组件代码:

// 子组件 烘托犯错
function Child() {
  let list = {};
  return (
    <div>
      子组件
      {list.map((item, key) => (
        <span key={key}>{item}</span>
      ))}
    </div>
  );
}
export default Child;

同vue项目的处理相似,react项目中,能够在 componentDidCatch 中将捕获的过错上报

componentDidCatch(error, errorInfo) {
  // handleError办法用来处理过错并上报
  handleError(err);
}

跨域问题

假如当时页面中,引入了其他域名的JS资源,假如资源呈现过错,error 事情只会监测到一个script error 的反常。

示例:

window.addEventListener("error", error => {
  console.log("捕获到反常:", error);
}, true );
// 当时页面加载其他域的资源,如https://www.test.com/index.js
<script src="https://www.test.com/index.js"></script>
// 加载的https://www.test.com/index.js的代码
function fn() {
  JSON.parse("");
}
fn();

报错信息:

从0到1树立前端监控渠道,面试必备的亮点项目

只能捕获到 script error 的原因:

是因为浏览器根据安全考虑,成心躲藏了其它域JS文件抛出的详细过错信息,这样能够有用防止敏感信息无意中被第三方(不受控制的)脚本捕获到,因此,浏览器只允许同域下的脚本捕获详细的过错信息

处理办法:

前端script加crossorigin,后端装备 Access-Control-Allow-Origin

<script src="https://www.test.com/index.js" crossorigin></script>

增加 crossorigin 后能够捕获到完好的报错信息:

从0到1树立前端监控渠道,面试必备的亮点项目

假如不能修改服务端的恳求头,能够考虑经过运用 try/catch 绕过,将过错抛出

<!doctype html>
<html>
<body>
  <script src="https://www.test.com/index.js"></script>
  <script>
  window.addEventListener("error", error => { 
    console.log("捕获到反常:", error);
  }, true );
  try {
    // 调用https://www.test.com/index.js中定义的fn办法
    fn(); 
  } catch (e) {
    throw e;
  }
  </script>
</body>
</html>

接口过错

接口监控的完结原理:针对浏览器内置的 XMLHttpRequest、fetch 目标,运用 AOP 切片编程重写该办法,完结对恳求的接口阻拦,然后获取接口报错的状况并上报

1)阻拦XMLHttpRequest恳求示例:

function xhrReplace() {
  if (!("XMLHttpRequest" in window)) {
    return;
  }
  const originalXhrProto = XMLHttpRequest.prototype;
  // 重写XMLHttpRequest 原型上的open办法
  replaceAop(originalXhrProto, "open", originalOpen => {
    return function(...args) {
      // 获取恳求的信息
      this._xhr = {
        method: typeof args[0] === "string" ? args[0].toUpperCase() : args[0],
        url: args[1],
        startTime: new Date().getTime(),
        type: "xhr"
      };
      // 履行原始的open办法
      originalOpen.apply(this, args);
    };
  });
  // 重写XMLHttpRequest 原型上的send办法
  replaceAop(originalXhrProto, "send", originalSend => {
    return function(...args) {
      // 当恳求完毕时触发,无论恳求成功仍是失利都会触发
      this.addEventListener("loadend", () => {
        const { responseType, response, status } = this;
        const endTime = new Date().getTime();
        this._xhr.reqData = args[0];
        this._xhr.status = status;
        if (["", "json", "text"].indexOf(responseType) !== -1) {
          this._xhr.responseText =
            typeof response === "object" ? JSON.stringify(response) : response;
        }
        // 获取接口的恳求时长
        this._xhr.elapsedTime = endTime - this._xhr.startTime;
        // 上报xhr接口数据
        reportData(this._xhr);
      });
      // 履行原始的send办法
      originalSend.apply(this, args);
    };
  });
}
/**
 * 重写指定的办法
 * @param { object } source 重写的目标
 * @param { string } name 重写的特点
 * @param { function } fn 阻拦的函数
 */
function replaceAop(source, name, fn) {
  if (source === undefined) return;
  if (name in source) {
    var original = source[name];
    var wrapped = fn(original);
    if (typeof wrapped === "function") {
      source[name] = wrapped;
    }
  }
}

2)阻拦fetch恳求示例:

function fetchReplace() {
  if (!("fetch" in window)) {
    return;
  }
  // 重写fetch办法
  replaceAop(window, "fetch", originalFetch => {
    return function(url, config) {
      const sTime = new Date().getTime();
      const method = (config && config.method) || "GET";
      let handlerData = {
        type: "fetch",
        method,
        reqData: config && config.body,
        url
      };
      return originalFetch.apply(window, [url, config]).then(
        res => {
          // res.clone克隆,防止被标记已消费
          const tempRes = res.clone();
          const eTime = new Date().getTime();
          handlerData = {
            ...handlerData,
            elapsedTime: eTime - sTime,
            status: tempRes.status
          };
          tempRes.text().then(data => {
            handlerData.responseText = data;
            // 上报fetch接口数据
            reportData(handlerData);
          });
          // 回来原始的结果,外部继续运用then接纳
          return res;
        },
        err => {
          const eTime = new Date().getTime();
          handlerData = {
            ...handlerData,
            elapsedTime: eTime - sTime,
            status: 0
          };
          // 上报fetch接口数据
          reportData(handlerData);
          throw err;
        }
      );
    };
  });
}

功用数据搜集

谈到功用数据搜集,就会提及加载进程模型图:

从0到1树立前端监控渠道,面试必备的亮点项目

以Spa页面来说,页面的加载进程大致是这样的:

从0到1树立前端监控渠道,面试必备的亮点项目

包含dns查询、树立tcp衔接、发送http恳求、回来html文档、html文档解析等阶段

最初,能够经过window.performance.timing来获取加载进程模型中各个阶段的耗时数据

// window.performance.timing 各字段阐明
{
    navigationStart,  // 同一个浏览器上下文中,上一个文档完毕时的时刻戳。假如没有上一个文档,这个值会和 fetchStart 相同。
    unloadEventStart,  // 上一个文档 unload 事情触发时的时刻戳。假如没有上一个文档,为 0。
    unloadEventEnd, // 上一个文档 unload 事情完毕时的时刻戳。假如没有上一个文档,为 0。
    redirectStart, // 表明榜首个 http 重定向开端时的时刻戳。假如没有重定向或许有一个非同源的重定向,为 0。
    redirectEnd, // 表明最终一个 http 重定向完毕时的时刻戳。假如没有重定向或许有一个非同源的重定向,为 0。
    fetchStart, // 表明浏览器准备好运用 http 恳求来获取文档的时刻戳。这个时刻点会在检查任何缓存之前。
    domainLookupStart, // 域名查询开端的时刻戳。假如运用了耐久衔接或许本地有缓存,这个值会和 fetchStart 相同。
    domainLookupEnd, // 域名查询完毕的时刻戳。假如运用了耐久衔接或许本地有缓存,这个值会和 fetchStart 相同。
    connectStart, // http 恳求向服务器发送衔接恳求时的时刻戳。假如运用了耐久衔接,这个值会和 fetchStart 相同。
    connectEnd, // 浏览器和服务器之前树立衔接的时刻戳,一切握手和认证进程悉数完毕。假如运用了耐久衔接,这个值会和 fetchStart 相同。
    secureConnectionStart, // 浏览器与服务器开端安全链接的握手时的时刻戳。假如当时网页不要求安全衔接,回来 0。
    requestStart, // 浏览器向服务器发起 http 恳求(或许读取本地缓存)时的时刻戳,即获取 html 文档。
    responseStart, // 浏览器从服务器接纳到榜首个字节时的时刻戳。
    responseEnd, // 浏览器从服务器承受到最终一个字节时的时刻戳。
    domLoading, // dom 结构开端解析的时刻戳,document.readyState 的值为 loading。
    domInteractive, // dom 结构解析完毕,开端加载内嵌资源的时刻戳,document.readyState 的状况为 interactive。
    domContentLoadedEventStart, // DOMContentLoaded 事情触发时的时刻戳,一切需求履行的脚本履行完毕。
    domContentLoadedEventEnd,  // DOMContentLoaded 事情完毕时的时刻戳
    domComplete, // dom 文档完结解析的时刻戳, document.readyState 的值为 complete。
    loadEventStart, // load 事情触发的时刻。
    loadEventEnd // load 时刻完毕时的时刻。
}

后来 window.performance.timing 被废弃,经过 PerformanceObserver 来获取。旧的 api,回来的是一个 UNIX 类型的绝对时刻,和用户的系统时刻相关,剖析的时候需求再次计算。而新的 api,回来的是一个相对时刻,能够直接用来剖析

现在 chrome 开发团队供给了 web-vitals 库,方便来计算各功用数据(注意:web-vitals 不支持safari浏览器)

关于 FP、FCP、LCP、CLS、TTFB、FID 等功用目标的含义和计算办法,我在 「历时8个月」10万字前端常识系统总结(工程化篇) 中有详细的解说,这儿不再赘述

用户行为数据搜集

用户行为包含:页面路由改变、鼠标点击、资源加载、接口调用、代码报错等行为

规划思路

1、经过Breadcrumb类来创立用户行为的目标,来存储和管理一切的用户行为

2、经过重写或增加相应的事情,完结用户行为数据的搜集

用户行为代码示例:

// 创立用户行为类
class Breadcrumb {
  // maxBreadcrumbs控制上报用户行为的最大条数
  maxBreadcrumbs = 20;
  // stack 存储用户行为
  stack = [];
  constructor() {}
  // 增加用户行为栈
  push(data) {
    if (this.stack.length >= this.maxBreadcrumbs) {
      // 超出则删去榜首条
      this.stack.shift();
    }
    this.stack.push(data);
    // 依照时刻排序
    this.stack.sort((a, b) => a.time - b.time);
  }
}
let breadcrumb = new Breadcrumb();
// 增加一条页面跳转的行为,从home页面跳转到about页面
breadcrumb.push({
  type: "Route",
  form: '/home',
  to: '/about'
  url: "http://localhost:3000/index.html",
  time: "1668759320435"
});
// 增加一条用户点击行为
breadcrumb.push({
  type: "Click",
  dom: "<button id='btn'>按钮</button>",
  time: "1668759620485"
});
// 增加一条调用接口行为
breadcrumb.push({
  type: "Xhr",
  url: "http://10.105.10.12/monitor/open/pushData",
  time: "1668760485550"
});
// 上报用户行为
reportData({
  uuid: "a6481683-6d2e-4bd8-bba1-64819d8cce8c",
  stack: breadcrumb.getStack()
});

页面跳转

经过监听路由的改变来判别页面跳转,路由有history、hash两种形式,history形式能够监听popstate事情,hash形式经过重写 pushState和 replaceState事情

vue项目中不能经过 hashchange 事情来监听路由改变,vue-router 底层调用的是 history.pushStatehistory.replaceState,不会触发 hashchange

vue-router源码:

function pushState (url, replace) {
  saveScrollPosition();
  var history = window.history;
  try {
    if (replace) {
      history.replaceState({ key: _key }, '', url);
    } else {
      _key = genKey();
      history.pushState({ key: _key }, '', url);
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url);
  }
}
...
// this.$router.push时触发
function pushHash (path) { 
  if (supportsPushState) {
    pushState(getUrl(path));
  } else {
    window.location.hash = path;
  }
}

经过重写 pushState、replaceState 事情来监听路由改变

// lastHref 前一个页面的路由
let lastHref = document.location.href;
function historyReplace() {
  function historyReplaceFn(originalHistoryFn) {
    return function(...args) {
      const url = args.length > 2 ? args[2] : undefined;
      if (url) {
        const from = lastHref;
        const to = String(url);
        lastHref = to;
        // 上报路由改变
        reportData("routeChange", {
          from,
          to
        });
      }
      return originalHistoryFn.apply(this, args);
    };
  }
  // 重写pushState事情
  replaceAop(window.history, "pushState", historyReplaceFn);
  // 重写replaceState事情
  replaceAop(window.history, "replaceState", historyReplaceFn);
}
function replaceAop(source, name, fn) {
  if (source === undefined) return;
  if (name in source) {
    var original = source[name];
    var wrapped = fn(original);
    if (typeof wrapped === "function") {
      source[name] = wrapped;
    }
  }
}

用户点击

给 document 目标增加click事情,并上报

function domReplace() {
  document.addEventListener("click",({ target }) => {
      const tagName = target.tagName.toLowerCase();
      if (tagName === "body") {
        return null;
      }
      let classNames = target.classList.value;
      classNames = classNames !== "" ? ` class="${classNames}"` : "";
      const id = target.id ? ` id="${target.id}"` : "";
      const innerText = target.innerText;
      // 获取包含id、class、innerTextde字符串的标签
      let dom = `<${tagName}${id}${
        classNames !== "" ? classNames : ""
      }>${innerText}</${tagName}>`;
      // 上报
      reportData({
        type: 'Click',
        dom
      });
    },
    true
  );
}

资源加载

获取页面中加载的资源信息,比方它们的 url 是什么、加载了多久、是否来自缓存等,最终生成 资源加载瀑布图

从0到1树立前端监控渠道,面试必备的亮点项目

瀑布图展现了浏览器为烘托网页而加载的一切的资源,包含加载的次序和每个资源的加载时刻

剖析这些资源是怎么加载的, 能够协助我们了解究竟是什么原因拖慢了网页,然后采纳对应的办法来提高网页速度

能够经过performance.getEntriesByType(‘resource’) 获取页面加载的资源列表,一起能够结合initiatorType 字段来判别资源类型,对资源进行过滤

其中 PerformanceResourceTiming 来剖析资源加载的详细数据

// PerformanceResourceTiming 各字段阐明
{
  connectEnd, // 表明浏览器完结树立与服务器的衔接以检索资源之后的时刻
  connectStart, // 表明浏览器开端树立与服务器的衔接以检索资源之前的时刻
  decodedBodySize, // 表明在删去任何运用的内容编码之后,从*消息主体*的恳求(HTTP 或缓存)中接纳到的巨细(以八位字节为单位)
  domainLookupEnd, // 表明浏览器完结资源的域名查找之后的时刻
  domainLookupStart, // 表明在浏览器当即开端资源的域名查找之前的时刻
  duration, // 回来一个timestamp,即responseEnd 和 startTime 特点的差值
  encodedBodySize, // 表明在删去任何运用的内容编码之前,从*有用内容主体*的恳求(HTTP 或缓存)中接纳到的巨细(以八位字节为单位)
  entryType, // 回来"resource"
  fetchStart, // 表明浏览器行将开端获取资源之前的时刻
  initiatorType, // 代表启动功用条目的资源的类型,如PerformanceResourceTiming.initiatorType中所指定
  name, // 回来资源 URL
  nextHopProtocol, // 代表用于获取资源的网络协议
  redirectEnd, // 表明收到上一次重定向呼应的发送最终一个字节时的时刻
  redirectStart, // 表明上一次重定向开端的时刻
  requestStart, // 表明浏览器开端向服务器恳求资源之前的时刻
  responseEnd, // 表明在浏览器接纳到资源的最终一个字节之后或在传输衔接封闭之前(以先到者为准)的时刻
  responseStart, // 表明浏览器从服务器接纳到呼应的榜首个字节后的时刻
  secureConnectionStart, // 表明浏览器行将开端握手进程以保护当时衔接之前的时刻
  serverTiming, // 一个PerformanceServerTiming数组,包含服务器计时目标的PerformanceServerTiming条目
  startTime, // 表明资源获取开端的时刻。该值等效于PerformanceEntry.fetchStart
  transferSize, // 代表所获取资源的巨细(以八位字节为单位)。该巨细包含呼应标头字段以及呼应有用内容主体
  workerStart // 假如服务 Worker 线程已经在运转,则回来在分配FetchEvent 之前的时刻戳,假如没有运转,则回来在启动 Service Worker 线程之前的时刻戳。假如服务 Worker 未阻拦该资源,则该特点将始终回来 0。
}

获取资源加载时长为 duration 字段,即responseEnd 与 startTime 的差值

获取加载资源列表:

function getResource() {
  if (performance.getEntriesByType) {
    const entries = performance.getEntriesByType('resource');
    // 过滤掉非静态资源的 fetch、 xmlhttprequest、beacon
    let list = entries.filter((entry) => {
      return ['fetch', 'xmlhttprequest', 'beacon'].indexOf(entry.initiatorType) === -1;
    });
    if (list.length) {
      list = JSON.parse(JSON.stringify(list));
      list.forEach((entry) => {
        entry.isCache = isCache(entry);
      });
    }
    return list;
  }
}
// 判别资料是否来自缓存
// transferSize为0,阐明是从缓存中直接读取的(强制缓存)
// transferSize不为0,可是`encodedBodySize` 字段为 0,阐明它走的是洽谈缓存(`encodedBodySize 表明恳求呼应数据 body 的巨细`)
function isCache(entry) {
  return entry.transferSize === 0 || (entry.transferSize !== 0 && entry.encodedBodySize === 0);
}

一个实在的页面中,资源加载大大都是逐步进行的,有些资源本身就做了推迟加载,有些是需求用户产生交互后才会去恳求一些资源

假如我们只重视主页资源,能够在 window.onload 事情中去搜集

假如要搜集一切的资源,需求经过定时器重复地去搜集,并且在一轮搜集完毕后,经过调用clearResourceTimings将 performance entries 里的信息清空,防止在下一轮搜集时取到重复的资源

个性化目标

long task

履行时刻超越50ms的使命,被称为 long task 长使命

获取页面的长使命列表:

const entryHandler = list => {
  for (const long of list.getEntries()) {
    // 获取长使命概况
    console.log(long);
  }
};
let observer = new PerformanceObserver(entryHandler);
observer.observe({ entryTypes: ["longtask"] });

memory页面内存

performance.memory 能够显现此刻内存占用状况,它是一个动态值,其中:

  • jsHeapSizeLimit 该特点代表的含义是:内存巨细的约束。

  • totalJSHeapSize 表明总内存的巨细。

  • usedJSHeapSize 表明可运用的内存的巨细。

通常,usedJSHeapSize 不能大于 totalJSHeapSize,假如大于,有可能呈现了内存泄漏

// load事情中获取此刻页面的内存巨细
window.addEventListener("load", () => {
  console.log("memory", performance.memory);
});

首屏加载时刻

首屏加载时刻和主页加载时刻不一样,首屏指的是屏幕内的dom烘托完结的时刻

比方主页很长需求好几屏展现,这种状况下屏幕以外的元素不考虑在内

计算首屏加载时刻流程

1)运用MutationObserver监听document目标,每逢dom改变时触发该事情

2)判别监听的dom是否在首屏内,假如在首屏内,将该dom放到指定的数组中,记载下当时dom改变的时刻点

3)在MutationObserver的callback函数中,经过防抖函数,监听document.readyState状况的改变

4)当document.readyState === 'complete',中止定时器和 撤销对document的监听

5)遍历寄存dom的数组,找出最终改变节点的时刻,用该时刻点减去performance.timing.navigationStart 得出首屏的加载时刻

监控SDK

监控SDK的作用:数据搜集与上报

全体架构

从0到1树立前端监控渠道,面试必备的亮点项目

全体架构运用 发布-订阅 规划形式,这样规划的好处是便于后续扩展与维护,假如想增加新的hook或事情,在该回调中增加对应的函数即可

SDK 进口

src/index.js

对外导出init事情,装备了vue、react项目的不同引入办法

vue项目在Vue.config.errorHandler中上报过错,react项目在ErrorBoundary中上报过错

从0到1树立前端监控渠道,面试必备的亮点项目

事情发布与订阅

经过增加监听事情来捕获过错,运用 AOP 切片编程,重写接口恳求、路由监听等功用,然后获取对应的数据

src/load.js

从0到1树立前端监控渠道,面试必备的亮点项目

用户行为搜集

core/breadcrumb.js

创立用户行为类,stack用来存储用户行为,当长度超越约束时,最早的一条数据会被覆盖掉,在上报过错时,对应的用户行为会增加到该过错信息中

从0到1树立前端监控渠道,面试必备的亮点项目

数据上报办法

支持图片打点上报和fetch恳求上报两种办法

图片打点上报的优势:
1)支持跨域,一般而言,上报域名都不是当时域名,上报的接口恳求会构成跨域
2)体积小且不需求插入dom中
3)不需求等候服务器回来数据

图片打点缺陷是:url受浏览器长度约束

core/transportData.js

从0到1树立前端监控渠道,面试必备的亮点项目

数据上报机遇

优先运用 requestIdleCallback,运用浏览器空闲时刻上报,其次运用微使命上报

从0到1树立前端监控渠道,面试必备的亮点项目

监控SDK,参阅了 sentry、 monitor、 mitojs

项目后台demo

首要用来演示过错复原功用,办法包含:定位源码、播映录屏、记载用户行为

从0到1树立前端监控渠道,面试必备的亮点项目

后台demo功用介绍:

1、运用 express 敞开静态服务器,模拟线上环境,用于完结定位源码的功用

2、server.js 中完结了 reportData(过错上报)、getmap(获取 map 文件)、getRecordScreenId(获取录屏信息)、 getErrorList(获取过错列表)的接口

3、用户可点击 ‘js 报错’、’异步报错’、’promise 过错’ 按钮,上报对应的代码过错,后台完结过错复原功用

4、点击 ‘xhr 恳求报错’、’fetch 恳求报错’ 按钮,上报接口报错信息

5、点击 ‘加载资源报错’ 按钮,上报对应的资源报错信息

经过这些异步的捕获,了解监控渠道的全体流程

安装与运用

npm官网搜索 web-see

从0到1树立前端监控渠道,面试必备的亮点项目

仓库地址

监控SDK: web-see

监控后台: web-see-demo

总结

现在市面上的前端监控计划可谓是百家争鸣,但底层原理都是相通的。从根底的理论常识到完结一个可用的监控渠道,收成仍是挺多的

有兴趣的小伙伴能够结合git仓库的源码玩一玩,再结合本文一起阅读,协助加深了解

后续

下一篇会继续评论前端监控,解说详细怎么完结:定位源码、播映录屏等功用

感兴趣的小伙伴能够点个重视,后续好文不断!