Как заставить Remix работать с Mantine и Cloudflare Pages/Workers

RedDeveloper
29.04.2023 14:16
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers

Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.

Инструкции в текущей официальной документации не работают с Cloudflare Pages или Workers. Они также приводят к плохому UX, вызванному ошибкой FOUC (как показано далее). Мне потребовалось более 6 часов, чтобы понять и исправить эти проблемы.

Я пишу исправления ниже, чтобы другим не пришлось проходить через ту же боль (ссылка на пример репозитория внизу этого сообщения).

Проблема 1: Заставить Cloudflare работать

Если вы следуете инструкциям Usage with Remix в официальной документации и пытаетесь запустить сервер, вы получаете эту ошибку:

Это происходит потому, что createStylesServer в @mantine/remix использует @emotion/server под капотом в качестве поставщика стилей. По состоянию на 29 апреля 2023 года эта реализация полагается на функции узла, к которым у работников Cloudflare нет доступа.

Оказалось, что функции node не требуются injectStyles, поэтому исправление здесь заключается в том, чтобы переписать createStylesServer без них:

// TL;DR: Mantine doesnt work with cloudflare workers because @emotion/server
// doesn't. Below is a polyfill without the node bits to get it working
//
// from this github issue: https://github.com/emotion-js/emotion/issues/2446#issuecomment-1372440174

import createCache from "@emotion/cache"

function createExtractCriticalToChunks(cache) {
  return function (html) {
    const RGX = new RegExp(`${cache.key}-([a-zA-Z0-9-_]+)`, "gm")

    const o = { html, styles: [] }
    let match
    const ids = {}
    while ((match = RGX.exec(html)) !== null) {
      if (ids[match[1]] === undefined) {
        ids[match[1]] = true
      }
    }

    const regularCssIds = []
    let regularCss=""

    Object.keys(cache.inserted).forEach((id) => {
      if (
        (ids[id] !== undefined || cache.registered[`${cache.key}-${id}`] === undefined) &&
        cache.inserted[id] !== true
      ) {
        if (cache.registered[`${cache.key}-${id}`]) {
          regularCssIds.push(id)
          regularCss += cache.inserted[id]
        } else {
          o.styles.push({
            key: `${cache.key}-global`,
            ids: [id],
            css: cache.inserted[id],
          })
        }
      }
    })

    o.styles.push({ key: cache.key, ids: regularCssIds, css: regularCss })

    return o
  }
}

function generateStyleTag(cssKey, ids, styles, nonceString) {
  return `<style data-emotion="${cssKey} ${ids}"${nonceString}>${styles}</style>`
}

function createConstructStyleTagsFromChunks(cache, nonceString) {
  return function (criticalData) {
    let styleTagsString=""

    criticalData.styles.forEach((item) => {
      styleTagsString += generateStyleTag(item.key, item.ids.join(" "), item.css, nonceString)
    })

    return styleTagsString
  }
}

function createEmotionServer(cache) {
  if (cache.compat !== true) {
    cache.compat = true
  }
  const nonceString = cache.nonce !== undefined ? ` nonce="${cache.nonce}"` : ""
  return {
    extractCriticalToChunks: createExtractCriticalToChunks(cache),
    constructStyleTagsFromChunks: createConstructStyleTagsFromChunks(cache, nonceString),
  }
}

Затем мы можем написать собственную версию createStylesServer :

// mantine-polyfill.js

// this is important for later
export const cache = createCache({key: "mantine"})
// own version of mantine's createStylesServer with same functionality but
// using the above createEmotionServer
export function createStylesServer() {
  return createEmotionServer(cache)
}

Это исправляет ошибку, и страница теперь отображается:

// entry.server.tsx
import { renderToString } from "react-dom/server"
import { RemixServer } from "@remix-run/react"
import type { EntryContext } from "@remix-run/cloudflare"
import { injectStyles } from "@mantine/remix"
// file we created above
import { createStylesServer } from "./mantine-polyfill"

const server = createStylesServer()

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  let markup = renderToString(<RemixServer context = {remixContext} url = {request.url} />)
  responseHeaders.set("Content-Type", "text/html")

  // we expect an error below because our server doesn't have the node-related
  // methods we removed (which are expected by injectStyles)
  return new Response(
    `<!DOCTYPE html>${injectStyles(
      markup,
      // @ts-expect-error
      server
    )}`,
    {
      status: responseStatusCode,
      headers: responseHeaders,
    }
  )
}

Проблема 2: Неприятный пользовательский опыт

