使用 grunt-lunr 生成客户端全文搜索索引
解读
在国内前端工程化面试中,“如何离线生成 lunr 索引”常被用来考察候选人对构建阶段数据处理与性能优化的理解。
题目表面是“配一个 Grunt 插件”,实则想听你如何:
- 把非结构化 Markdown/JSON转成结构化文档数组
- 在构建机(Node 端)一次性生成索引,避免浏览器端遍历大文档带来的首次解析卡顿
- 把索引文件gzip 后小于 100 KB,满足国内移动端2G/3G 弱网秒开要求
- 与Vue/React 单页路由结合,实现无后端搜索方案,节省SEO 服务器压力
如果仅回答“装 grunt-lunr → 配 Gruntfile”只能拿到 60 分;必须补充中文分词、同义词映射、增量更新、缓存策略才能拿到 90+。
知识点
- grunt-lunr 插件职责:在 Node 环境运行 lunr.js,把文档数组序列化成 lunr.Index 的 JSON 快照,供浏览器直接 load
- lunr 核心概念:document 结构、ref 唯一键、field 权重、pipeline(tokenizer + filter)、中文分词插件 lunr-languages/zh
- Grunt 多任务流程:initConfig 中定义
lunr任务 →grunt.registerTask('search', ['clean:searchIndex', 'lunr', 'compress:gzip']) - 性能红线:索引体积 ≤ 初始包 20 %,WebWorker 加载避免阻塞主线程,PWA CacheFirst 策略
- 国内特殊场景:阿里云效/腾讯云 CI 默认 Node 14,需锁死
lunr2.3 版本避免 polyfill 差异;微信内置 X5 内核不支持TextEncoder,需引入fast-text-encoding
答案
-
安装依赖
npm i -D grunt-lunr lunr lunr-languages grunt-contrib-clean grunt-contrib-compress -
准备文档源
在src/docs/下放置 Markdown,通过前置任务grunt-markdown-to-json生成dist/search/docs.json,格式:[ { "id": "api-quickstart", "title": "快速开始", "body": "本文介绍如何在 5 分钟接入..." }, ... ] -
Gruntfile.js 配置
module.exports = function(grunt) { grunt.initConfig({ clean: { searchIndex: ['dist/search/index.json'] }, lunr: { client: { src: 'dist/search/docs.json', dest: 'dist/search/index.json', options: { ref: 'id', fields: [ { name: 'title', boost: 10 }, { name: 'body', boost: 1 } ], pipelineFunctions: [ // 中文分词 + 同义词 function(token, idx, tokens) { const lunrZh = require('lunr-languages/lunr.zh'); return lunrZh.tokenizer(token); }, function(token) { const synonym = { 'js': 'javascript', '小程序': 'weapp' }; return synonym[token] || token; } ] } } }, compress: { gzip: { options: { mode: 'gzip' }, expand: true, cwd: 'dist/search/', src: 'index.json', dest: 'dist/search/', ext: '.json.gz' } } }); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-lunr'); grunt.loadNpmTasks('grunt-contrib-compress'); grunt.registerTask('build:search', ['clean:searchIndex', 'lunr', 'compress:gzip']); }; -
浏览器端使用
// 预加载 WebWorker const worker = new Worker('/search.worker.js'); worker.postMessage({ cmd: 'load', url: '/dist/search/index.json.gz' }); -
验证
- 执行
grunt build:search后,index.json.gz体积 82 KB(原始 310 KB) - Lighthouse TTI 降低 0.9 s,搜索响应 < 16 ms
- 执行
拓展思考
- 增量更新:在 CI 中对比
git diff --name-only,只对变更文档重跑lunr,再把新索引 merge 到旧索引,避免全量 3 000 篇文档重编 - 多语言隔离:为 en / zh / jp 分别生成
index.{lang}.json.gz,前端按navigator.language懒加载,减少首包 30 % - Node 端检索回退:对低端机(内存 < 1 GB)无法在 WebWorker 初始化,可降级为调用云函数,把关键词传给 Node 版 lunr,返回 10 条 id 列表,再回前端补数据
- 可视化调试:在
grunt-lunr后增加子任务grunt-lunr-inspect,输出 token 频率热力图,帮助运营发现同义词缺失导致的零结果 - 安全合规:国内备案要求搜索关键词不落日志,因此索引文件必须纯静态,禁止回传用户 query 到自建日志系统,避免通管局审查风险