news 2026/9/19 5:47:55

在 Gatsby 站点中使用 js-search 实现客户端搜索:两种数据规模的完整实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 Gatsby 站点中使用 js-search 实现客户端搜索:两种数据规模的完整实战指南

在 Gatsby 站点中使用 js-search 实现客户端搜索:两种数据规模的完整实战指南

【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby

本文基于 Gatsby 官方文档《Adding Search with JS Search》与仓库中的完整示例站点 examples/using-js-search 编写,讲解如何在 Gatsby 站点中通过 js-search 在客户端实现即时搜索。你将掌握两种实现策略:面向中小规模数据集、由组件自身负责数据拉取与索引的轻量方案,以及面向大规模数据集、借助 Gatsby 的 Node API 与pageContext在构建期注入数据的方案,最终得到一个完全跑在浏览器端、无需额外搜索服务端的搜索功能。

前置准备

在动手之前,你需要对 Gatsby 的基础概念有一定了解:建议先阅读 tutorial,并在需要时查阅 documentation。此外,示例代码大量使用了箭头函数、解构赋值、类字段与async/await等 ES6 语法,掌握这些语法会让阅读更顺畅。

本指南对应的完整可运行代码存放在仓库的 examples/using-js-search 目录下,其中 README.md 明确说明该目录是本文档所描述方案的完整实现,你可以对照阅读。

什么是 js-search

JS Search 是由 Brian Vaughn(Facebook 核心团队成员)创建的 JavaScript 库,它提供了一种在客户端用 JavaScript 与 JSON 对象高效搜索数据的方式,并带有大量的自定义选项。它的核心思想是:数据先被"索引"到内存中的数据结构里,之后每次输入变化时直接在索引上执行查询,从而避免在每次按键时对全量数据做线性扫描。

js-search 的完整代码与文档在其 GitHub 仓库中维护。本指南基于其官方示例改写,以适配 Gatsby 站点的开发模式。在继续之前,理解其几个核心概念对后文非常重要:

  • 索引策略(Index Strategy):决定索引如何对文本进行切分匹配,例如前缀匹配、精确词匹配、任意子串匹配;
  • 清洗器(Sanitizer):决定索引与查询之前对文本做怎样的归一化处理,例如统一转小写或保留大小写;
  • 搜索索引(Search Index):决定匹配结果如何被记录与排序,例如基于 TF-IDF 加权或无序索引;
  • 分词器(Tokenizer):决定文本如何被切分成词元,例如是否过滤掉常见停用词。

环境搭建

创建项目并安装依赖

首先基于官方 hello world 起步模板创建一个新站点,在终端中执行:

gatsby new js-search-example https://github.com/gatsbyjs/gatsby-starter-default

创建完成后进入项目目录并安装所需依赖:

cd js-search-example npm install js-search axios

如果你使用 Yarn:

yarn add js-search axios

其中 axios 在本例中负责处理所有基于 Promise 的 HTTP 请求(拉取示例数据)。

从仓库中 examples/using-js-search/package.json 可以看到,该示例的实际依赖为js-search@^1.4.3axios@^0.20.0,脚本方面提供了gatsby developgatsby buildgatsby serve等标准命令。

示例数据说明

两个方案都使用 js-search 作者托管在https://bvaughn.github.io/js-search/books.json的图书示例数据集(每本书包含isbntitleauthor字段)。在真实项目中,这一步应替换为你自己的数据源——例如从 CMS、GraphQL 查询或本地 JSON/Markdown 文件获取内容。

策略选择

接下来你将学习在站点中实现js-search的两种方法,选择哪一种取决于你想要搜索的数据量:

  • 中小规模数据集:使用第一种方案即可,所有逻辑封装在一个组件里,简单直接;
  • 大规模数据集:使用第二种方案,大部分工作在构建期通过 Gatsby 的内部 API 预先完成,页面打开时数据已经就绪,无需在客户端发起请求。

两种实现都比较通用,均使用了库的默认选项,便于在深入了解库的细节之前先进行实验。同时请注意:示例代码并没有严格遵循生产环境的最佳实践(例如把 axios 请求换成 Gatsby 的数据层、把样式内联改成 CSS 模块等),它仅用于演示;在真实站点中你会以不同的方式组织代码。

方案一:中小规模数据集——组件内完成一切

该方案的核心思路是:搜索组件挂载后自行通过 axios 拉取数据,在内存中建立 js-search 索引,然后随用户输入实时检索。

创建 SearchContainer 组件

src/components/下创建SearchContainer.js,代码如下:

