Remix集成antd和pro-components的過(guò)程示例
正文
讀者如果嘗試過(guò) Remix 那么覺(jué)得 Remix 頁(yè)面與路由用的真的很舒服(簡(jiǎn)單易用結(jié)構(gòu)清晰),但是有一個(gè)問(wèn)題,目前 Remix 項(xiàng)目集成了 antd/pro-components 等 國(guó)內(nèi)UI 組件庫(kù)好模板示例很少,于是想創(chuàng)建一個(gè)集成 Remix 與 antd 生態(tài)的組件庫(kù)模板,能更快的體驗(yàn)并創(chuàng)建具有 antd 生態(tài)的 remix 項(xiàng)目。
閱讀本文需要 React/Remix 基礎(chǔ)知識(shí)和服務(wù)端渲染的相關(guān)知識(shí)
要注意的問(wèn)題
核心要注意的問(wèn)題就是:
問(wèn)題 | 說(shuō)明 |
---|---|
模塊(包) | 兼容性和 peer 依等問(wèn)題 |
ssr | Remix 服務(wù)端渲染支持問(wèn)題 |
兼容性
兼容性主要體現(xiàn)在 React18 和其他的包的兼容性
- 使用腳手架創(chuàng)建的項(xiàng)目默認(rèn)使用 React 18,由此帶來(lái)兼容性問(wèn)題?
- React 18 api 發(fā)生了變化,渲染 api 調(diào)用是否手動(dòng)修改為 React18 的方式?
- npm 的 peer 依賴(lài)安裝與否?
- 其他的依賴(lài)的兼容 React 18 的問(wèn)題?
Remix 服務(wù)端渲染的支持情況
我們知道 Remix 其實(shí)基于 esbuild 很多代碼都跑在服務(wù)端,所以服務(wù)端的渲染的注意點(diǎn)是我們要提前知道:
- antd 支持服務(wù)端渲染
- pro-components 不支持服務(wù)端渲染,一般用于客戶渲染,因?yàn)橹苯邮褂昧?window/document 等客戶端才有的全局對(duì)象
- remix-utils 工具包支持 <ClientOnly>{() => <>You Content</>}</ClientOnly> 使用組件僅僅在客戶端進(jìn)行渲染。
初始化項(xiàng)目安裝必要的包
pnpm dlx create-umi@latest [your_package_name] # remix 選擇默認(rèn)的選項(xiàng)即可 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 單獨(dú)的抽離一個(gè) Document 組件,方便日后修改
- 在 Document 組建中增加 __ANTD__ 方便后期替換 antd 客戶端內(nèi)容
增加客戶端渲染入口文件: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, setTheme 給 SettingContext 使用控制 antd 配置變化,要說(shuō)明的點(diǎn) StyleProvider 是用于 antd 服務(wù)端渲染 配置, 而 ConfigProvider 是 antd 主題配置的提供者。
- 注意:React18 中不能使用 hydrateRoot api 來(lái)進(jìn)行水合。
增加服務(wù)端渲染入口文件:entry.server.tsx
與 客戶端一樣需要 @ant-design/cssinjs 來(lái)配置 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, }); }
客戶端和服務(wù)端的改造中包含了:
markup = markup.replace("__ANTD__", styleText); {typeof document === "undefined" ? "__ANTD__" : ""}
__ANTD__ 在服務(wù)端環(huán)境中替換
創(chuàng)建一個(gè)布局用于承載 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> ); }
注意:布局組件中使用有以下幾個(gè)點(diǎn)需要注意:
- useContext 獲取當(dāng)前的上下文
- ClientOnly 組件用于僅僅在客戶端渲染 Remix 組件
- ProConfigProvider 組件為 SettingDrawer/Outlet 組件提供上下文
- SettingDrawer 給使用當(dāng)前布局 _layout 的組件提供顏色等配置
使用 antd 創(chuàng)建一個(gè)簡(jiǎn)單的基于 _layout._index.tsx 頁(yè)面
// 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 布局的 / 頁(yè)面路由。
使用 pro-component 創(chuàng)建一個(gè)簡(jiǎn)單的基于 _layout._procomponents.tsx 頁(yè)面
// 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 頁(yè)面基本是 / 頁(yè)面使用 pro-components 的改造版本。需要我們注意的是 表單聯(lián)動(dòng) 使用用方式不一樣。
- 項(xiàng)目地址
到目前為止基于 antd 的項(xiàng)目 remix 已經(jīng)探究出一部分,對(duì)應(yīng) remix antd 感興趣可訪問(wèn) create-remix-antd-pro-app 該項(xiàng)目托管在 Github。
小結(jié)
到這里就在 Remix 中就集成了 antd/pro-components 組件庫(kù)就基本結(jié)束了
- 核心還是使用 ClientOnly 在客戶端渲染組件。
- 提供了 SettingDrawer 更換當(dāng)前主題的功能。
- 核心難點(diǎn): 庫(kù)之間的兼容性問(wèn)題的解決方案或者替代方案
以上就是Remix集成antd和pro-components的過(guò)程示例的詳細(xì)內(nèi)容,更多關(guān)于Remix集成antd pro-components的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Remix如何支持原生?CSS方法詳解
- Remix后臺(tái)開(kāi)發(fā)之remix-antd-admin配置過(guò)程
- Remix 后臺(tái)桌面開(kāi)發(fā)electron-remix-antd-admin
- Remix?路由模塊輸出對(duì)象handle函數(shù)
- Remix路由模塊輸出對(duì)象loader函數(shù)詳解
- 一文學(xué)會(huì)使用Remix寫(xiě)API接口
- 通過(guò)示例講解Remix?設(shè)計(jì)哲學(xué)理念
- 歸納總結(jié)Remix?表單常用方法及示例詳解
- Remix中mdx?table不支持表格解決
相關(guān)文章
react 實(shí)現(xiàn)圖片正在加載中 加載完成 加載失敗三個(gè)階段的原理解析
這篇文章主要介紹了react 實(shí)現(xiàn)圖片正在加載中 加載完成 加載失敗三個(gè)階段的,通過(guò)使用loading的圖片來(lái)占位,具體原理解析及實(shí)現(xiàn)代碼跟隨小編一起通過(guò)本文學(xué)習(xí)吧2021-05-05React中style的使用及注意事項(xiàng)(推薦)
React中style的使用和直接在HTML中使用有些不同,第一,React中必須是style="opacity:{this.state.opacity};"這種寫(xiě)法,第二如果設(shè)置多個(gè)style格式如下,多個(gè)style中間使用逗號(hào)分割,這篇文章主要介紹了React中style的使用注意事項(xiàng),需要的朋友可以參考下2023-02-02React Native中的RefreshContorl下拉刷新使用
本篇文章主要介紹了React Native中的RefreshContorl下拉刷新使用,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10使用react+redux實(shí)現(xiàn)彈出框案例
這篇文章主要為大家詳細(xì)介紹了使用react+redux實(shí)現(xiàn)彈出框案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08詳解antd+react項(xiàng)目遷移vite的解決方案
這篇文章主要介紹了詳解antd+react項(xiàng)目遷移vite的解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04react navigation中點(diǎn)擊底部tab怎么傳遞參數(shù)
本文主要介紹了react navigation中點(diǎn)擊底部tab怎么傳遞參數(shù),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04