React入门(七)– 游戏库应用(上)

​我们学到的React知识已有能力搭建项目了,本篇开始创建一个漂亮的游戏库网站,界面仿照全球知名的游戏发现网站rawg.io。

https://rawg.io/

我们将设计深色/浅色模式的切换,游戏的搜索,游戏类型的筛选,排序等,项目涉及多种现代WEB的UI,这是一个极好的练习和巩固知识的机会。


设置项目

创建React项目game-hub。选择React和TypeScript。

npm create vite@4.1.0
​
✔ Project name: … game-hub
✔ Select a framework: › React
✔ Select a variant: › TypeScript

安装依赖库及运行项目。

cd game-hub
npm install
npm run dev

设置git库并进行首次提交。

git init
git add .
git commit -m 'Initial commit'

安装Chakra UI

界面UI选择 Chakra UI,这是完全组件化的UI,比较方便使用。访问chakra官网,查看安装方法。

https://chakra-ui.com/getting-started/vite-guide

npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion

再根据官网指导修改 src/main.tsx,用ChakraProvider包装 App组件。

// main.tsx
​
import React from "react";
import ReactDOM from "react-dom/client";
import { ChakraProvider } from "@chakra-ui/react";
import App from "./App";
import "./index.css";
​
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <ChakraProvider>
      <App />
    </ChakraProvider>
  </React.StrictMode>
);

安装完成后,我们查看官网文档上组件的使用。

https://chakra-ui.com/docs/components

测试按钮组件Button,搜索官网用法,将相关代码拷贝到App.tsx组件里。

// App.tsx
​
import { Button, ButtonGroup } from "@chakra-ui/react";
function App() {
  return <Button colorScheme="blue">Button</Button>;
}
​
export default App;

为避免vite的 css对chakra UI 造成干扰,清空 src/index.css文件的内容。

保存,查看网页效果,确认与官网ChakraUI的Button一致,说明ChakraUI已安装成功。

再提交一次 git。这次我们用vscode来提交更方便,提交前建议先检查相关更改的内容,确认无误,然后输入信息,比如 Install Chakra UI,再点 Commit。

可以通过命令检查提交。

git log --oneline

创建响应式布局

我们考虑的布局是:顶部导航栏、左侧面板以及右侧主区域。使用Grid组件,Chakra的Grid与HTML的Grid很相似。

