После просмотра нескольких глянцевых новых видео с Конференция Next.js 2022я подумал, что стоит поближе познакомиться с Next.js и посмотреть, как этот фреймворк может помочь мне создать мое следующее веб-приложение на основе Neo4j.

По сути, добавление интеграции Neo4j в проект Next.js аналогично любому другому проекту на основе Node.js/TypeScript. Однако различные методы извлечения данных и рендеринг как на стороне сервера, так и на стороне клиента вызывают некоторые интересные проблемы.

Давайте посмотрим, как мы можем использовать Neo4j в проекте Next.js.


Что такое Next.js?

Next.js – это фреймворк на основе React, который обеспечивает продуманную отправную точку для создания веб-приложений. Платформа предоставляет стандартные блоки для многих общих функций, которые разработчики должны учитывать при создании современных приложений, таких как компоненты пользовательского интерфейса, выборка данных и визуализация.

Фреймворк также ориентирован на производительность, предоставляя возможность предварительно генерировать статические HTML-страницы с использованием Генерация статических сайтов (SSG)отображать HTML на сервере во время запроса, используя Рендеринг на стороне сервера (SSR) а также визуализировать компоненты React на стороне клиента, используя Рендеринг на стороне клиента (CSR).

Вы можете подробнее о Next.js здесь.


Что такое Neo4j?

Скорее всего, если вы нашли эту статью через поиск, вы знаете больше о Next.js, чем о Neo4j. Neo4j — это База данных графовбаза данных, состоящая из Узлы — которые представляют сущности или вещисвязанные вместе и Отношения.

Neo4j отлично подходит для работы с сильно связанными наборами данных или в качестве альтернативы для сложных схем реляционных баз данных, где требуется много объединений. Золотое правило заключается в том, что если ваши запросы имеют три или более соединения, вам действительно следует рассмотреть возможность использования графовой базы данных.

Вы можете подробнее о Neo4j здесь.


Почему Neo4j и Next.js?

Next.js набирает обороты как один из самых популярных фреймворков для создания современных веб-приложений. Преимущество использования Next.js заключается в том, что ваш внешний и внутренний код являются автономными в одних и тех же подпапках api/ каталог.

Если вы создаете проект с поддержкой Neo4j, создание интеграции с Драйвер JavaScript для Neo4j является относительно простым. Все, что вам нужно сделать, это создать новый экземпляр драйвера в приложении, а затем использовать драйвер для выполнения операторов Cypher и получения результатов.

Конечно, вы можете использовать драйвер Neo4j JavaScript непосредственно из компонентов React, но это означает раскрытие учетных данных базы данных через клиент, что может представлять угрозу безопасности. Вместо этого, если вам требуются данные по запросу от Neo4j при рендеринге на стороне клиента, вы можете создать обработчик API для выполнения инструкции Cypher на стороне сервера и возврата результатов.


Создание бесплатного экземпляра Neo4j AuraDB

Neo4j AuraDBполностью управляемый облачный сервис Neo4j предоставляет один АураБД Бесплатно instance для всех пользователей, совершенно бесплатно и без кредитной карты.

Если вы войдете или зарегистрируетесь в Neo4j Aura на облако.neo4j.ioвы увидите Новый экземпляр кнопку в верхней части экрана. Если вы нажмете эту кнопку, вы сможете выбрать между пустой базой данных или базой данных, предварительно заполненной образцами данных.

Создать экземпляр

Для этой статьи я предлагаю выбрать Graph-based Recommendations набор данных, который состоит из фильмов, актеров, режиссеров и оценок пользователей. Этот набор данных является хорошим введением в концепции графов и может использоваться для построения алгоритма рекомендации фильмов. Мы используем его в GraphAcademy, включая курс «Создание приложений Neo4j с помощью Node.js».

Нажмите Создавать для создания вашего экземпляра. Как только вы это сделаете, появится модальное окно со сгенерированным паролем.

Учетные данные ауры

Нажмите на Скачать кнопку, чтобы загрузить свои учетные данные, они понадобятся нам чуть позже. Через пару минут ваш экземпляр будет готов к исследованию. Вы можете нажать на Исследовать кнопка для исследовать график с Neo4j Bloomили запросите график с помощью Cypher, щелкнув значок Вкладка «Запрос».

Экземпляр Neo4j AuraDB

Вы можете взглянуть на это в свое время, а пока давайте сосредоточимся на нашем приложении Next.js.


Создание нового проекта Next.js

Вы можете создать новый проект Next.js из шаблона, используя CLI-команда «Создать следующее приложение».

npx create-next-app@latest
Войти в полноэкранный режим

Выйти из полноэкранного режима

Команда запросит у вас имя проекта и установит любые зависимости.


Добавление вспомогательных функций Neo4j

Чтобы установить драйвер Neo4j JavaScript, сначала установите зависимость:

npm install --save neo4j-driver
# or yarn add neo4j-driver
Войти в полноэкранный режим

Выйти из полноэкранного режима

Next.js поставляется с встроенная поддержка переменных средыпоэтому мы можем просто скопировать файл учетных данных, загруженный из консоли Neo4j Aura выше, переименовать его в .env и поместить в корень каталога.

Затем мы можем получить доступ к этим переменным через process.env переменная:

const { NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD } = process.env
Войти в полноэкранный режим

Выйти из полноэкранного режима

Затем создайте новую папку с именем lib/ а затем создать новый neo4j.js файл. Вы захотите импортировать neo4j объект из neo4j-driver зависимость и используйте указанные выше учетные данные для создания экземпляра драйвера

