Remix 集成 antd 和 pro-components

读者如果尝试过 Remix 那么觉得 Remix 页面与路由用的真的很舒服(简单易用结构清晰),但是有一个问题,目前 Remix 项目集成了 antd/pro-components 等 国内UI 组件库好模板示例很少,于是想创建一个集成 Remix 与 antd 生态的组件库模板,能更快的体验并创建具有 antd 生态的 remix 项目。

阅读本文需要 React/Remix 基础知识和服务端渲染的相关知识

要注意的问题

核心要注意的问题就是:

问题说明
模块(包)兼容性和 peer 依等问题
ssrRemix 服务端渲染支持问题

兼容性

兼容性主要体现在 React18 和其他的包的兼容性

  • 使用脚手架创建的项目默认使用 React 18,由此带来兼容性问题?
  • React 18 api 发生了变化,渲染 api 调用是否手动修改为 React18 的方式?
  • npm 的 peer 依赖安装与否?
  • 其他的依赖的兼容 React 18 的问题?

Remix 服务端渲染的支持情况

我们知道 Remix 其实基于 esbuild 很多代码都跑在服务端,所以服务端的渲染的注意点是我们要提前知道:

  • antd 支持服务端渲染
  • pro-components 不支持服务端渲染,一般用于客户渲染,因为直接使用了 window/document 等客户端才有的全局对象
  • remix-utils 工具包支持 <ClientOnly>{() => <>You Content</>}</ClientOnly> 使用组件仅仅在客户端进行渲染。

初始化项目安装必要的包

pnpm dlx create-umi@latest [your_package_name]
# remix 选择默认的选项即可
pnpm install remix-utils antd @ant-design/pro-components @ant-design/cssinjs @ant-design/icons

使用新特性 v2 版本的文件路由模式

  • remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
 future: {
 v2_routeConvention: true,
 },
 ignoredRouteFiles: ["**/.*"],
};

添加 pro-components SettingDrawer 组件上下文

import { createContext } from "react";
const SettingContext = createContext({
 theme: {},
 setTheme: (theme: any) => {}
});
export default SettingContext;

全局配置放在 SettingContext 上下文中,需要修改和使用都基于此上下文。

root 文件修改

// type
import type { MetaFunction } from "@remix-run/node";
// core
import {
 Links,
 LiveReload,
 Meta,
 Outlet,
 Scripts,
 ScrollRestoration,
} from "@remix-run/react";
export const meta: MetaFunction = () => ({
 charset: "utf-8",
 title: "New Remix App",
 viewport: "width=device-width,initial-scale=1",
});
function Document({
 children,
 title = "App title",
}: {
 children: React.ReactNode;
 title?: string;
}) {
 return (
 <html lang="en">
 <head>
 <Meta />
 <title>{title}</title>
 <Links />
 {typeof document === "undefined" ? "__ANTD__" : ""}
 </head>
 <body>
 {children}
 <ScrollRestoration />
 <Scripts />
 <LiveReload />
 </body>
 </html>
 );
}
export default function App() {
 return (
 <Document>
 <Outlet />
 </Document>
 );
}
  • 将 html 单独的抽离一个 Document 组件,方便日后修改
  • 在 Document 组建中增加 __ANTD__ 方便后期替换 antd 客户端内容

增加客户端渲染入口文件:entry.client.tsx

客户端主要配合: @ant-design/cssinjs

// cores
import { startTransition, useState } from "react";
import { hydrateRoot } from "react-dom/client";
import { RemixBrowser } from "@remix-run/react";
// components and others
import { createCache, StyleProvider } from "@ant-design/cssinjs";
import { ConfigProvider } from "antd";
// context
import SettingContext from "./settingContext";
const hydrate = () => {
 startTransition(() => {
 const cache = createCache();
 function MainApp() {
 const [theme, setTheme] = useState({
 colorPrimary: "#00b96b"
 });
 return (
 <SettingContext.Provider value={{ theme, setTheme }}>
 <StyleProvider cache={cache}>
 <ConfigProvider
 theme={{
 token: {
 colorPrimary: theme.colorPrimary,
 },
 }}
 >
 <RemixBrowser />
 </ConfigProvider>
 </StyleProvider>
 </SettingContext.Provider>
 );
 }
 hydrateRoot(document, <MainApp />);
 });
};
if (typeof requestIdleCallback === "function") {
 requestIdleCallback(hydrate);
} else {
 // Safari doesn't support requestIdleCallback
 // https://caniuse.com/requestidlecallback
 setTimeout(hydrate, 1);
}

