我们学到的React知识已有能力搭建项目了,本篇开始创建一个漂亮的游戏库网站,界面仿照全球知名的游戏发现网站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进行设计,计划分上中下三篇完成。
下一篇继续构建游戏库项目。