一个纯 JavaScript、轻量级的 Ghost 浏览器内全文搜索插件(博客)
SearchinGhost 是一款专用于 Ghost 博客平台的轻量级可扩展搜索引擎。它的核心是使用 Ghost Content API 加载您的博客内容,并使用强大的 FlexSearch 库来索引和执行搜索查询。
一切都发生在客户端浏览器中,它帮助我们提供极快的搜索结果并将其实时显示给您的用户(也称为“键入时搜索”)。我们还通过依赖浏览器localStorage
仅在必要时发送请求来尽量减少网络负载。
您的博客使用西里尔文、中文、韩文、希腊文、印度文或任何其他非拉丁语言?不用担心,它是支持的,请参阅专用部分。
额外奖励:如果您喜欢这个概念,但希望快速轻松地安装它(真的,不到 3 分钟!),请访问 SearchinGhostEasy 项目。
在深入安装和配置之前,请通过此现场演示亲自尝试一下。
在此演示中,可搜索的内容来自官方 Ghost 演示 API(即 https://demo.ghost.io)。选项设置为默认值,以便将每个输入单词搜索到帖子标题、标签、摘录和主要内容。
例如,搜索单词“marmalade”。它不存在于任何帖子标题、摘录或标签中,但在“Down The Rabbit Hole”文章中使用过一次,这就是为什么您会得到它的结果。
首先,更新主题的default.hbs
文件以包含输入字段和输出元素以显示搜索结果。然后,添加 SearchinGhost 脚本的链接并使用您自己的CONTENT_API_KEY
对其进行初始化。要获取内容 API 密钥,请参阅 Ghost 官方文档。
< input id =" search-bar " >
< ul id =" search-results " > </ ul >
< script src =" https://cdn.jsdelivr.net/npm/[email protected]/dist/searchinghost.min.js " > </ script >
< script >
var searchinGhost = new SearchinGhost ( {
key : 'CONTENT_API_KEY'
} ) ;
</ script >
就这样,一切都完成了!如果您需要更细粒度的配置,请阅读接下来的部分。
您可以使用多种方法安装 SearchinGhost,以下是可能的方法:
这是安装 SearchinGhost 最简单且首选的方法。将这些脚本之一添加到您的主题default.hbs
中。我们还建议使用 jsdelivr 而不是 unpkg,因为它具有可靠性和性能。
< script src =" https://cdn.jsdelivr.net/npm/[email protected]/dist/searchinghost.min.js " > </ script >
<!-- OR -->
< script src =" https://unpkg.com/[email protected]/dist/searchinghost.min.js " > </ script >
如果您想从自己的服务器提供 SearchinGhost 或将其包含在构建过程中,您可以从发布页面资产中获取它或下载dist/searchinghost.min.js
文件。
安装 SearchinGhost 作为项目依赖项。
$ npm install searchinghost
# OR
$ yarn add searchinghost
然后,从任何 Javascript 文件加载它。
import SearchinGhost from 'searchinghost' ;
// OR
var SearchinGhost = require ( 'searchinghost' ) ;
唯一的强制配置字段是key
。任何其他字段都有默认值并且变为可选。
SearchinGhost 被设计为开箱即用,这种最小的配置却很强大!每次击键时,它都会搜索帖子标题、标签、摘录和内容。这是默认行为,因为它似乎是最常见的。
// SearchinGhost minimal configuration
var searchinGhost = new SearchinGhost ( {
key : '<CONTENT_API_KEY>'
} ) ;
尽管如此,为了满足您的需求,一些额外的配置可能是值得的。假设您只想搜索title
并显示找到的每个帖子的title
和published_at
字段。您可以使用此配置:
var searchinGhost = new SearchinGhost ( {
key : '<CONTENT_API_KEY>' ,
postsFields : [ 'title' , 'url' , 'published_at' ] ,
postsExtraFields : [ ] ,
postsFormats : [ ] ,
indexedFields : [ 'title' ] ,
template : function ( post ) {
return `<a href=" ${ post . url } "> ${ post . published_at } - ${ post . title } </a>`
}
} ) ;
SearchinGhost 通过其配置可以轻松定制和扩展,请花点时间研究下一节中的每个选项。
{
key : '' ,
url : window . location . origin ,
version : 'v3' ,
loadOn : 'focus' ,
searchOn : 'keyup' ,
limit : 10 ,
inputId : [ 'search-bar' ] ,
outputId : [ 'search-results' ] ,
outputChildsType : 'li' ,
postsFields : [ 'title' , 'url' , 'excerpt' , 'custom_excerpt' , 'published_at' , 'feature_image' ] ,
postsExtraFields : [ 'tags' ] ,
postsFormats : [ 'plaintext' ] ,
indexedFields : [ 'title' , 'string_tags' , 'excerpt' , 'plaintext' ] ,
template : function ( post ) {
var o = `<a href=" ${ post . url } ">`
if ( post . feature_image ) o += `<img src=" ${ post . feature_image } ">`
o += '<section>'
if ( post . tags . length > 0 ) {
o += `<header>
<span class="head-tags"> ${ post . tags [ 0 ] . name } </span>
<span class="head-date"> ${ post . published_at } </span>
</header>`
} else {
o += `<header>
<span class="head-tags">UNKNOWN</span>
<span class="head-date"> ${ post . published_at } </span>
</header>`
}
o += `<h2> ${ post . title } </h2>`
o += `</section></a>`
return o ;
} ,
emptyTemplate : function ( ) { } ,
customProcessing : function ( post ) {
if ( post . tags ) post . string_tags = post . tags . map ( o => o . name ) . join ( ' ' ) . toLowerCase ( ) ;
return post ;
} ,
date : {
locale : document . documentElement . lang || "en-US" ,
options : { year : 'numeric' , month : 'short' , day : 'numeric' }
} ,
cacheMaxAge : 1800 ,
onFetchStart : function ( ) { } ,
onFetchEnd : function ( posts ) { } ,
onIndexBuildStart : function ( ) { } ,
onIndexBuildEnd : function ( index ) { } ,
onSearchStart : function ( ) { } ,
onSearchEnd : function ( posts ) { } ,
indexOptions : { } ,
searchOptions : { } ,
debug : false
} ) ;
用于访问博客数据的公共内容 API 密钥。
例如:
'22444f78447824223cefc48062'
Ghost API 的完整域名。
示例:
'https://demo.ghost.io'
默认值:
window.location.origin
设置 Ghost API 版本。同时使用
'v2'
和'v3'
。默认值:
'v3'
设置库加载策略。它可以在 HTML 页面加载时触发,也可以在用户单击搜索栏时按需触发,也可以从不触发。
要自行触发搜索栏初始化,请将此值设置为
false
(布尔值)。这样,当其余代码准备就绪时,您可以调用searchinGhost.loadData()
。预期值:
'page'
、'focus'
或false
默认值:
'focus'
选择何时执行搜索查询。要在每个用户击键和表单提交时进行搜索,请使用
'keyup'
。要仅在用户通过按钮提交表单或输入“回车”键时进行搜索,请使用'submit'
。如果您想从自己的 javascript 代码中完全控制它,请使用false
(布尔值)并使用searchinGhost.search("...")
自行执行搜索。预期值:
'keyup'
、'submit'
或false
默认值:
'keyup'
设置搜索查询返回的最大帖子数。
1
到50
之间的任何值都将快如闪电,低于1000
的值不应过多降低性能。但请记住,当搜索引擎达到此限制时,它会停止挖掘并返回结果:越低越好。尽管强烈建议不要这样做,但请将此值设置为
0
以显示所有可用结果。默认值:
10
[已弃用] 在
v1.6.0
之前,该字段是一个string
,此行为已被弃用。您的网站可能有一个或多个搜索栏,每个搜索栏都必须有一个唯一的 HTML
id
属性。将每个搜索栏id
放入此数组中。名称中请勿包含“#”。如果不需要任何输入字段,请将值设置为
[]
(空数组),并将searchOn
设置为false
(布尔值)。然后,使用searchinGhost.search("<your query>")
运行搜索。默认值:
['search-bar']
[已弃用] 在
v1.6.0
之前,该字段是一个string
,此行为已被弃用。您的网站可以使用一个或多个 HTML 元素来显示搜索结果。该数组引用所有这些输出元素的
id
属性。如果这些元素中的任何一个已经有内容,它将被搜索结果覆盖。如果您使用JS框架显示搜索结果,请将此值设置为
[]
(空数组)。您将获得作为函数searchinGhost.search("<your query>")
返回的值找到的帖子。默认值:
['search-results']
[已弃用] 在
v1.6.0
之前,该字段是string
。这已被弃用。每个搜索结果在添加到
outputId
父元素之前都包装在子元素内。默认类型是li
,但您可以将其设置为任何有效的 HTML 元素(请参阅 MDN 文档)。如果您不想使用包装元素将
template
和emptyTemplate
的结果直接附加到输出元素,请将该值设置为false
(布尔值)。默认值:
'li'
所有所需帖子字段的数组。所有这些字段都将在
template
功能中可用,以显示有用的帖子信息。参考“fields”官方文档。
注意:如果您使用
'custom_excerpt'
,其内容将自动放入'excerpt'
中以使模板化更容易。默认值:
['title', 'url', 'excerpt', 'custom_excerpt', 'published_at', 'feature_image']
该数组允许您使用额外的字段,例如
tags
或authors
。我个人不知道为什么它们不与其他“字段”在一起,但 Ghost API 是这样设计的......将其值设置为
[]
(空数组)以完全禁用它。参考“include”官方文档。
默认值:
['tags']
这对应于“格式”Ghost API,它允许使用 HTML 或纯文本获取帖子内容。
将其值设置为
[]
(空数组)以完全禁用它。请参考“formats”官方文档。
默认值:
['plaintext']
索引字段列表。所有这些字段的内容都将是可搜索的。
此列表中的所有值都必须在帖子中定义。否则,搜索结果将不准确,但应用程序不会崩溃!仔细检查
postsFields
、postsExtraFields
和postsFormats
值。注意:
'string_tags'
奇怪的字段被添加到customProcessing
选项中。如果你想使用标签,这个丑陋的东西是必要的(目前),因为 FlexSearch 无法正确处理数组。如果您不想要/喜欢它,请覆盖customProcessing
以仅返回posts
而不进行额外修改。如果您决定使用标签,也请在此处使用'string_tags'
。默认值:
['title', 'string_tags', 'excerpt', 'plaintext']
定义您自己的结果模板。该模板将用于找到的每个帖子以生成结果,并作为子元素附加到输出元素。没有模板引擎,只有一个使用
post
对象作为参数的本机 JavaScript 函数。这个模板选项比您想象的要强大得多。我们也可以将其视为对搜索结果调用的自定义处理函数。例如,如果您想做一些过滤,则不返回任何内容(例如
return;
)或返回空字符串(例如return "";
)以丢弃项目。请注意使用反引号(例如“`”)而不是单/双引号。这是启用非常有用的 javascript 变量插值所必需的。
可用变量是
postsFields
选项中定义的变量。例子:
template: function ( post ) { return `<a href=" ${ post . url } "># ${ post . tags } - ${ post . published_at } - ${ post . title } </a>` }
当没有找到结果时,定义您自己的结果模板。
例子:
emptyTemplate: function ( ) { return '<p>Sorry, nothing found...</p>' }
您需要对从 Ghost 获取的帖子数据进行一些额外的修改吗?使用此功能可以完成您需要的任何操作。此函数在每个帖子上调用,在
onFetchEnd()
之后和onIndexBuildStart()
之前执行。如果您想丢弃帖子,请返回任何 JS 假值(例如
null
、undefined
、false
、""
等)。要轻松调试输入/输出,请使用
onFetchEnd()
和onIndexBuildEnd()
通过console.log()
显示结果。如果您是更高级的用户,最好的选择仍然是使用调试器。另外,测试时不要忘记清理本地缓存!注意:默认情况下,此选项已填充辅助函数,以便更轻松地在帖子中使用“标签”字段。请参阅
indexedFields
选项。例子:
customProcessing: function ( post ) { post . extra_field = "hello" ; return post ; }
定义从帖子中获取的日期格式。
请参阅 MDN 参考以获取更多信息。
例子:
date: { locale : "fr-FR" , options : { weekday : 'long' , year : 'numeric' , month : 'long' , day : 'numeric' } }
设置缓存最长期限(以秒为单位)。在此期间,如果在本地存储中找到已经存在的索引,则将加载该索引,而无需任何额外的 HTTP 请求来确认其有效性。当缓存被清除时,该值被重置。
这对于节省服务器的宽带和网络负载特别有用。默认值设置为半小时。该值来自 Google Analytics 使用的默认用户会话持续时间。
默认值:
1800
在从 Ghost API 获取数据之前定义一个回调函数。
该函数不带任何参数。
例子:
onFetchStart: function ( ) { console . log ( "before data fetch" ) ; }
定义获取完成时的回调函数。即使对
posts
所做的修改得以保留,我们也建议使用customProcessing()
函数来执行此操作。该函数采用一个参数:Ghost 本身返回的所有帖子的数组。
例子:
onFetchEnd: function ( posts ) { console . log ( "Total posts found on Ghost:" , posts . length ) ; }
在开始构建搜索索引之前定义一个回调函数。
该函数不带参数。
例子:
onIndexBuildStart: function ( ) { console . log ( "before building the index" ) ; }
定义搜索索引构建完成时的回调函数。
该函数采用一个参数:构建 FlexSearch 索引对象。
例子:
onIndexBuildEnd: function ( index ) { console . log ( "index built:" , index ) ; }
在开始执行搜索查询之前定义回调函数。例如,它可用于在等待
onSearchEnd
完成时隐藏结果 HTML 元素或添加任何花哨的过渡效果。但在大多数情况下,这是没有必要的,因为搜索功能足够快,对眼睛来说很舒服。该函数不带参数。
例子:
onSearchStart: function ( ) { console . log ( "before executing the search query" ) ; }
当搜索结果准备好时定义回调函数。
该函数采用 1 个参数:匹配帖子的数组。
例子:
onSearchEnd: function ( posts ) { console . log ( "search complete, posts found:" , posts ) ; }
添加额外的搜索索引配置或覆盖默认配置。这些选项将与已提供的选项合并:
{ doc : { id : "id" , field : this . config . indexedFields } , encode : "simple" , tokenize : "forward" , threshold : 0 , resolution : 4 , depth : 0 }还可以使用此参数来启用非拉丁语言支持,请参阅本节。
默认:
{}
专为高级用户设计,允许您微调搜索查询。请参阅此 FlexSearch 文档。
我们使用这个特定的查询结构:
index.search("your query", searchOptions)
因此添加到searchOptions
任何内容都将以这种方式传递到 FlexSearch。当根据标签过滤帖子时,此参数非常方便。举个例子:
searchOptions: { where : { string_tags : "getting started" } }另请注意,
limit
Searchinghost 选项会自动合并到searchOptions
中。在我们的例子中,它最终会变成:searchOptions: { where : { string_tags : "getting started" } , limit : 10 }默认:
{}
当某些内容未按预期工作时,设置为
true
以显示应用程序日志。默认值:
false
如果您的博客使用拉丁字母语言(例如英语、法语、西班牙语)或北欧/东欧语言(例如德语、瑞典语、匈牙利语、斯洛文尼亚语、爱沙尼亚语),则默认配置将正常工作。在其他情况下,找到适当的indexOptions
值并将其添加到您的主要SearchinGhost配置中。
要创建您自己的特定设置,请参阅 FlexSearch 自述文件和这三个问题。
如果没有什么对你有用或者结果行为不正确,请随意创建一个问题。
indexOptions: {
encode : false ,
rtl : true ,
split : / s+ /
}
indexOptions: {
encode : false ,
tokenize : function ( str ) {
return str . replace ( / [x00-x7F] / g , "" ) . split ( "" ) ;
}
}
任何使用复杂字符的空格分隔单词语言都可以使用此选项。
indexOptions: {
encode : false ,
split : / s+ /
}
如果您需要使用多种语言类型(例如西里尔文/英语或印度语/西班牙语),请使用下面的专用配置。我知道,乍一看可能看起来很吓人,但只需复制/粘贴它并相信我。
indexOptions: {
split : / s+ / ,
encode : function ( str ) {
var regexp_replacements = {
"a" : / [àáâãäå] / g ,
"e" : / [èéêë] / g ,
"i" : / [ìíîï] / g ,
"o" : / [òóôõöő] / g ,
"u" : / [ùúûüű] / g ,
"y" : / [ýŷÿ] / g ,
"n" : / ñ / g ,
"c" : / [ç] / g ,
"s" : / ß / g ,
" " : / [-/] / g ,
"" : / ['!"#$%&\()*+,-./:;<=>?@[]^_`{|}~] / g ,
" " : / s+ / g ,
}
str = str . toLowerCase ( ) ;
for ( var key of Object . keys ( regexp_replacements ) ) {
str = str . replace ( regexp_replacements [ key ] , key ) ;
}
return str === " " ? "" : str ;
}
}
首先,我们还尝试了其他解决方案:Lunr.js、minisearch 和 fusion.js。最后,FlexSearch 提供了最佳的整体结果,结果快速准确,捆绑包大小足够小,而且易于设置/配置。一切都需要选择!
不用担心,这是正常的。 SearchinGhost 使用缓存系统将您的博客数据存储在浏览器中,从而限制网络交互。默认情况下,30 分钟内存储的缓存数据仍被视为有效。此后,您将可以看到新文章。
请记住,其他用户可能不需要等待 30 分钟,具体取决于他们上次进行研究的时间。如果您是 1 小时前,他们的缓存将被清除并更新,以便显示该文章。
如果您希望您的用户始终保持最新状态,请将cacheMaxAge
设置为0
。这样做时,您还应该将loadOn
设置为'focus'
以限制 HTTP 请求的数量。
默认情况下,当您使用feature_image
URL 变量在搜索结果中显示图像时,您将始终获得原始/全尺寸的图像,并且它们通常对于我们的需求来说太大(尺寸和重量),微型会更好合身。
从 Ghost V3 开始,嵌入了媒体处理引擎来创建响应式图像。默认情况下,Ghost 会重新创建给定图像的 6 个不同图像。可用尺寸有: w30
、 w100
、 w300
、 w600
、 w1000
、 w2000
。
在我们的例子中,更快地加载图像的最简单方法就是使用较小的图像。基本上,我们希望此 URL https://www.example.fr/content/images/2020/05/picture.jpg
(默认从 Ghost API 获取)变为https://www.example.fr/content/images/size/w600/2020/05/picture.jpg
(宽度为 600 像素)。
为此,请通过使用以下代码示例添加"customProcessing"
字段来更新配置。当然,您可以使用上面提到的任何可用大小来代替w600
。
customProcessing: function ( post ) {
if ( post . tags ) post . string_tags = post . tags . map ( o => o . name ) . join ( ' ' ) . toLowerCase ( ) ;
if ( post . feature_image ) post . feature_image = post . feature_image . replace ( '/images/' , '/images/size/w600/' ) ; // reduce image size to w600
return post ;
}
此修改不是立即的,您需要刷新缓存才能真正看到差异。
创建一个 ID 为"search-counter"
HTML 元素,并利用onSearchEnd()
函数用结果填充它。这是一个例子:
< p id =" search-counter " > </ p >
onSearchEnd: function ( posts ) {
var counterEl = document . getElementById ( 'search-counter' ) ;
counterEl . textContent = ` ${ posts . length } posts found` ;
}
是的,通过使用 SearchinGhost 内部方法,但这是可能的。它可能看起来像黑魔法,但将下面的代码添加到您当前的配置中。这里, searchinGhost
指的是您自己使用new SearchinGhost(...)
创建的实例。
emptyTemplate: function ( ) {
var allPostsArray = Object . values ( searchinGhost . index . l ) ;
var latestPosts = allPostsArray . slice ( 0 , 6 ) ;
searchinGhost . display ( latestPosts ) ;
}
如果使用 React、Vue 或 Angular 等框架,您可能不希望让 SearchinGhost 自行操作 DOM。因为您肯定需要在框架内保留任何内容更新,所以您应该使用以下配置:
var searchinGhost = new SearchinGhost ( {
key : '<CONTENT_API_KEY>' ,
inputId : false ,
outputId : false ,
[ ... ]
} ) ;
现在,要运行搜索查询,请调用此 SearchinGhost 方法:
var postsFound = searchinGhost . search ( "my query" ) ;
// Where 'postsFound' content looks like:
[
{
"title" : "A Full and Comprehensive Style Test" ,
"published_at" : "Sep 1, 2012" ,
[ ... ]
} ,
{
"title" : "Publishing options" ,
"published_at" : "Aug 20, 2018" ,
[ ... ]
}
]
这样,就不会在您背后渲染任何内容,并且一切都将在 ShadowDom 中得到控制。
debug: true
onFetchStart()
、 onSearchStart()
) 等从现在开始,任何修改都会在此专用的 CHANGELOG.md 文件中进行跟踪。
任何贡献都非常受欢迎!如果您发现错误或想要改进代码,请随时创建问题或 PR。
所有代码更新必须在src
目录下完成。
要自己构建项目,请运行:
$ npm install
$ npm run build
开发时,请使用 watch 命令,它会在每次文件修改时更快地重建,并包含源映射的链接,使调试更容易。
$ npm run watch
注意:在创建此项目时,我使用 Node v12.16.2 和 NPM v6.14.4,但也应该适用于旧/新版本
SearchinGhost 在 Ghost 搜索插件领域并不孤单。这是其他相关项目的简短列表。您绝对应该尝试一下,看看它们是否更适合您的需求:
幽灵猎人 (v0.6.0 - 101 kB, 26 kB gzip)
优点:
- 最著名的,很多关于它的文章和教程
- 基于localStorage的强大缓存系统
- 全文索引(不仅是帖子标题)
缺点:
- 依赖 jQuery
- 仅适用于 Ghost v2 API(目前)
- 随着时间的推移,源代码变得混乱
幽灵搜索(v1.1.0 - 12 kB,4.2 kB gzip)
优点:
- 编写良好且易于阅读的代码库
- 利用“模糊”功能
缺点:
- 搜索长单词时浏览器滞后
- 可能发送太多 API 请求
- 不使用评分系统首先显示最好的结果
幽灵查找器 (v3.1.2 - 459 kB, 116 kB gzip)
优点:
- 纯 Javascript 库
缺点:
- 最终捆绑尺寸巨大
- 为每个按下的键发送一个 HTTP 请求!
- 不使用搜索引擎,仅查找帖子标题中的子字符串
- 无法正确索引重音字符(例如“é”应与“e”一起找到)