在Chakra UI中构建响应式组件

2,536 阅读11分钟

简介

用户通过不同的设备与网络互动,从手机、笔记本电脑到智能手表和AR/VR头盔。作为开发者,我们必须确保我们创建的网站和网络应用不仅具有良好的性能和功能,而且在所有的屏幕尺寸上都能响应。

Chakra UI是一个简单的、模块化的、易于扩展的组件库,由基本的构建模块组成,使我们能够构建我们的网络应用程序的前端。Chakra UI是可定制的,完全可访问的,可重复使用的,并且易于使用。它还带有有用的钩子,如useColorMode 钩子,我们可以用它来为我们的应用程序添加黑暗模式。总的来说,Chakra UI配备了许多令人难以置信的功能,使它成为工作的正确工具。

在这篇文章中,我们将学习如何用Chakra UI构建响应式组件,并使用这些知识来构建这个仪表盘应用程序

前提条件

本文的重点不是向读者介绍Chakra UI,而是展示如何用这个伟大的工具构建响应式组件。虽然这不是严格的要求,但使用Chakra UI的经验将是有利的。

Chakra UI的响应式网页设计方法

当谈到编写响应式CSS时,开发者可以选择移动优先和桌面优先的方法。Chakra UI采用了移动优先的方法,使用@media(min-width) media query。

Chakra UI中的响应式样式设计依赖于在theme 对象中定义的断点。Chakra UI的theme 对象默认带有以下断点。

 //Breakpoints for responsive design
{
  sm: "30em",
  md: "48em",
  lg: "62em",
  xl: "80em",
  "2xl": "96em",
}

