使用 grunt-contrib-jade 预编译 Pug 模板为函数

解读

国内前端项目面试常把“老插件 + 新名称”混用,考察候选人能否快速定位历史遗留配置并给出最小侵入式改造方案。grunt-contrib-jade 在 npm 上已重命名为 grunt-contrib-pug,但企业旧代码库仍保留 jade 关键字。面试官想看三点:

  1. 是否知道 jade 与 pug 的渊源
  2. 能否在 Gruntfile 里**正确写出预编译为“可复用函数”**而非简单 HTML 的配置;
  3. 是否理解客户端运行时复用服务端渲染两种产物差异,避免把模板函数当字符串返回。

知识点

  • grunt-contrib-jade 插件的 runtime:true 选项:把模板预编译成 function(locals){ return ... },不直接输出 HTML。
  • wrap:trueamd:true 参数:决定产物是 CommonJS、AMD 还是全局变量,国内项目多数要求 UMD 以便同时支持 Webpack 与 Sea.js。
  • namespace 字段:默认 JST,国内团队常改成项目代码仓库统一命名空间,如 window.APP.TPL
  • concat 任务后置处理:预编译产物是零散 JS,需配合 grunt-contrib-concat 合并为单个 tpl.js,减少浏览器请求。
  • sourcemap 关闭:国内生产环境普遍关闭 sourcemap,避免源码泄漏;但面试时要说明开发环境可打开以方便调试。
  • grunt-legacy 兼容性:Node 14+ 运行 grunt-contrib-jade 需加 --legacy-peer-deps,否则 npm 7+ 会报 peer 冲突,面试官会追问解决方案。

答案

  1. 安装并锁定版本(保证 CI 可复现):

    npm i -D grunt-contrib-jade@1.0.0 --legacy-peer-deps
    
  2. Gruntfile.js 关键片段:

    module.exports = function(grunt) {
      grunt.initConfig({
        jade: {
          options: {
            // 核心:输出可执行函数
            runtime: true,
            // 产物用 UMD 包装,兼容 Webpack/Sea.js
            wrap: true,
            amd: false,
            // 统一命名空间
            namespace: 'APP.TPL',
            // 开发阶段打开 sourcemap,面试时主动说明
            sourcemap: grunt.option('dev') ? true : false
          },
          compile: {
            files: [{
              expand: true,
              cwd: 'src/tpl',
              src: '**/*.jade',
              dest: '.tmp/tpl',
              ext: '.js'
            }]
          }
        },
        concat: {
          tpl: {
            src: ['.tmp/tpl/**/*.js'],
            dest: 'dist/assets/js/tpl.js'
          }
        },
        clean: {
          tmp: ['.tmp']
        }
      });
    
      grunt.loadNpmTasks('grunt-contrib-jade');
      grunt.loadNpmTasks('grunt-contrib-concat');
      grunt.loadNpmTasks('grunt-contrib-clean');
    
      grunt.registerTask('tpl', ['jade', 'concat:tpl', 'clean:tmp']);
    };
    
  3. 使用示例(浏览器端):

    // 预编译产物已挂到 window.APP.TPL
    var html = APP.TPL['user/list']({users: data});
    document.querySelector('#panel').innerHTML = html;
    
  4. 面试加分句:
    “如果后续迁移到 Webpack5,可用 pug-loader 达到同样效果,但保留 grunt 阶段可让存量项目零重构上线。”

拓展思考

  • 性能优化:预编译函数体积比字符串模板大 20% 左右,可配合 grunt-contrib-uglify 开启 --compress --mangle 把局部变量缩短,再使用 gzip 在 Nginx 层压缩,国内云厂商 CDN 默认开启 br 压缩,可再减少 15% 体积。
  • 服务端同构:若同一套模板需要服务端渲染,可在 jade 任务里再建一个 ssr target,关闭 runtime:true,输出纯 HTML 片段,通过 grunt-contrib-copy 推到 views 目录,供 Express 使用,实现“一次编写,两端运行”。
  • 类型安全:在 TypeScript 项目中,可手写 d.ts 声明模块 APP.TPL,把每个模板函数签名写成 (locals?: Locals) => string,避免 any 类型,提高维护性;面试时提到这一点可体现工程化深度