在您的下一个项目中使用neo4j
#javascript #网络开发人员 #nextjs #neo4j

在观看了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,则在屏幕顶部会看到新实例按钮。如果您单击此按钮,则可以在一个空数据库或一个预先填充示例数据的一个。

之间进行选择。

Create an instance

在本文中,我建议选择由电影,演员,董事和用户评分组成的Graph-based Recommendations数据集。该数据集是对图形概念的很好介绍,可用于构建电影推荐算法。我们在GraphAcademy上使用它,包括Building Neo4j Applications with Node.js课程。

单击创建以创建您的实例。完成后,将使用生成的密码出现模式窗口。

Aura Credentials

单击下载按钮以下载您的凭据,我们以后需要一些。几分钟后,您的实例将准备探索。您可以单击探索按钮至explore the graph with Neo4j Bloom,或单击查询选项卡

Neo4j AuraDB Instance

您可以在自己的时间内看一下,让我们专注于我们的下一个申请。

创建一个新的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允许以几种方式渲染内容。

  1. 静态站点生成(SSG) - 在 build time
  2. 上生成静态HTML页面
  3. 服务器端渲染(SSR)-HTML是在请求中生成服务器端的
  4. 客户端渲染(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>
  )
}

这应该生成每种类型的链接的无序列表:
Genre List

看起来不错...

如果您运行了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然后呈现一个带有每个请求的电影列表。

A list of Movies in the Adventure genre

使用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