我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

MicroLighter语法懒加载背后的秘密:依赖自动解析与Promise缓存机制全解析

MicroLighter语法懒加载背后的秘密:依赖自动解析与Promise缓存机制全解析 【免费下载链接】microlighterA zero-dep syntax highlighter that uses the CSS Highlights API项目地址https://gitcode.com/gh_mirrors/mi/microlighter点击查看免费下载MicroLighter 是一款零依赖、压缩后仅约 2 KiB 的浏览器语法高亮库基于 CSS Custom Highlight API 与 TextMate 语法。它内置 37 种编程语言但全部采用语法懒加载——页面里出现什么语言才真正加载什么语法。本文带你拆解 src/grammar-dependencies.js 中的依赖自动解析与 Promise 缓存机制看看按需加载是如何做到的。 为什么 37 种语言可以只占 2 KiB大多数高亮库的做法是一次性打包所有语言页面越大、加载越慢。MicroLighter 选择了另一条路传统方案MicroLighter 方案所有语法打包进主文件每种语法一个独立 ES 模块用户为不用的语言买单用到哪个才import哪个跨语言复用困难通过依赖解析自动共享底层语法核心设计非常朴素37 个语法文件就放在 37 个独立的 JS 模块里src/grammars/主库只保留高亮引擎。构建时 package.json 里还设了sizeLimit体积预算——一旦主文件超过 2150 字节gzip构建直接失败。这种斤斤计较正是它敢承诺 2 KiB 的底气。但懒加载会引出两个新问题语法之间互相依赖——比如 SCSS 语法里include了source.css单加载 SCSS 会残废并发请求会重复下载——同一毫秒内两次要求加载 JavaScript会发两遍网络请求吗这就是 grammar-dependencies.js 要解决的。 秘密一依赖是怎么被发现的显式声明dependencies字段最简单的方案是手动声明。打开 src/grammars/scss.js你会发现每个语法模块的头部都长这样export default { scopeName: source.scss, dependencies: [css], patterns: [ /* ... 规则 ... */ ] };依赖关系一览部分语法自动加载的依赖typescriptjavascriptcpp/objective-ccscsscssmarkdownyamltsxjavascripttypescriptsvelte/vuehtmlcssscssjavascripttypescriptheexhtmlelixir自动扫描getExternalLanguages但手动声明容易漏。所以库还提供了一条自动发现路径getExternalLanguages函数会深度遍历整个语法对象找出所有 TextMate 的跨语法include引用// 简化逻辑只看 source.xxx / text.xxx 这类外部引用 const match scope.match(/^(?:source|text)\.([a-z0-9_-])/);include: source.css→ 发现依赖css✅include: #comments#开头指向本语法内部→ 跳过include: $self/$base语法自身引用→ 跳过CONTRIBUTING.md 也提到新语法只需声明scopeName跨语言引用会在运行时被自动识别。 秘密二动态import()按需加载依赖发现之后真正把语法弄进来靠的是 ES 模块的动态import()const getGrammar language import(./grammars/${language}.js) .then(module module.default) .catch(() null);三个值得注意的细节模板字符串拼接路径——浏览器才知道要下载哪个文件这正是懒的本体.catch(() null)——语言不存在时静默降级为null不抛错、不阻断整页高亮返回PromiseGrammar | null——为下一节的 Promise 缓存打好基础。 秘密三Promise 缓存机制这是全文最精华的部分。createGrammarLoader工厂函数内部维护了一张 Map/** type {Mapstring, PromiseGrammar | null} */ const loadingLanguages new Map(); const loadGrammar language { if (!loadingLanguages.has(language)) { const grammarModule importLanguage(language).then(grammar { if (grammar) { grammars.languages[language] grammar; grammars.scopes.set(grammar.scopeName, grammar); } return grammar; }); loadingLanguages.set(language, grammarModule); } return loadingLanguages.get(language); };关键点 1缓存的是 Promise而不是结果注意 Map 里存的是Promise对象本身。这意味着第一次调用loadGrammar(javascript)→ 发起import()Promise 存入 Map同一个import()还没完成时第二次调用 →直接拿到同一个 Promise两个调用方await同一个 Promise各等一次、只下载一份。测试文件 test/grammar-dependencies.spec.js 里有个用例专门验证这一点shares in-flight grammar imports between concurrent loads两次并发加载javascript底层import只执行了1 次。 这种模式叫in-flight promise memoization。如果缓存的是解析后的结果而不是 Promise在异步间隙里第二次调用就可能触发重复请求——存 Promise 才是滴水不漏的写法。关键点 2双索引数据结构语法加载成功后会被同时登记到两个索引里grammars.languages[language] grammar; // 按语言名{ javascript: {...} } grammars.scopes.set(grammar.scopeName, grammar); // 按 scope 名{ source.js → {...} }为什么要两套因为 TextMate 的include是按scope 名寻址的source.css而用户代码里写的是语言名language-css。双索引让高亮引擎 src/highlight.js 里的resolveInclude能一步命中被引用的语法实现跨语言规则复用。关键点 3单例 loader 贯穿生命周期在 src/highlight.js 中loader 只在模块顶层创建一次const loadGrammars createGrammarLoader();因此缓存的生命周期 页面生命周期。第二次highlightAll()比如编辑代码后重新高亮时所有语法命中缓存零网络请求直接进入高亮流程。 秘密四BFS 式依赖解析光加载请求语言还不够还得把依赖递归补齐。loader 的返回函数用了一个while循环做广度优先遍历return async languages { const visited new Set(); let pending [...new Set(languages)].filter(l /^[a-z0-9_-]$/.test(l)); while (pending.length) { pending.forEach(language visited.add(language)); const loaded (await Promise.all(pending.map(loadGrammar))).filter(Boolean); pending [...new Set(loaded.flatMap(grammar grammar.dependencies || [...getExternalLanguages(grammar)] ))] .map(language normalizeLanguage(language)) .filter(language !visited.has(language)); } return grammars; };逐行翻译一下这个循环步骤作用new Set(languages)去重同一语言只处理一次正则/^[a-z0-9_-]$/过滤只接受合法模块名防止异常输入拼出奇怪路径Promise.all(pending.map(loadGrammar))整批并发加载而不是串行visited集合防止依赖成环时死循环grammar.dependencies \|\| getExternalLanguages(...)优先用显式声明没有就自动扫描normalizeLanguage(...)把别名转正见下文用一张图描述加载tsx的过程第 1 批请求tsx │ ├── 声明依赖 → 第 2 批并发请求javascript、typescript │ │ └── typescript 依赖 javascript ──→ 已在 visited 中跳过 ✅ │ 全部就绪 → 返回双索引 grammars顺手一提别名标准化normalizeLanguage维护了一张别名表让你写language-js、language-yml也能正确路由你写的实际加载js/jsxjavascripttstypescriptsh/shell/zshbashymlyamlmdmarkdownsassscss 把四个秘密串起来一次高亮的完整旅程以页面里有一个precode classlanguage-svelte代码块为例highlightAll()扫描 DOM识别出语言sveltehighlight.js 的getLanguageloader 检查缓存——svelte 没加载过发起import(./grammars/svelte.js)Promise 入缓存svelte 解析完毕声明依赖html、css、scss、javascript、typescript→ 第 2 批并发加载html又声明依赖jsonscss声明依赖css已在批中跳过scss内部include: source.css也指向同一份 css——自动发现与显式声明互相兜底所有语法登记进languages/scopes双索引高亮引擎开始把 TextMate 规则转成Range写入CSS.highlights注册表。全程没有往 DOM 里塞一个span——这就是标题里CSS Highlights API的另一半故事。✅ 测试如何守护这套机制这套机制并非能跑就行test/grammar-dependencies.spec.js 里有完整的行为契约并发共享两个并发加载底层 import 只触发 1 次依赖报告逐个断言每个内置语法的依赖集合是否正确如getExternalLanguages(tsx)应返回js、ts传递加载加载tsx必须顺带加载出javascript和typescript且scopes.get(source.c)与languages.c指向同一对象别名归一加载js后loaded.languages.js应为undefined只有loaded.languages.javascript存在。想自己验证git clone https://gitcode.com/gh_mirrors/mi/microlighter后运行npm test即可。 总结懒加载的完整拼图机制代码位置解决的问题每语言一个 ES 模块src/grammars/物理上可拆分的加载单元动态import()grammar-dependencies.js#L70-L72运行时按需下载Promise 缓存 Mapgrammar-dependencies.js#L93-L106并发去重 终身复用BFS 依赖解析grammar-dependencies.js#L108-L124跨语法依赖自动补齐getExternalLanguagesgrammar-dependencies.js#L43-L63不声明也能发现依赖normalizeLanguage别名表grammar-dependencies.js#L12-L35各种写法都能正确路由对新手来说这套机制最值得偷师的就是**缓存 Promise 而不是缓存结果**——几行代码就能让异步加载天然免疫重复请求。MicroLighter 用它守住了 2 KiB 的体积也守住了零依赖的承诺。想动手体验npm install microlighter加上一个language-javascript的 code 块、导入任意一个主题见 src/themes/await highlightAll()之后懒加载的魔法就发生了。✨赞分享【免费下载链接】microlighterA zero-dep syntax highlighter that uses the CSS Highlights API项目地址https://gitcode.com/gh_mirrors/mi/microlighter点击查看免费下载相关推荐FastAPI 子依赖Sub-dependencies实战依赖嵌套、自动解析与依赖缓存机制FastAPI 子依赖Sub dependencies实战依赖嵌套、自动解析与依赖缓存机制 FastAPI 的依赖注入系统允许依赖函数嵌套出任意深度的“依后端Web框架API设计异步编程BambuStudio 冲洗体积窗口一片白三步自检完整修复路径BambuStudio 冲洗体积窗口一片白三步自检完整修复路径 如果你在 BambuStudio 里点开多材料设置进入 Flushing Volumes桌面应用3D渲染图形学智能硬件pnpm存储系统包缓存与依赖解析机制pnpm存储系统包缓存与依赖解析机制 pnpm采用先进的CAFSContent Addressable File System内容寻址存储引擎通过文件内包管理器开发工具CLI上一篇Qml.Net新手入门30分钟搭建你的第一个跨平台桌面应用下一篇HyenaDNA预训练模型全面解析HG38基因组上的100万核苷酸学习奇迹创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表