我们可以使用[createBreakpoints](https://chakra-ui.com/docs/features/responsive-styles) 主题工具从默认断点切换到适合我们应用程序规格的断点,就像这样。

import { createBreakpoints } from "@chakra-ui/theme-tools"

const breakpoints = createBreakpoints({
  sm: "320px",
  md: "768px",
  lg: "960px",
  xl: "1200px",
})

虽然我们可以使用createBreakpoints 来创建自定义断点,但它在未来将被弃用。Chakra UI团队建议我们把断点定义为我们创建的自定义主题对象中的一个对象。

import { extendTheme } from "@chakra-ui/react";

const customeTheme = extendTheme({
  colors: {},
  fonts: {},
  fontSizes: {},
  breakpoints: {
    sm: "320px",
    md: "768px",
    lg: "960px",
    xl: "1200px",
  },
});

const theme = extendTheme({ customeTheme });

export default customeTheme;

Chakra UI为创建响应式风格提供了两种语法:数组语法和对象语法。这些语法抽象了编写媒体查询的复杂性,在开发响应式组件时提供了很好的开发者体验。

下面是这些语法的一个例子。

//unresponsive width styles
<Box bg="red.200" w="400px">
  This is a box
</Box>

//responsive width styles using the Array syntax
<Box bg="red.200" w={[300, 400, 500]}>
  This is a box
</Box>

//responsive width style susing the Object syntax
<Box bg="red.200" w={{ base: "300px", md: "400px", lg: "500px" }}>
  This is a box
</Box>

对于数组语法,Box 的宽度转化为。

  • 从0em往上的300px
  • 400px从30em往上
  • 500px,从48em往上

对于对象的语法,Box 的宽度翻译为:

  • "base" 300px from 0em upwards
  • "md "从48em往上
  • "lg "从62em往上

我们可以看到,除了语法上的不同,数组和对象的响应式定义执行相同的功能。

了解useMediaQuery 钩子

[useMediaQuery](https://chakra-ui.com/docs/hooks/use-media-query) 是一个自定义钩子,用于帮助检测单个媒体查询或多个媒体查询是否单独匹配。它根据我们定义的媒体查询返回一个布尔值。

让我们看看useMediaQuery 钩子如何工作。

import { useMediaQuery } from "@chakra-ui/react"

function Home() {
 const [isMobile] = useMediaQuery("(max-width: 768px)") 
 return (
   <Text>
    {isMobile ? "This is a mobile device" : "This is a desktop device"}
   </Text>
  )
}

在上面的代码片段中,我们定义了一个max-width: 768px 媒体查询并访问isMobile 布尔值。接下来,我们根据isMobile 的值,有条件地渲染一些文本。

现在我们了解了如何在Chakra UI中创建响应式样式以及useMediaQuery 是如何工作的,让我们开始构建我们的仪表盘应用程序。

创建仪表盘布局

对于仪表盘,我们将从创建布局开始。仪表盘布局由SidebarHeader 组件组成。

让我们来分解一下布局的功能和组件,如图所示。

import Header from "@components/Header";
import Sidebar from "@components/Sidebar";
import { Box, Drawer, DrawerContent, useDisclosure } from "@chakra-ui/react";

export default function Layout({ children }) {
  const { isOpen, onOpen, onClose } = useDisclosure();
  return (
    <Box minH="100vh" bg="gray.100">
      <Sidebar
        onClose={() => onClose}
        display={{ base: "none", md: "block" }}
      />
      <Drawer
        autoFocus={false}
        isOpen={isOpen}
        placement="left"
        onClose={onClose}
        returnFocusOnClose={false}
        onOverlayClick={onClose}
        size="full"
      >
        <DrawerContent>
          <Sidebar onClose={onClose} />
        </DrawerContent>
      </Drawer>

      {/* Header */}
      <Header onOpen={onOpen} />
      <Box ml={{ base: 0, md: 60 }} p="4">
        {children}
      </Box>
    </Box>
  );
}

演示中可以注意到关于HeaderSidebar 组件的几件事。

首先,有一个汉堡包菜单,出现在移动端的标题上。当汉堡包被点击时,侧边栏会滑入视野。最后,当侧边栏滑入视野时,有一个 "关闭 "图标按钮,当点击它时,侧边栏就会滑回视野之外。

我们可以使用Chakra UI的 useDisclosure钩子。useDisclosure如上面的代码片段所示,我们从isOpenonOpenonClose

接下来,我们将onOpen 函数传递给Header ,这样我们就可以在汉堡包菜单中使用它。汉堡包将只在移动端可见;当我们在文章后面开发Header 组件时,我们将看到这一点。

现在让我们来分解一下Sidebar 组件。我们从上面的代码中看到,Sidebar 被使用了两次。

第一个Sidebar 是桌面侧边栏。我们希望这个侧边栏只在大屏幕设备上显示,所以我们把它在移动设备上的显示设置为 "none",在大屏幕设备上设置为 "block"。我们还将onClose 函数传递给侧边栏,这样我们就可以用它来关闭移动端的侧边栏。

第二个Sidebar 是移动侧边栏。我们使用Chakra UI的[Drawer](https://chakra-ui.com/docs/overlay/drawer) 组件来设置它。侧边栏将是抽屉的一个子项,而抽屉只有在isOpentrueisOpen ,只有在点击页眉的汉堡包时才是真的。

通过这些步骤,我们已经创建了仪表板的布局,并使侧边栏有了响应。

另一种我们可以用来创建响应式侧边栏的方法是利用useMediaQuery 钩子。通过这种方法,我们创建一个桌面侧栏和一个移动侧栏,并根据当前的屏幕尺寸有条件地显示它们,就像这样。

import { Box, Stack } from "@chakra-ui/layout";
import Header from "./navbar";
import DesktopSidebar from "./DesktopSidebar";
import MobileSidebar from "./DesktopSidebar";
import { useMediaQuery } from "@chakra-ui/media-query";

export default function Layout({ children }) {
  const [isSmallScreen] = useMediaQuery("(max-width: 768px)");
  return (
    <Box>
      <Header />
      <Box>
        <Stack>
          {isSmallScreen ? <MobileSidebar /> : <DesktopSidebar />}
          {children}
        </Stack>
      </Box>
    </Box>
  );
}

现在我们已经创建了仪表板布局,让我们在我们的_app.js 文件中使用它。

import Head from "next/head";
import { ChakraProvider } from "@chakra-ui/react";
import Layout from "@layout/index";

function MyApp({ Component, pageProps }) {
  return (
    <>
      <Head>
        <title>Chakra UI Dashboard</title>
      </Head>
      <ChakraProvider>
        <Layout>
          <Component {...pageProps} />
        </Layout>
      </ChakraProvider>
    </>
  );
}
export default MyApp;

创建Header 组件

Header 组件由一个标志、一个UserProfile 组件和一个汉堡包菜单组成,我们将用它来切换移动设备上的侧边栏。UserProfile 由用户的名字、他们的头像图片和他们的角色组成。UserProfile 在点击时触发一个下拉菜单。

让我们从UserProfile 开始。

import {
  IconButton, Avatar, Box, Flex, HStack, VStack, Text, Menu, MenuButton, MenuDivider,
  MenuItem, MenuList,
} from "@chakra-ui/react";
import { FiChevronDown, FiBell } from "react-icons/fi";

export default function UserProfile() {
  return (
    <HStack spacing={{ base: "0", md: "6" }}>
      <IconButton
        size="lg"
        variant="ghost"
        aria-label="open menu"
        icon={<FiBell />}
      />
      <Flex alignItems="center">
        <Menu>
          <MenuButton
            py={2}
            transition="all 0.3s"
            _focus={{ boxShadow: "none" }}
          >
            <HStack spacing="4">
              <Avatar
                size="md"
                src={
                  "https://images.unsplash.com/photo-1619946794135-5bc917a27793?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&fit=crop&h=200&w=200&s=b616b2c5b373a80ffc9636ba24f7a4a9"
                }
              />
              <VStack
                display={{ base: "none", md: "flex" }}
                alignItems="flex-start"
                spacing="1px"
                ml="2"
              >
                <Text fontSize="lg">Ademola Jones</Text>
                <Text fontSize="md" color="gray.600">
                  Admin
                </Text>
              </VStack>
              <Box display={{ base: "none", md: "flex" }}>
                <FiChevronDown />
              </Box>
            </HStack>
          </MenuButton>
          <MenuList fontSize="lg" bg="white" borderColor="gray.200">
            <MenuItem>Profile</MenuItem>
            <MenuItem>Settings</MenuItem>
            <MenuItem>Billing</MenuItem>
            <MenuDivider />
            <MenuItem>Sign out</MenuItem>
          </MenuList>
        </Menu>
      </Flex>
    </HStack>
  );
}

在这里,我们使用对象语法来设置HStack 组件的间距。我们为小屏幕和移动设备去除HStack'的间距,为大设备设置为6 。我们将VStackBox 组件的显示在小型设备上设置为"none" ,在大型设备上设置为"flex"

现在,对于Header

import { IconButton, Flex, Text } from "@chakra-ui/react";
import { FiMenu } from "react-icons/fi";
import UserProfile from "./UserProfile";

export default function Header({ onOpen }) {
  return (
    <Flex
      ml={{ base: 0, md: 60 }}
      px="4"
      position="sticky"
      top="0"
      height="20"
      zIndex="1"
      alignItems="center"
      bg="white"
      borderBottomWidth="1px"
      borderBottomColor="gray.200"
      justifyContent={{ base: "space-between", md: "flex-end" }}
    >
      <IconButton
        display={{ base: "flex", md: "none" }}
        onClick={onOpen}
        variant="outline"
        aria-label="open menu"
        icon={<FiMenu />}
      />
      <Text
        display={{ base: "flex", md: "none" }}
        fontSize="2xl"
        fontFamily="monospace"
        fontWeight="bold"
      >
        Logo
      </Text>
      <UserProfile />
    </Flex>
  );
}

通过使用Flex 组件,我们将Header'的显示设置为"flex" 。我们想根据断点来调整Flex 组件的左边距,ml 。我们在小型设备上将左边距设置为0 ,在大型设备上设置为60 。你可以在文档中了解更多关于Chakra空间的信息。

我们希望汉堡包和标志只在移动设备上可见,所以我们通过display 的道具将它们设置为只在移动设备上显示。

最后,我们将onClose 函数传递给汉堡包菜单的onClick 事件,以便在移动端触发侧边栏,就像我们前面解释的那样。

创建Sidebar 组件

Sidebar 组件由一个标志、一个链接列表和一个 "关闭 "按钮组成,我们将用它来关闭移动端的侧边栏。

让我们开始设置侧边栏。

import { Box, CloseButton, Flex, Text } from "@chakra-ui/react";
import {
  FiHome,
  FiTrendingUp,
  FiCompass,
  FiStar,
  FiSettings,
} from "react-icons/fi";
import NavLink from "./NavLink";

const LinkItems = [
  { label: "Home", icon: FiHome, href: "/" },
  { label: "Trending", icon: FiTrendingUp, href: "/" },
  { label: "Explore", icon: FiCompass, href: "/" },
  { label: "Favourites", icon: FiStar, href: "/" },
  { label: "Settings", icon: FiSettings, href: "/" },
];

export default function Sidebar({ onClose, ...rest }) {
  return (
    <Box
      transition="3s ease"
      bg="white"
      borderRight="1px"
      borderRightColor="gray.200"
      w={{ base: "full", md: 60 }}
      pos="fixed"
      h="full"
      {...rest}
    >
      <Flex h="20" alignItems="center" mx="8" justifyContent="space-between">
        <Text fontSize="2xl" fontFamily="monospace" fontWeight="bold">
          Logo
        </Text>
        <CloseButton display={{ base: "flex", md: "none" }} onClick={onClose} />
      </Flex>
      {LinkItems.map((link, i) => (
        <NavLink key={i} link={link} />
      ))}
    </Box>
  );
}

我们希望侧边栏在移动端采用全屏宽度,所以我们将其基本宽度设置为"full" 。接下来,我们在大型设备上将其宽度设置为60

我们希望CloseButton 只在移动设备上可见,所以我们将其在移动设备上的显示设置为"flex" ,在大型设备上设置为"none"

我们还将onClose 函数传递给CloseButton ,以便在需要时关闭侧边栏。CloseButton 是需要关闭我们先前在Layout 组件中设置的抽屉的。

我们已经创建了侧边栏,而且它是有反应的,但是有一个问题。当我们在移动端点击侧边栏的链接时,侧边栏并没有滑回视野。我们怎样才能解决这个问题,并确保在点击链接时隐藏侧边栏?我们用Next.js的router.events API解决这个问题。

Next.js中的Router.events

这个API允许我们监听Next.js路由器内部发生的不同事件,并对其做出反应。

让我们更新一下Sidebar 组件的代码。

import { useEffect } from "react";
import { useRouter } from "next/router";

export default function Sidebar({ onClose, ...rest }) {
  const router = useRouter();

  useEffect(() => {
    router.events.on("routeChangeComplete", onClose);
    return () => {
      router.events.off("routeChangeComplete", onClose);
    };
  }, [router.events, onClose]);

  return (
    <Box
      transition="3s ease"
      bg="white"
      borderRight="1px"
      borderRightColor="gray.200"
      w={{ base: "full", md: 60 }}
      pos="fixed"
      h="full"
      {...rest}
    >
      //other elements of the sidebar go here....
    </Box>
  );
}

在这里,我们使用useEffect 钩子来注册routeChangeComplete 路由器事件。当路由完全改变时,routeChangeComplete 会启动,在我们的例子中,当我们点击侧边栏的链接时。当routeChangeComplete 发生时,我们调用onClose ,这将导致侧边栏关闭。

最后,你会注意到,我们定义了一个LinkItems 数组。我们循环浏览这个数组,对于数组中的每个元素,我们渲染一个NavLink 组件。NavLink 将包含侧边栏中每个链接的数据。

让我们来创建NavLink

import NextLink from "next/link";
import { Flex, Icon, Text } from "@chakra-ui/react";

export default function NavLink({ link, ...rest }) {
  const { label, icon, href } = link;
  return (
    <NextLink href={href} passHref>
      <a>
        <Flex
          align="center"
          p="4"
          mx="4"
          borderRadius="lg"
          role="group"
          cursor="pointer"
          _hover={{
            bg: "cyan.400",
            color: "white",
          }}
          {...rest}
        >
          {icon && (
            <Icon
              mr="4"
              fontSize="16"
              _groupHover={{
                color: "white",
              }}
              as={icon}
            />
          )}
          <Text fontSize="1.2rem">{label}</Text>
        </Flex>
      </a>
    </NextLink>
  );
} 

NavLink 没有响应式的样式。在这里,我们传入标签、图标和URL,这些都是我们通过LinkItems 数组映射时得到的。

创建主页视图

现在我们已经建立了仪表盘布局和它的组件,让我们看看主页的构件。

import { useState } from "react";
import { cardVariant, parentVariant } from "@root/motion";
import ProductModal from "@components/ProductModal";
import { motion } from "framer-motion";
import data from "@root/data";
import ProductCard from "@components/ProductCard";
import { Box, SimpleGrid } from "@chakra-ui/react";

const MotionSimpleGrid = motion(SimpleGrid);
const MotionBox = motion(Box);

export default function Home() {
  const [modalData, setModalData] = useState(null);
  return (
    <Box>
      <MotionSimpleGrid
        mt="4"
        minChildWidth="250px"
        spacing="2em"
        minH="full"
        variants={parentVariant}
        initial="initial"
        animate="animate"
      >
        {data.map((product, i) => (
          <MotionBox variants={cardVariant} key={i}>
            <ProductCard product={product} setModalData={setModalData} />
          </MotionBox>
        ))}
      </MotionSimpleGrid>
      <ProductModal
        isOpen={modalData ? true : false}
        onClose={() => setModalData(null)}
        modalData={modalData}
      />
    </Box>
  );
}

//sample of product cards array
const data = [
  {
    title: "First Product",
    price: 250,
    img: "https://res.cloudinary.com/nefejames/image/upload/v1593631406/market%20square/clothes/cloth1.jpg",
  },
  {
    title: "Second Product",
    price: 250,
    img: "https://res.cloudinary.com/nefejames/image/upload/v1593631406/market%20square/clothes/cloth2.jpg",
  },
//other product objects below

主页视图由两个组件组成,ProductCardProductModal

我们循环浏览一个产品数据阵列,并创建一个ProductCard 网格。你可以在上面的代码中看到一个数组的样本。

当一个产品卡片被点击时,我们希望弹出一个充满产品数据的模式。为了做到这一点,我们定义了一个modalData 状态,它将保存被点击的产品数据。

我们将包含产品数据的product 对象和setModalData 方法传递给ProductCard

接下来,我们将isOpenonClose ,以及modalData 状态传递给ProductModal 。当modalData 持有产品数据时,isOpen 为真。onClose 方法将modalData 设置为空。

现在我们明白了ProductCardProductModal 组件是如何工作的,让我们来创建它们。

创建ProductCard 组件

ProductCard 组件包括一个上部分和一个下部分。上部分是产品的图片,下部分包含产品的信息--它的标题、价格和评论数量。

import Image from "next/image";
import { Box, Flex, chakra } from "@chakra-ui/react";
import { AiTwotoneStar } from "react-icons/ai";
const ChakraStar = chakra(AiTwotoneStar);

export default function ProductCard({ product, setModalData }) {
  const { img, title, price } = product;

  return (
    <Flex
      w="full"
      h="full"
      alignItems="center"
      justifyContent="center"
      cursor="pointer"
      bg="white"
      rounded="xl"
      shadow="lg"
      borderWidth="1px"
      onClick={() => setModalData(product)}
    >
      <Box w="full" h="full">
        <Box
          w="100%"
          height="200px"
          position="relative"
          overflow="hidden"
          roundedTop="lg"
        >
          <Image
            src={img}
            objectFit="cover"
            alt="picture of a house"
            layout="fill"
          />
        </Box>
        <Box p="6">
          <Box fontWeight="semibold" as="h4" lineHeight="tight" isTruncated>
            {title}
          </Box>
          <Box>${price}</Box>
        </Box>
      </Box>
    </Flex>
  );
}

就像我们前面看到的,我们把product 对象和setModalData 方法传给ProductCard 。当产品被点击时,我们调用setModalData 方法,用产品的数据更新modalData 的状态。

创建ProductModal 组件

ProductModal 组件由产品的图片、标题和价格组成。它还包含一个购买按钮,以模拟一个实际的电子商务应用的购买流程。

import Image from "next/image";
import { Box, Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody,
  ModalCloseButton, Button, useToast, Flex,
} from "@chakra-ui/react";

export default function ProductModal({ isOpen, onClose, modalData }) {
  const { title, price, img } = modalData || {};
  const toast = useToast();

  const handleModalClose = () => {
    toast({
      title: "Purchase successsful.",
      description: "Fashion ++",
      status: "success",
      duration: 3000,
      isClosable: true,
    });
    setTimeout(() => {
      onClose();
    }, 1000);
  };

  return (
    <Modal isOpen={isOpen} onClose={onClose} size="xl">
      <ModalOverlay />
      <ModalContent>
        <ModalCloseButton />
        <ModalHeader>Product Details</ModalHeader>
        <ModalBody>
          <Box w="full" h="full">
            <Flex w="full" h="300px" position="relative">
              <Image src={img} alt="a house" objectFit="cover" layout="fill" />
            </Flex>
            <Box pt="3">
              <Box mt="3" fontWeight="semibold" as="h4" lineHeight="tight" isTruncated>
                {title}
              </Box>
              ${price}
            </Box>
          </Box>
        </ModalBody>
        <ModalFooter>
          <Button
            bg="cyan.700" color="white" w="150px" size="lg" onClick={handleModalClose}
            _hover={{ bg: "cyan.800" }}
          >
            Purchase
          </Button>
        </ModalFooter>
      </ModalContent>
    </Modal>
  );
}

就像我们之前看到的,我们把isOpenonClosemodalData 的状态传递给ProductModal 。我们在这里使用Chakra UI的[Modal](https://chakra-ui.com/docs/overlay/modal)

modalData 包含被点击的产品的数据。我们从modalData 状态中访问产品的标题、图片和价格。

我们还定义了一个handleModalClose 函数并将其传递给购买按钮。当按钮被点击时,会显示一个烤面包,并调用onClose 方法。

总结

我喜欢使用Chakra UI,因为它能帮助我在构建应用程序的前端时保持高效率。样式道具模式不仅使Chakra UI成为工作的乐趣,而且我也不必处理为不同尺寸设置媒体查询的复杂问题。Chakra UI的设计是为了提供良好的开发者体验,它始终是我的首选组件库。

在这篇文章中,我们已经学会了如何在Chakra UI中编排响应式组件。我们学习了useMediaQueryuseDisclosure 钩子的工作原理,并使用它们来创建一个响应式仪表盘。

我们建立的仪表盘应用程序的源代码可以在GitHub上找到。