美文网首页
Lighthouse使用说明

Lighthouse使用说明

作者: 说叁两事 | 来源:发表于2021-07-25 22:46 被阅读0次

    声明

    本来想实时做一个示例讲述Lighthouse的用法,然后,本地安装了lighthousev8.1.0,悲剧了,使用的v7.5.0的配置不兼容。

    遂,整理一下自己使用v7.5.0的输出,最后补充一点点v8.1.0的使用,真的只有一点点...因为没执行下去。

    Lighthouse使用说明

    这里不是具体的场景,只是介绍Lighthouse的用法,相信,您会找到自己的应用场景的。

    前端性能监控模型

    Lighthouse主要是用于前端性能监控,两种常见模型:合成监控、真实用户监控。

    合成监控(Synthetic Monitoring,SYN)

    合成监控,就是在一个模拟场景里,去提交一个需要做性能检测的页面,通过一系列的工具、规则去运行你的页面,提取一些性能指标,得出一个性能/审计报告

    真实用户监控(Real User Monitoring,RUM)

    真实用户监控,就是用户在我们的页面上访问,访问之后就会产生各种各样的性能数据,我们在用户离开页面的时候,把这些性能数据上传到我们的日志服务器上,进行数据的提取清洗加工,最后在我们的监控平台上进行展示的一个过程

    对比项 合成监控SYN 真实用户监控RUM
    实现难度及成本 较低 较高
    采集数据丰富度 丰富 基础
    数据样本量 较小 大(视业务体量)
    适合场景 支持团队自有业务,对性能做定性分析,或配合CI做小数据量的监控分析 作为中台产品支持前台业务,对性能做定量分析,结合业务数据进行深度挖掘

    Lighthouse是什么

    Lighthouse 是一个开源的自动化工具,用于分析和改善 Web 应用的质量。

    Lighthouse使用环境

    • Chrome开发者工具
    • 安装扩展程序
    • Node CLI
    • Node module

    Lighthouse组成部分

    • 驱动Driver

      通过Chrome Debugging Protocol和Chrome进行交互。

    • 收集器Gatherer

      决定在页面加载过程中采集哪些信息,将采集的信息输出为Artifact

    • 审查器Audit

      将 Artifact 作为输入,审查器会对其运行 1 个测试,然后分配通过/失败/得分的结果。

    • 报告Reporter

      将审查的结果分组到面向用户的报告中(如最佳实践)。对该部分加权求和然后得出评分。

    Lighthouse工作流程

    简单来说Lighthouse的工作流程就是:建立连接 -> 收集日志 -> 分析 -> 生成报告。

    Lighthousev7.5.0 NPM包使用示例

    初始化开发环境

    mkdir lh && cd $_ // 在命令行中创建项目目录、进入目录
    npm init -y // 初始化Node环境
    npm i -S puppeteer // 提供浏览器环境,其它NPM包也可以
    npm i -S lighthouse // 安装lighthouse
    //         ^  lighthouse@7.5.0
    

    初始化运行

    const puppeteer = require('puppeteer');
    const lighthouse = require('lighthouse');
    const { URL } = require('url');
    (async() => {
        const url = 'https://huaban.com/discovery/';
        const browser = await puppeteer.launch({
          headless: false, // 调试时设为 false,可显式的运行Chrome
          defaultViewport: null,
        });
        const lhr = await lighthouse(
          url,
          {
            port: (new URL(browser.wsEndpoint())).port,
            output: 'json',
            logLevel: 'info',
          }
        );
        console.log(lhr)
        await browser.close();
    })();
    

    以花瓣为例,puppeteer目前仅提供浏览器环境,lighthouse通过browser.wsEndpoint()与浏览器进行通信。

    生成报告

    通过初始化运行,我们能看到lighthouse的执行,却无法直观的看到结果。

    const puppeteer = require('puppeteer');
    const lighthouse = require('lighthouse');
    const { URL } = require('url');
    const path = require('path');
    const printer = require('lighthouse/lighthouse-cli/printer');
    const Reporter = require('lighthouse/lighthouse-core/report/report-generator');
    function generateReport(runnerResult) {
      const now = new Date();
      const Y = now.getFullYear();
      const M = now.getMonth();
      const D = now.getDate();
      const H = now.getHours();
      const m = now.getMinutes();
      const filename = `lhr-report@${Y}-${M + 1 < 10 ? '0' + (M + 1) : M + 1}-${D}-${H}-${m}.html`;
      const htmlReportPath = path.join(__dirname, 'public', filename);
      const reportHtml = Reporter.generateReport(runnerResult.lhr, 'html');
      printer.write(reportHtml, 'html', htmlReportPath);
    }
    (async() => {
        const url = 'https://huaban.com/discovery/';
        const browser = await puppeteer.launch({
          headless: false,
          defaultViewport: null,
        });
        const lhr = await lighthouse(
          url,
          {
            port: (new URL(browser.wsEndpoint())).port,
            output: 'json',
            logLevel: 'info',
          }
        );
        generateReport(lhr)
        await browser.close();
    })();
    

    手动创建一个public方法(这里不通过代码检测是否有目录了),新增generateReport方法,将报告结果输出为html

    将输出的HTML用浏览器打开,可以直观地看到输出结果。

    默认展示

    默认有五个分类:


    五个默认分类

    国际化语言设置

    (async() => {
        const url = 'https://huaban.com/discovery/';
        const browser = await puppeteer.launch({
          headless: false,
          defaultViewport: null,
        });
        const lhr = await lighthouse(
          url,
          {
            port: (new URL(browser.wsEndpoint())).port,
            output: 'json',
            logLevel: 'info',
          },
          {
            settings: {
              locale: 'zh' //  国际化
            }
          }
        );
        generateReport(lhr)
        await browser.close();
    })();
    

    这里,我们可以看到lighthouse(url, flags, configJSON)接收三个参数。

    自定义收集器Gatherer

    父级Gatherer —— 继承目标

    // https://github.com/GoogleChrome/lighthouse/blob/v7.5.0/lighthouse-core/gather/gatherers/gatherer.js
    class Gatherer {
      /**
       * @return {keyof LH.GathererArtifacts}
       */
      get name() {
        // @ts-expect-error - assume that class name has been added to LH.GathererArtifacts.
        return this.constructor.name;
      }
    
      /* eslint-disable no-unused-vars */
    
      /**
       * Called before navigation to target url.
       * @param {LH.Gatherer.PassContext} passContext
       * @return {LH.Gatherer.PhaseResult}
       */
      beforePass(passContext) { }
    
      /**
       * Called after target page is loaded. If a trace is enabled for this pass,
       * the trace is still being recorded.
       * @param {LH.Gatherer.PassContext} passContext
       * @return {LH.Gatherer.PhaseResult}
       */
      pass(passContext) { }
    
      /**
       * Called after target page is loaded, all gatherer `pass` methods have been
       * executed, and — if generated in this pass — the trace is ended. The trace
       * and record of network activity are provided in `loadData`.
       * @param {LH.Gatherer.PassContext} passContext
       * @param {LH.Gatherer.LoadData} loadData
       * @return {LH.Gatherer.PhaseResult}
       */
      afterPass(passContext, loadData) { }
    
      /* eslint-enable no-unused-vars */
    }
    

    自定义示例

    import { Gatherer } from 'lighthouse';
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const DevtoolsLog = require('lighthouse/lighthouse-core/gather/devtools-log.js');
    
    class LIMGGather extends Gatherer {
      /** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */
      meta = {
        supportedModes: [ 'navigation' ],
        dependencies: { DevtoolsLog: DevtoolsLog.symbol },
      };
    
      /**
       * @param {LH.Artifacts.NetworkRequest[]} networkRecords
       */
      indexNetworkRecords(networkRecords) {
        return networkRecords.reduce((arr, record) => {
          // An image response in newer formats is sometimes incorrectly marked as "application/octet-stream",
          // so respect the extension too.
          const isImage = /^image/.test(record.mimeType) || /\.(avif|webp)$/i.test(record.url);
          // The network record is only valid for size information if it finished with a successful status
          // code that indicates a complete image response.
          if (isImage) {
            arr.push(record);
          }
    
          return arr;
        }, []);
      }
    
      /**
       * @param {LH.Gatherer.FRTransitionalContext} context
       * @param {LH.Artifacts.NetworkRequest[]} networkRecords
       * @return {Promise<LH.Artifacts['ImageElements']>}
       */
      async _getArtifact(_context, networkRecords) {
        const imageNetworkRecords = this.indexNetworkRecords(networkRecords);
    
        return imageNetworkRecords;
      }
    
      /**
       * @param {LH.Gatherer.PassContext} passContext
       * @param {LH.Gatherer.LoadData} loadData
       * @return {Promise<LH.Artifacts['ImageElements']>}
       */
      async afterPass(passContext, loadData) {
        return this._getArtifact({ ...passContext, dependencies: {} }, loadData.networkRecords);
      }
    }
    
    module.exports = LIMGGather;
    

    单独的Gatherer是没有任何用途的,只是收集Audit需要用到的中间数据,最终展示的数据为Audit的输出。

    所以,我们讲述完自定义Audit后,再说明Gatherer如何使用。

    Notes:收集器Gatherer必须有个name属性,默认是父类get name()返回的this.constructor.name;,如需自定义,重写get name()方法。

    Notes: Gatherer最好不要和默认收集器重名,若configJSON.configPath配置的等同默认路径,会忽略自定义 —— 后续会详细讲述。

    自定义审计Audit

    父级Audit —— 继承目标

    Audit类有很多方法,个人用到的主要重写的也就metaaudit

    // https://github.com/GoogleChrome/lighthouse/blob/v7.5.0/lighthouse-core/audits/audit.js
    class Audit {
      ...
      /**
       * @return {LH.Audit.Meta}
       */
      static get meta() {
        throw new Error('Audit meta information must be overridden.');
      }
      ...
      /**
       * @return {Object}
       */
      static get defaultOptions() {
        return {};
      }
      ...
      /* eslint-disable no-unused-vars */
    
      /**
       *
       * @param {LH.Artifacts} artifacts
       * @param {LH.Audit.Context} context
       * @return {LH.Audit.Product|Promise<LH.Audit.Product>}
       */
      static audit(artifacts, context) {
        throw new Error('audit() method must be overriden');
      }
      ...
    }
    

    自定义示例

    import { Audit } from 'lighthouse';
    import { Pages, AuditStrings, Scores } from '../../config/metrics';
    import { toPrecision } from '../../util';
    class LargeImageOptimizationAudit extends Audit {
      static get meta() {
        return {
          ...Pages[AuditStrings.limg],
          // ^ 重要的是id字段,定义分类时需要指定
          scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
          // ^ 分数的展示模式,只有Audit.SCORING_MODES.NUMERIC才有分值,若取其它值,分值score始终为0.
          requiredArtifacts: [ 'LIMGGather' ],
          // ^ 定义依赖的Gatherers
        };
      }
      static audit(artifacts) {
        const limitSize = 50;
        const images = artifacts.LIMGGather;
        // ^ 解构或直接访问获取Gatherer收集的数据。
        /**
        * 后续的逻辑是,判断大于50K的图片数量。
        * 以当前指标总分 - 大于50K图片数量 * 出现一次扣step分
        * 计算最终分数
        */
        const largeImages = images.filter(img => img.resourceSize > limitSize * 1024);
        const scroeConfig = Scores[AuditStrings.limg];
        const score = scroeConfig.scores - largeImages.length * scroeConfig.step;
        const finalScore = toPrecision((score < 0 ? 0 : score) / scroeConfig.scores);
        //      ^ Lighthouse中Audit、categories中的分数都是0~1范围内的
    
        const headings = [
          { key: 'url', itemType: 'thumbnail', text: '资源预览' },
          { key: 'url', itemType: 'url', text: '图片资源地址' },
          { key: 'resourceSize', itemType: 'bytes', text: '原始大小' },
          { key: 'transferSize', itemType: 'bytes', text: '传输大小' },
          /**
          * key:返回对象中details字段Audit.makeTableDetails方法第二个参数中对应的键值
          * itemType是Lighthouse识别对象key对应值,来使用不同的样式展示的。
          * itemType类型文档https://github.com/GoogleChrome/lighthouse/blob/v7.5.0/lighthouse-core/report/html/renderer/details-renderer.js#L266
          */
          // const headings = [
          //   { key: 'securityOrigin', itemType: 'url', text: '域' },
          //   { key: 'dnsStart', itemType: 'ms', granularity: '0.001', text: '起始时间' },
          //   { key: 'dnsEnd', itemType: 'ms', granularity: '0.001', text: '结束时间' },
          //   { key: 'dnsConsume', itemType: 'ms', granularity: '0.001', text: '耗时' },
          // ];
          /////itemType === 'ms'时,可以设置精度granularity
        ];
        return {
          score: finalScore,
          displayValue: `${largeImages.length} / ${images.length} Size >${limitSize}KB`,
          // ^ 当前审计项的总展示
          details: Audit.makeTableDetails(headings, largeImages),
          // ^ 当前审计项的细节展示
          // ^ table类型的展示,要求第二个参数largeImages是平铺的对象元素构成的数组。
          // https://github.com/GoogleChrome/lighthouse/blob/v7.5.0/lighthouse-core/report/html/renderer/details-renderer.js
        };
      }
    }
    module.exports = LargeImageOptimizationAudit;
    

    这里,我们仍然不讲述Gatherer、Audit如何使用,我们继续讲述Categories时,统一讲述如何使用。

    自定义分类Categories

    import { LargeImageOptimizationAudit } from '../gathers';
    //          ^ gathers文件夹下定义了统一入口index.js,导出所有的自定义收集器
    import { LIMGGather } from '../audits';
    //        ^ audits文件夹下定义了统一入口index.js,导出所有的自定义审查器
    module.exports = {
      extends: 'lighthouse:default', // 决定是否包含默认audits,就是上述默认五个分类
      passes: [{
        passName: 'defaultPass',
        gatherers: [
          LIMGGather , // 自定义Gather的应用 
        ],
      }],
      audits: [
        LargeImageOptimizationAudit , // 自定义Audit的应用 
      ],
      categories: {
        mysite: {
          title: '自定义指标',
          description: '我的自定义指标',
          auditRefs: [
            {
              id: 'large-images-optimization', 
              // ^ Audit.id,自定义Audit.meta时指定;
              // 给自定义Audit单独定义一个分类。
              weight: 1
              // ^ 当前审计项所占的比重,权重weight总和为100!
            },
          ],
        },
      },
    };
    

    来一个非上述配置的自定义审计、分类的展示效果(因为这篇文章是后续整理的)。

    自定义审计displayValue
    以上是自定义审计返回对象中定义的displayValue字段。
    自定义审计details
    以上是自定义审计返回对象中details: Audit.makeTableDetails(headings, largeImages),展示示例。
    最终的展示效果:
    最终效果
    这里的展示效果,是去除默认五个分类后的展示,通过注释掉上述配置的extends: 'lighthouse:default',即可去掉。

    Notes:去掉extends: 'lighthouse:default',后,若使用内置或自定义的Audit依赖requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext'],lighthouse运行会报错:

    errorMessage: "必需的 traces 收集器未运行。"
    或
    errorMessage: "Required traces gatherer did not run."
    

    需要补充以下配置:

    passes = [
      {
        passName: 'defaultPass',
        recordTrace: true,// 开启Traces收集器
        gatherers: [
        ],
      },
    ];
    

    优化

    NPM包与Chrome Devtools中的Lighthouse分值差异大

    NPM包使用Lighthouse进行合成监控,模拟页面在较慢的连接上加载,会限制 DNS 解析的往返行程以及建立 TCP 和 SSL 连接。
    而Chrome Devtools的Lighthouse做了很多优化,也没有进行节流限制,即使设置节流,也只是接收服务器响应时的延迟,而不是模拟每次往返双向。

    NPM包使用时,添加参数--throttling-method=devtools来平衡差异。

    Lighthouse切换桌面模式

    Lighthouse默认是移动端模式,不同版本配置不同,配置需要对应版本。

    // https://github.com/GoogleChrome/lighthouse/discussions/12058
    // 以下是configJSON.settings的配置
    ...
      getSettings() {
        return (function(isDesktop) {
          if (isDesktop) {
            return {
              // extends: 'lighthouse:default', // 是否包含默认audits
              locale: 'zh',
              formFactor: 'desktop', // 必须的lighthouse模拟Device
              screenEmulation: { // 结合formFactor使用,要匹配
                ...constants.screenEmulationMetrics.desktop,
              },
              emulatedUserAgent: constants.userAgents.desktop, // 结合formFactor使用,要匹配
            };
          }
          return {
            // extends: 'lighthouse:default', // 是否包含默认audits
            locale: 'zh',
            formFactor: 'mobile', // 必须的lighthouse模拟Device
            screenEmulation: { // 结合formFactor使用,要匹配
              ...constants.screenEmulationMetrics.mobile,
            },
            emulatedUserAgent: constants.userAgents.mobile, // 结合formFactor使用,要匹配
          };
        })(this.isDesktop);
      }
    

    Lighthouse的Gather、Audits路径配置

    对象配置

    见上述使用;

    路径配置

    // 引入方式:相对于项目根目录,设置相对路径
    ...
          lightHouseConfig: {
            // onlyCategories: onlyCategories.split(','), // https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md
            reportPath: path.join(__dirname, '../public/'), // 测试报告存储目录
            passes: [
              {
                passName: 'defaultPass',
                gatherers: [
                  'app/gathers/large-images-optimization.ts',
                ],
              },
            ],
            audits: [
              'app/audits/large-images-optimization.ts',
            ],
            categories: {    
              mysite: {
                title: '自定义指标',
                description: '我的自定义指标',
                auditRefs: [
                  {
                    id: 'large-images-optimization', 
                    weight: 1
                  },
                ],
            },
    ...
    

    其中:源代码使用require(path)的形式调用,而不是require(path).default,只支持module.epxorts = class Module导出

    BasePath路径配置

    理论上配置:

      // lighthouse运行的第二个参数flags
      {
        port: new URL(this.browser.wsEndpoint()).port,
        configPath: path.join(__filename), // 查找gather、audit的基准
                                //    ^路径定位到文件,源码内会上溯到目录结构;若定位到目录,源码内会上溯到上一级目录
      },
      // lighthouse运行的第三个参数configJson
      ...
        passes: [
          {
            passName: 'defaultPass',
            gatherers: [
              'large-images-optimization',
            ],
          },
        ],
        audits: [
          'large-images-optimization',
        ],
    ...
    

    然而,"lighthouse": "^7.5.0",源码中:

    requirePath = resolveModulePath(gathererPath, configDir, 'gatherer'); // 检索gather
    const absolutePath = resolveModulePath(auditPath, configDir, 'audit'); // 检索audit
    

    第三个参数并没有使用。
    即v7.5.0会在配置的configPath下查找对应的'large-images-optimization'。

    现做如下妥协:

    // lighthouse第三个参数configJson
      ...
        passes: [
          {
            passName: 'defaultPass',
            gatherers: [
              'gathers/large-images-optimization',
            ],
          },
        ],
        audits: [
          'audits/large-images-optimization',
        ],
    ...
    

    Gather、Audti配置引入源码实现

    function expandGathererShorthand(gatherer) {
      if (typeof gatherer === 'string') {
        // just 'path/to/gatherer'
        return {path: gatherer};
      } else if ('implementation' in gatherer || 'instance' in gatherer) {
        // {implementation: GathererConstructor, ...} or {instance: GathererInstance, ...}
        return gatherer;
      } else if ('path' in gatherer) {
        // {path: 'path/to/gatherer', ...}
        if (typeof gatherer.path !== 'string') {
          throw new Error('Invalid Gatherer type ' + JSON.stringify(gatherer));
        }
        return gatherer;
      } else if (typeof gatherer === 'function') {
        // just GathererConstructor
        return {implementation: gatherer};
      } else if (gatherer && typeof gatherer.beforePass === 'function') {
        // just GathererInstance
        return {instance: gatherer};
      } else {
        throw new Error('Invalid Gatherer type ' + JSON.stringify(gatherer));
      }
    }
    //https://github.com/GoogleChrome/lighthouse/blob/master/lighthouse-core/config/config-helpers.js#L342
    
    function resolveGathererToDefn(gathererJson, coreGathererList, configDir) {
      const gathererDefn = expandGathererShorthand(gathererJson);
      if (gathererDefn.instance) {
        return {
          instance: gathererDefn.instance,
          implementation: gathererDefn.implementation,
          path: gathererDefn.path,
        };
      } else if (gathererDefn.implementation) {
        const GathererClass = gathererDefn.implementation;
        return {
          instance: new GathererClass(),
          implementation: gathererDefn.implementation,
          path: gathererDefn.path,
        };
      } else if (gathererDefn.path) {
        const path = gathererDefn.path;
        return requireGatherer(path, coreGathererList, configDir);
      } else {
        throw new Error('Invalid expanded Gatherer: ' + JSON.stringify(gathererDefn));
      }
    }
    

    补充:Gather、Audit重名问题

    默认GathererAudit的检索只是lighthouse-core包中对应目录的检索,若自定义的GatherAudit,且使用时使用字符串标识,一定不能和lighthouse-core中的重名。

    重名的话,优先使用lighthouse-core中的gatheraudit

    function requireGatherer(gathererPath, coreGathererList, configDir) {
      const coreGatherer = coreGathererList.find(a => a === `${gathererPath}.js`);
    
      let requirePath = `../gather/gatherers/${gathererPath}`;
      if (!coreGatherer) {
        // Otherwise, attempt to find it elsewhere. This throws if not found.
        requirePath = resolveModulePath(gathererPath, configDir, 'gatherer');
      }
    
      const GathererClass = /** @type {GathererConstructor} */ (require(requirePath));
    
      return {
        instance: new GathererClass(),
        implementation: GathererClass,
        path: gathererPath,
      };
    }
    

    不重名的话,会有一个检索顺序

    1. 相对路径检索;
    2. process.cwd()同级目录下检索;
    3. 配置flags.configPath的话,该路径下查找;
    4. flags.configPath只是查找gathereraudits资源的基准,config的设置和该配置无关。
    function resolveModulePath(moduleIdentifier, configDir, category) {
      try {
        return require.resolve(moduleIdentifier);
      } catch (e) {}
    
      try {
        return require.resolve(moduleIdentifier, {paths: [process.cwd()]});
      } catch (e) {}
    
      const cwdPath = path.resolve(process.cwd(), moduleIdentifier);
      try {
        return require.resolve(cwdPath);
      } catch (e) {}
    
      const errorString = 'Unable to locate ' + (category ? `${category}: ` : '') +
        `\`${moduleIdentifier}\`.
         Tried to require() from these locations:
           ${__dirname}
           ${cwdPath}`;
    
      if (!configDir) {
        throw new Error(errorString);
      }
      const relativePath = path.resolve(configDir, moduleIdentifier);
      try {
        return require.resolve(relativePath);
      } catch (requireError) {}
      try {
        return require.resolve(moduleIdentifier, {paths: [configDir]});
      } catch (requireError) {}
    
      throw new Error(errorString + `
           ${relativePath}`);
    }
    

    报告解析

    我们看到报告中有部分是已通过,这部分怎么解析的呢?

    附上源码片段:

    // node_modules/lighthouse/lighthouse-core/report/html/renderer/category-renderer.js
    ...
    // 报告模版 解析
     render(category, groupDefinitions = {}) {
        const element = this.dom.createElement('div', 'lh-category');
        this.createPermalinkSpan(element, category.id);
        element.appendChild(this.renderCategoryHeader(category, groupDefinitions));
    
        // Top level clumps for audits, in order they will appear in the report.
        /** @type {Map<TopLevelClumpId, Array<LH.ReportResult.AuditRef>>} */
        const clumps = new Map();
        clumps.set('failed', []);
        clumps.set('warning', []);
        clumps.set('manual', []);
        clumps.set('passed', []);
        clumps.set('notApplicable', []);
    
        // Sort audits into clumps.
        for (const auditRef of category.auditRefs) {
          const clumpId = this._getClumpIdForAuditRef(auditRef);
          const clump = /** @type {Array<LH.ReportResult.AuditRef>} */ (clumps.get(clumpId)); // already defined
          clump.push(auditRef);
          clumps.set(clumpId, clump);
        }
    
        // Render each clump.
        for (const [clumpId, auditRefs] of clumps) {
          if (auditRefs.length === 0) continue;
    
          if (clumpId === 'failed') {
            const clumpElem = this.renderUnexpandableClump(auditRefs, groupDefinitions);
            clumpElem.classList.add(`lh-clump--failed`);
            element.appendChild(clumpElem);
            continue;
          }
    
          const description = clumpId === 'manual' ? category.manualDescription : undefined;
          const clumpElem = this.renderClump(clumpId, {auditRefs, description});
          element.appendChild(clumpElem);
        }
    
        return element;
      }
    ...
      _getClumpIdForAuditRef(auditRef) {
        const scoreDisplayMode = auditRef.result.scoreDisplayMode;
        if (scoreDisplayMode === 'manual' || scoreDisplayMode === 'notApplicable') {
          return scoreDisplayMode;
        }
    
        if (Util.showAsPassed(auditRef.result)) {
          if (this._auditHasWarning(auditRef)) {
            return 'warning';
          } else {
            return 'passed';
          }
        } else {
          return 'failed';
        }
      }
    ...
    // node_modules/lighthouse/lighthouse-core/report/html/renderer/util.js
    ...
    const ELLIPSIS = '\u2026';
    const NBSP = '\xa0';
    const PASS_THRESHOLD = 0.9;
    const SCREENSHOT_PREFIX = 'data:image/jpeg;base64,';
    
    const RATINGS = {
      PASS: {label: 'pass', minScore: PASS_THRESHOLD},
      AVERAGE: {label: 'average', minScore: 0.5},
      FAIL: {label: 'fail'},
      ERROR: {label: 'error'},
    };
    ...
      static showAsPassed(audit) {
        switch (audit.scoreDisplayMode) {
          case 'manual':
          case 'notApplicable':
            return true;
          case 'error':
          case 'informative':
            return false;
          case 'numeric':
          case 'binary':
          default:
            return Number(audit.score) >= RATINGS.PASS.minScore; // 当audit分值大于0.9时,报告展示通过;
        }
      }
    ...
    

    补充:收集器中依赖浏览器

    如这里需要知道是否支持webp格式

    async function supportWebp(context) {
      const { driver } = context;
      const expression = function() {
        const elem = document.createElement('canvas');
        // eslint-disable-next-line no-extra-boolean-cast
        if (!!(elem.getContext && elem.getContext('2d'))) {
          // was able or not to get WebP representation
          return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
        }
        // very old browser like IE 8, canvas not supported
        return false;
      };
      return await driver.executionContext.evaluate(
      //                                      ^ 返回Promise
        expression,
        /**
        * expression函数声明,不能是字符串,evaluateSync需要是字符串
        * 你可能看到过evaluateSync,这是lighthousev7.0.0及其以前版本支持的API。
        */
        {
          args: [], // {required} args,否则报错
        },
      );
    }
    ...
    

    完整示例:

    import { Gatherer } from 'lighthouse';
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const DevtoolsLog = require('lighthouse/lighthouse-core/gather/devtools-log.js');
    
    async function supportWebp(context) {
      const { driver } = context;
      const expression = function() {
        const elem = document.createElement('canvas');
        // eslint-disable-next-line no-extra-boolean-cast
        if (!!(elem.getContext && elem.getContext('2d'))) {
          // was able or not to get WebP representation
          return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
        }
        // very old browser like IE 8, canvas not supported
        return false;
      };
      return await driver.executionContext.evaluate(
        expression,
        {
          args: [],
        },
      );
    }
    class WebpImageGather extends Gatherer {
      /** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */
      meta = {
        supportedModes: [ 'navigation' ],
        dependencies: { DevtoolsLog: DevtoolsLog.symbol },
      };
    
      /**
       * @param {LH.Artifacts.NetworkRequest[]} networkRecords
       */
      indexNetworkRecords(networkRecords) {
        return networkRecords.reduce((arr, record) => {
          const isImage = /^image/.test(record.mimeType) || /\.(avif|webp)$/i.test(record.url);
          if (isImage) {
            arr.push(record);
          }
    
          return arr;
        }, []);
      }
    
      /**
       * @param {LH.Gatherer.FRTransitionalContext} _context
       * @param {LH.Artifacts.NetworkRequest[]} networkRecords
       * @return {Promise<LH.Artifacts['ImageElements']>}
       */
      async _getArtifact(context, networkRecords) {
        const isSupportWebp = await supportWebp(context);
        const imagesNetworkRecords = this.indexNetworkRecords(networkRecords);
    
        return {
          isSupportWebp,
          imagesNetworkRecords,
        };
      }
      async afterPass(context, loadData) {
        return this._getArtifact(context, loadData.networkRecords);
      }
    }
    
    module.exports = WebpImageGather;
    

    补充:Lighouthousev.8.1.0

    对比v.7.5.0

    report-generator引用地址

    const Reporter = require('lighthouse/report/report-generator');
    //                            ^ 该引入地址,是v8.1.0更新的,老版引入地址为'lighthouse/lighthouse-core/report/report-generator'
    

    lighthouse执行

    若使用第三个参数,则locale为必填项。

        const lhr = await lighthouse(
          url,
          {
            port: (new URL(browser.wsEndpoint())).port,
            output: 'json',
            logLevel: 'info',
          }
        );
    

    locale配置

    lighthousev8.1.0,若nodev12.0.0及其以前的版本,需要手动安装full-icu并在命令行中添加参数node --icu-data-dir="./node_modules/full-icu" index.js
    然后,在配置项中才能定义:

    const lhr = await lighthouse(
          url,
          {
            port: (new URL(browser.wsEndpoint())).port,
            output: 'json',
            logLevel: 'info',
          },
          {
            settings: {
              extends: 'lighthouse:default', // 发现不再是默认配置的定义了
              locale: 'zh',
            },
            passes: [
              {
                passName: 'defaultPass',
              }
            ],
            audits: [
    
            ],
          }
        );
    

    参考文档

    Lighthouse 网易云音乐实践技术文档

    Lighthouse政采云实践技术文档

    Puppeteer政采云实践技术文档

    Chromium浏览器实例参数配置

    Lighthouse Driver源码官方实现

    Lighthouse Drive模块与浏览器双向通信协议JSON

    Devtools-Protocol协议官方文档

    Puppeteer5.3.0中文文档

    Puppeteer官方文档

    Lighthouse源码

    相关文章

      网友评论

          本文标题:Lighthouse使用说明

          本文链接:https://www.haomeiwen.com/subject/boyimltx.html