资讯详情

资讯详情

为 webpack 接入自定义 JavaScript 解析器:以 acorn、oxc、meriyah 为例的 ParseFunction 适配实战

为 webpack 接入自定义 JavaScript 解析器以 acorn、oxc、meriyah 为例的 ParseFunction 适配实战【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack本文以仓库中的 examples/custom-javascript-parser/README.md 为主线讲解如何在 webpack 5当前仓库版本中把默认的 JavaScript 解析流程整体替换为第三方解析器oxc、meriyah并保留默认的 acorn 作为对照。读完你可以掌握 webpack 暴露的ParseFunction解析器契约、在「全局」与「module 规则级」两个维度接入自定义解析器的配置方法以及如何为 oxc、meriyah 这类风格迥异的解析器补齐 estree 兼容的注释与位置信息最终让同一份源码在不同解析引擎下产出结构一致的构建结果。示例要解决的问题webpack 的模块分析依赖提取、import()代码分割、export信息收集、Tree Shaking 判定等全部建立在对模块源码的语法解析之上。官方默认解析引擎是 acorn更准确地说lib/javascript/JavascriptParser.js 中默认使用的WebpackParser即 acorn 封装代码中直接以AcornParser类型引用。但 acorn 并非唯一选择oxc 是用 Rust 编写的高性能解析器meriyah 是另一个纯 JavaScript 的 ECMAScript 解析器。本示例的目的非常明确——验证并演示「同一份 webpack 配置把解析器换成任何一家构建产物都保持一致」。它包含一个需要解析的入口模块example.jsESM 语法 动态import()三个各自独立、接口相同的解析器适配层internals 目录下一个把三种解析器编译成不同产物的多编译器multi-compiler配置一个用于横向对比解析性能的基准脚本。// examples/custom-javascript-parser/example.js import { increment as inc } from ./increment; var a 1; inc(a); // 2 // async loading import(./async-loaded).then(function (asyncLoaded) { console.log(asyncLoaded); });入口代码虽然简单却覆盖了 webpack 解析阶段的几类核心场景ESM 静态导入import { increment as inc } from ./increment要求解析器输出能被 webpack 依赖插件识别的ImportDeclaration节点其依赖模块为 increment.js示例目录中还有同级的 math.js动态导入代码分割import(./async-loaded)会触发 async chunk 的创建被拆出去的模块是 async-loaded.js仅导出answer 42普通语句与注释var a 1;、inc(a)这类表达式以及行注释要求解析器能正确给出节点区间与注释集合webpack 后续要靠注释做 magic comments 等处理。webpack 的解析器契约ParseFunction在动手写适配层之前先看 webpack 对「自定义解析函数」的定义。契约就定义在 lib/javascript/JavascriptParser.js/** * typedef {object} ParseOptions * property {module | script} sourceType * property {EcmaVersion} ecmaVersion * property {boolean} locations * property {boolean} comments * property {boolean} ranges * property {boolean} allowHashBang * property {boolean} allowReturnOutsideFunction * property {boolean} importPhases // 是否解析 import phase 提案语法 */ /** * typedef {object} ParseResult * property {Program} ast * property {Comment[]} comments */ /** * typedef {(code: string, options: ParseOptions) ParseResult} ParseFunction */也就是说自定义解析器本质上就是一个(源码字符串, ParseOptions) { ast, comments }的函数。webpack 只要求两件事ast是estree 规范的Program节点——webpack 全部依赖插件Harmony、CommonJS、AMD 等都在这个标准 AST 上工作后续并不关心 AST 是谁产出的comments是 estree 兼容的注释数组供 magic comments如import(/* webpackChunkName: ... */ ...)等能力使用。调用链与参数的真实来源真正驱动解析的地方在 JavascriptParser.parse()。webpack 在调用解析函数时固定传入以下内部选项({ ast, comments } JavascriptParser._parse( source, { sourceType: this.sourceType, // line/column locations are derived from node offsets via // getLocation — parsers never need to track them locations: false, ranges: true, comments: true, importPhases: this.options.importPhases true }, this.options.parse ));这里有几条值得注意的约定约定说明locations: falsewebpack不需要解析器提供 loc。行列号由源码文本和节点 offset 通过getLocation自行推导源码注释parsers never need to track them。这正是下文 oxc 适配层可以理直气壮不做 location 收集的原因ranges: true节点必须带range[start, end]偏移区间comments: true注释收集开关会如实传给你this.options.parse就是你通过配置注入的解析函数不注入时走到默认解析路径在静态方法_parse中lib/javascript/JavascriptParser.js当检测到传入的是自定义customParse函数时webpack 会为每次解析新建一份干净的选项对象{ ...defaultParserOptions, ...options }再按 sourceType 归一化避免跨模块残留解析状态。这也是自定义解析函数的 JSDoc 注释里要导入ParseOptions、ParseResult类型的原因。配置项在 Schema 中的位置parse是module.parser.javascript即 JavascriptParserOptions的合法成员schemas/WebpackOptions.json 中定义如下parse: { description: Function to parser source code., instanceof: Function, tsType: import(../lib/javascript/JavascriptParser).ParseFunction }因此 webpack 配置文件里凡是能写parser选项的地方module.parser全局或module.rules[].parser按规则覆盖都可以注入parse函数。三种解析器的适配实现三个适配文件都放在 examples/custom-javascript-parser/internals 目录下接口一致输入源码与ParseOptions、输出{ ast, comments }内部实现却因为各解析器 API 的差异而各具特色。acorn默认解析器的对标实现文件internals/acorn-parse.jsuse strict; const acorn require(acorn); /** import { Comment, SourceLocation } from estree */ /** * import { * ParseOptions, * ParseResult * } from ../../../lib/javascript/JavascriptParser */ /** * param {string} sourceCode the source code * param {ParseOptions} options options * returns {ParseResult} the parsed result */ const acornParse (sourceCode, options) { /** type {(Comment { start: number, end: number, loc: SourceLocation })[]} */ const comments []; const ast /** type {import(estree).Program} */ ( acorn.parse(sourceCode, { ...options, onComment: options.comments ? comments : undefined }) ); return { ast, comments }; }; module.exports acornParse;acorn 适配层的写法最「偷懒」也最稳妥把 webpack 传来的ParseOptions原样展开传给acorn.parseacorn 与 webpack 默认引擎同源字段几乎一一对应再用 acorn 的onComment回调把注释收集进数组。这一份实现基本等价于 webpack 内置的默认行为所以它天然是本示例的对照组产物结构、大小理应与其他两个解析器完全一致。oxc文件internals/oxc-parse.jsuse strict; const oxc require(oxc-parser); /** import { Program, Comment } from estree */ /** * import { * ParseOptions, * ParseResult * } from ../../../lib/javascript/JavascriptParser */ /** * Oxc has no location API — none is needed: webpack derives line/column * locations from node offsets and the source text itself. ASI positions are * likewise read from the source, so no semicolon collection is required. * param {string} sourceCode the source code * param {ParseOptions} options options * returns {ParseResult} the parsed result */ const oxcParse (sourceCode, options) { const result oxc.parseSync(file.js, sourceCode, { astType: js, range: true, sourceType: options.sourceType module ? module : script, // ts-expect-error no types experimentalRawTransfer: true }); const comments /** type {(Comment { start: number, end: number })[]} */ (result.comments); // webpacks magic-comment lookup reads comment.range for (const comment of comments) { if (!comment.range) comment.range [comment.start, comment.end]; } return { ast: /** type {Program} */ (/** type {unknown} */ (result.program)), comments }; }; module.exports oxcParse;oxc 的适配是三种实现里最有信息量的一份因为它揭示了 oxc 与 acorn 生态的几处 API 差异及 webpack 侧的兼容要求无 location API 反而省事适配层顶部的注释直言 oxc 没有位置 API——但 webpack 本来就不需要参见上文locations: false与getLocation推导。ASI自动分号插入位置也从源码读取因此无需收集分号位置必须开启range: truewebpack 全部基于range/offset 工作sourceType 需显式归一化把options.sourceType映射为 oxc 的module | script注释对象要补range适配层特别注释了webpacks magic-comment lookup readscomment.range——如果 oxc 返回的注释只有start/end而没有range必须手工补上range [start, end]否则依赖注释的 magic comment 功能指定 chunk 名等会失效experimentalRawTransfer: true用于让 AST 以高效的原生方式传递此处因 oxc 类型未提供而加了ts-expect-error。meriyah文件internals/meriyah-parse.jsuse strict; const meriyah require(meriyah); /** import { Program, Comment, SourceLocation } from estree */ /** * import { * ParseOptions, * ParseResult * } from ../../../lib/javascript/JavascriptParser */ /** * param {string} sourceCode the source code * param {ParseOptions} options options * returns {ParseResult} the parsed result */ const meriyahParse (sourceCode, options) { /** type {(Comment { start: number, end: number, loc: SourceLocation })[]} */ const comments []; const ast /** type {import(estree).Program} */ ( meriyah.parse(sourceCode, { ...options, module: options.sourceType module, loc: options.locations, onComment: options.comments ? (type, value, start, end, loc) { if (type SingleLine || type MultiLine) { comments.push({ type: type SingleLine ? Line : Block, value, start, end, range: [start, end], loc }); } } : undefined }) ); return { ast, comments }; }; module.exports meriyahParse;meriyah 的 API 与 acorn 神似但字段名不同适配要点是做一层「方言翻译」meriyah 的模块开关叫module需从options.sourceType module推导webpack 传来的ecmaVersion/ranges等通过...options直接透传meriyah 的 location 开关叫loc对应options.locationsmeriyah 的注释回调签名是(type, value, start, end, loc)且注释类型叫SingleLine/MultiLine而 estree 规范要求Line/Block——适配层在回调里做了类型映射并为每条注释补齐 estree 风格的range: [start, end]与loc未开启注释options.comments为假时传入undefined避免无谓的回调开销。三个适配层的差异一览维度acornoxcmeriyah选项透传...options直通仅取sourceType另设astType/range/experimentalRawTransfer...optionsmodule/loc重命名sourceType 映射无需module ? module : script转成布尔module注释获取onComment回调直接从result.comments读onComment回调 类型翻译注释类型estree 原生需补rangeSingleLine/MultiLine→Line/Blocklocation通过 loc 携带不需要webpack 用 offset 推导显式开loc并透传可以看到接口是 webpack 定的estreeProgramcomments成本全部发生在「把各家解析器的产物对齐到 estree 约定」上。配置全局覆盖与 module 级覆盖完整的演示配置在 webpack.config.js它导出的是一个三元素配置数组multi-compiler依次为 oxc、meriyah、acorn 各建一次构建产物分别命名为oxc.[name].js、meriyah.[name].js、acorn.[name].js便于直接对比。use strict; const acornParse require(./internals/acorn-parse.js); const meriyahParse require(./internals/meriyah-parse.js); const oxcParse require(./internals/oxc-parse.js); /** type {import(webpack).Configuration[]} */ const config [ // oxc { mode: production, optimization: { chunkIds: deterministic // To keep filename consistent between different modes (for example building only) }, output: { filename: oxc.[name].js }, module: { // Global override parser: { javascript: { parse: oxcParse } } // Override on the module level, only for modules which match the test // rules: [ // { // test: /\.js$/, // parser: { // parse: oxcParse // } // } // ] } }, // meriyah { mode: production, optimization: { chunkIds: deterministic // To keep filename consistent between different modes (for example building only) }, output: { filename: meriyah.[name].js }, module: { // Global override parser: { javascript: { parse: meriyahParse } } // Override on the module level, only for modules which match the test // rules: [ // { // test: /\.js$/, // parser: { // parse: meriyahParse // } // } // ] } }, // acorn { mode: production, output: { filename: acorn.[name].js }, optimization: { chunkIds: deterministic // To keep filename consistent between different modes (for example building only) }, module: { // Global override parser: { javascript: { parse: acornParse } } // Override on the module level, only for modules which match the test // rules: [ // { // test: /\.js$/, // parser: { // parse: acornParse // } // } // ] } } ]; module.exports config;这段配置演示了接入自定义解析器的两种作用域全局覆盖示例采用的方案module.parser.javascript.parse fn。module.parser之下是各模块类型javascript等的解析选项parse作为javascript解析器选项的一员被注入之后所有 JS 模块的解析都会走该函数module 级覆盖注释中保留的写法module.rules[{ test: /\.js$/, parser: { parse: fn } }]。只对命中test的模块启用自定义解析器未命中的模块仍走默认解析。这在实际工程中更有吸引力——例如只对体积最大的依赖库使用更快的 oxc其余模块保持默认把风险面控制到最小。配置中两处容易被忽略的细节optimization.chunkIds: deterministic注释写明是为了keep filename consistent between different modes (for example building only)。三份配置用同一套 chunk 命名策略才能保证「只是换了解析器」时产物名完全可对比、可复现多份配置共用同一份test.filter.js环境门控当前示例要求 Node.js 主版本 ≥ 20见 test.filter.js它读取process.versions.node的主版本号做判定这与基准脚本使用import.meta.dirname等较新的 Node 能力有关。构建输出解读不同解析器同一份结构README 的Info小节记录了该示例在两种模式下的构建统计每个模式下 README 对三种解析器各记录了一份内容完全相同——这正印证了示例的核心主张解析器可替换产物结构一致。这里各取一份代表性输出。Unoptimized未做生产优化的构建asset output.js 11.4 KiB [emitted] (name: main) asset 655.output.js 761 bytes [emitted] chunk (runtime: main) 655.output.js 24 bytes [rendered] ./async-loaded ./example.js 6:0-24 ./async-loaded.js 24 bytes [built] [code generated] [exports: answer] [used exports unknown] import() ./async-loaded ./example.js 6:0-24 chunk (runtime: main) output.js (main) 457 bytes (javascript) 5.34 KiB (runtime) [entry] [rendered] ./example.js main runtime modules 5.34 KiB 8 modules dependent modules 281 bytes [dependent] 2 modules ./example.js 176 bytes [built] [code generated] [no exports] [used exports unknown] entry ./example.js main webpack X.X.X compiled successfully这条输出能读出不少信息产物被拆成入口 chunkoutput.js含入口./example.js、2 个依赖模块与 5.34 KiB 的运行时 异步 chunk655.output.js24 字节的./async-loaded.js异步 chunk 正是由example.js 6:0-24处的import()触发——代码分割在该示例下工作正常[exports: answer]表明解析器正确提取了async-loaded.js的导出answer未优化模式下[used exports unknown]使用信息未知对应不做 Tree Shaking 分析的开发式构建状态。Production modeasset output.js 2.01 KiB [emitted] [minimized] (name: main) asset 655.output.js 121 bytes [emitted] [minimized] chunk (runtime: main) 655.output.js 24 bytes [rendered] ./async-loaded ./example.js 6:0-24 ./async-loaded.js 24 bytes [built] [code generated] [exports: answer] import() ./async-loaded ./example.js 2 modules ./example.js 6:0-24 chunk (runtime: main) output.js (main) 457 bytes (javascript) 5.34 KiB (runtime) [entry] [rendered] ./example.js main runtime modules 5.34 KiB 8 modules ./example.js 2 modules 457 bytes [built] [code generated] [no exports] [no exports used] entry ./example.js main webpack X.X.X compiled successfully生产模式的变化一目了然主 chunk 从 11.4 KiB 压缩到2.01 KiB异步 chunk 从 761 bytes 压缩到 121 bytes均带[minimized]标记./example.js 2 modules表示生产构建下模块被合并module concatenation进同一作用域状态从[used exports unknown]变为[no exports used]说明在 Tree Shaking 分析后该模块没有导出被使用可被摇树处理——再次证明自定义解析器产出的 estree AST 完整支撑了 webpack 的依赖/导出分析管线。提示以上统计中的webpack X.X.X为示例模板生成的版本占位符异步 chunk 数字命名如655.output.js与 chunk 命名策略有关配置中显式声明chunkIds: deterministic后即保持稳定。附带的解析器性能基准示例目录中还提供了一个公平起见的三方解析器微基准 internals/bench.mjs它使用 tinybench对三种适配函数做横向计时import fs from node:fs; import path from node:path; import { Bench } from tinybench; import oxcParse from ./oxc-parse.js; import meriyahParse from ./meriyah-parse.js; import acornParse from ./acorn-parse.js; const options { sourceType: module, ecmaVersion: latest, ranges: true, locations: true, comments: true, allowHashBang: true, allowReturnOutsideFunction: false }; const bench new Bench({ name: simple benchmark, time: 100 }); const sourceCode fs.readFileSync( path.resolve( import.meta.dirname, ../../../node_modules/three/build/three.module.js ), utf8 ); bench .add(oxc, () { oxcParse(sourceCode, options); }) .add(meriyah, () { meriyahParse(sourceCode, options); }) .add(acorn, () { acornParse(sourceCode, options); }); await bench.run(); console.log(bench.name); console.table(bench.table());这个脚本的设计很值得学习输入与参数完全一致三个解析器解析同一份node_modules/three下的three.module.js体积大、模块化程度高能体现真实差异并使用同一组ParseOptions按 webpack 的真实入参测试基准调用的是internals/*-parse.js这三个带适配逻辑的函数而非裸解析器 API因此计时结果最接近「替换 webpack 解析器后的真实收益」公平的测量tinybench的time: 100表示固定跑约 100ms 后对比吞吐。运行该脚本需要 Node ≥ 20脚本使用了import.meta.dirname这也是 test.filter.js 对 Node 主版本做门控的原因。值得注意的是 oxc 与 meriyah 作为解析器依赖需要能被require解析到——仓库开发环境中 acorn、oxc-parser、meriyah 均以依赖形式安装见 package.json、package.json、package.json。如果你在自己的项目里接入需要自行npm install对应的解析器包。延伸阅读与仓库佐证围绕「自定义解析器」这个主题可以在仓库中找到更多印证类型与契约lib/javascript/JavascriptParser.js 定义了ParseOptions、ParseResult、ParseFunction三种 JSDoc 类型lib/javascript/JavascriptParser.js 是调用自定义parse的实际位置lib/javascript/JavascriptParser.js 展示了对自定义解析器每次新建选项对象、以及对默认解析器复用单例选项的差异Schema 校验schemas/WebpackOptions.json 中parse被定义为Function类型指向JavascriptParser的ParseFunction这决定了配置校验与 TypeScript 提示的来源示例的 README 模板机制template.md 用_{{example.js}}_、_{{internals/oxc-parse.js}}_、_{{webpack.config.js}}_、_{{stdout}}_等占位符把上述源码与构建输出自动注入到 README——也就是说 examples/custom-javascript-parser/README.md 是一份由真实源码和真实构建结果自动生成的文档其中的每个代码块都能在仓库中找到原文件。总结webpack 的自定义解析器机制把「语法解析」这个最昂贵的环节之一做成了可插拔接口。你只要交付一个返回 estreeProgram 注释数组的函数就能在保持依赖分析、代码分割、Tree Shaking 等能力不变的前提下自由替换解析引擎。本示例给出的 acorn/oxc/meriyah 三份适配实现恰好覆盖了「同源直通、异源归一、方言翻译」三种典型的对接形态是你为自己的项目接入自定义解析器时最直接的参考蓝本。【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →