您是否已经尝试使用 Gatsby 无头 WordPress? 如果没有,您可以查看 这篇文章 关于 WordPress 的新 Gatsby 源插件; gatsby-source-wordpress 是 2021 年 3 月作为 Gatsby 3 版本的一部分引入的官方源插件。 它显著改进了与 WordPress 的集成。 此外,提供 GraphQL API 的 WordPress 插件 WPGraphQL 现在可通过官方 WordPress 代码库获得。
有了稳定且维护良好的工具,开发由 WordPress 提供支持的 Gatsby 网站变得更加容易且更有趣。 我自己也参与了这个领域,我与 Alexandra Spalato 共同创办,并最近推出了 Gatsby WP Themes——一个面向使用 Gatsby 构建 WordPress 网站的开发人员的利基市场。 在本文中,我将分享我的见解,尤其是讨论搜索功能。
搜索并非开箱即用,但有许多选择可供考虑。 我将重点关注两种不同的可能性——利用 WordPress 原生搜索(WordPress 搜索查询)与使用 Jetpack 即时搜索。
入门
让我们从设置一个由 WordPress 提供支持的 Gatsby 网站开始。 为了简单起见,我将遵循 入门说明 并安装 gatsby-starter-wordpress-blog
启动器。
gatsby new gatsby-wordpress-w-search https://github.com/gatsbyjs/gatsby-starter-wordpress-blog
这个简单的基本启动器仅为单个帖子和博客页面创建路由。 但我们在这里可以保持这种简单性。 让我们假设我们不想在搜索结果中包含页面。
目前,我将保留 WordPress 源网站的现状,并从 启动器作者的 WordPress 演示 中提取内容。 如果您使用自己的源,请记住,WordPress 端需要两个插件(两者均可通过插件代码库获得)
- WPGraphQL – 一个在 WordPress 实例上运行 GraphQL 服务器的插件
- WPGatsby – 一个以 Gatsby 特定方式修改 WPGraphQL 架构的插件(它还添加了一些机制来优化构建过程)
设置 Apollo 客户端
使用 Gatsby,我们通常会使用在页面创建时运行的查询中的数据 (页面查询) 或调用 useStaticQuery
钩子。 后者可在组件中使用,不允许动态查询参数; 它的作用是在构建时检索 GraphQL 数据。 这两个查询解决方案都不适用于用户发起的搜索。 相反,我们将请求 WordPress 运行搜索查询并向我们发送回结果。 我们可以发送 GraphQL 搜索查询吗? 是的! WPGraphQL 提供搜索; 您可以在 WPGraphQL 中像这样搜索帖子
posts(where: {search: "gallery"}) {
nodes {
id
title
content
}
}
为了直接与我们的 WPGraphQL API 通信,我们将安装 Apollo 客户端; 它负责请求和缓存数据以及更新我们的 UI 组件。
yarn add @apollo/client cross-fetch
为了在我们的组件树中的任何位置访问 Apollo 客户端,我们需要使用 ApolloProvider 包装我们的应用程序。 Gatsby 不会公开包装整个应用程序的 App
组件。 相反,它提供了 wrapRootElement
API。 它是 Gatsby 浏览器 API 的一部分,需要在位于项目根目录的 gatsby-browser.js
文件中实现。
// gatsby-browser.js
import React from "react"
import fetch from "cross-fetch"
import { ApolloClient, HttpLink, InMemoryCache, ApolloProvider } from "@apollo/client"
const cache = new InMemoryCache()
const link = new HttpLink({
/* Set the endpoint for your GraphQL server, (same as in gatsby-config.js) */
uri: "https://wpgatsbydemo.wpengine.com/graphql",
/* Use fetch from cross-fetch to provide replacement for server environment */
fetch
})
const client = new ApolloClient({
link,
cache,
})
export const wrapRootElement = ({ element }) => (
<ApolloProvider client={client}>{element}</ApolloProvider>
)
SearchForm
组件
现在我们已经设置了 ApolloClient
,让我们构建 Search
组件。
touch src/components/search.js src/components/search-form.js src/components/search-results.js src/css/search.css
Search
组件包装了 SearchForm
和 SearchResults
// src/components/search.js
import React, { useState } from "react"
import SearchForm from "./search-form"
import SearchResults from "./search-results"
const Search = () => {
const [searchTerm, setSearchTerm] = useState("")
return (
<div className="search-container">
<SearchForm setSearchTerm={setSearchTerm} />
{searchTerm && <SearchResults searchTerm={searchTerm} />}
</div>
)
}
export default Search
<SearchForm />
是一个简单的表单,带有受控输入和提交处理程序,该处理程序将 searchTerm
状态值设置为用户提交的内容。
// src/components/search-form.js
import React, { useState } from "react"
const SearchForm = ({ searchTerm, setSearchTerm }) => {
const [value, setValue] = useState(searchTerm)
const handleSubmit = e => {
e.preventDefault()
setSearchTerm(value)
}
return (
<form role="search" onSubmit={handleSubmit}>
<label htmlFor="search">Search blog posts:</label>
<input
id="search"
type="search"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
)
}
export default SearchForm
SearchResults
组件通过 props 接收 searchTerm
,我们将在那里使用 Apollo 客户端。
对于每个 searchTerm
,我们希望将匹配的帖子显示为一个列表,其中包含帖子的标题、摘要和指向此单个帖子的链接。 我们的查询将如下所示
const GET_RESULTS = gql`
query($searchTerm: String) {
posts(where: { search: $searchTerm }) {
edges {
node {
id
uri
title
excerpt
}
}
}
}
`
我们将使用来自 @apollo-client
的 useQuery
钩子来运行 GET_RESULTS
查询,并带有搜索变量。
// src/components/search-results.js
import React from "react"
import { Link } from "gatsby"
import { useQuery, gql } from "@apollo/client"
const GET_RESULTS = gql`
query($searchTerm: String) {
posts(where: { search: $searchTerm }) {
edges {
node {
id
uri
title
excerpt
}
}
}
}
`
const SearchResults = ({ searchTerm }) => {
const { data, loading, error } = useQuery(GET_RESULTS, {
variables: { searchTerm }
})
if (loading) return <p>Searching posts for {searchTerm}...</p>
if (error) return <p>Error - {error.message}</p>
return (
<section className="search-results">
<h2>Found {data.posts.edges.length} results for {searchTerm}:</h2>
<ul>
{data.posts.edges.map(el => {
return (
<li key={el.node.id}>
<Link to={el.node.uri}>{el.node.title}</Link>
</li>
)
})}
</ul>
</section>
)
}
export default SearchResults
useQuery
钩子返回一个对象,该对象包含 loading
、error
和 data
属性。 我们根据查询的状态渲染不同的 UI 元素。 只要 loading
为真,我们就会显示 <p>正在搜索帖子...</p>
。 如果 loading
和 error
都为假,则查询已完成,我们可以遍历 data.posts.edges
并显示结果。
if (loading) return <p>Searching posts...</p>
if (error) return <p>Error - {error.message}</p>
// else
return ( //... )
目前,我将 <Search />
添加到布局组件中。(我稍后会将其移动到其他位置。) 然后,通过一些样式和 visible
状态变量,我让它感觉更像一个小部件,在单击时打开并在右上角固定定位。
分页查询
如果没有指定条目数量,WPGraphQL 帖子查询将返回前十个帖子; 我们需要处理分页。 WPGraphQL 实现分页 遵循 Relay 规范 用于 GraphQL 架构设计。 我不会详细介绍; 让我们只注意到它是一个标准化的模式。 在 Relay 规范中,除了 posts.edges
(它是 { cursor, node }
对象的列表)之外,我们还可以访问 posts.pageInfo
对象,该对象提供了
endCursor
–posts.edges
中最后一项的游标,startCursor
–posts.edges
中第一项的游标,hasPreviousPage
– “是否还有更多结果可用(向后)”的布尔值,以及hasNextPage
– “是否还有更多结果可用(向前)”的布尔值。
我们可以使用其他查询变量修改我们想要访问的数据切片
first
– 返回的条目数量after
– 我们应该从哪个游标开始
如何处理使用 Apollo 客户端的分页查询? 推荐的方法 是使用 fetchMore
函数,即(与 loading
、error
和 data
一起)是 useQuery
钩子返回的对象的一部分。
// src/components/search-results.js
import React from "react"
import { Link } from "gatsby"
import { useQuery, gql } from "@apollo/client"
const GET_RESULTS = gql`
query($searchTerm: String, $after: String) {
posts(first: 10, after: $after, where: { search: $searchTerm }) {
edges {
node {
id
uri
title
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`
const SearchResults = ({ searchTerm }) => {
const { data, loading, error, fetchMore } = useQuery(GET_RESULTS, {
variables: { searchTerm, after: "" },
})
if (loading && !data) return <p>Searching posts for {searchTerm}...</p>
if (error) return <p>Error - {error.message}</p>
const loadMore = () => {
fetchMore({
variables: {
after: data.posts.pageInfo.endCursor,
},
// with notifyOnNetworkStatusChange our component re-renders while a refetch is in flight so that we can mark loading state when waiting for more results (see lines 42, 43)
notifyOnNetworkStatusChange: true,
})
}
return (
<section className="search-results">
{/* as before */}
{data.posts.pageInfo.hasNextPage && (
<button type="button" onClick={loadMore} disabled={loading}>
{loading ? "Loading..." : "More results"}
</button>
)}
</section>
)
}
export default SearchResults
first
参数有其默认值,但在这里是必需的,以指示我们正在发送分页请求。 没有 first
,pageInfo.hasNextPage
将始终为 false
,无论搜索关键字是什么。
调用 fetchMore
会获取下一部分结果,但我们仍然需要告诉 Apollo 如何将“获取更多”结果 与现有的缓存数据合并。 我们将在 InMemoryCache
构造函数(在 gatsby-browser.js
文件中)传递给它的选项中指定所有分页逻辑。 猜猜怎么了? 使用 Relay 规范,我们已经涵盖了它——Apollo 客户端提供了 relayStylePagination
函数,它为我们完成所有操作。
// gatsby-browser.js
import { ApolloClient, HttpLink, InMemoryCache, ApolloProvider } from "@apollo/client"
import { relayStylePagination } from "@apollo/client/utilities"
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: relayStylePagination(["where"]),
},
},
},
})
/* as before */
只有一个重要的细节:我们不是对所有帖子进行分页,而是对对应于特定 where
条件的帖子进行分页。 将 ["where"]
添加为 relayStylePagination
的参数为不同的搜索词创建了一个不同的存储键。
使搜索持久化
现在,我的 Search
组件位于 Layout
组件中。 它在每个页面上显示,但在每次路由更改时都会被卸载。 如果我们可以在导航时保留搜索结果怎么办? 我们可以利用 Gatsby 的 wrapPageElement
浏览器 API 在页面周围设置持久性 UI 元素。
让我们将 <Search />
从布局组件移动到 wrapPageElement
// gatsby-browser.js
import Search from "./src/components/search"
/* as before */
export const wrapPageElement = ({ element }) => {
return <><Search />{element}</>
}
API wrapPageElement
和 wrapRootElement
存在于浏览器和 服务器端渲染 (SSR) API 中。 Gatsby 建议我们实现wrapPageElement
和 wrapRootElement
在 gatsby-browser.js
和 gatsby-ssr.js
中。让我们创建gatsby-ssr.js
(在项目的根目录下)并重新导出我们的元素
// gatsby-ssr.js
export { wrapRootElement, wrapPageElement } from "./gatsby-browser"
我部署了一个 演示,您可以在其中看到它的实际操作。您还可以在 这个仓库 中找到代码。
wrapPageElement
方法可能并不适合所有情况。我们的搜索小部件“分离”自布局组件。它与“固定”位置(如我们的工作示例中)或像 这个 Gatsby WordPress 主题 中的画布侧边栏中一样工作良好。
但是,如果您想在“经典”侧边栏中显示“持久”搜索结果,该怎么办?在这种情况下,您可以将 searchTerm
状态从 Search
组件移动到放置在 wrapRootElement
中的搜索上下文提供者
// gatsby-browser.js
import SearchContextProvider from "./src/search-context"
/* as before */
export const wrapRootElement = ({ element }) => (
<ApolloProvider client={client}>
<SearchContextProvider>
{element}
</SearchContextProvider>
</ApolloProvider>
)
…使用下面定义的 SearchContextProvider
// src/search-context.js
import React, {createContext, useState} from "react"
export const SearchContext = createContext()
export const SearchContextProvider = ({ children }) => {
const [searchTerm, setSearchTerm] = useState("")
return (
<SearchContext.Provider value={{ searchTerm, setSearchTerm }}>
{children}
</SearchContext.Provider>
)
}
您可以在另一个 Gatsby WordPress 主题中看到它的实际操作
请注意,由于 Apollo Client 缓存了搜索结果,因此我们在路由更改时会立即获得它们。
帖子和页面的结果
如果您查看了上面的主题示例,您可能已经注意到我是如何处理查询不止帖子的。我的方法是复制页面相同的逻辑,并分别显示每个帖子类型的结果。
或者,您可以使用 内容节点 接口在一个连接中查询不同帖子类型的节点
const GET_RESULTS = gql`
query($searchTerm: String, $after: String) {
contentNodes(first: 10, after: $after, where: { search: $searchTerm }) {
edges {
node {
id
uri
... on Page {
title
}
... on Post {
title
excerpt
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`
超越默认 WordPress 搜索
我们的解决方案似乎有效,但请记住,实际上为我们执行搜索的底层机制是本机 WordPress 搜索查询。而 WordPress 默认搜索功能并不出色。它的问题在于搜索字段有限(特别是,没有考虑分类法),没有模糊匹配,也没有对结果顺序的控制。大型网站也可能遇到性能问题——没有预建的搜索索引,并且搜索查询直接在网站 SQL 数据库上执行。
有一些 WordPress 插件可以增强默认搜索。像 WP Extended Search 这样的插件添加了在搜索查询中包含选定元键和分类法的功能。
Relevanssi 插件使用数据库的全文索引功能将其搜索引擎替换为标准 WordPress 搜索。Relevanssi 禁用默认搜索查询,这会破坏 WPGraphQL 的 where: {search : …}
。在 通过 WPGraphQL 启用 Relevanssi 搜索 上已经完成了一些工作;代码可能不兼容最新的 WPGraphQL 版本,但对于那些选择 Relevanssi 搜索的人来说,这似乎是一个不错的开始。
在本文的第二部分,我们将探讨另一种可能的途径,并仔细研究 Jetpack 的高级服务——由 Elasticsearch 提供支持的高级搜索。顺便说一句,Jetpack 即时搜索是 CSS-Tricks 采用的解决方案。
使用 Jetpack 即时搜索与 Gatsby
Jetpack 搜索是 Jetpack 的每个站点付费解决方案。安装并激活后,它将负责构建 Elasticsearch 索引。搜索查询不再命中 SQL 数据库。相反,搜索查询请求被发送到云 Elasticsearch 服务器,更准确地说,是发送到
https://public-api.wordpress.com/rest/v1.3/sites/{your-blog-id}/search
在上面的 URL 中,有很多搜索参数需要指定。在我们的例子中,我们将添加以下内容
filter[bool][must][0][term][post_type]=post
:我们只需要是帖子的结果,因为我们的 Gatsby 网站仅限于帖子。在实际使用中,您可能需要花费一些时间 配置布尔查询。size=10
设置返回结果的数量(最多 20 个)。- 使用
highlight_fields[0]=title
,我们在<mark>
标签中获得带有搜索词的标题字符串(或其一部分)。 highlight_fields[0]=content
与下面相同,但用于帖子的内容。
还有三个搜索参数取决于用户的操作
query
:来自搜索输入的搜索词,例如 gallerysort
:结果应该如何排序,默认情况下是按分数"score_default"
(相关性)排序,但也包括"date_asc"
(最新)和"date_desc"
(最旧)page_handle
:类似于分页结果的“after”游标。我们一次只请求 10 个结果,并将有一个“加载更多”按钮。
现在,让我们看看成功的响应是如何构建的
{
total: 9,
corrected_query: false,
page_handle: false, // or a string it the total value > 10
results: [
{
_score: 196.51814,
fields: {
date: '2018-11-03 03:55:09',
'title.default': 'Block: Gallery',
'excerpt.default': '',
post_id: 1918,
// we can configure what fields we want to add here with the query search parameters
},
result_type: 'post',
railcar: {/* we will not use this data */},
highlight: {
title: ['Block: <mark>Gallery</mark>'],
content: [
'automatically stretch to the width of your <mark>gallery</mark>. ... A four column <mark>gallery</mark> with a wide width:',
'<mark>Gallery</mark> blocks have two settings: the number of columns, and whether or not images should be cropped',
],
},
},
/* more results */
],
suggestions: [], // we will not use suggestions here
aggregations: [], // nor the aggregations
}
results
字段提供一个包含数据库帖子 ID 的数组。为了在 Gatsby 网站中显示搜索结果,我们需要从 Gatsby 数据层中提取相应的帖子节点(特别是它们的 uri
)。我的方法是实现即时搜索,异步调用 rest API,并将结果与返回所有帖子节点的静态 GraphQL 查询的结果进行交叉。
让我们从构建一个与搜索 API 通信的即时搜索小部件开始。由于这与 Gatsby 无关,让我们看看它在这个 Pen 中的实际操作
这里,useDebouncedInstantSearch
是一个自定义钩子,负责从 Jetpack 搜索 API 获取结果。我的解决方案使用 awesome-debounce-promise 库,它允许我们对获取机制进行额外的处理。即时搜索直接响应输入,而不等待来自用户的明确“Go!”。如果我输入速度很快,在第一个响应到达之前,请求可能会多次更改。因此,可能会有一些不必要的网络带宽浪费。awesome-debounce-promise 在调用 API 之前等待给定的时间间隔(例如 300 毫秒);如果在此间隔内有新的调用,则之前的调用将永远不会执行。它也只解析从调用返回的最后一个 Promise——这可以防止并发问题。
现在,有了搜索结果,让我们回到 Gatsby 并构建另一个自定义钩子
import {useStaticQuery, graphql} from "gatsby"
export const useJetpackSearch = (params) => {
const {
allWpPost: { nodes },
} = useStaticQuery(graphql`
query AllPostsQuery {
allWpPost {
nodes {
id
databaseId
uri
title
excerpt
}
}
}
`)
const { error, loading, data } = useDebouncedInstantSearch(params)
return {
error,
loading,
data: {
...data,
// map the results
results: data.results.map(el => {
// for each result find a node that has the same databaseId as the result field post_id
const node = nodes.find(item => item.databaseId === el.fields.post_id)
return {
// spread the node
...node,
// keep the highlight info
highlight: el.highlight
}
}),
}
}
}
我将在 <SearchResults />
中调用 useJetpackSearch
。Gatsby 版本的 <SearchResults />
与上面的 Pen 中几乎相同。代码块中突出显示了差异。钩子 useDebouncedInstantSearch
被 useJetpackSearch
(在内部调用前者)替换。还有一个 Gatsby Link
替换了 h2
,以及 el.fields["title.default"]
和 el.fields["excerpt.default"]
被 el.title
和 el.excerpt
替换。
const SearchResults = ({ params, setParams }) => {
const { loading, error, data } = useJetpackSearch(params)
const { searchTerm } = params
if (error) {
return <p>Error - {error}</p>
}
return (
<section className="search-results">
{loading ? (
<p className="info">Searching posts .....</p>
) : (
<>
{data.total !== undefined && (
<p>
Found {data.total} results for{" "}
{data.corrected_query ? (
<>
<del>{searchTerm}</del> <span>{data.corrected_query}</span>
</>
) : (
<span>{searchTerm}</span>
)}
</p>
)}
</>
)}
{data.results?.length > 0 && (
<ul>
{data.results.map((el) => {
return (
<li key={el.id}>
<Link to={el.uri}>
{el.highlight.title[0]
? el.highlight.title.map((item, index) => (
<React.Fragment key={index}>
{parse(item)}
</React.Fragment>
))
: parse(el.title)}
</Link>
<div className="post-excerpt">
{el.highlight.content[0]
? el.highlight.content.map((item, index) => (
<div key={index}>{parse(item)}</div>
))
: parse(el.excerpt)}
</div>
</li>
);
})}
</ul>
)}
{data.page_handle && (
<button
type="button"
disabled={loading}
onClick={() => setParams({ pageHandle: data.page_handle })}
>
{loading ? "loading..." : "load more"}
</button>
)}
</section>
)
}
您可以在 这个仓库 中找到完整的代码,并在 这个演示 中看到它的实际操作。请注意,我不再从 Gatsby 启动器使用的通用 WordPress 演示中获取 WordPress 数据。我需要有一个启用了 Jetpack 搜索的网站。
总结
我们刚刚看到了处理无头 WordPress 中搜索的两种方法。除了几个 Gatsby 特定的技术细节(例如使用 Gatsby 浏览器 API)之外,您还可以在其他框架中实现这两种讨论的方法。我们已经了解了如何利用本机 WordPress 搜索。我想在很多情况下,这是一个可以接受的解决方案。
但是,如果您需要更好的东西,还有更好的选择。其中之一是 Jetpack 搜索。Jetpack 即时搜索在 CSS-Tricks 上做得很好,正如我们所见,它也可以与无头 WordPress 一起使用。可能还有其他方法可以实现它。您还可以进一步配置查询、过滤器功能以及如何显示结果。