import React, { Component } from "react" import Axios from "axios" import * as JsSearch from "js-search" class Search extends Component { state = { bookList: [], search: [], searchResults: [], isLoading: true, isError: false, searchQuery: "", } /** * React lifecycle method to fetch the data */ async componentDidMount() { Axios.get("https://bvaughn.github.io/js-search/books.json") .then(result => { const bookData = result.data this.setState({ bookList: bookData.books }) this.rebuildIndex() }) .catch(err => { this.setState({ isError: true }) console.log("====================================") console.log(`Something bad happened while fetching the data\n${err}`) console.log("====================================") }) } /** * rebuilds the overall index based on the options */ rebuildIndex = () => { const { bookList } = this.state const dataToSearch = new JsSearch.Search("isbn") /** * defines an indexing strategy for the data * more about it in here https://github.com/bvaughn/js-search#configuring-the-index-strategy */ dataToSearch.indexStrategy = new JsSearch.PrefixIndexStrategy() /** * defines the sanitizer for the search * to prevent some of the words from being excluded * */ dataToSearch.sanitizer = new JsSearch.LowerCaseSanitizer() /** * defines the search index * read more in here https://github.com/bvaughn/js-search#configuring-the-search-index */ dataToSearch.searchIndex = new JsSearch.TfIdfSearchIndex("isbn") dataToSearch.addIndex("title") // sets the index attribute for the data dataToSearch.addIndex("author") // sets the index attribute for the data dataToSearch.addDocuments(bookList) // adds the data to be searched this.setState({ search: dataToSearch, isLoading: false }) } /** * handles the input change and perform a search with js-search * in which the results will be added to the state */ searchData = e => { const { search } = this.state const queryResult = search.search(e.target.value) this.setState({ searchQuery: e.target.value, searchResults: queryResult }) } handleSubmit = e => { e.preventDefault() } render() { const { bookList, searchResults, searchQuery } = this.state const queryResults = searchQuery === "" ? bookList : searchResults return ( <div> <div style={{ margin: "0 auto" }}> <form onSubmit={this.handleSubmit}> <div style={{ margin: "0 auto" }}> <label htmlFor="Search" style={{ paddingRight: "10px" }}> Enter your search here </label> <input id="Search" value={searchQuery} onChange={this.searchData} placeholder="Enter your search here" style={{ margin: "0 auto", width: "400px" }} /> </div> </form> <div> Number of items: {queryResults.length} <table style={{ width: "100%", borderCollapse: "collapse", borderRadius: "4px", border: "1px solid #d3d3d3", }} > <thead style={{ border: "1px solid #808080" }}> <tr> <th style={{ textAlign: "left", padding: "5px", fontSize: "14px", fontWeight: 600, borderBottom: "2px solid #d3d3d3", cursor: "pointer", }} > Book ISBN </th> <th style={{ textAlign: "left", padding: "5px", fontSize: "14px", fontWeight: 600, borderBottom: "2px solid #d3d3d3", cursor: "pointer", }} > Book Title </th> <th style={{ textAlign: "left", padding: "5px", fontSize: "14px", fontWeight: 600, borderBottom: "2px solid #d3d3d3", cursor: "pointer", }} > Book Author </th> </tr> </thead> <tbody> {queryResults.map(item => { return ( <tr key={`row_${item.isbn}`}> <td style={{ fontSize: "14px", border: "1px solid #d3d3d3", }} > {item.isbn} </td> <td style={{ fontSize: "14px", border: "1px solid #d3d3d3", }} > {item.title} </td> <td style={{ fontSize: "14px", border: "1px solid #d3d3d3", }} > {item.author} </td> </tr> ) })} </tbody> </table> </div> </div> </div> ) } } export default Search

该组件与仓库中的 examples/using-js-search/src/components/SearchContainer.js 完全对应(仓库版本额外处理了isLoadingisError状态下的加载/错误提示 UI)。

代码逐段拆解

  1. 数据获取:组件挂载时触发componentDidMount()生命周期方法,通过 axios 请求books.json拉取数据。
  2. 状态写入与重建索引:请求无错误时,把收到的数据加入 state,并调用rebuildIndex()
  3. 创建并配置搜索引擎new JsSearch.Search("isbn")指定以isbn字段作为每一条记录的唯一标识;随后依次配置默认的索引策略(PrefixIndexStrategy前缀匹配)、清洗器(LowerCaseSanitizer统一转小写,避免大小写导致漏匹配)与搜索索引(TfIdfSearchIndex基于 TF-IDF 进行相关度排序)。
  4. 索引数据addIndex("title")addIndex("author")声明参与搜索的字段,addDocuments(bookList)把整个数据集加入索引。
  5. 实时检索:输入框内容每次变化时,searchData取出当前输入值调用search.search(value),结果写入 state,最终通过table元素呈现给用户。当输入为空时,queryResults回退为完整bookList,即默认展示全部数据。

组装进页面

要让搜索在站点中生效,只需把新组件导入到某个页面。仓库示例中的做法见 examples/using-js-search/src/pages/index.js:

import React from "react" import Search from "../components/SearchContainer" const IndexPage = () => ( <div> <h1 style={{ marginTop: `3em`, textAlign: `center` }}> Search data using JS Search </h1> <div> <Search /> </div> </div> ) export default IndexPage

运行gatsby develop,一切正常后在浏览器打开http://localhost:8000,即可使用一个功能完整的搜索组件。

方案二:大数据集——利用 Gatsby API 在构建期预处理

方案一中,数据需要在浏览器端由组件自行请求,数据量很大时首屏体验会受影响。第二种方案把工作交给 Gatsby:在构建期通过createPagesAPI 拉取数据,并通过pageContext注入页面,浏览器端不再发起请求,只需对已经就绪的数据建索引并搜索。

这一机制在 Gatsby 中被称为"程序化创建页面",其数据传递通道正是 pageContext。从 gatsby-internals-terminology.md 对页面数据的内部结构示例可以看到,每个页面的 page data 对象中都会包含一个pageContext字段,用于承载构建期传入页面的上下文数据(如 slug、上一篇/下一篇等),而页面组件则通过 props 中的pageContext读取它。

修改 gatsby-node.js 动态创建页面

在项目根目录的gatsby-node.js中添加如下代码:

const path = require("path") const axios = require("axios") exports.createPages = ({ actions }) => { const { createPage } = actions return new Promise((resolve, reject) => { axios .get("https://bvaughn.github.io/js-search/books.json") .then(result => { const { data } = result /** * creates a dynamic page with the data received * injects the data into the context object alongside with some options * to configure js-search */ createPage({ path: "/search", component: path.resolve(`./src/templates/ClientSearchTemplate.js`), context: { bookData: { allBooks: data.books, options: { indexStrategy: "Prefix match", searchSanitizer: "Lower Case", TitleIndex: true, AuthorIndex: true, SearchByTerm: true, }, }, }, }) resolve() }) .catch(err => { console.log("====================================") console.log(`error creating Page:${err}`) console.log("====================================") reject(new Error(`error on page creation:\n${err}`)) }) }) }

这段代码与 examples/using-js-search/gatsby-node.js 完全一致。它的作用:

  • 在构建期通过 axios 获取图书数据;
  • 调用actions.createPage动态生成路径为/search的页面;
  • bookData(全部图书 + 一组控制 js-search 行为的 options)写入contextcontext中的内容最终会进入该页面的pageContext,从而在页面组件中通过 props 读取。

创建页面模板 ClientSearchTemplate

src/templates/下创建ClientSearchTemplate.js

import React from "react" import ClientSearch from "../components/ClientSearch" const SearchTemplate = props => { const { pageContext } = props const { bookData } = pageContext const { allBooks, options } = bookData return ( <div> <h1 style={{ marginTop: `3em`, textAlign: `center` }}> Search data using JS Search using Gatsby API </h1> <div> <ClientSearch books={allBooks} engine={options} /> </div> </div> ) } export default SearchTemplate

模板(与 examples/using-js-search/src/templates/ClientSearchTemplate.js 一致)从props.pageContext中解构出bookData,再把allBooks作为booksoptions作为engine传给搜索组件。这印证了前文所述:gatsby-node.jscontext里的数据最终会以pageContext的形式到达页面模板。

创建通用搜索组件 ClientSearch

src/components/下创建ClientSearch.js

import React, { Component } from "react" import * as JsSearch from "js-search" class ClientSearch extends Component { state = { isLoading: true, searchResults: [], search: null, isError: false, indexByTitle: false, indexByAuthor: false, termFrequency: true, removeStopWords: false, searchQuery: "", selectedStrategy: "", selectedSanitizer: "", } /** * React lifecycle method that will inject the data into the state. */ static getDerivedStateFromProps(nextProps, prevState) { if (prevState.search === null) { const { engine } = nextProps return { indexByTitle: engine.TitleIndex, indexByAuthor: engine.AuthorIndex, termFrequency: engine.SearchByTerm, selectedSanitizer: engine.searchSanitizer, selectedStrategy: engine.indexStrategy, } } return null } async componentDidMount() { this.rebuildIndex() } /** * rebuilds the overall index based on the options */ rebuildIndex = () => { const { selectedStrategy, selectedSanitizer, removeStopWords, termFrequency, indexByTitle, indexByAuthor, } = this.state const { books } = this.props const dataToSearch = new JsSearch.Search("isbn") if (removeStopWords) { dataToSearch.tokenizer = new JsSearch.StopWordsTokenizer( dataToSearch.tokenizer ) } /** * defines an indexing strategy for the data * read more about it here https://github.com/bvaughn/js-search#configuring-the-index-strategy */ if (selectedStrategy === "All") { dataToSearch.indexStrategy = new JsSearch.AllSubstringsIndexStrategy() } if (selectedStrategy === "Exact match") { dataToSearch.indexStrategy = new JsSearch.ExactWordIndexStrategy() } if (selectedStrategy === "Prefix match") { dataToSearch.indexStrategy = new JsSearch.PrefixIndexStrategy() } /** * defines the sanitizer for the search * to prevent some of the words from being excluded */ selectedSanitizer === "Case Sensitive" ? (dataToSearch.sanitizer = new JsSearch.CaseSensitiveSanitizer()) : (dataToSearch.sanitizer = new JsSearch.LowerCaseSanitizer()) termFrequency === true ? (dataToSearch.searchIndex = new JsSearch.TfIdfSearchIndex("isbn")) : (dataToSearch.searchIndex = new JsSearch.UnorderedSearchIndex()) // sets the index attribute for the data if (indexByTitle) { dataToSearch.addIndex("title") } // sets the index attribute for the data if (indexByAuthor) { dataToSearch.addIndex("author") } dataToSearch.addDocuments(books) // adds the data to be searched this.setState({ search: dataToSearch, isLoading: false }) } /** * handles the input change and perform a search with js-search * in which the results will be added to the state */ searchData = e => { const { search } = this.state const queryResult = search.search(e.target.value) this.setState({ searchQuery: e.target.value, searchResults: queryResult }) } handleSubmit = e => { e.preventDefault() } render() { const { searchResults, searchQuery } = this.state const { books } = this.props const queryResults = searchQuery === "" ? books : searchResults return ( <div> <div style={{ margin: "0 auto" }}> <form onSubmit={this.handleSubmit}> <div style={{ margin: "0 auto" }}> <label htmlFor="Search" style={{ paddingRight: "10px" }}> Enter your search here </label> <input id="Search" value={searchQuery} onChange={this.searchData} placeholder="Enter your search here" style={{ margin: "0 auto", width: "400px" }} /> </div> </form> <div> Number of items: {queryResults.length} <table style={{ width: "100%", borderCollapse: "collapse", borderRadius: "4px", border: "1px solid #d3d3d3", }} > <thead style={{ border: "1px solid #808080" }}> <tr> <th style={{ textAlign: "left", padding: "5px", fontSize: "14px", fontWeight: 600, borderBottom: "2px solid #d3d3d3", cursor: "pointer", }} > Book ISBN </th> <th style={{ textAlign: "left", padding: "5px", fontSize: "14px", fontWeight: 600, borderBottom: "2px solid #d3d3d3", cursor: "pointer", }} > Book Title </th> <th style={{ textAlign: "left", padding: "5px", fontSize: "14px", fontWeight: 600, borderBottom: "2px solid #d3d3d3", cursor: "pointer", }} > Book Author </th> </tr> </thead> <tbody> {queryResults.map(item => { return ( <tr key={`row_${item.isbn}`}> <td style={{ fontSize: "14px", border: "1px solid #d3d3d3", }} > {item.isbn} </td> <td style={{ fontSize: "14px", border: "1px solid #d3d3d3", }} > {item.title} </td> <td style={{ fontSize: "14px", border: "1px solid #d3d3d3", }} > {item.author} </td> </tr> ) })} </tbody> </table> </div> </div> </div> ) } } export default ClientSearch

该组件与 examples/using-js-search/src/components/ClientSearch.js 保持一致(仓库版本同样提供了 loading/error 分支 UI)。它比方案一组件更进一步,把搜索引擎的配置全部"参数化":

代码逐段拆解

  1. 注入引擎配置:组件挂载前,getDerivedStateFromProps()生命周期方法被调用,它会评估 props 中的engine并把indexStrategysearchSanitizerTitleIndexAuthorIndexSearchByTerm映射为组件 state,从而允许从gatsby-node.js的 context 远程控制搜索行为。

  2. 重建索引:随后componentDidMount()触发rebuildIndex()

  3. 按选项创建搜索引擎new JsSearch.Search("isbn")创建引擎,随后根据 state 中选项逐一配置:

    • 索引策略All对应AllSubstringsIndexStrategy(任意子串匹配)、Exact match对应ExactWordIndexStrategy(精确整词匹配)、Prefix match对应PrefixIndexStrategy(前缀匹配);
    • 清洗器Case Sensitive使用CaseSensitiveSanitizer(保留大小写),否则使用LowerCaseSanitizer(统一小写);
    • 搜索索引termFrequency === true时使用TfIdfSearchIndex(按 TF-IDF 相关度排序),否则使用UnorderedSearchIndex(无序匹配);还可以通过removeStopWords打开StopWordsTokenizer过滤常见停用词;
    • 索引字段indexByTitleindexByAuthor分别决定是否把titleauthor加入索引。
  4. 索引数据addDocuments(books)把通过 props 传入(即构建期注入的allBooks)的完整数据集加入索引。

  5. 实时检索:输入变化时调用search.search(value)并把结果写入 state,通过table呈现;输入为空时回退展示全部books

组装进站点

同样地,把 gatsby-node.js、模板 ClientSearchTemplate.js 与组件 ClientSearch.js 复制到你的站点中即可。再次执行gatsby develop,一切顺利的话,打开http://localhost:8000/search,你将得到一个与 Gatsby API 深度结合的完整搜索页面。

两种方案对比与进一步思考

对比维度方案一:组件内完成方案二:Gatsby API 预处理
适用数据规模中小规模大规模
数据获取时机浏览器端组件挂载后构建期(createPages
数据传递方式组件内 axios 请求pageContext注入
核心文件SearchContainer.jsgatsby-node.js+ClientSearchTemplate.js+ClientSearch.js
浏览器端负担需等待请求返回再建索引数据已随页面就绪,直接建索引
可配置性默认选项,改动需改组件代码通过 context 参数化控制引擎选项

两种方案都采用 js-search 的默认/常用选项组合(前缀索引 + 小写清洗 + TF-IDF 排序),便于先跑通再深入定制。js-search 还支持通过StopWordsTokenizerAllSubstringsIndexStrategy等扩展点做更精细的调优,具体可查阅其官方文档。

最后提醒:示例代码刻意保持了"教学式"的直白写法。在真实项目中,更合理的做法是让数据经由 Gatsby 的 GraphQL 数据层(例如gatsby-transformer-jsongatsby-source-filesystem)流入,并在 gatsby-node.js 中读取后注入pageContext,样式部分也建议使用 Gatsby 支持的各种样式方案而不是内联样式。但无论如何,js-search 加客户端索引这条技术路径,足以让你在不引入任何外部搜索服务的前提下,为 Gatsby 站点快速构建出流畅的即时搜索体验。

【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 5:47:48

2026年前端AI编程工具深度测评:四大工程场景决策指南

1. 这份测评不是“工具排行榜”&#xff0c;而是前端工程师的决策沙盘2026年&#xff0c;前端开发早已不是写几个HTML、CSS、JS就能交付项目的年代。组件库爆炸式增长、微前端架构成为标配、TypeScript类型约束愈发严苛、构建链路从Webpack转向ViteRspack混合编译、甚至服务端渲…

作者头像 李华
网站建设 2026/9/19 5:46:55

小波变换在语音端点检测中的优化与应用

1. 语音端点检测的核心挑战与解决方案在嘈杂环境中准确识别语音信号的起止点&#xff0c;一直是语音处理领域的经典难题。传统基于短时能量和过零率的方法在信噪比较低时性能急剧下降&#xff0c;而基于小波变换的多分辨率分析恰好能解决这一痛点。我曾在工业级噪音环境下实测对…

作者头像 李华
网站建设 2026/9/19 5:44:26

AI陪伴机器人把记忆塞进Prompt的代价-为什么只能取20条

04-把记忆塞进Prompt的代价-为什么只能取20条黒漂技术佬的 AI 伙伴&#xff08;AI-Partner&#xff09;源码拆解系列。前面三篇把"记忆怎么存、怎么查、怎么注入"讲完了。本篇算一笔容易被忽略的账&#xff1a;把用户的记忆塞进系统提示词&#xff0c;到底要花多少 T…

作者头像 李华
网站建设 2026/9/19 5:37:48

MATLAB/Simulink在BMS仿真分析中的建模、SOC估算与代码生成

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 5:37:36

CANN opbase 算子开发:aclTensor::SetData 接口详解与源码实现

CANN opbase 算子开发&#xff1a;aclTensor::SetData 接口详解与源码实现 【免费下载链接】opbase 本项目是CANN算子库的基础框架库&#xff0c;为算子提供公共依赖文件和基础调度能力。 项目地址: https://gitcode.com/cann/opbase 本指南围绕 CANN 算子库基础框架 op…

作者头像 李华