跳到主要内容

3 篇博文 含有标签「remix」

查看所有标签

重新学习 React Router

· 阅读需 2 分钟
1adybug
子虚伊人

前言

React Router 在推出 Remix 后变成了一个全栈框架,在 V7 版本之后,更是将 React RouterRemix 合并在一起,提供了一个全栈的解决方案。

常见问题

  1. loaderclientLoader 有什么区别?

    • 执行环境

      • loader: 在服务器端执行
      • clientLoader: 在客户端(浏览器)执行
    • 运行时机

      • loader: 在页面初始加载和每次导航时在服务器上运行
      • clientLoader: 在客户端导航时运行,首次页面加载时不执行
    • 数据获取能力

      • loader: 可以访问服务器资源(数据库、文件系统、内部API等)
      • clientLoader: 只能访问浏览器可用的资源(如公共API、localStorage等)
    • 安全性

      • loader: 可以包含敏感逻辑和凭证,因为代码不会发送到客户端
      • clientLoader: 所有代码会发送到浏览器,不应包含敏感信息
    • 用途场景

      • loader: 适用于需要服务器权限或敏感数据的操作
      • clientLoader: 适用于提升客户端导航体验,减轻服务器负担的场景

    这两个函数通常可以配合使用:loader 用于初始数据加载和服务器相关操作,clientLoader 用于优化后续的客户端导航体验。

    loader 将在你首次打开页面就是这个路由时执行,clientLoader 将在你由其他路由导航到这个路由时执行。clientLoader 在首次页面加载时不会执行。在 clientLoader 中,你可以使用 serverLoader 调用 loader

    import type { Route } from "./+types/product"

    // route("products/:pid", "./product.tsx");
    import { fakeDb } from "../db"

    export async function loader({ params }: Route.LoaderArgs) {
    return fakeDb.getProduct(params.pid)
    }

    export async function clientLoader({ serverLoader, params }: Route.ClientLoaderArgs) {
    const res = await fetch(`/api/products/${params.pid}`)
    const serverData = await serverLoader()
    return { ...serverData, ...res.json() }
    }

    export default function Product({ loaderData }: Route.ComponentProps) {
    const { name, description } = loaderData

    return (
    <div>
    <h1>{name}</h1>
    <p>{description}</p>
    </div>
    )
    }

在 SSR 中按需提取 Ant Design CSS

· 阅读需 3 分钟
1adybug
子虚伊人

使用方法

