描述在 grunt 中实现 heading 层级连续性检查
解读
在国内前端工程化面试里,“heading 层级连续性检查” 并不是让候选人手写一个 Markdown 解析器,而是考察三件事:
- 能否把「业务规则」抽象成可配置的 Grunt 任务;
- 是否熟悉 Grunt 的多任务(multi-task)机制与文件对象格式;
- 能否把AST 遍历、错误收集、错误报告封装成插件,并接入既有构建流程。
一句话:让 Grunt 在 grunt default 阶段就把「h1→h3→h5」这类跳级错误抛出来,阻断后续 CI,保证文档/页面质量。
知识点
- Grunt 插件四件套:
grunt.registerMultiTask+this.options+this.filesSrc+grunt.log.error - Markdown/HTML 解析:国内项目 90% 用
marked或cheerio,cheerio 体积小、可流式、支持 jQuery 语法,最适合 Grunt 场景 - heading 连续性算法:先拍平所有 heading 标签,再跑一次贪心扫描,时间复杂度 O(n),内存 O(1)
- 错误定位:必须返回文件路径 + 行号 + 列号,方便 GitLab/Jenkins 在 MR 页面高亮
- 失败策略:
grunt.fail.warn会返回非 0 exit code,直接阻断 GitLab CI 的 pipeline,符合国内「质量门禁」要求
答案
-
安装依赖
npm i -D cheerio grunt-contrib-internal -
在项目根新建
tasks/grunt-heading-lint.js(符合国内「源码级自定义任务」规范)module.exports = function(grunt) { grunt.registerMultiTask('headingLint', '保证 heading 层级连续', function() { const options = this.options({ allowJump: 1 // 允许跨 1 级,如 h2→h4 }); let totalErr = 0; this.filesSrc.forEach(filepath => { const $ = cheerio.load(grunt.file.read(filepath), { decodeEntities: false }); const levels = []; $('h1,h2,h3,h4,h5,h6').each((i, el) => { const level = parseInt(el.tagName.substr(1), 10); const line = $(el).attr('data-line') || 0; // 若源码用 marked 可注入行号 levels.push({ level, line, text: $(el).text().slice(0, 20) }); }); for (let i = 1; i < levels.length; i++) { const gap = levels[i].level - levels[i - 1].level; if (gap > options.allowJump) { grunt.log.error(`[${filepath}:${levels[i].line}] heading 跳级: h${levels[i-1].level} → h${levels[i].level} 「${levels[i].text}」`); totalErr++; } } }); if (totalErr > 0) { grunt.fail.warn(`heading 层级错误共 ${totalErr} 处,请修正后再提交!`); } }); }; -
在
Gruntfile.js中注册grunt.loadTasks('tasks'); grunt.initConfig({ headingLint: { options: { allowJump: 1 }, src: ['docs/**/*.html', 'src/**/*.vue'] // 国内项目常把 Vue 模板也扫一遍 } }); grunt.registerTask('default', ['headingLint', 'cssmin', 'uglify']); -
触发
grunt default若出现跳级,终端直接爆红,GitLab Runner 收到非 0 码,MR 无法合并,完美契合国内「质量红线」制度。
拓展思考
-
如何支持 Markdown 源码级行号?
在marked渲染阶段注入data-line属性,cheerio 解析后仍能拿到行号,实现「所见即所报」。 -
性能优化:单仓库 2000+ 文档时,可用
grunt.util.async.forEachLimit做并发控制,防止一次性读爆内存。 -
规则升级:把
allowJump改成函数,支持「第一章只允许 h1→h2,附录允许任意跳级」这类多维度策略,体现你对Grunt options 动态化的深度掌握。 -
与 ESLint 统一错误格式:把错误对象序列化为
eslint-formatter-json,GitLab 可直接识别并在 UI 中高亮行级错误,让面试官看到你「不仅懂 Grunt,还懂平台集成」。