Grid的templateAreas设置布局,使用模板语法(反引号 ` ),内容比如 “nav nav””aside main” 就表示设置两行,第1行两栏是 “nav nav”,第2行两栏是”aside main”。接着设置GridItem,分别引用栏的名称。

响应式布局需要自动适配大小屏,我们不希望手机显示aside 栏,于是设置了基本是 “nav” “main”,而笔记本电脑等大屏设备才显示Aside,语法上就是用 <Show above=’lg’>组件包装,表示 breakpoint为 lg 时才显示。

// App.tsx
​
import { Grid, GridItem, Show } from "@chakra-ui/react";
function App() {
  return (
    <Grid
      templateAreas={{
        base: `"nav""main"`,
        lg: `"nav nav""aside main"`,
      }}
    >
      <GridItem area="nav" bgColor="coral">
        Nav
      </GridItem>
      <Show above="lg">
        <GridItem area="aside" bgColor="gold">
          Aside
        </GridItem>
      </Show>
      <GridItem area="main" bgColor="dodgerblue">
        Main
      </GridItem>
    </Grid>
  );
}
​
export default App;

breakpoint的值可以查看网页:

https://chakra-ui.com/docs/styled-system/responsive-styles

const breakpoints = {
  sm: '30em', // 480px
  md: '48em', // 768px
  lg: '62em', // 992px
  xl: '80em', // 1280px
  '2xl': '96em', // 1536px
}

用谷歌浏览器可以模拟大屏和小屏设备的效果,小屏手机或平板是会隐藏Aside的。

我们再提交一次git: Build a responsive layout.


构建导航栏

导航栏有Logo,搜索栏,以及“深色/浅色”切换开关,我们先做Logo。程序开发就是一次完成一小块功能,逐步推进,不能一次做太多。

先在assets文件放置logo.webp文件,这是我们的logo,webp格式是优化的web图像。

src文件夹下新建components文件夹,再新建NavBar.tsx。

我们使用ChakraUI的<HStack>组件来设置水平方向排列。最左侧是logo图标,我们使用 Image组件,先在头部导入log.webp,然后在Imager的src属性中使用。

// NavBar.tsx
​
import { HStack, Image, Text } from "@chakra-ui/react";
import logo from "../assets/logo.webp";
const NavBar = () => {
  return (
    <div>
      <HStack>
        <Image src={logo} boxSize="60px"></Image>
        <Text>NavBar</Text>
      </HStack>
    </div>
  );
};
​
export default NavBar;
​

然后在App组件中名字为nav 的GridItem中放置NavBar。并删除bgColor属性,Nav不再用该背景色。

// App.tsx
​
......
​
      <GridItem area="nav">
        <NavBar />
      </GridItem>
​
......

查看效果,Logo显示在左侧了。

提交Git,命名为:Build a navbar


实施深色模式

查看ChadkraUI的官方文档,对照步骤实施。

https://chakra-ui.com/docs/styled-system/color-mode

第1步,在src文件夹下新建theme.ts文件,代码从官网复制。

// theme.ts
​
import { extendTheme, type ThemeConfig } from '@chakra-ui/react'
const config: ThemeConfig = {
  initialColorMode: 'dark',
  useSystemColorMode: false,
}
const theme = extendTheme({ config })
export default theme

再到main.tsx修改ChakraProvider。

// main.tsx
​
......
​
import theme from "./theme";
​
......
​
    <ChakraProvider theme={theme}>
      <ColorModeScript initialColorMode={theme.config.initialColorMode} />
      <App />
    </ChakraProvider>
​
......

保存,查看页面已变成深色了。如果没变化,很可能是浏览器缓存问题,可以将有关Chakra的缓存删除即可。

提交git,命名:Inplement dark mode.


创建颜色模式切换开关

创建src/components/ColorModeSwitch.tsx组件。

由于水平布置,使用HStack包装 Switch 和 Text 组件,使用useColorMode来切模式,为使开关更清晰,调整colorScheme为green。

// ColorModeSwitch.tsx
​
import { HStack, Switch, Text, useColorMode } from "@chakra-ui/react";
​
const ColorModeSwitch = () => {
  const { colorMode, toggleColorMode } = useColorMode();
  return (
    <HStack>
      <Switch
        colorScheme="green"
        isChecked={colorMode === "dark"}
        onChange={toggleColorMode}
      ></Switch>
      <Text>Dark Mode</Text>
    </HStack>
  );
};
​
export default ColorModeSwitch;

再到NavBar使用ColorModeSwitch组件,并设置justifyContent和padding使之靠右。

//NavBar.tsx
......
​
      <HStack justifyContent="space-between" padding="10px">
        <Image src={logo} boxSize="60px"></Image>
        <ColorModeSwitch />
      </HStack>
​
......

接着在App.tsx中将GridItem的 背景属性bgColor都删除掉。

提交git,命名:Build the color mode switch.


获取游戏

访问rawg.io,右上角点“Get an API key”,花几钟注册,然后会得到一个API key,复制。

安装axios。

npm i axios

新建 src/services文件夹,再新建 src/services/api-client.ts文件。设置基本访问url及key,key是前面得到的API key。

// api-client.ts
​
import axios from "axios";
​
export default axios.create({
    baseURL:'https://api.rawg.io/api',
    params:{
        'key':'01b29da6c41841458b00aa22c947a464',
    }
})

创建 src/components/GameGrid.tsc。GameGrid很常规,目前我们只需要 id,name信息,所以设置了数据结构Game,另外根据rawg.io的返回设置FetchGamesResponse结构,也只关心count和results。

组件显示方面,目前只简单地显示出错信息和游戏名列表。

// GameGrid.tsx
​
import React, { useEffect, useState } from "react";
import apiClient from "../services/api-client";
import { Text } from "@chakra-ui/react";
​
interface Game {
  id: number;
  name: string;
}
​
interface FetchGamesResponse {
  count: number;
  results: Game[];
}
const GameGrid = () => {
  const [games, setGames] = useState<Game[]>([]);
  const [error, setError] = useState("");
  useEffect(() => {
    apiClient
      .get<FetchGamesResponse>("/games")
      .then((res) => setGames(res.data.results))
      .catch((err) => setError(err.message));
  }, []);
  return (
    <>
      {error && <Text>{error}</Text>}
      <ul>
        {games.map((game) => (
          <li key={game.id}>{game.name}</li>
        ))}
      </ul>
    </>
  );
};
​
export default GameGrid;
​

再到App.tsc中,在main的GridItem中调用 <GameGrid>。

// App.tsc
​
......
​
      <GridItem area="main">
        <GameGrid />
      </GridItem>
​
......

最后,跟之前一样,提交git,命名:Fetching the game。


创建自定义获取游戏的Hook

目前我们的GameGrid组件里太了解Http细节了,我们可以像之前一样将相关Http细节提取出来形成比如gameService模块,也可以创建一个自定义Hook,这里就用后一种。

创建文件夹 src/hooks,

再创建文件 src/hooks/useGames.ts,将GameGrid组件里的interface, State,  Effect全移过来。

再增加AbortController用于清除Effect,以及判断如果是取消的err的话不显示错误。

自定义useGames的Hook返回 games和 error。

// useGame.ts
​
import { useEffect, useState } from "react";
import apiClient from "../services/api-client";
import { CanceledError } from "axios";
​
interface Game {
    id: number;
    name: string;
}
  
interface FetchGamesResponse {
    count: number;
    results: Game[];
}
const useGames = ()=>{ 
    const [games, setGames] = useState<Game[]>([]);
    const [error, setError] = useState("");
    useEffect(() => {
        const controller = new AbortController();
        apiClient
            .get<FetchGamesResponse>("/games",{signal:controller.signal})
            .then((res) => setGames(res.data.results))
            .catch((err) => {
                if (err instanceof CanceledError) return;
                setError(err.message)
            });
        return ()=>controller.abort();
    }, []);
    return {games,error};
}
export default useGames;

再到GameGrid调用useGames,就很简洁了。

// GameGrid.tsx
​
import { Text } from "@chakra-ui/react";
import useGames from "../hooks/useGames";
​
const GameGrid = () => {
  const { games, error } = useGames();
  return (
    <>
      {error && <Text>{error}</Text>}
      <ul>
        {games.map((game) => (
          <li key={game.id}>{game.name}</li>
        ))}
      </ul>
    </>
  );
};
​
export default GameGrid;

完成后,提交git,“Create a custom hook to fetch the games”。


建立游戏卡片

游戏卡片组件需要传入Props 一个game对象。game的结构已定义在useGames里,我们先将之export出来,并检查rawg的API数据结构增加background_image属性,因为我们需要显示图片。

// useGames.ts
​
.......
​
export interface Game {
    id: number;
    name: string;
    background_image: string;
}
​
......

再添加src/GameCard.tsx组件。

传game到GameCard,添加Card组件,再在其中添加Image和CardBody组件。Image的src指向game的background_image链接,CardBody里添加Heading组件,简单显示游戏的名字 game.name.

Card组件添加了borderRadius属性为10,是希望显示圆角的效果,overflow设为hidden是为了防由图片大小于容器显示不正确。

Heading默认的字号较大,我们修改成 2xl,这是在Chakra定义好的,可以查看官网了解该字号的大小。

https://chakra-ui.com/docs/components/text/usage#changing-the-font-size

// GameCard.tsx
​
import { Card, CardBody, Heading, Image } from "@chakra-ui/react";
import { Game } from "../hooks/useGames";
​
const GameCard = ({ game }: { game: Game }) => {
  return (
    <Card borderRadius={10} overflow="hidden">
      <Image src={game.background_image}></Image>
      <CardBody>
        <Heading fontSize="2xl">{game.name}</Heading>
      </CardBody>
    </Card>
  );
};
​
export default GameCard;

再到GameGrid组件,将之前的<ul>包装的修改成 SimpleGrid组件,spacing属性是设置网格间距,columns属性设置栏数,我们给到一个对象{ sm: 1, md: 2, lg: 3, xl: 5 },分别表示小屏、中屏、大屏以及超大屏显示的栏数,这样就能自适应显示。

// GameGrid.tsx
​
......
​
      <SimpleGrid
        columns={{ sm: 1, md: 2, lg: 3, xl: 5 }}
        padding="10px"
        spacing={10}
      >
        {games.map((game) => (
          <GameCard key={game.id} game={game} />
        ))}
      </SimpleGrid>
​
......

目前效果还不错。

最后,提交git,“Builds game cards”


显示平台图标

这节来展示游戏支持的平台。查看API有parent_platforms属性,该属性是一个列表,结构如下,这个设计明显有点过度,但我们必须按它的结构处理。

修改Game的数据结构,增加parent_platforms属性以及再定义一个Platform结构。

// useGames.ts
​
......
​
export interface Platform{
    id:number;
    name:string;
    slug:string;
}
export interface Game {
    id: number;
    name: string;
    background_image: string;
    parent_platforms:{platform:Platform}[];
}
​
......

处理这个平台图标,我们准备设计一个独立的组件PlatformIconList,GameCard调用这个组件,并将该游戏支持的平台列表platforms作为Props传给它。

先修改GameCard组件如下:

// GameCard.tsx
​
......
​
      <CardBody>
      
        ......
        <PlatformIconList
          platforms={game.parent_platforms.map((p) => p.platform)}
        />
      </CardBody>
​
......

接着设计PlatformIconList组件。

新建文件src/components/PlatformIconList.tsc,组件主要任务是将传过来的平台列表platforms里的平台slug转换成相关的Icon图标。

安装react-icons库:

npm i react-icons@4.7.1

导入各种IconType,如FaWindows,FaApple等,其中react-icons/fa 的 fa 是 font awesome的简写,bs是bootstrap的缩写等。

再创建icon与platform的slug的对应关系 iconMap,这样可以避免使用丑陋的 IF 语句,至于为什么不跟name对应,主要是slug不能修改,比较准确,而且slug是全小写的,更合适。iconMap要指定类型 iconMap: { [key: string]: IconType },否则会有编译错误。

由于各Icon水平放置,包在<HStack>组件里,为了与Heading保持一定距离,使用了 margin={1}。这里顺便说一下为什么这个 1 数字不带单位,在ChakraUI里,1 = “4px”,2等于 8px,依次类推。为了精确当然也可以直接使用类似margin=’10px’ 这样带px单位的。

为了使Icon的颜色不要太影响视觉,我们要设置浅一些,这里使用了 gray.500。这是什么鬼?

这是Chakra定义的,具体查看官网了解相应色调:

https://chakra-ui.com/docs/styled-system/theme

import {
  FaWindows,
  FaPlaystation,
  FaXbox,
  FaApple,
  FaLinux,
  FaAndroid,
} from "react-icons/fa";
import { MdPhoneIphone } from "react-icons/md";
import { SiNintendo } from "react-icons/si";
import { BsGlobe } from "react-icons/bs";
import { HStack, Icon } from "@chakra-ui/react";
import { Platform } from "../hooks/useGames";
import { IconType } from "react-icons";
​
interface Platforms {
  platforms: Platform[];
}
​
const PlatformIconList = ({ platforms }: Platforms) => {
  const iconMap: { [key: string]: IconType } = {
    pc: FaWindows,
    playstation: FaPlaystation,
    xbox: FaXbox,
    nintendo: SiNintendo,
    mac: FaApple,
    linux: FaLinux,
    ios: MdPhoneIphone,
    android: FaAndroid,
    web: BsGlobe,
  };
  return (
    <HStack margin={1}>
      {platforms.map((platform) => (
        <Icon as={iconMap[platform.slug]} color="gray.500" />
      ))}
    </HStack>
  );
};
​
export default PlatformIconList;

至此的效果。

提交git,“Displaying platform icons”。


显示评论分数

查看API数据结构,评分是metacritic,修改Game结构。

// useGames.tsx
​
......
​
export interface Game {
    ......
    metacritic: number;
}
​
......

新建src/components/CriticScore.tsx文件。

定义Props的score,传入组件。调整组件Badge的fontsize,增加宽度调整paddingX,再调整四角有圆弧。

定义color,判断75以上显示绿色,60以上显示黄色,赋给Badge的colorScheme。我们注意到颜色还有一个属性color,color只调整前景色,而colorScheme调整所有颜色,包括前景、背景、边线等。

// CriticScore.tsx
import { Badge } from "@chakra-ui/react";
​
interface Props {
  score: number;
}
​
const CriticScore = ({ score }: Props) => {
  let color = score > 75 ? "green" : score > 60 ? "yellow" : "";
  return (
    <Badge fontSize="14px" paddingX={2} borderRadius="4px" colorScheme={color}>
      {score}
    </Badge>
  );
};
​
export default CriticScore;

再在GameCard使用CriticScore。

由于平台图标与评分在同一行,用了 HStack组件将两者包起来,justifyContent属性使用space-between,这样会分别显示在左右两侧。

// GameCard.tsx
​
......
​
      <CardBody>
        <Heading fontSize="2xl">{game.name}</Heading>
        <HStack justifyContent="space-between">
          <PlatformIconList
            platforms={game.parent_platforms.map((p) => p.platform)}
          />
          <CriticScore score={game.metacritic} />
        </HStack>
      </CardBody>
​
......

效果截图。

同样的,提交git。“Displaying critic score”


小结

本篇利用已学的React知识搭建一个小型游戏库项目,按照游戏发现网站rawg.io的UI进行设计,计划分上中下三篇完成。

下一篇继续构建游戏库项目。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注