Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Инструкции в текущей официальной документации не работают с Cloudflare Pages или Workers. Они также приводят к плохому UX, вызванному ошибкой FOUC (как показано далее). Мне потребовалось более 6 часов, чтобы понять и исправить эти проблемы.
Я пишу исправления ниже, чтобы другим не пришлось проходить через ту же боль (ссылка на пример репозитория внизу этого сообщения).
Если вы следуете инструкциям 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, } ) }
Теперь мы столкнулись со второй проблемой: 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> ) }
Теперь клиент прекрасно рендерит без вспышки.
Тем не менее, остается одна досадная проблема. Если мы проверим страницу, которая возвращается после первого 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
20.08.2023 18:21
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".
20.08.2023 17:46
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
19.08.2023 18:39
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.
19.08.2023 17:22
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!
18.08.2023 20:33
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий их языку и культуре.
14.08.2023 14:49
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.