乐趣区

关于react.js:Remix-集成-antd-和-procomponents

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

浏览本文须要 React/Remix 基础知识和服务端渲染的相干常识

要留神的问题

外围要留神的问题就是:

问题 阐明
模块(包) 兼容性和 peer 依等问题
ssr Remix 服务端渲染反对问题

兼容性

兼容性次要体现在 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 更换以后主题的性能。
  • 外围难点: 库之间的兼容性问题的解决方案或者代替计划
退出移动版