解释在 grunt 中实现按需数据预取

解读

“按需数据预取”在前端工程里通常指:

  1. 仅当某些入口或路由被真正访问时,才去拉取对应的数据包或模板;
  2. 构建阶段要把“哪些路由需要哪些数据”提前分析出来,并生成一份映射表;
  3. 运行时根据这份表做懒加载或 prefetch。

Grunt 本身只是一个任务编排器,不会替你跑在浏览器里,因此“实现”必须拆成两步:

  • 构建时:用 Grunt 任务把路由依赖关系扫描出来,输出一份 manifest(JSON/JS);
  • 运行时:业务代码读取 manifest,决定何时发请求或动态插入 <link rel="prefetch">

面试时,考官想听的是“你怎么用 Grunt 把第一步做成可配置、可缓存、可增量”,而不是泛泛地说“我写个 task 就好了”。

知识点

  1. Grunt 多任务机制grunt.registerMultiTask 可接收文件数组与自定义选项,天然适合做“扫描→输出”流水线。
  2. AST 静态分析:用 @babel/parser + traverse 扫描 import()getInitialProps 等约定式调用,提取路由与数据接口的映射。
  3. 缓存指纹:把上次解析结果序列化成 .cache.json,下次跑任务先比对文件 mtime,无变动直接跳过,大幅提升 CI 增量构建速度
  4. manifest 格式:推荐输出 {"/product/:id":["/api/product/detail","/api/stock"]},方便运行时按路由正则做最长匹配。
  5. Grunt 插件生态:官方 grunt-contrib-watch 可监听源码变化,再触发自定义 prefetch 任务,实现本地开发阶段实时更新映射表grunt-webpack 亦可把映射表注入到 webpack 的 DefinePlugin,供前端代码消费。
  6. 合规与性能:国内项目常需兼容低版本微信内核,prefetch 数量不宜超过 3 条,且要加 crossorigin="anonymous" 避免 Cookie 冗余;Grunt 任务里可配阈值做自动裁剪。

答案

我曾在电商大屏项目中落地过一套“Grunt + 按需数据预取”方案,核心思路是“构建时扫描、运行时消费”。

第一步,写一个私有 Grunt 插件 grunt-route-manifest,注册为多任务:

grunt.initConfig({
  route_manifest: {
    options: {
      // 只扫描 pages 目录下的 *.jsx
      pattern: 'pages/**/*.jsx',
      // 提取函数名约定
      dataHook: ['getInitialProps', 'loader'],
      // 输出路径
      dest: 'dist/route-manifest.json'
    },
    src: ['src']
  }
});

任务内部用 @babel/parser 把文件解析成 AST,再遍历 CallExpression,如果命中 getInitialProps,就把同文件导出的 route 字段与接口 URL 收集起来;同时把文件 mtime.cache.json 比对,实现增量扫描

第二步,在 grunt.registerTask('default', ['clean','route_manifest','webpack']) 里把该任务插到 webpack 打包之前;manifest 生成后,通过 grunt.file.write 直接落盘为 dist/route-manifest.json

第三步,前端运行时封装 prefetch.js

const map = __MANIFEST__; // 由 webpack DefinePlugin 注入
export function prefetchFor(path) {
  const apis = matchPath(path, map);
  apis.forEach(url => {
    const link = document.createElement('link');
    link.rel = 'prefetch';
    link.href = url;
    link.crossOrigin = 'anonymous';
    document.head.appendChild(link);
  });
}

当路由切换时,先调用 prefetchFor(location.pathname)真正做到了“只预取当前路由可能用到的数据”

上线后,首屏请求量下降 38%,白屏时间减少 220 ms,CDN 费用也省下一笔。整个流程对业务透明,只通过 Grunt 配置即可开关,符合国内团队“配置驱动”的交付习惯

拓展思考

  1. 如果项目后续迁移到 Vite 或 esbuild,可以把同一套 AST 扫描逻辑抽成语言无关的 Rust 模块,通过 NAPI 供 Grunt 调用,保证老项目不改配置也能享受新解析器带来的 5× 速度提升
  2. 对于千人千面的推荐接口,manifest 里会出现长尾 URL,此时可在 Grunt 任务里再做一层“接口归一化”:把 /api/recommend?userId=123 聚合成 /api/recommend,运行时再用占位符动态替换,避免映射表爆炸。
  3. 国内小程序容器不支持 <link prefetch>,可以把 Grunt 任务再扩展一条分支,输出 prefetch.wxml,在小程序里用 <wxs> 做逻辑层预拉取,实现同一套构建产物多端适配