定义 theme, setThemeSettingContext 使用控制 antd 配置变化,要说明的点 StyleProvider 是用于 antd 服务端渲染 配置, 而 ConfigProvider 是 antd 主题配置的提供者。

  • 注意:React18 中不能使用 hydrateRoot api 来进行水合

增加服务端渲染入口文件:entry.server.tsx

与 客户端一样需要 @ant-design/cssinjs 来配置 antd 的样式。

// types
import type { EntryContext } from "@remix-run/node";
// core
import { useState } from "react";
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";
// components
import { ConfigProvider } from "antd";
import { createCache, extractStyle, StyleProvider } from "@ant-design/cssinjs";
// context
import SettingContext from "./settingContext";
export default function handleRequest(
 request: Request,
 responseStatusCode: number,
 responseHeaders: Headers,
 remixContext: EntryContext
) {
 const cache = createCache();
 function MainApp() {
 const [theme, setTheme] = useState({
 colorPrimary: "#00b96b"
 });
 return (
 <SettingContext.Provider value={{ theme, setTheme }}>
 <StyleProvider cache={cache}>
 <ConfigProvider
 theme={{
 token: {
 colorPrimary: theme.colorPrimary,
 },
 }}
 >
 <RemixServer context={remixContext} url={request.url} />
 </ConfigProvider>
 </StyleProvider>
 </SettingContext.Provider>
 );
 }
 let markup = renderToString(<MainApp />);
 const styleText = extractStyle(cache);
 markup = markup.replace("__ANTD__", styleText);
 responseHeaders.set("Content-Type", "text/html");
 return new Response("<!DOCTYPE html>" + markup, {
 status: responseStatusCode,
 headers: responseHeaders,
 });
}

客户端和服务端的改造中包含了:

markup = markup.replace("__ANTD__", styleText);
{typeof document === "undefined" ? "__ANTD__" : ""}

__ANTD__ 在服务端环境中替换

创建一个布局用于承载 pro-components 组件

  • /routes/_layout.tsx

// core
import { useContext } from "react";
import { Outlet } from "@remix-run/react";
// components
import { ClientOnly } from "remix-utils";
import { ProConfigProvider, SettingDrawer } from "@ant-design/pro-components";
// context
import SettingContext from "~/settingContext";
export default function Layout() {
 const value = useContext(SettingContext);
 return (
 <ClientOnly fallback={<div>Loading...</div>}>
 {() => (
 <ProConfigProvider>
 <Outlet />
 
 <SettingDrawer
 getContainer={() => document.body}
 enableDarkTheme
 onSettingChange={(settings: any) => {
 value?.setTheme(settings);
 }}
 settings={{ ...value.theme }}
 themeOnly
 />
 </ProConfigProvider>
 )}
 </ClientOnly>
 );
}

注意:布局组件中使用有以下几个点需要注意:

  • useContext 获取当前的上下文
  • ClientOnly 组件用于仅仅在客户端渲染 Remix 组件
  • ProConfigProvider 组件为 SettingDrawer/Outlet 组件提供上下文
  • SettingDrawer 给使用当前布局 _layout 的组件提供颜色等配置

使用 antd 创建一个简单的基于 _layout._index.tsx 页面

// core
import { json } from "@remix-run/node";
import { useFetcher } from "@remix-run/react";
// components
import { Button, Form, Input, Select } from "antd";
export async function action() {
 return json({
 title: 1,
 });
}
const { Option } = Select;
const layout = {
 labelCol: { span: 8 },
 wrapperCol: { span: 16 },
};
const tailLayout = {
 wrapperCol: { offset: 8, span: 16 },
};
export default function Index() {
 const fetcher = useFetcher();
 const [form] = Form.useForm();
 const onGenderChange = (value: string) => {
 switch (value) {
 case "male":
 form.setFieldsValue({ note: "Hi, man!" });
 break;
 case "female":
 form.setFieldsValue({ note: "Hi, lady!" });
 break;
 case "other":
 form.setFieldsValue({ note: "Hi there!" });
 break;
 default:
 }
 };
 const onFinish = (value: any) => {
 const formData = new FormData();
 formData.append("username", value.username);
 formData.append("password", value.password);
 fetcher.submit(formData, { method: "post" });
 };
 const onReset = () => {
 form.resetFields();
 };
 const onFill = () => {
 form.setFieldsValue({ note: "Hello world!", gender: "male" });
 };
 return (
 <div>
 <Form
 {...layout}
 form={form}
 name="control-hooks"
 onFinish={onFinish}
 style={{ maxWidth: 600 }}
 >
 <Form.Item name="note" label="Note" rules={[{ required: true }]}>
 <Input />
 </Form.Item>
 <Form.Item name="gender" label="Gender" rules={[{ required: true }]}>
 <Select
 placeholder="Select a option and change input text above"
 onChange={onGenderChange}
 allowClear
 >
 <Option value="male">male</Option>
 <Option value="female">female</Option>
 <Option value="other">other</Option>
 </Select>
 </Form.Item>
 <Form.Item
 noStyle
 shouldUpdate={(prevValues, currentValues) =>
 prevValues.gender !== currentValues.gender
 }
 >
 {({ getFieldValue }) =>
 getFieldValue("gender") === "other" ? (
 <Form.Item
 name="customizeGender"
 label="Customize Gender"
 rules={[{ required: true }]}
 >
 <Input />
 </Form.Item>
 ) : null
 }
 </Form.Item>
 <Form.Item {...tailLayout}>
 <Button type="primary" htmlType="submit">
 Submit
 </Button>
 <Button htmlType="button" onClick={onReset}>
 Reset
 </Button>
 <Button type="link" htmlType="button" onClick={onFill}>
 Fill form
 </Button>
 </Form.Item>
 </Form>
 </div>
 );
}

