
在 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-searchJS Search 是由 Brian VaughnFacebook 核心团队成员创建的 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如果你使用 Yarnyarn add js-search axios其中 axios 在本例中负责处理所有基于 Promise 的 HTTP 请求拉取示例数据。从仓库中 examples/using-js-search/package.json 可以看到该示例的实际依赖为js-search^1.4.3与axios^0.20.0脚本方面提供了gatsby develop、gatsby build、gatsby serve等标准命令。示例数据说明两个方案都使用 js-search 作者托管在https://bvaughn.github.io/js-search/books.json的图书示例数据集每本书包含isbn、title、author字段。在真实项目中这一步应替换为你自己的数据源——例如从 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 htmlForSearch style{{ paddingRight: 10px }} Enter your search here /label input idSearch value{searchQuery} onChange{this.searchData} placeholderEnter 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 完全对应仓库版本额外处理了isLoading与isError状态下的加载/错误提示 UI。代码逐段拆解数据获取组件挂载时触发componentDidMount()生命周期方法通过 axios 请求books.json拉取数据。状态写入与重建索引请求无错误时把收到的数据加入 state并调用rebuildIndex()。创建并配置搜索引擎new JsSearch.Search(isbn)指定以isbn字段作为每一条记录的唯一标识随后依次配置默认的索引策略PrefixIndexStrategy前缀匹配、清洗器LowerCaseSanitizer统一转小写避免大小写导致漏匹配与搜索索引TfIdfSearchIndex基于 TF-IDF 进行相关度排序。索引数据addIndex(title)与addIndex(author)声明参与搜索的字段addDocuments(bookList)把整个数据集加入索引。实时检索输入框内容每次变化时searchData取出当前输入值调用search.search(value)结果写入 state最终通过table元素呈现给用户。当输入为空时queryResults回退为完整bookList即默认展示全部数据。组装进页面要让搜索在站点中生效只需把新组件导入到某个页面。仓库示例中的做法见 examples/using-js-search/src/pages/index.jsimport 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写入context。context中的内容最终会进入该页面的pageContext从而在页面组件中通过 props 读取。创建页面模板 ClientSearchTemplate在src/templates/下创建ClientSearchTemplate.jsimport 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作为books、options作为engine传给搜索组件。这印证了前文所述gatsby-node.js中context里的数据最终会以pageContext的形式到达页面模板。创建通用搜索组件 ClientSearch在src/components/下创建ClientSearch.jsimport 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 htmlForSearch style{{ paddingRight: 10px }} Enter your search here /label input idSearch value{searchQuery} onChange{this.searchData} placeholderEnter 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。它比方案一组件更进一步把搜索引擎的配置全部参数化代码逐段拆解注入引擎配置组件挂载前getDerivedStateFromProps()生命周期方法被调用它会评估 props 中的engine并把indexStrategy、searchSanitizer、TitleIndex、AuthorIndex、SearchByTerm映射为组件 state从而允许从gatsby-node.js的 context 远程控制搜索行为。重建索引随后componentDidMount()触发rebuildIndex()。按选项创建搜索引擎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过滤常见停用词索引字段indexByTitle与indexByAuthor分别决定是否把title、author加入索引。索引数据addDocuments(books)把通过 props 传入即构建期注入的allBooks的完整数据集加入索引。实时检索输入变化时调用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.jsClientSearchTemplate.jsClientSearch.js浏览器端负担需等待请求返回再建索引数据已随页面就绪直接建索引可配置性默认选项改动需改组件代码通过 context 参数化控制引擎选项两种方案都采用 js-search 的默认/常用选项组合前缀索引 小写清洗 TF-IDF 排序便于先跑通再深入定制。js-search 还支持通过StopWordsTokenizer、AllSubstringsIndexStrategy等扩展点做更精细的调优具体可查阅其官方文档。最后提醒示例代码刻意保持了教学式的直白写法。在真实项目中更合理的做法是让数据经由 Gatsby 的 GraphQL 数据层例如gatsby-transformer-json或gatsby-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),仅供参考