React 应用架构实战 0x2:构建和文档化组件

2023-05-17 20:58:27 浏览数 (1)

在 React 中,所有的东西都是组件。这种范式允许我们将用户界面拆分成更小的部分,从而更容易开发应用程序。它还启用了组件的可重用性,因为我们可以在多个地方重复使用相同的组件。

# Chakra UI

当我们为应用程序构建 UI 时,必须决定使用什么来为组件设置样式。此外,我们还必须考虑是从零实现所有组件还是使用带有预制组件的组件库。

使用组件库的优点是它可以提高我们的开发效率,如按钮、对话框和选项卡。此外,某些库默认具有很好的可访问性,因此我们不必像从头开始构建一切那样考虑太多。当然,这些库可能会带来成本,如难以自定义或对最终包大小产生显著影响,但它们可以为我们节省大量开发时间。

在这个实战系列中,我们将使用 Chakra UI,这是一个基于 emotionstyled-system 的组件库。

# 安装及配置

安装:

代码语言:javascript复制
pnpm add @chakra-ui/react @emotion/react @emotion/styled framer-motion

为了使用 Chakra UI,首先我们需要配置它的主题 provider 来启用其组件的样式。

由于我们所有的 providerwrapper 都定义在 src/providers/app.tsx 中,因此我们可以在其中添加 ChakraProvider:

代码语言:javascript复制
import { ReactNode } from "react";
import { ChakraProvider, GlobalStyle } from "@chakra-ui/react";

import { theme } from "@/config/theme";

type AppProviderProps = {
  children: ReactNode;
};

export const AppProvider = ({ children }: AppProviderProps) => {
  return (
    <ChakraProvider theme={theme}>
      <GlobalStyle />
      {children}
    </ChakraProvider>
  );
};

Chakra UI 的设置和组件非常可定制化,并且可以在自定义主题中进行配置,我们可以将其传递给 provider ,它将覆盖默认的主题配置。

src/config/theme.ts 是我们的主题配置文件:

代码语言:javascript复制
import { extendTheme } from "@chakra-ui/react";

const colors = {
  primary: "#1a365d",
  primaryAccent: "#ffffff",
};

const styles = {
  global: {
    "html, body": {
      height: "100%",
      bg: "gray.50",
    },
    "#__next": {
      height: "100%",
      bg: "gray.50",
    },
  },
};

export const theme = extendTheme({
  colors,
  styles,
});

定义了一些全局样式,这些样式将通过 GlobalStyles 组件注入,我们已经在 AppProvider 中添加了该组件。还定义了我们希望在组件中使用的主题颜色。然后,使用 extendTheme 将这些配置与默认主题值组合在一起,它将合并所有配置并为我们提供完整的主题对象。

集中主题配置非常有用,因为如果应用程序的品牌发生变化,它很容易使用和更改。例如,我们可以轻松地在一个地方更改主色值,并将其应用于整个应用程序,而无需进行任何其他更改。

# 创建组件

试着创建一些常见的组件:

src/pages/index.tsx:

代码语言:javascript复制
import { Button } from "@/components/button";
import { InputField } from "@/components/form";
import { Link } from "@/components/link";

const LandingPage = () => {
  return (
    <>
      <Button>Click me</Button>
      <br />
      <InputField label="name" />
      <br />
      <Link href="/">Home</Link>
    </>
  );
};

export default LandingPage;

# Button

src/components/button/button.tsx:

代码语言:javascript复制
import React, { MouseEventHandler, ReactNode } from "react";
import { Button as ChakraButton } from "@chakra-ui/react";

const variants = {
  solid: {
    variant: "solid",
    bg: "primary",
    color: "primaryAccent",
    _hover: {
      opacity: "0.8",
    },
  },
  outline: {
    variant: "outline",
    bg: "white",
    color: "primary",
  },
};

export type ButtonProps = {
  children: ReactNode;
  type?: "button" | "submit" | "reset";
  variant?: keyof typeof variants;
  isLoading?: boolean;
  isDisabled?: boolean;
  onClick?: MouseEventHandler<HTMLButtonElement>;
  icon?: JSX.Element;
};

export const Button = ({
  variant = "solid",
  type = "button",
  children,
  icon,
  ...props
}: ButtonProps) => {
  return (
    <ChakraButton {...props} {...variants[variant]} type={type} leftIcon={icon}>
      {children}
    </ChakraButton>
  );
};

src/components/button/index.ts:

代码语言:javascript复制
export * from "./button";

# InputField

src/components/form/input-field.tsx:

代码语言:javascript复制
import React from "react";

import {
  FormControl,
  FormHelperText,
  FormLabel,
  forwardRef,
  Input,
  Textarea,
} from "@chakra-ui/react";
import { FieldError, UseFormRegister } from "react-hook-form";

