Внешняя производительность и отчеты об исключениях

внешний интерфейс сервер браузер Vue.js
Внешняя производительность и отчеты об исключениях

Обзор

Для фоновой разработки ведение журнала является очень распространенной привычкой разработки, обычно мы используемtry...catchБлок кода используется для активного захвата ошибок.Для каждого вызова интерфейса также записывается время, затраченное на каждый вызов интерфейса, чтобы мы могли отслеживать производительность интерфейса сервера и устранять проблемы.

Когда я впервые пришел в компанию,Node.jsКогда я разрабатывал интерфейс , я не привык заходить на сервер через трамплин каждый раз, когда проверял проблему, а потом постепенно привык к этому методу.

Например:

/**
 * 获取列表数据
 * @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Требования к оптимизации времени рендеринга и предварительной загрузке изображения, если отсутствует мониторинг производительности, как рассчитать коэффициент оптимизации оптимизации рендеринга и оптимизации предварительной загрузки изображения и как доказать ценность того, что вы сделали? Это может быть тест «черный ящик» тестовых одноклассников, запись времени до и после оптимизации и анализ того, сколько кадров изображений прошло от входа на страницу до завершения рендеринга изображения. Такие данные могут быть неточными и односторонними.Предполагается, что испытуемые не являются реальными пользователями, и восстановить сетевое окружение реальных пользователей невозможно. Оглядываясь назад, мы обнаружили, что хотя наш проект ведет журналы и статистику производительности на уровне сервера, он отслеживает исключения и выполняет статистику производительности на внешнем интерфейсе. Необходимо изучить возможности интерфейсной производительности и отчетов об исключениях.

ловушка исключения

Для внешнего интерфейса нам нужны следующие два захвата исключений:

  • Ситуация вызова интерфейса;
  • Неправильная ли логика страницы, например, страница отображает белый экран после входа пользователя на страницу;

Для интерфейсных вызовов клиентскому интерфейсу обычно необходимо сообщать параметры, связанные с клиентом, такие как: пользовательская ОС и версия браузера, параметры запроса (например, идентификатор страницы); а для логических ошибок страницы, как правило, в дополнение к пользовательской ОС и версии браузера. , Что необходимо, так это информация о стеке ошибки и конкретное местоположение ошибки.

метод отлова исключений

глобальный захват

Может быть перехвачен прослушиванием исключений глобально,window.onerrorилиaddEventListener, см. следующий пример:

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('这是一个错误');

пройти черезwindow.onerrorСобытие, вы можете получить конкретную информацию об исключении, URL-адрес файла исключения, номер строки и номер столбца исключения, а также информацию о стеке исключения.После захвата исключения оно будет передано на наш сервер журналов.

Или, поwindow.addEventListenerспособ сообщения об исключениях по той же причине:

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

try... catch

использоватьtry... catchХотя он может лучше ловить исключения, чтобы страница не зависла из-за ошибки, аtry ... catchМетод захвата слишком раздут, и в большинстве кодов используетсяtry ... catchpackage, что влияет на читаемость кода.

Общая проблема

Сценарии между источниками не могут точно перехватывать исключения

Обычно мы помещаем статические ресурсы, такие как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>
// error.js
throw new Error('这是一个错误');

Результаты показывают, что после междоменногоwindow.onerrorПравильная информация об исключении вообще не может быть захвачена, но возвращается унифицированный возврат.Script error,

Решение: даscriptдобавить тегcrossorigin=”anonymous”, и сервер добавляетAccess-Control-Allow-Origin.

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

sourceMap

Обычно код в производственной средеwebpackОбфусцированный код сжимается после упаковки, поэтому мы можем столкнуться с такими проблемами, как показано на рисунке:

Мы обнаружили, что все строки кода, сообщающие об ошибке, находятся в первой строке. Почему? Это потому, что в продакшене наш код сжат в одну строку:

!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После этого идет код в упакованном продакшене, а все ошибки располагаются в первой строке.

Решение состоит в том, чтобы включитьwebpackизsource-map, мы используемwebpackУпакованная сгенерированная копия.mapФайл скрипта позволяет браузеру отслеживать местонахождение ошибки. Здесь вы можете обратиться кwebpack document.

На самом деле этоwebpack.config.jsдобавить строкуdevtool: 'source-map', как показано ниже, напримерwebpack.config.js:

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, чтобы браузер мог найти конкретное место ошибки:

включиsource-mapНедостатком является совместимость, в настоящее время толькоChromeбраузер иFirefoxбраузер правsource-mapслужба поддержки. Однако у нас есть решения и для подобных ситуаций. можно импортировать с помощьюnpmбиблиотека для поддержкиsource-map, вы можете обратиться кmozilla/source-map. этоnpmБиблиотека может работать как на клиенте, так и на сервере, но более рекомендуется использовать ее на сервере.Node.jsИспользуется при получении информации журналаsource-mapРазбор во избежание риска утечки исходного кода, как показано в следующем коде:

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;

Как показано на рисунке ниже, мы уже можем видеть, что номер строки и номер столбца конкретной ошибки были успешно проанализированы на стороне сервера, и мы можем записать его в журнал для достижения цели внешнего мониторинга исключений.

Исключение Vue catch

Я столкнулся с такой проблемой в своем проекте, используяjs-trackerТакой плагин используется для унифицированного глобального захвата исключений и лог-отчета, получается, что мы его вообще не захватываем.VueНеисправность компонента, согласно информации, вVue, исключения могут бытьVueдать себеtry ... catch, он не будет переданwindow.onerrorтриггеры событий, то как мы поместимVueКак насчет унифицированного захвата исключений в компонентах?

использоватьVue.config.errorHandlerТакойVueглобальную конфигурацию, которую можно найти вVueУказывает обработчик необработанных ошибок во время рендеринга и просмотра компонента. При вызове этого обработчика вы можете получить информацию об ошибке иVueПример.

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

существуетReact, вы можете использоватьErrorBoundaryКомпоненты включают бизнес-компоненты для захвата исключений, взаимодействия сReact 16.0+новыйcomponentDidCatch API, что позволяет добиться унифицированного захвата исключений и составления журналов.

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>

мониторинг производительности

Самый простой мониторинг производительности

Наиболее распространенное требование мониторинга производительности заключается в том, что нам нужно подсчитывать пользователей с начала запроса страниц до всех.DOMВремя завершения рендеринга элемента, которое обычно называют временем загрузки первого экрана.DOMЭтот интерфейс предназначен для мониторингаdocumentизDOMContentLoadedсобытие иwindowизloadСобытия могут считать время загрузки первого экрана страницы, то есть всего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>

Для использования таких фреймов, какVueИлиReact, компонент рендерится асинхронно, а затем монтируется вDOMДа, когда страница инициализирована, делать особо нечего.DOMузел, вы можете обратиться к следующемуО решении первой автоматизации сбора экранного временидля акцентирования времени рендеринга.

performance

Однако мониторинг вышеуказанного времени слишком грубый, например, мы хотим посчитать время загрузки сети и парсинга документа.DOMтрудоемкий и рендерингDOMтрудоемко, это не очень просто сделать, к счастью, браузер предоставляетwindow.performanceинтерфейс см.Документация MDN

Почти все браузеры поддерживаютwindow.performanceИнтерфейс, посмотрим на печать в консолиwindow.performanceЧто получить:

можно увидеть,window,performanceв основном включаютmemory,navigation,timingтак же какtimeOriginа такжеonresourcetimingbufferfullметод.

  • navigationОбъект предоставляет информацию о действиях, которые произошли в течение указанного периода времени, в том числе о том, была ли загружена или обновлена ​​страница, сколько произошло перенаправлений и т. д.
  • timingОбъект содержит информацию о производительности, связанную с задержкой. Это актуальная информация, в основном сообщаемая в наших требованиях к оптимизации производительности загрузки страницы.
  • memoryдляChromeДобавлено нестандартное расширение, это свойство предоставляет объект, который может получить базовое использование памяти. Это следует учитывать в других браузерах.APIсовместимая обработка.
  • timeOriginзатем возвращает высокоточную метку времени начала измерения производительности. Как показано, он точен до четырех знаков после запятой.
  • onresourcetimingbufferfullметод, этоresourcetimingbufferfullбудет вызываться, когда событие срабатываетevent handler. Это событие запускается, когда буфер производительности браузера в режиме «ресурс-время» заполнен. Страница может быть оценена путем прослушивания этого триггера событияcrash, страница статистикиcrashвероятность последующей оптимизации производительности, как показано в следующем примере:
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;
}
<body onload="init()">

Рассчитать производительность сайта

использоватьperformanceизtimingатрибут, вы можете получить данные, связанные с производительностью страницы, здесь упоминаются во многих статьях об использованииwindow.performance.timingСтатьи, документирующие производительность страницы, такие какalloyteamнаписано командойПервый взгляд на производительность — мониторинг веб-страницы и производительности программы,дляtimingСвойства свойств можно понять, используя следующий рисунок, следующий код взят из этой статьи в качестве функции инструмента для расчета производительности веб-сайта.

// 获取 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
    }
};
// 计算加载时间
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;
}

отчет журнала

Отдельное доменное имя журнала

Цель использования отдельного имени домена журнала для отчетов журнала — избежать влияния на бизнес. Во-первых, для сервера мы определенно не хотим занимать вычислительные ресурсы бизнес-сервера, а также не хотим, чтобы на бизнес-сервере накапливалось слишком много журналов, что приводило к нехватке места для хранения бизнес-сервера. Во-вторых, мы знаем, что в процессе инициализации страницы будут сообщаться такие данные, как время загрузки страницы, PV, UV и т. д. Эти отчетные запросы будут отправляться почти одновременно с загрузкой бизнес-данных, и браузеры обычно сообщить об одном и том же доменном имени. Количество запросов ограничено количеством одновременных запросов, напримерChromeБудет пара параллелизма6предел. Поэтому необходимо установить отдельное доменное имя для системы ведения журнала, чтобы минимизировать влияние на производительность загрузки страниц.

междоменная проблема

Для отдельного имени домена журнала обязательно возникнут междоменные проблемы.Как правило, принимаются следующие два решения:

  • Один состоит в том, чтобы построить пустойImageОбъектный метод, причина в том, что запрос изображений не связан с междоменными проблемами;
var url = 'xxx';
new Image().src = url;
  • использоватьAjaxЧтобы сообщать журналы, заголовок междоменного запроса должен быть включен для интерфейса сервера журналов.Access-Control-Allow-Origin:*,здесьAjaxне является обязательным для использованияGETпросить, преодолеватьURLпроблема с ограничением длины.
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)); // 发送参数
}

В моем проекте используется первый метод, который заключается в создании пустогоImageобъекта, но мы знаем, что дляGETЗапрос будет иметь ограничение по длине, необходимо убедиться, что длина запроса не превышает пороговое значение.

опустить тело ответа

Нам для отчёта лога, по сути, для клиента, нам не нужно считать результат отчёта, и даже при провале отчёта нам не нужно делать никакого взаимодействия на фронтенде, поэтому для отчет, на самом деле использоватьHEADЗапроса достаточно, и интерфейс возвращает пустой результат, что сводит к минимуму трату ресурсов, вызванную созданием журналов отчетов.

отчет о слиянии

Подобно идее Sprite, если нашему приложению необходимо сообщать о большом количестве журналов, необходимо объединить журналы для унифицированной отчетности.

Решение может состоять в том, чтобы попытаться отправить асинхронный вызов, когда пользователь покидает страницу или когда компонент уничтожается.POSTзапрос на эскалацию, но пытаюсь удалить (unload) перед документомwebСервер отправляет данные. Обеспечение отправки данных во время выгрузки документа всегда было сложной задачей. Поскольку пользовательские агенты обычно игнорируют асинхронность, сгенерированную в обработчике события выгрузки.XMLHttpRequest, потому что в этот момент он перейдет на следующую страницу. Так что здесь он должен быть установлен на синхронныйXMLHttpRequestЗапрос?

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()Метод отправляет данные на сервер.

window.addEventListener('unload', logData, false);

function logData() {
    navigator.sendBeacon("/log", analyticsData);
}

резюме

Как фронтенд-разработчик, мы должны поддерживать благоговение перед продуктами, всегда стремиться к максимальной производительности и иметь невыносимое отношение к исключениям. Мониторинг производительности внешнего интерфейса и отчеты об исключениях особенно важны.

Неизбежны проблемы с кодом, и вы можете использовать его для исключений.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воздействие времени.