Lighthouse 复用 axe valid-langs 模块的实践:trie 编码的 ISO 639 语言代码校验与 hreflang 审计
Lighthouse 复用 axe valid-langs 模块的实践trie 编码的 ISO 639 语言代码校验与 hreflang 审计【免费下载链接】lighthouseAutomated auditing, performance metrics, and best practices for the web.项目地址: https://gitcode.com/GitHub_Trending/lig/lighthouseLighthouse 的 SEO 审计需要在运行时判断hreflang属性中的语言代码是否合法而全量加载 axe-core 又过于笨重。为此项目以third-party/axe/目录为媒介将 axe-core 内部的高密度 trie 编码语言表valid-langs.js抽取为独立文件供hreflang审计直接调用。本文以 third-party/axe/README.md 为骨架结合该目录下的实现文件、hreflang审计源码及其测试用例完整还原这一借用第三方内部模块的工程决策与编码原理读完你可以理解为什么 axe 的 lib 不能直接 import、trie 编码如何把 20KB 的语言表压到 2KB、以及 Lighthouse 是如何逐字符查表校验语言代码的。一、README 说了什么一次克制的第三方代码复用third-party/axe/README.md 全文只有两个核心结论却准确交代了一次vendoring代码抽取的全部动机用途Lighthouse 在href审计中使用 axe 包内部的valid-langs.js模块。README 中的 href 审计在当前代码库中对应的实际消费者是 SEO 分类下的hreflang审计core/audits/seo/hreflang.js该审计同时校验relalternate链接的hreflang语言代码与href指向是否合格。原因axe 发布时并没有提供允许导入其lib的package.json即没有把内部模块声明为可被外部 import 的导出路径因此 Lighthouse 必须把所需的内部文件抽取出来、随仓库维护。这一决策直接决定了third-party/axe/目录的形态——它不是一个依赖安装目录而是一个精选单文件抽取目录目前只包含三个文件valid-langs.js实现、LICENSEMPL 2.0 许可证、README。同时tsconfig.json 第 22 行将third-party/axe/valid-langs.js显式纳入工程的 include 列表文件首行带有// ts-nocheck不参与严格类型检查但进入编译上下文。二、valid-langs.js 内部把语言表编码成 trie 嵌套数组valid-langs.js 的头部注释第 611 行揭示了整个文件的设计动机与编码手法In order to reduce the file size of axe-core, this file was specially encoded. Normally, an array of strings of each valid ISO 639-1/2 code is 20kb gzipped. Encoding the codes is only 2kb gzipped.即直接以字符串数组存放全部合法 ISO 639-1/2 语言代码gzip 后约 20KB而采用 trie 编码后仅约 2KB体积缩小约 90%。该编码手法借鉴自 Js13kGames 游戏开发竞赛中的 ZzFXM 技术——把大量数据存成嵌套的整数数组这种结构对 gzip 极其友好。编码规则语言代码被组织成一棵字典树trie用嵌套数组表示每个数组下标代表一个字母a对应 1b对应 2依此类推值为1表示该路径在此处是合法的语言代码值为数组则表示继续向下分叉。所有代码统一补齐到 3 个字符长度不足 3 的代码末尾用反引号填充。反引号的charCodeAt(0) - 96 0正好落在数组的 0 号下标上用来标记较短字符串在此处即为合法。例如aaa与aa同时合法时存储为[,[,[1,1]]]——外层第二个元素a是数组其中第二个元素aa又是数组该数组的 0 号位由填充而来与 1 号位第三个 a均为 1。文件主体第 59 行就是一个被prettier-ignore与eslint-disable注释保护的大常量langs即整个 IANA 语言表的 trie 形态。解码函数 isValidLang文件末尾导出的核心 API 是isValidLang第 6884 行function isValidLang(lang) { let array langs; // padEnd is not supported in IE11 while (lang.length 3) { lang ; } for (let i 0; i lang.length - 1; i) { const index lang.charCodeAt(i) - 96; array array[index]; if (!array) { return false; } } return true; }逐行拆解其原理补位由于padEnd在 IE11 不受支持这里用while循环手动把不足 3 位的代码用反引号补足与编码端的规则严格对齐。逐字符查表对每个字符计算charCodeAt(i) - 96a为 97减 96 得 1作为数组下标逐层下钻。提前终止一旦某层下标不存在array为undefined立即返回false。全部字符走完仍存在说明该代码在 trie 中合法。值得注意的是查表是严格逐字符匹配的isValidLang( es)带前导空格会因空格字符计算出负下标而返回false这一点在测试用例中专门覆盖见下文。被标记弃用的反向解码器 validLangs同文件还保留了 axe-core 原版的validLangs第 93108 行其作用与isValidLang相反——把 trie 数组展开回完整语言代码列表。它递归遍历嵌套数组用String.fromCharCode(index 96)还原字母并拼接前缀遇到值为1的叶子则输出完整代码。该函数在 JSDoc 中标注deprecatedLighthouse 并不消费它保留它主要是为了与上游 axe-core 保持同步成本最低。三、数据从哪来基于 IANA 注册表的再生成脚本valid-langs.js头部的注释块第 1355 行附带了一段完整的数据再生成脚本这保证了语言表可以随 IANA 注册表更新而重新生成而不是靠手工维护几千条代码const str document.querySelector(pre).innerHTML; const langs new Set(); str.split(%%).forEach(language { const properties language.split(\n); for (let i 0; i properties.length; i) { const property properties[i]; const match property.match(/(?type\w): (?value\w)/); if (!match) continue; const { type, value } match.groups; if (type Type value ! language) return; if (type Subtag) langs.add(value); if (type Deprecated) langs.delete(value); // 剔除已废弃代码 } }); Array.from(langs).forEach(lang { lang lang.padEnd(3, ); let array encodedLangs; lang.split().forEach((char, i) { const index char.charCodeAt(0) - 96; if (i lang.length - 1) { array[index] array[index] || []; array array[index]; } else { array[index] 1; } }); });这段脚本的运行前提是在 IANA 的 language-subtag-registry 页面浏览器开发者工具中执行解析其pre文本按%%切分每个语言记录只收集Type: language的Subtag并自动剔除标记为Deprecated的代码这也是isValidLang能拒绝过期代码的原因最后按前述规则构建 trie 并JSON.stringify。从仓库历史看该文件经历过至少两次变更一次是 changelog-pre10.md 记录的移除 eval、直接 import axe 的 valid-langs.js另一次是 changelog.md 记录的更新 valid-langs.js。四、消费端hreflang 审计如何用它判分抽取出来的isValidLang在 core/audits/seo/hreflang.js 第 13 行被直接 importimport {isValidLang} from ../../../third-party/axe/valid-langs.js;审计对语言代码的校验封装在isExpectedLanguageCode第 4654 行function isExpectedLanguageCode(hreflang) { if (hreflang.toLowerCase() NO_LANGUAGE) { // NO_LANGUAGE x-default return true; } // hreflang can consist of language-script-region, we are validating only language const [lang] hreflang.split(-); return isValidLang(lang.toLowerCase()); }这里有三个值得注意的工程细节特例放行x-default是 hreflang 规范中的保留值表示默认语言版本先于查表判断不进入语言表校验。只校验语言主标签hreflang可能形如language-script-region如zh-Hans、nl-be而语言表只收录 ISO 639 主代码因此先用split(-)取出第一段再做校验——这也与valid-langs.js注释中ISO 639-1/2的定位一致。大小写归一先toLowerCase()再查表因此FR-BE、XX-be这类写法会被统一处理fr合法通过xx不合法被拒。审计主流程audit()第 75144 行接收采集器产出gatherer 在 core/gather/gatherers/link-elements.js 中从 DOM 的link元素与 HTTP 响应头提取rel、hreflang、hrefRaw等字段通过rel alternate、存在hreflang属性、且source ! body三个条件过滤出可审计的链接body内的链接按规范不属于 hreflang 声明直接忽略对每个候选链接执行两项校验并收集失败原因语言代码不合法记入Unexpected language codehref不是http:/https:全限定 URL 记入Relative href value失败项以表格细节输出展示link标签的代码片段head来源或Link:响应头原文headers来源并把失败原因作为子项subItems逐条列出只要存在任意失败项审计得分即为 0否则为 1。测试如何锁死行为core/test/audits/seo/hreflang-test.js 用一组精心设计的用例验证了上述规则与isValidLang的边界行为非法代码被判失败xx1、XX-be、XX-be-Hans、es前导空格全部产生失败项印证trie 逐字符匹配的严格性——XX转为小写xx后并不存在于 IANA 语言表空格则直接破坏下标计算合法代码放行pl、nl-be、zh-Hans、x-default、FR-BE全部通过同时验证了只校验语言主标签zh-Hans取zh与大小写归一FR-BE取fr两条规则body 来源一律忽略即使 body 中的链接携带xx等非法值审计仍得 1 分href 校验独立example.com、//example.com这类非全限定 URL 单独触发Relative href value当语言代码与 href 同时非法时单个失败项会携带两个子项原因测试断言subItems.items.length 2。五、为什么要抽取而不是依赖包导出边界的现实约束回到 README 的第二句话——The axe package is not published with apackage.jsonthat allows importing of itslib, so we must extract it。这是整个目录存在的根本原因axe-core 作为一个面向页面注入场景的库其包发布形态并未声明lib/为可外部导入的入口直接import axe-core/lib/...在模块解析与打包层面都不可靠也无法被 Lighthouse 的构建体系稳定引用因此 Lighthouse 采用单文件抽取 仓库内维护的 vendoring 策略把需要的valid-langs.js连同 MPL 2.0 许可证一起收进third-party/axe/以第一方代码的方式随仓库构建、随仓库测试这一策略把对上游的依赖收敛为偶尔按需同步单个文件避免了引入完整 axe 依赖树的体积与安全面开销。需要说明的是这种复用是有边界的——Lighthouse 只借用了语言表这一纯数据 纯函数模块而没有把 axe 的页面注入与规则引擎搬进来无状态、无副作用、输入输出清晰的模块性质正是它适合被抽取复用的前提。六、小结一次可借鉴的小而精第三方复用范本回顾整个链路README 用两句话交代动机hreflang 审计需要、lib 不可导入→valid-langs.js用 trie 编码把 20KB 语言表压到 2KB 并提供逐字符查表 API → IANA 注册表脚本保证数据可再生 → hreflang 审计只取主语言标签做大小写归一化校验 → 测试用例锁死全部边界行为。这条链路上每一个环节都有源码或测试可查实现与编码说明third-party/axe/valid-langs.js消费方审计core/audits/seo/hreflang.js行为验证core/test/audits/seo/hreflang-test.js数据采集core/gather/gatherers/link-elements.js许可声明third-party/axe/LICENSE对于任何需要在生产代码中复用大而全依赖内部小工具的场景Lighthouse 的这一做法都提供了可复制的模板先评估模块是否无状态、可独立再以最小文件 许可证随仓库抽取最后用测试把行为边界固化下来。【免费下载链接】lighthouseAutomated auditing, performance metrics, and best practices for the web.项目地址: https://gitcode.com/GitHub_Trending/lig/lighthouse创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考