_layout._index.tsx 表示使用:_layout 布局的 / 页面路由。

使用 pro-component 创建一个简单的基于 _layout._procomponents.tsx 页面

// core
import { json } from "@remix-run/node";
import { useFetcher } from "@remix-run/react";
// components
import { Button, Form, Space } from "antd";
import {
 ProForm,
 ProFormDependency,
 ProFormSelect,
 ProFormText,
} from "@ant-design/pro-components";
export async function action() {
 return json({
 title: 1,
 });
}
const layout = {
 labelCol: { span: 8 },
 wrapperCol: { span: 16 },
};
const tailLayout = {
 wrapperCol: { offset: 8, span: 16 },
};
export default function Index() {
 const fetcher = useFetcher();
 const [form] = Form.useForm();
 const onGenderChange = (value: string) => {
 switch (value) {
 case "male":
 form.setFieldsValue({ note: "Hi, man!" });
 break;
 case "female":
 form.setFieldsValue({ note: "Hi, lady!" });
 break;
 case "other":
 form.setFieldsValue({ note: "Hi there!" });
 break;
 default:
 }
 };
 const onFinish = (value: any) => {
 const formData = new FormData();
 formData.append("username", value.username);
 formData.append("password", value.password);
 fetcher.submit(formData, { method: "post" });
 };
 const onReset = () => {
 form.resetFields();
 };
 const onFill = () => {
 form.setFieldsValue({ note: "Hello world!", gender: "male" });
 };
 return (
 <div>
 <Form
 {...layout}
 form={form}
 name="control-hooks"
 onFinish={onFinish}
 style={{ maxWidth: 600 }}
 >
 <ProFormText name="note" label="Note" rules={[{ required: true }]} />
 <ProFormSelect
 name="gender"
 label="Gender"
 rules={[{ required: true }]}
 fieldProps={{
 onChange: onGenderChange
 }}
 options={[
 {
 label: "male",
 value: "male",
 },
 {
 label: "female",
 value: "female",
 },
 {
 label: "other",
 value: "other",
 },
 ]}
 />
 <ProFormDependency name={["gender"]}>
 {({ gender }) => {
 return gender === "other" ? (
 <ProFormText
 noStyle
 name="customizeGender"
 label="Customize Gender"
 rules={[{ required: true }]}
 />
 ) : null;
 }}
 </ProFormDependency>
 <ProForm.Item {...tailLayout}>
 <Space>
 <Button type="primary" htmlType="submit">
 Submit
 </Button>
 <Button htmlType="button" onClick={onReset}>
 Reset
 </Button>
 <Button type="link" htmlType="button" onClick={onFill}>
 Fill form
 </Button>
 </Space>
 </ProForm.Item>
 </Form>
 </div>
 );
}

/procomponents 页面基本是 / 页面使用 pro-components 的改造版本。需要我们注意的是 表单联动 使用用方式不一样。

项目地址

到目前为止基于 antd 的项目 remix 已经探究出一部分,对应 remix antd 感兴趣可访问 create-remix-antd-pro-app 该项目托管在 Github。

小结

到这里就在 Remix 中就集成了 antd/pro-components 组件库就基本结束了

  • 核心还是使用 ClientOnly 在客户端渲染组件。
  • 提供了 SettingDrawer 更换当前主题的功能。
  • 核心难点: 库之间的兼容性问题的解决方案或者替代方案
作者:magnesium原文地址:https://segmentfault.com/a/1190000043579996

%s 个评论

要回复文章请先登录注册