在观看了Next.js Conf 2022的一些有光泽的新视频后,我想我会仔细研究下一步。
从根本上讲,将neo4j集成添加到next.js项目中类似于任何其他基于node.js/typescript的项目。但是,各种数据获取方法以及服务器端和客户端渲染都引起了一些有趣的挑战。
让我们看一下如何在next.js项目中使用neo4j。
什么是什么。
Next.js是一个基于React的框架,为构建Web应用程序提供了一个自信的起点。该框架为开发人员在构建UI组件,数据获取和渲染等现代应用时需要考虑的许多共同功能提供了构建块。
该框架还侧重于性能,提供了使用 static site生成(ssg) 的能力预先生成静态HTML页面,并在请求时在服务器上使用 server-侧面渲染(SSR)以及使用客户端渲染(CSR)。。。
您可以read more about Next.js here。
什么是neo4j?
,如果您通过搜索找到了本文,那么您会比neo4j更了解Next.js。 neo4j是A Graph Database ,一个由 nodes - 代表实体或 things 的数据库,连接在一起。
。neo4j在使用高度连接的数据集或作为复杂关系数据库模式的替代方案时就可以自行融入其中。黄金法则是,如果您的查询具有三个或更多的加入,则应该真正考虑使用图形数据库。
您可以read more about Neo4j here。
为什么要neo4j和next.js?
Next.js成为构建现代Web应用程序最受欢迎的框架之一。使用Next.js的好处是,您的前端和后端代码都在api/
目录的同一子文件夹中独立。
如果您要构建一个NEO4J支持的项目,则与Neo4j JavaScript Driver建立集成相对简单。您需要做的就是在应用程序中创建驱动程序的新实例,然后使用驱动程序执行Cypher语句并检索结果。
当然,您可以直接从React组件中使用Neo4J JavaScript驱动程序,但这意味着通过客户端将数据库凭据公开,这可能是安全风险。相反,如果您需要在客户端渲染中从neo4j的按需数据,则可以创建一个API处理程序来执行Cypher语句服务器端并返回结果。
创建免费的neo4j auradb实例
Neo4j AuraDB,neo4j的完全管理的云服务为所有用户提供了一个 auradb 实例,完全免费,不需要信用卡。
如果您在cloud.neo4j.io上登录或注册Neo4J Aura,则在屏幕顶部会看到新实例按钮。如果您单击此按钮,则可以在一个空数据库或一个预先填充示例数据的一个。
之间进行选择。在本文中,我建议选择由电影,演员,董事和用户评分组成的Graph-based Recommendations
数据集。该数据集是对图形概念的很好介绍,可用于构建电影推荐算法。我们在GraphAcademy上使用它,包括Building Neo4j Applications with Node.js课程。
单击创建以创建您的实例。完成后,将使用生成的密码出现模式窗口。
单击下载按钮以下载您的凭据,我们以后需要一些。几分钟后,您的实例将准备探索。您可以单击探索按钮至explore the graph with Neo4j Bloom,或单击查询选项卡。
您可以在自己的时间内看一下,让我们专注于我们的下一个申请。
创建一个新的next.js项目
您可以使用Create Next App CLI command从模板中创建一个新的Next.js项目。
npx create-next-app@latest
命令将提示您获取项目名称并安装任何依赖项。
添加neo4j助手功能
要安装Neo4J JavaScript驱动程序,首先安装依赖项:
npm install --save neo4j-driver
# or yarn add neo4j-driver
next.js随附built-in support for Environment Variables,因此我们可以简单地复制从上面的Neo4J Aura控制台下载的凭据文件,将其重命名为.env
并放在目录root中。
然后,我们可以通过process.env
变量访问这些变量:
const { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD } = process.env
接下来,创建一个名为lib/
的新文件夹,然后创建一个新的neo4j.js
文件。您将需要从neo4j-driver
依赖项导入neo4j
对象,并使用上面的凭据创建驱动程序实例
// lib/neo4j.js
const driver = neo4j.driver(
process.env.NEO4J_URI,
neo4j.auth.basic(
process.env.NEO4J_USERNAME,
process.env.NEO4J_PASSWORD
)
)
使用NEO4J实例执行Cypher语句时,您需要打开会话,并在读取或写入交易中执行语句。一段时间后,这可能会变得有些麻烦,因此,我建议编写辅助函数以读写查询:
// lib/neo4j.js
export async function read(cypher, params = {}) {
// 1. Open a session
const session = driver.session()
try {
// 2. Execute a Cypher Statement
const res = await session.executeRead(tx => tx.run(cypher, params))
// 3. Process the Results
const values = res.records.map(record => record.toObject())
return values
}
finally {
// 4. Close the session
await session.close()
}
}
export async function write(cypher, params = {}) {
// 1. Open a session
const session = driver.session()
try {
// 2. Execute a Cypher Statement
const res = await session.executeWrite(tx => tx.run(cypher, params))
// 3. Process the Results
const values = res.records.map(record => record.toObject())
return values
}
finally {
// 4. Close the session
await session.close()
}
}
如果您想深入研究此代码或最佳实践,我建议您在Graphacademy上查看Neo4j & Node.js Course。
现在我们可以查询neo4j,让我们看一下Next.js
中数据获取的选项next.js的数据获取
next.js允许以几种方式渲染内容。
- 静态站点生成(SSG) - 在 build time 上生成静态HTML页面
- 服务器端渲染(SSR)-HTML是在请求中生成服务器端的
- 客户端渲染(CSR)-HTTP请求在使用JavaScript的浏览器中执行,并且响应更新DOM
根据用例,您可能需要这些方法的混合物。假设您正在运行电影推荐网站,使用SSG来构建营销页面可能很有意义。电影信息保存在数据库中并定期更改,因此这些页面应由服务器使用SSR渲染。当用户来评估电影时,应通过API请求进行互动,并使用CSR呈现结果。
让我们看一下这些记录的实现。
静态页面生成
例如,假设通用流派页面不会经常更改,并且它们不需要任何用户交互。通过生成静态页面,我们可以提供页面的缓存版本并将负载从服务器中取走。
pages/
目录中导出getStaticProps()
函数(称为页面)的任何组件将在构建时间生成并用作静态文件。
在页面文件夹中创建的组件将自动映射到路由。要创建将在/genres
上可用的页面,您需要创建一个pages/genres/index.jsx
文件。该组件需要导出返回JSX组件和getStaticProps()
函数的default
函数。
首先,要获取组件所需的数据,请创建getStaticProps()
函数并在a read 交易中执行this Cypher statement。
// pages/genres/index.jsx
export async function getStaticProps() {
const res = await read(`
MATCH (g:Genre)
WHERE g.name <> '(no genres listed)'
CALL {
WITH g
MATCH (g)<-[:IN_GENRE]-(m:Movie)
WHERE m.imdbRating IS NOT NULL AND m.poster IS NOT NULL
RETURN m.poster AS poster
ORDER BY m.imdbRating DESC LIMIT 1
}
RETURN g {
.*,
movies: toString(size((g)<-[:IN_GENRE]-(:Movie))),
poster: poster
} AS genre
ORDER BY g.name ASC
`)
const genres = res.map(row => row.genre)
return {
props: {
genres,
}
}
}
从此功能中返回的props
中的任何内容都将作为道具传递到默认组件中。
现在,导出一个默认函数,显示出类型的列表。
// pages/genres/index.jsx
export default function GenresList({ genres }) {
return (
<div>
<h1>Genres</h1>
<ul>
{genres.map(genre => <li key={genre.name}>
<Link href={`/genres/${genre.name}`}>{genre.name} ({genre.movies})</Link>
</li>)}
</ul>
</div>
)
}
看起来不错...
如果您运行了npm run build
命令,则会在.next/server/pages/
目录中看到一个genres.html
文件。
使用neo4j进行服务器端渲染
每个类型页面上的电影列表可能会经常更改,或者您希望在页面上添加额外的交互。在这种情况下,在服务器上渲染此页面是有意义的。默认情况下,next.js将在短时间内缓存此页面,非常适合流量高的网站。
上一页上的每个类型链接链接到/genres/[name]
-例如/genres/Action
。通过创建pages/genres/[name].jsx
文件,next.js知道自动知道以/genres/
开头的任何URL上的请求,并在斜线之后检测任何内容作为name
URL参数。
可以通过getServerSideProps()
函数访问这一点,该函数将指示Next.js在请求中使用服务器端渲染渲染此页面。
应使用getServerSideProps()
功能来获取渲染页面所需的数据并在props
键中返回。
export async function getServerSideProps({ query, params }) {
const limit = 10
const page = parseInt(query.page ?? '1')
const skip = (page - 1) * limit
const res = await read(`
MATCH (g:Genre {name: $genre})
WITH g, size((g)<-[:IN_GENRE]-()) AS count
MATCH (m:Movie)-[:IN_GENRE]->(g)
RETURN
g { .* } AS genre,
toString(count) AS count,
m {
.tmdbId,
.title
} AS movie
ORDER BY m.title ASC
SKIP $skip
LIMIT $limit
`, {
genre: params.name,
limit: int(limit),
skip: int(((query.page || 1)-1) * limit)
})
const genre = res[0].genre
const count = res[0].count
return {
props: {
genre,
count,
movies: res.map(record => record.movie),
page, skip, limit,
}
}
}
在上面的示例中,我在请求上下文中从params
对象中获取了电影名称,该对象将其作为getServerSideProps()
函数的唯一参数传递。我还尝试从URL获取?page=
查询参数,以提供分页的电影列表。
这些值将再次通过道具传递到默认函数中,因此可以用来列出电影和分页链接。
export default function GenreDetails({ genre, count, movies, page, skip, limit }) {
return (
<div>
<h1>{genre.name}</h1>
<p>There are {count} movies listed as {genre.name}.</p>
<ul>
{movies.map(movie => <li key={movie.tmdbId}>{movie.title}</li>)}
</ul>
<p>
Showing page #{page}. <br />
{page > 1 ? <Link href={`/genres/${genre.name}?page=${page-1}`}> Previous</Link> : ' '}
{' '}
{skip + limit < count ? <Link href={`/genres/${genre.name}?page=${page+1}`}>Next</Link> : ' '}
</p>
</div>
)
}
next.js然后呈现一个带有每个请求的电影列表。
使用neo4j进行客户端数据获取
按照目前的看法,对于上面的上一个和下一个链接的每次点击,整个页面都将重新加载,这并不理想。尽管这是到目前为止的一个微不足道的示例,但再次加载价值HTML的KBS以渲染标题和页脚意味着服务器上的额外负载,并通过电线发送了更多数据。
相反,您可以构建一个React组件,该组件将通过客户端HTTP请求异步加载电影列表。这意味着可以在不重新加载整个页面的情况下更新电影的列表,从而为最终用户提供更平滑的观看体验。
为了支持这一点,我们将不得不创建一个API Route,它将返回电影列表。
pages/api/
目录中的任何文件都被视为路由处理程序,一个默认导出的功能,该功能接受请求和响应参数,并期望返回HTTP状态和响应。
因此,要创建一个API路由以在http://locahost:3000/api/movies/[name]/movies
上提供电影列表,请在pages/api/genres/[name]
文件夹中创建一个新的movies.js
文件。
// pages/api/genres/[name]/movies.js
export default async function handler(req, res) {
const { name } = req.query
const limit = 10
const page = parseInt(req.query.page as string ?? '1')
const skip = (page - 1) * limit
const result = await read<MovieResult>(`
MATCH (m:Movie)-[:IN_GENRE]->(g:Genre {name: $genre})
RETURN
g { .* } AS genre,
toString(size((g)<-[:IN_GENRE]-())) AS count,
m {
.tmdbId,
.title
} AS movie
ORDER BY m.title ASC
SKIP $skip
LIMIT $limit
`, {
genre: name,
limit: int(limit),
skip: int(skip)
})
res.status(200).json({
total: parseInt(result[0]?.count) || 0,
data: result.map(record => record.movie)
})
}
上面的函数在读取事务中执行一个Cypher语句,处理结果并返回
的列表
电影作为JSON回应。
快速获取对http://localhost:3000/api/genres/Action/movies的请求显示了电影列表:
[
{
"tmdbId": "72867",
"title": "'Hellboy': The Seeds of Creation"
},
{
"tmdbId": "58857",
"title": "13 Assassins (Jûsan-nin no shikaku)"
},
/* ... */
]
然后可以通过useEffect
钩中的反应组件来调用此API处理程序。
// components/genre/movie-list.tsx
export default function GenreMovieList({ genre }: GenreMovieListProps) {
const [page, setPage] = useState<number>(1)
const [limit, setLimit] = useState<number>(10)
const [movies, setMovies] = useState<Movie[]>()
const [total, setTotal] = useState<number>()
// Get data from the API
useEffect(() => {
fetch(`/api/genres/${genre.name}/movies?page=${page}&limit=${limit}`)
.then(res => res.json())
.then(json => {
setMovies(json.data)
setTotal(json.total)
})
}, [genre, page, limit])
// Loading State
if (!movies || !total) {
return <div>Loading...</div>
}
return (
<div>
<ul>
{movies.map(movie => <li key={movie.tmdbId}>{movie.title}</li>)}
</ul>
<p>Showing page {page}</p>
{page > 1 && <button onClick={() => setPage(page - 1)}>Previous</button>}
{page * limit < total && <button onClick={() => setPage(page + 1)}>Next</button>}
</div>
)
}
然后,组件负责分页,列表的任何更新都不会重新渲染整个页面。
结论
这远非Next.js或neo4j集成的综合指南,但希望它可以作为任何想知道整合Neo4J或任何其他数据库的人的快速参考,与Next.js应用程序。
该实验的所有代码都是available on Github。
如果您有兴趣了解Next.js的更多信息,他们将course for developers to learn the basics组合在一起。
如果您想了解有关Neo4J的更多信息,那么我建议您看看Beginners Neo4j Courses on GraphAcademy。如果您想进一步了解如何在node.js或typescript项目中使用neo4j javascript驱动程序,我也会推荐Building Neo4j Applications with Node.js course。
如果您有任何评论或问题,请随时使用reach out to me on Twitter。