ESLint 自定义处理器(Custom Processors)完全指南:从零为 Markdown/HTML 文件编写 preprocess 与 postprocess

发布时间:2026/9/11 0:12:35
ESLint 自定义处理器(Custom Processors)完全指南:从零为 Markdown/HTML 文件编写 preprocess 与 postprocess ESLint 自定义处理器Custom Processors完全指南从零为 Markdown/HTML 文件编写 preprocess 与 postprocess【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint自定义处理器Custom Processors是 ESLint 扩展机制中用于处理「非标准 JavaScript 文件」的核心能力它让 ESLint 能够从 Markdown、HTML、模板字符串等文件中提取出 JavaScript 代码进行校验再把报告回映射到原始文件的位置。本文基于 custom-processors.md 官方文档结合仓库内的 processor-service.js 实现与 processor-service.js 测试 测试用例完整讲解处理器的接口规范、配置方式、自动修复支持与meta对象的使用细节读完即可动手编写一个可用于 flat config 的自定义处理器。::: tip 本文讲解的自定义处理器基于flat config格式即eslint.config.js。如果你仍在使用已废弃的.eslintrc格式请参考 plugin-migration-flat-config.md 完成迁移。 :::为什么需要自定义处理器ESLint 默认只能解析标准的 JavaScript 代码。但在真实项目中大量 JavaScript 片段散落在其他格式的文件里例如Markdown 文档中的 js 代码块HTML 文件中script标签内的内联脚本Vue、Svelte 等单文件组件中的script部分模板字符串中嵌入的代码片段。自定义处理器Custom Processor的作用就是告诉 ESLint如何从这类文件中提取 JavaScript 片段并单独对其进行 lint。例如社区流行的eslint/markdown插件就内置了一个专门从 Markdown 中提取并校验 JS 代码块的自定义处理器。处理器的核心思想是「两步走」preprocess预处理读取原始文件的全部文本剥离非 JS 内容把其中的 JS 片段拆分返回给 ESLint 逐一校验postprocess后处理收集每个片段产生的 lint 消息把错误位置映射回原始文件的真实行列并合并成一维数组返回。处理器接口规范Custom Processor Specification要创建一个自定义处理器你的模块导出的对象必须满足以下接口。处理器通常作为插件的一部分放在processors键下但也可以单独导出const plugin { meta: { name: eslint-plugin-example, version: 1.2.3, }, processors: { processor-name: { meta: { name: eslint-processor-name, version: 1.2.3, }, // 接收文件文本与文件名 preprocess(text, filename) { // 在这里剥离非 JS 内容 // 并按需拆分成多个待 lint 的代码片段 return [ // 返回待 lint 的代码块数组 { text: code1, filename: 0.js }, { text: code2, filename: 1.js }, ]; }, // 接收 Message[][] 与文件名 postprocess(messages, filename) { // messages 是二维数组每个顶层元素对应 // preprocess() 返回数组中的一块代码产生的消息 // 需要返回保留的一维消息数组 return [].concat(...messages); }, supportsAutofix: true, // 可选默认 false }, }, }; // 用于 ESM export default plugin; // 或用于 CommonJS module.exports plugin;preprocess方法拆分代码块preprocess方法接收两个参数文件的完整内容text和文件路径filename返回一个待 lint 的代码块数组。每个代码块会被独立 lint但报告仍归属到原始文件名之下。代码块对象包含两个属性属性说明text代码块的实际内容filename代码块的「虚拟文件名」可以任意命名但应当包含文件扩展名代码块的扩展名非常关键ESLint 会根据扩展名决定如何处理该代码块——只有当代码块的虚拟文件名以.js结尾、或与项目配置中的files条目匹配时它才会被真正 lint详见下文源码分析中的filterCodeBlock。因此如果你从 Markdown 中提取的片段要按 JS 规则校验就应命名为0.js、1.js等。至于返回一块还是多块完全由插件自行决定处理.html文件时你可以把所有script内容合并后只返回一项处理.md文件时由于每个 JS 代码块相互独立你可以返回多个条目分别校验。postprocess方法聚合与位置映射postprocess方法接收一个二维数组每个顶层元素对应preprocess返回的一块代码所产生的消息列表以及文件名必须完成两件事调整所有错误的行列位置使其对应到原始未处理文件中的真实位置因为校验发生的位置是片段内部需要按片段在原始文件中的偏移量换算回去把所有消息聚合成一个一维数组并返回。最简单但最常见的实现就是[].concat(...messages)即messages.flat()它在插件不打算映射位置时原样合并所有消息。Lint 消息的数据结构报告出的问题lint message包含以下位置与修复信息type LintMessage { /// 消息出现的行号1 起始。 line?: number; /// 消息出现的列号1 起始。 column?: number; /// 结束位置的行号1 起始。 endLine?: number; /// 结束位置的列号1 起始。 endColumn?: number; /// 若为 true表示致命错误。 fatal?: boolean; /// 自动修复信息。 fix: Fix; /// 错误消息文本。 message: string; /// 产生该消息的规则 ID不适用时为 null。 ruleId: string | null; /// 消息的严重级别。 severity: 0 | 1 | 2; /// 建议suggestion信息。 suggestions?: Suggestion[]; }; type Fix { range: [number, number]; text: string; }; type Suggestion { desc?: string; messageId?: string; fix: Fix; };其中Fix.range是一对索引指向代码中将被替换的连续文本区间的起止位置Fix.text是要替换进该区间的文本。severity取值0off、1warn、2error。从源码看处理器的真实调用链文档描述的接口在仓库中有完整的实现支撑。核心类是 lib/services/processor-service.js 中的ProcessorService// lib/services/processor-service.js preprocessSync(file, config) { const { processor } config; ... blocks processor.preprocess(file.rawBody, file.path); ... if (typeof blocks.then function) { throw new Error(Unsupported: Preprocessor returned a promise.); } ... files: blocks.map((block, i) { // 兼容旧行为块可以是纯字符串 if (typeof block string) { return block; } const filePath path.join(file.path, ${i}_${block.filename}); return new VFile(filePath, block.text, { physicalPath: file.physicalPath }); }), } postprocessSync(file, messages, config) { const { processor } config; return processor.postprocess(messages, file.path); }值得注意的实现细节preprocess以file.rawBody原始文本和file.path为参数同步调用处理器必须是同步的——若返回 Promise会直接抛出Unsupported: Preprocessor returned a promise.错误对应测试 中的should throw an error if the preprocessor returns a promise用例代码块既可以是{ filename, text }对象现代写法会被包装成VFile虚拟路径为原文件路径 索引前缀 块文件名例如foo.md下的第 0 块成为foo.md/0_block.js也可以是纯字符串旧版兼容写法直接按 JS 字符串校验preprocess抛出异常时会被捕获并转换成fatal: true, severity: 2, ruleId: null的致命 lint 消息消息前缀为Preprocessing error:同时会剥离错误信息开头的line N:前缀对应测试用例should strip leading line N: prefixpostprocess的异常不会被捕获会直接向上传播。调用链从verify到ProcessorService处理器在 lib/linter/linter.js 的_verifyWithFlatConfigArrayAndProcessor方法中被接入主流程当配置对象中存在config.processor时linter.js#L1393-L1404ESLint 取出preprocess、postprocess与supportsAutofix并计算disableFixes options.disableFixes || !supportsAutofix—— 这就是「未声明supportsAutofix: true时即使带--fix也不会自动修复」的底层原因调用processorService.preprocessSync(file, { processor })得到各代码块linter.js#L898每个代码块通过filterCodeBlock过滤后分别独立 lint。默认的过滤规则是// lib/linter/linter.js#L909-L911 const filterCodeBlock options.filterCodeBlock || (blockFilename blockFilename.endsWith(.js));即默认只 lint 虚拟文件名以.js结尾的代码块。这正是文档中强调「代码块文件名应包含扩展名」的原因——想 lint 其他扩展名的片段如.jsx需要在配置中增加匹配的files条目若代码块内容或扩展名与原始文件不一致还会使用递归配置解析configForRecursive重新匹配更精确的配置linter.js#L933-L951这保证了 Markdown 内的.jsx块能命中你为**/*.jsx单独书写的 parserOptions最后调用processorService.postprocessSync合并所有块的消息并返回linter.js#L963。在 tests/lib/services/processor-service.js 中这些行为都有对应的单元测试覆盖例如验证preprocess以原始文本与路径被调用、对象块被包装为带索引前缀的VFile、字符串块保持旧行为、以及错误消息的格式化逻辑。为处理器启用自动修复Autofix默认情况下即使命令行带上--fix标志ESLint 在使用自定义处理器时也不会执行自动修复。这是因为处理器产出的 lint 消息位置在「处理后的 JS 片段」内直接应用修复会改错地方。要让 ESLint 支持带处理器的自动修复需要额外做两步在postprocess中转换fix属性所有可自动修复的问题都带有fix属性其结构为{ range: [number, number], text: string }range包含两个索引指向「将被替换的连续文本区间」的起止位置text是要替换进该区间的文本。初始消息列表中的fix.range指向的是处理后的 JavaScript 片段中的位置postprocess必须将其换算为原始未处理文件中的位置按片段在原始文件中的偏移量整体平移range修复才能落到正确的位置上。在处理器对象上添加supportsAutofix: true属性如上文源码所示supportsAutofix为false默认值时disableFixes会被置为true即使传入--fix也不会应用任何修复。注意不打算支持自动修复的处理器可以省略supportsAutofix此时 ESLint 仍然正常报告问题只是跳过修复阶段。一个插件的多种处理器与多扩展名支持你可以在同一个插件里同时包含规则rules和多个自定义处理器也可以在一个插件里放多个处理器。若要支持多种文件扩展名把每个处理器都加入processors元素并指向同一对象即可const plugin { processors: { .md: processorImpl, // 处理 Markdown .html: processorImpl, // 同一实现也处理 HTML }, // 也可以同时放规则 rules: { my-rule: { /* ... */ }, }, };多个处理器共用一个实现对象是允许的因为处理器只负责「如何提取代码块」对不同扩展名的差异完全可以收敛到同一个逻辑中。meta对象的作用与两种使用场景meta对象帮助 ESLint缓存使用处理器的配置并提供更友好的调试信息。处理器相关的meta分为两层都建议提供。插件级meta对象插件的 meta 对象 提供插件自身的信息name与version。当你在配置中用字符串格式plugin-name/processor-name引用处理器时ESLint 会自动使用插件meta为处理器生成名称这是最常见的使用方式// eslint.config.js import { defineConfig } from eslint/config; import example from eslint-plugin-example; export default defineConfig([ { files: [**/*.txt], // 对文本文件应用处理器 plugins: { example, }, processor: example/processor-name, }, // ... 其他配置 ]);此例中处理器名就是example/processor-name该值会用于配置的序列化。处理器级meta对象每个处理器也可以声明自己的meta对象。当你在配置中直接把处理器对象传给processor键时ESLint 无法从插件名推断归属就会用到处理器自身的metameta.name应匹配处理器名称meta.version应匹配该处理器所在 npm 包的版本号。最省事的做法是从你的package.json中直接读取这些信息// eslint.config.js import { defineConfig } from eslint/config; import example from eslint-plugin-example; export default defineConfig([ { files: [**/*.txt], processor: example.processors[processor-name], }, // ... 其他配置 ]);这里直接指定example.processors[processor-name]时使用的是处理器自己的meta对象它必须被定义才能保证处理器不通过插件名引用时的正确解析。为什么两层meta都需要建议插件和每个处理器都提供各自的meta对象。这样无论处理器在配置中以字符串还是对象形式被指定依赖meta的功能例如--print-config输出配置、--cache缓存结果都能正常工作。从源码看这一约束在 lib/config/config.js 的解析逻辑中同样存在当processor为字符串时会通过splitPluginIdentifier拆分出插件名与处理器名并从plugins[pluginName].processors[localProcessorName]中取出处理器对象找不到时报错Key processor: Could not find ...当processor为对象时则直接使用对象本身。若对象缺少meta序列化配置时会报Could not serialize processor object (missing meta object).。而 lib/config/flat-config-schema.js#L432-L450 中的processorSchema也做了双保险校验字符串必须是合法的插件成员名对象则必须同时具备preprocess与postprocess方法否则抛Object must have a preprocess() and a postprocess() method.。在配置文件中指定处理器要在配置文件中使用插件提供的处理器需要先导入插件并放入plugins键、指定命名空间再通过processor键引用例如// eslint.config.js import { defineConfig } from eslint/config; import example from eslint-plugin-example; export default defineConfig([ { files: [**/*.txt], plugins: { example, }, processor: example/processor-name, }, ]);更完整的实战配置可参考 docs/src/use/configure/plugins.md 中eslint/markdown的用法先用files: [**/*.md]让 Markdown 文件走markdown/markdown处理器再为**/*.jsx增加一个配置对象以启用 JSX 解析从而使 Markdown 内的 JSX 块也能按 JSX 规则校验必要时还可以通过ignores: [**/test.md/*.jsx]忽略特定文件中的特定块。文档同时提醒全局 ignoresglobal ignores同样作用于命名代码块。配置中processor键的取值在 flat-config-schema.js 中被验证为「字符串或包含preprocess/postprocess方法的对象」二者在 config.js 中会被归一化为同一个处理器对象供 linter.js 使用。编写一个最小可用的自定义处理器综合以上规范下面是一个「从.txt文件中提取 JS 片段并 lint」的最小插件骨架完整逻辑可对照本文各节逐步补全// my-processor-plugin.js export default { meta: { name: eslint-plugin-myprocessor, version: 1.0.0, }, processors: { extract-js: { meta: { name: eslint-processor-extract-js, version: 1.0.0, }, preprocess(text, filename) { // 提取 text 中所有 js ... 片段 const blocks []; const regex /js\s*([\s\S]*?)/gu; let match; let i 0; while ((match regex.exec(text)) ! null) { blocks.push({ text: match[1], filename: ${i}.js }); i; } return blocks; }, postprocess(messages, filename) { // 此处若要支持 --fix还需按片段在原始文件中的 // 偏移量平移每条消息的 line/column 与 fix.range return [].concat(...messages); }, supportsAutofix: false, // 尚未实现位置映射保持关闭 }, }, };然后按前文方式在eslint.config.js中通过processor: myprocessor/extract-js或processor: myProcessor.processors[extract-js]接入即可。小结接口三要素preprocess拆块、postprocess聚合 位置映射、supportsAutofix可选默认false代码块命名决定能否被 lint默认只有.js结尾的块会被过滤通过其他扩展名需补充files配置自动修复不是免费的必须同时改写fix.range到原始文件坐标并声明supportsAutofix: truemeta双保险插件级与处理器级meta分别支撑字符串引用与对象直接引用两种配置写法保证--print-config、--cache等依赖序列化的功能稳定可用源码印证ProcessorServiceprocessor-service.js、linter 调用链linter.js与 schema 校验flat-config-schema.js完整实现了本文描述的所有行为单元测试 为这些行为提供了可验证的依据。掌握了自定义处理器你就可以让 ESLint 的检查能力覆盖 Markdown 文档、HTML 模板乃至任何自定义格式文件中的 JavaScript把统一的质量门禁延伸到项目的每一个角落。【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询