描述在 grunt 中实现 heading 层级连续性检查

解读

在国内前端工程化面试里,“heading 层级连续性检查” 并不是让候选人手写一个 Markdown 解析器,而是考察三件事:

  1. 能否把「业务规则」抽象成可配置的 Grunt 任务
  2. 是否熟悉 Grunt 的多任务(multi-task)机制文件对象格式
  3. 能否把AST 遍历、错误收集、错误报告封装成插件,并接入既有构建流程。

一句话:让 Grunt 在 grunt default 阶段就把「h1→h3→h5」这类跳级错误抛出来,阻断后续 CI,保证文档/页面质量。

知识点

  • Grunt 插件四件套grunt.registerMultiTask + this.options + this.filesSrc + grunt.log.error
  • Markdown/HTML 解析:国内项目 90% 用 markedcheeriocheerio 体积小、可流式、支持 jQuery 语法,最适合 Grunt 场景
  • heading 连续性算法:先拍平所有 heading 标签,再跑一次贪心扫描,时间复杂度 O(n),内存 O(1)
  • 错误定位:必须返回文件路径 + 行号 + 列号,方便 GitLab/Jenkins 在 MR 页面高亮
  • 失败策略grunt.fail.warn 会返回非 0 exit code,直接阻断 GitLab CI 的 pipeline,符合国内「质量门禁」要求

答案

  1. 安装依赖

    npm i -D cheerio grunt-contrib-internal
    
  2. 在项目根新建 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} 处,请修正后再提交!`);
        }
      });
    };
    
  3. Gruntfile.js 中注册

    grunt.loadTasks('tasks');
    grunt.initConfig({
      headingLint: {
        options: { allowJump: 1 },
        src: ['docs/**/*.html', 'src/**/*.vue']   // 国内项目常把 Vue 模板也扫一遍
      }
    });
    grunt.registerTask('default', ['headingLint', 'cssmin', 'uglify']);
    
  4. 触发

    grunt default
    

    若出现跳级,终端直接爆红,GitLab Runner 收到非 0 码,MR 无法合并,完美契合国内「质量红线」制度。

拓展思考

  • 如何支持 Markdown 源码级行号?
    marked 渲染阶段注入 data-line 属性,cheerio 解析后仍能拿到行号,实现「所见即所报」。

  • 性能优化:单仓库 2000+ 文档时,可用 grunt.util.async.forEachLimit并发控制,防止一次性读爆内存。

  • 规则升级:把 allowJump 改成函数,支持「第一章只允许 h1→h2,附录允许任意跳级」这类多维度策略,体现你对Grunt options 动态化的深度掌握。

  • 与 ESLint 统一错误格式:把错误对象序列化为 eslint-formatter-jsonGitLab 可直接识别并在 UI 中高亮行级错误,让面试官看到你「不仅懂 Grunt,还懂平台集成」。