react router(或者 remix)中使用 Ant Design 时,如果不对 css 进行处理,会导致首屏样式丢失的问题。之前介绍过整体导入的解决方案 在 Remix 中使用 Ant Design,但是这种方式会导致打包出来的 css 文件很大。本文介绍一种更优雅的解决方案:在 SSR 中按需提取 Ant Designcss

  1. 需要在项目中暴露 entry.client.tsxentry.server.tsx,如果已经暴露了,可以跳过这一步:

    npx react-router reveal
  2. 安装相应依赖:

    npm i @ant-design/cssinjs @ant-design/static-style-extract
  3. root.tsx 中放入 __ANTD_STYLE_PLACEHOLDER__

    const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined"

    const isDev = process.env.NODE_ENV === "development"

    export const Layout: FC<PropsWithChildren> = ({ children }) => (
    <html lang="zh">
    <head>
    <meta charSet="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <Meta />
    <Links />
    {/** 如果需要在开发环境开启,去除 !isDev */}
    {!isBrowser && !isDev && "__ANTD_STYLE_PLACEHOLDER__"}
    </head>
    <body>
    <ConfigProvider
    theme={{
    token: {
    colorPrimary: "#FF0000",
    },
    }}
    >
    {children}
    </ConfigProvider>
    <ScrollRestoration />
    <Scripts />
    </body>
    </html>
    )
  4. 修改 entry.client.tsx

    import { startTransition, StrictMode } from "react"
    import { hydrateRoot } from "react-dom/client"

    import { legacyLogicalPropertiesTransformer, StyleProvider } from "@ant-design/cssinjs"
    import { HydratedRouter } from "react-router/dom"

    startTransition(() => {
    hydrateRoot(
    document,
    <StrictMode>
    <StyleProvider transformers={[legacyLogicalPropertiesTransformer]} hashPriority="high">
    <HydratedRouter />
    </StyleProvider>
    </StrictMode>,
    )
    })
  5. 修改 entry.server.tsx

    import { type RenderToPipeableStreamOptions, renderToPipeableStream } from "react-dom/server"

    import { PassThrough } from "node:stream"

    import { createCache, extractStyle, StyleProvider } from "@ant-design/cssinjs"
    import { createReadableStreamFromReadable } from "@react-router/node"
    import { isbot } from "isbot"
    import { type AppLoadContext, type EntryContext, ServerRouter } from "react-router"

    const ABORT_DELAY = 5_000

    export default function handleRequest(
    request: Request,
    responseStatusCode: number,
    responseHeaders: Headers,
    routerContext: EntryContext,
    loadContext: AppLoadContext,
    ) {
    return new Promise((resolve, reject) => {
    let shellRendered = false
    const userAgent = request.headers.get("user-agent")

    const fromBot = !!userAgent && isbot(userAgent)

    // Ensure requests from bots and SPA Mode renders wait for all content to load before responding
    // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
    const readyOption: keyof RenderToPipeableStreamOptions = (userAgent && isbot(userAgent)) || routerContext.isSpaMode ? "onAllReady" : "onShellReady"

    const cache = createCache()

    const { pipe, abort } = renderToPipeableStream(
    <StyleProvider cache={cache}>
    <ServerRouter context={routerContext} url={request.url} abortDelay={ABORT_DELAY} />
    </StyleProvider>,
    {
    [readyOption]() {
    shellRendered = true

    const body = new PassThrough({
    transform(chunk, encoding, callback) {
    chunk = String(chunk).replace("__ANTD_STYLE_PLACEHOLDER__", fromBot ? "" : extractStyle(cache))
    callback(null, chunk)
    },
    })

    const stream = createReadableStreamFromReadable(body)

    responseHeaders.set("Content-Type", "text/html")

    resolve(
    new Response(stream, {
    headers: responseHeaders,
    status: responseStatusCode,
    }),
    )

    pipe(body)
    },
    onShellError(error: unknown) {
    reject(error)
    },
    onError(error: unknown) {
    responseStatusCode = 500

    // Log streaming rendering errors from inside the shell. Don't log
    // errors encountered during initial shell rendering since they'll
    // reject and get logged in handleDocumentRequest.
    if (shellRendered) console.error(error)
    },
    },
    )

    setTimeout(abort, ABORT_DELAY)
    })
    }

原理分析

  1. 首先是在服务端的 HTML 代码中插入了 __ANTD_STYLE_PLACEHOLDER__

    !isBrowser && !isDev && "__ANTD_STYLE_PLACEHOLDER__"
  2. 然后是将 antd 的样式抽取为 style 标签

    const cache = createCache()

    renderToPipeableStream(
    <StyleProvider cache={cache}>
    <ServerRouter context={routerContext} url={request.url} abortDelay={ABORT_DELAY} />
    </StyleProvider>,
    )

    const css = extractStyle(cache)
  3. __ANTD_STYLE_PLACEHOLDER__ 替换为抽取的 style 标签

    chunk = String(chunk).replace("__ANTD_STYLE_PLACEHOLDER__", fromBot ? "" : extractStyle(cache))

在 Remix 中使用 Ant Design

· 阅读需 1 分钟
1adybug
子虚伊人

Remix.js 中使用 Ant Design 会出现首次渲染样式丢失的问题,参考 Ant Design 官方的解决方案 整体导出

npm i @ant-design/static-style-extract
npm i cross-env tsx -D
{
"scripts": {
"predev": "tsx ./scripts/genAntdCss.tsx",
"prebuild": "cross-env NODE_ENV=production tsx ./scripts/genAntdCss.tsx"
},
"dependencies": {
"@ant-design/static-style-extract": "^1.0.2"
},
"devDependencies": {
"cross-env": "^7.0.3",
"tsx": "^4.15.6"
}
}
import fs from "fs"

import { extractStyle } from "@ant-design/static-style-extract"
import { ConfigProvider } from "antd"

const outputPath = "./app/antd.min.css"

const css = extractStyle(node => <ConfigProvider theme={{ token: { colorPrimary: "red" } }}>{node}</ConfigProvider>)

fs.writeFileSync(outputPath, css)
import "./antd.min.css"

如果有自定义主题的需求,只需要传递给 ConfigProvider 相应的配置即可:

const css = extractStyle(node => <ConfigProvider theme={{ token: { colorPrimary: "red" } }}>{node}</ConfigProvider>)
注意

这种办法只能是妥协之计,打包出来的 css 文件很大。具体的优化还需要官方实现