Теперь мы столкнулись со второй проблемой: FOUC (вспышка нестилизованного содержимого), когда клиент повторно увлажняется сервером. Это приводит к нестабильной работе пользователя:

Вспышка нестилизованного содержимого
Вспышка нестилизованного содержимого

Это происходит потому, что теги <style> из рендеринга на стороне сервера не совпадают с рендерингом на стороне клиента. Поэтому, когда происходит повторное увлажнение, клиенту приходится снова загружать стили.

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

// entry.client.tsx

import { ClientProvider } from "@mantine/remix"
import { RemixBrowser } from "@remix-run/react"
import { startTransition, StrictMode } from "react"
import { hydrateRoot } from "react-dom/client"
import { cache } from "./mantine-polyfill"

// we pass in the cache to ClientProvider
function hydrate() {
  startTransition(() => {
    hydrateRoot(
      document,
      <StrictMode>
        <ClientProvider emotionCache = {cache}>
          <RemixBrowser />
        </ClientProvider>
      </StrictMode>
    )
  })
}

if (typeof requestIdleCallback === "function") {
  requestIdleCallback(hydrate)
} else {
  // Safari doesn't support requestIdleCallback
  // https://caniuse.com/requestidlecallback
  setTimeout(hydrate, 1)
}
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react"
import { MantineProvider, Navbar, AppShell, createEmotionCache } from "@mantine/core"
import { StylesPlaceholder } from "@mantine/remix"
import { IconBook, IconHome } from "@tabler/icons-react"
import type { LinksGroupProps } from "./components/navbar-links-group"
import { LinksGroup } from "./components/navbar-links-group"
import { Notifications } from "@mantine/notifications"
import { cache } from "./mantine-polyfill"

// ... other code

export default function App() {
// we pass in the same cache to MantineProvider using the emotionCache prop
  return (
    <html lang="en">
      <head>
        <StylesPlaceholder />
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <MantineProvider withGlobalStyles withNormalizeCSS emotionCache = {cache}>
          <Notifications position="top-right" />
          <AppShell padding="md" navbar = {<AppNavbar />}>
            <Outlet />
          </AppShell>
          <ScrollRestoration />
          <Scripts />
          <LiveReload />
        </MantineProvider>
      </body>
    </html>
  )
}

Теперь клиент прекрасно рендерит без вспышки.

Проблема 3: Стили отображаются в теле страницы

Тем не менее, остается одна досадная проблема. Если мы проверим страницу, которая возвращается после первого GET на сервер, то увидим следующее:

Глобальные стили отображаются внутри тела
Глобальные стили отображаются внутри тела

Мы видим, что глобальные стили (включенные с помощью параметра withGlobalStyles в MantineProvider) отображаются внутри <body> вместо <head>. Это заняло у меня много времени, чтобы понять - это сводится к тому, что мы не рендерим Emotion CacheProvider при первом рендере сервера.

Мы исправили это, отредактировав наш entry.server.tsx следующим образом:

import { renderToString } from "react-dom/server"
import { RemixServer } from "@remix-run/react"
import type { EntryContext } from "@remix-run/cloudflare"
import { injectStyles } from "@mantine/remix"
// add import for the cache from earlier
import { createStylesServer, cache } from "./mantine-polyfill"
// import CacheProvider from emotion
import { CacheProvider } from "@emotion/react"

const server = createStylesServer()

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  // Wrap the server in the cache provider and set the cache
  let markup = renderToString(
    <CacheProvider value = {cache}>
      <RemixServer context = {remixContext} url = {request.url} />
    </CacheProvider>
  )
  responseHeaders.set("Content-Type", "text/html")

  return new Response(
    `<!DOCTYPE html>${injectStyles(
      markup,
      // @ts-expect-error
      server
    )}`,
    {
      status: responseStatusCode,
      headers: responseHeaders,
    }
  )
}

Теперь глобальные стили отображаются в <голове>, как мы и хотели:

Глобальные стили теперь отображаются в <голове>
Глобальные стили теперь отображаются в <голове>

Успех! Теперь у нас есть плавный рендеринг и супербыстрая производительность Remix, как мы и ожидали:

Почти мгновенная перезагрузка без вспышек или смещения контента.
Почти мгновенная перезагрузка без вспышек или смещения контента.

Точный код можно найти в этом репо: https://github.com/nastynaz/mantine-remix-cloudflare

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?

20.08.2023 18:21

Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией

20.08.2023 17:46

В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.

Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox

19.08.2023 18:39

Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.

Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest

19.08.2023 17:22

В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!

Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️

18.08.2023 20:33

Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий их языку и культуре.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL

14.08.2023 14:49

Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.