// lib/neo4j.js
const driver = neo4j.driver(
  process.env.NEO4J_URI,
  neo4j.auth.basic(
    process.env.NEO4J_USERNAME,
    process.env.NEO4J_PASSWORD
  )
)
Войти в полноэкранный режим

Выйти из полноэкранного режима

При выполнении оператора Cypher для экземпляра Neo4j вам необходимо открыть сеанс и выполнить оператор в транзакции чтения или записи. Через некоторое время это может стать немного громоздким, поэтому вместо этого я рекомендую писать вспомогательные функции для запросов на чтение и запись:

// 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()
  }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Если вы хотите глубже погрузиться в этот код или лучшие практики, я рекомендую вам ознакомиться с курсом Neo4j & Node.js на GraphAcademy.

Теперь, когда у нас есть способ запросить Neo4j, давайте посмотрим на параметры выборки данных в Next.js.


Получение данных в Next.js

Next.js позволяет отображать контент несколькими способами.

  1. Генерация статического сайта (SSG) — когда статические HTML-страницы генерируются в строить время
  2. Рендеринг на стороне сервера (SSR) — HTML генерируется на стороне сервера по мере поступления запроса.
  3. Рендеринг на стороне клиента (CSR) — HTTP-запросы выполняются в браузере с помощью JavaScript, а ответ обновляет DOM.

В зависимости от вашего варианта использования вам может понадобиться сочетание этих методов. Допустим, у вас есть сайт с рекомендациями по фильмам. Возможно, имеет смысл использовать SSG для предварительного создания маркетинговых страниц. Информация о фильмах хранится в базе данных и регулярно меняется, поэтому эти страницы должны обрабатываться сервером с использованием SSR. Когда пользователь приходит, чтобы оценить фильм, взаимодействие должно происходить через запрос API, а результат должен отображаться с использованием CSR.

Давайте посмотрим на реализацию каждой из этих записей.


Генерация статической страницы

Допустим, например, что общие жанровые страницы не будут часто меняться и не требуют взаимодействия с пользователем. Генерируя статические страницы, мы можем обслуживать кешированные версии страниц и снимать нагрузку с сервера.

Любой компонент в pages/ каталог, который экспортирует getStaticProps() функция (известная как страница) будет сгенерирована во время сборки и будет использоваться как статический файл.

Компоненты, созданные в папке pages, будут автоматически сопоставлены с маршрутом. Чтобы создать страницу, которая будет доступна на /genres вам нужно будет создать pages/genres/index.jsx файл. Компонент должен экспортировать default функция, которая возвращает компонент JSX, и getStaticProps() функция.

Во-первых, чтобы получить данные, необходимые компоненту, создайте getStaticProps() функционировать и выполнять это заявление Сайфера в читать сделка.

// 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 команда, вы увидите genres.html файл внутри .next/server/pages/ каталог.


Использование Neo4j для рендеринга на стороне сервера

Список фильмов на странице каждого жанра может часто меняться, или вы можете добавить на страницу дополнительное взаимодействие. В таком случае имеет смысл отрендерить эту страницу на сервере. По умолчанию Next.js кэширует эту страницу на короткое время, что идеально подходит для веб-сайтов с большим объемом трафика.

Каждая ссылка на жанр на предыдущей странице ведет на /genres/[name] — Например /genres/Action. Создав pages/genres/[name].jsx файла, Next.js автоматически прослушивает запросы по любому URL-адресу, начинающемуся с /genres/ и обнаруживать что-либо после косой черты как 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() функция. Я также пытаюсь получить ?page= параметр запроса из URL-адреса, чтобы предоставить разбитый на страницы список фильмов.

Эти значения снова будут переданы в качестве реквизита в дефолт функция, и поэтому может использоваться для списка фильмов и ссылок на страницы.

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 объемом в КБ для отображения верхнего и нижнего колонтитула означает дополнительную нагрузку на сервер и больше данных, отправляемых по сети.

Вместо этого вы можете создать компонент React, который будет асинхронно загружать список фильмов через HTTP-запрос на стороне клиента. Это означало бы, что список фильмов можно было бы обновить без перезагрузки всей страницы, что обеспечило бы конечному пользователю более плавный просмотр.

Чтобы поддержать это, нам нужно будет создать Маршрут API который вернет список фильмов в формате JSON.

Любой файл в pages/api/ directory рассматривается как обработчик маршрута, единственная экспортируемая функция по умолчанию, которая принимает параметры запроса и ответа и ожидает возврата состояния HTTP и ответа.

Таким образом, чтобы создать маршрут API для обслуживания списка фильмов в http://locahost:3000/api/movies/[name]/moviesсоздать новый movies.js файл в pages/api/genres/[name] папка.

// 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.

Быстрый запрос GET на показывает список фильмов:

[
  {
    "tmdbId": "72867",
    "title": "'Hellboy': The Seeds of Creation"
  },
  {
    "tmdbId": "58857",
    "title": "13 Assassins (Jûsan-nin no shikaku)"
  },
  /* ... */
]
Войти в полноэкранный режим

Выйти из полноэкранного режима

Затем этот обработчик API можно вызвать через компонент React в useEffect крюк.

// 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.

Весь код этого эксперимента доступно на Гитхабе.

Если вам интересно узнать больше о Next.js, они собрали курс для разработчиков, чтобы изучить основы.

Если вы хотите узнать больше о Neo4j, я бы порекомендовал взглянуть на курсы Neo4j для начинающих на GraphAcademy. Если вы хотите узнать больше о том, как использовать драйвер Neo4j JavaScript в проекте Node.js или Typescript, я бы также рекомендовал курс «Создание приложений Neo4j с помощью Node.js».

Если у вас есть какие-либо комментарии или вопросы, не стесняйтесь связаться со мной в Твиттере.