export type InputFieldProps = {
  type?: "text" | "email" | "password" | "textarea";
  label?: string;
  error?: FieldError;
} & Partial<ReturnType<UseFormRegister<Record<string, unknown>>>>;

export const InputField = forwardRef((props: InputFieldProps, ref) => {
  const { type = "text", label, error, ...rest } = props;

  return (
    <FormControl>
      {label && <FormLabel>{label}</FormLabel>}
      {type === "textarea" ? (
        <Textarea bg="white" rows={8} {...rest} ref={ref} />
      ) : (
        <Input bg="white" type={type} {...rest} ref={ref} />
      )}
      {error && <FormHelperText color="red">{error.message}</FormHelperText>}
    </FormControl>
  );
});

src/components/form/index.ts:

代码语言:javascript复制
export * from "./input-field";

# Link

src/components/link/link.tsx:

代码语言:javascript复制
import React, { ReactNode } from "react";
import NextLink from "next/link";
import { Button } from "@chakra-ui/react";

const variants = {
  link: {
    variant: "link",
    color: "primary",
  },
  solid: {
    variant: "solid",
    bg: "primary",
    color: "primaryAccent",
    _hover: {
      opacity: "0.8",
    },
  },
  outline: {
    variant: "outline",
    bg: "white",
    color: "primary",
  },
};

export type LinkProps = {
  variant?: keyof typeof variants;
  children: ReactNode;
  href: string;
  icon?: JSX.Element;
  shallow?: boolean;
};

export const Link = ({ href, children, variant = "link", icon, shallow = false }: LinkProps) => {
  return (
    <NextLink shallow={shallow} href={href} passHref>
      <Button leftIcon={icon} as={undefined} {...variants[variant]}>
        {children}
      </Button>
    </NextLink>
  );
};

src/components/link/index.ts:

代码语言:javascript复制
export * from "./link";

# 使用 Storybook

Storybook 是一个允许我们在隔离环境下开发和测试 UI 组件的工具。可以将其视为制作所有组件目录的工具,它非常适合用于记录组件。

使用 Storybook 的一些好处:

  • Storybook 允许在隔离环境中开发组件,而无需重现应用程序的精确状态,从而使开发人员可以专注于他们正在构建的东西
  • Storybook 作为 UI 组件的目录,允许所有相关人员在不在应用程序中使用组件的情况下试用它们

下面命令将安装 Storybook 相关依赖,并初始化 Storybook 配置:

代码语言:javascript复制
pnpx sb init

# 配置 Storybook

第一个文件包含了主要的配置,它控制了 Storybook 服务的行为以及如何处理我们的 stories。

src/.storybook/main.ts:

代码语言:javascript复制
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
  stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
  ],
  framework: {
    name: "@storybook/nextjs",
    options: {},
  },
  docs: {
    autodocs: "tag",
  },
};
export default config;

第二个文件包含了我们的预览配置,它控制了我们的 stories 在 Storybook 中的展示方式。

src/.storybook/preview.ts:

代码语言:javascript复制
import type { Preview } from "@storybook/react";
import { theme } from "../src/config/theme";

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: "^on[A-Z].*" },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
    chakra: {
      theme,
    },
  },
};

export default preview;

可以使用 pnpm run storybook 命令启动 Storybook 服务。

可以使用 pnpm run build-storybook 命令构建 Storybook。

# 文档化组件

src 文件夹中以 .stories.tsx 结尾的任何文件都会被 Storybook 筛选出来并作为 story 处理。

因此,我们将把 story 与组件一起放置在同一个文件夹中,那么每个组件的结构将如下所示:

代码语言:javascript复制
src
├── components
│   ├── button
│   │   ├── button.stories.tsx
│   │   ├── button.tsx
│   │   └── index.ts

我们将基于 Component Story Format(CSF) 创建我们的 story,这是一种编写组件示例的开放标准。

CSF 需要以下内容:

  • 默认导出应定义有关组件的元数据,包括组件本身、组件名称、修饰符和参数
  • 命名导出应定义所有 story

# 创建 Story

src/components/button/button.stories.tsx:

代码语言:javascript复制
import { PlusSquareIcon } from "@chakra-ui/icons";
import { Meta, Story } from "@storybook/react";

import { Button, ButtonProps } from "./button";

const meta: Meta = {
  title: "Components/Button",
  component: Button,
};

export default meta;

const Template: Story<ButtonProps> = (args) => <Button {...args} />;

export const Default = Template.bind({});

Default.args = {
  children: "Button",
};

export const WithIcon = Template.bind({});

WithIcon.args = {
  children: "Button",
  icon: <PlusSquareIcon />,
};

通过 pnpm run storybook 命令启动 Storybook 服务,可以看到如下效果:

0 人点赞