0 前言
近期工作中偶然遇到一个搭建前端网页的支线任务,回忆起此前实现 xtimer 时,我曾基于 react 写过配套的前端项目,但转眼都已经是两年前的事情了,后续由于日常工作中接触前端任务的机会很少,用进废退,知识已经遗忘,技能已经生疏.
以此为契机,我在近期学习了一轮有关 react 的前端知识,通过一个自娱自乐的前端项目实操一把,并记录下来作为这篇学习笔记,分享给大家也留给未来的自己,要铭记曾经的学习足迹,时而温故,不可荒废.
本篇内容比较基础,属于留给个人的学习笔记,相对契合的读者群体是侧重于实操的前端初学者,我的前端水平堪堪入门,但还是想把握住这个记录与交流的机会,不足之处请多批评指正
本文所介绍的是我近期实现的前端项目——xworld,使用的技术框架是 react(18) + typescripe + vite
页面地址已开放,欢迎来体验(需要用电脑浏览器):
地址:X-World (http://106.14.133.57:3000/)
账号:令狐冲 密码:xajh
登录页面——英雄帖
地图传送点——思过崖、藏经阁、开悟坡
正式页面——思过崖
正式页面——藏经阁
正式页面——开悟坡
该项目已于 github 开源:https://github.com/xiaoxuxiansheng/xworld ,需要源码可自取,觉得有帮助请不吝留个 star,感谢感谢
实践过程中,学习了 bilibili 的几个优质视频,在此分享并致敬:
前端项目 demo —— 前端乐哥 https://www.bilibili.com/video/BV1FV4y157Zx
react 基础知识—— 尚硅谷 禹神 https://www.bilibili.com/video/BV1wy4y1D7JT
typescript 基础知识—— 尚硅谷 禹神 https://www.bilibili.com/video/BV1YS411w7Bf
1 环境准备
此刻摆在面前的是一台干净的 linux 服务器,而本章的目标是在此基础上完成前置准备工作,做到能把 react 项目运行起来.
1.0 相关知识点
接下来会涉及软件安装与配置工作,我们在这里先简要介绍即将涉及到的工具以及概念:
• ts(typescript) 与 js(javascript) 的关系:js 是前端网页开发的通用语言,而 ts 是 js 的超集,在 js 基础上新增了类型定义与检查等功能,提升了代码的可维护性,更加适用于中大型工程化项目中.
• node.js:跨平台的 js 运行时环境,支持开发者基于 js 搭建服务端
• nvm(node version manager):node.js 版本管理工具,支持多版本 node.js 的安装和切换
• npm(node package manager):node.js 的依赖包管理工具,并支持项目的调试编译运行
• react:用于构建用户界面的开源 js 框架,其隐藏了 dom(document object model)的细节,提供高效、灵活、可组合且易维护的能力供开发者使用
• vite :用于快速构建前端项目的脚手架工具
1.1 软件安装
1)nvm 安装
下载 nvm:
git clone https://gitee.com/mirrors/nvm.git ~/.nvm && cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`
环境变量配置:
vim ~/.bashrc
===========================
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm
==========================
source ~/.bashrc
验证 nvm 是否安装成功:
nvm --version
=========================
0.39.7
2)node 安装
基于 nvm 安装node:
NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node/ nvm install 18.17.0
验证 node 与 npm 是否安装成功:
node --version
=========================
v18.17.0
npm --version
=========================
9.6.7
设置 npm 镜像源(避免依赖安装超时):
npm config set registry=https://registry.npmmirror.com
1.2 项目初始化
1)创建项目
基于 vite 创建项目:
npm init vite
输入项目名称:
? Project name: › {Your Project Name}
选择框架和语言分别为 react 和 typescript:
? Select a framework: › - Use arrow-keys. Return to submit.
❯ React
? Select a variant: › - Use arrow-keys. Return to submit.
❯ TypeScript
2)更新依赖
更新 package.json 中的依赖包内容,涉及到的改动包括:
• 设置监听 ip 地址以及启动端口号:
{
// ...
"scripts": {
"dev": "vite --host 0.0.0.0 --port 3000",
// ...
}
// ...
}
• 新增依赖(xworld 项目中所额外用到的)
{
// ...
// 生产环境依赖包
"dependencies":{
// ant-design 组件库
"antd":"^5.0.0",
// axios 网络请求库
"axios":"^1.1.3",
// 代码编辑界面
"react-monaco-editor":"^0.56.2",
// 路由库
"react-router-dom":"^6.4.3"
},
// 仅限开发、测试需要的依赖包
"devDependencies":{
// 动画效果库
"@react-spring/web":"^9.7.5",
// 热点图
"@uiw/react-heat-map":"^2.3.0",
// 支持 module.scss 样式文件使用
"sass-embedded":"^1.81.0",
// ...
}
}
• 导入依赖
执行指令
npm i
成功后,会在项目下创建 ./node_modules 目录,所有下载好的依赖会处在其中
1.3 项目启动
执行指令,以开发模式启动项目:
npm run dev
============================
VITE v5.4.10 ready in 633 ms
➜ Local: http://localhost:3000/
➜ Network: http://{你的ip地址}:3000/
打开浏览器,输入 ip:port,当看到如下页面,证明项目已成功启动:
react 项目启动主界面
2 整体架构
2.0 相关知识点
本章会涉及到如下知识点:
• html、css、ts:html (HyperText Markup Language)提供了网页内容结构;css(Cascading Style Sheets)控制网页的视觉样式;ts(typescript)负责网页的交互行为.
以人来举例的话,html 是人的骨架,决定人的姿势,支撑人能直立行走;css 是人的皮肤和装饰品,控制人的外观,提升颜值;ts 是人的思考和行为,决定在外界事件发生后人会如何应对
• .ts 文件与 .tsx 文件:.ts 是 typescript 代码文件的扩展名. 而使用 .tsx(typescript xml )文件时,则支持在 typescript 代码注入以 html 标签的形式 react 组件代码,更便于 react 开发者的使用
• spa(single page application):单页面应用. 仅存在单个 html 页面,通过动态加载和部分替换的方式来完成所有交互操作
• react-router-dom:react 中的路由库,用于在浏览器中实现编程式导航(useNavigate)、嵌套路由(useRoute)、路径获取(useLocation)等功能
• react component:组件是 react 中构建用户界面的基本单位,是抽象出来的一个个可复用代码模块,用于渲染 html、管理状态(state)和处理用户交互
2.1 项目代码架构
react 项目的源代码位于 ./src 目录下,而一些公共资源则放置在 .public 目录下.
xworld 代码结构
在层级架构上自顶向下可以分为几部分内容:
• 1)页面文件 index.html
文件位于 ./src/index.html,作为 spa 项目,是全局唯一的 html 页面:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- 设置页面图标 -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- 设置页面标题 -->
<title>X-World</title>
</head>
<body>
<!-- root 容器 -->
<div id="root"></div>
<!-- main 文件入口 -->
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
2)入口文件 main.tsx
文件位于 ./src/main.tsx,负责将 react 全局主组件 App 注入到 index.html 的 root 容器中.
理论上该文件是全局唯一一处会直接调用 document api 的地方,其余处应该使用 react 封装好的能力
import ReactDOM from"react-dom/client";
import {BrowserRouter} from"react-router-dom";
import App from"./App";
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<BrowserRouter>
<App/>
</BrowserRouter>
);
3)主组件文件 App.tsx
文件位于 ./src/App.tsx,将整个项目抽象为一个全局组件.
后续我们实现的所有内容,其实都是作为子组件注入到 App 当中
import React from"react";
import"./global.scss";
import Router from"./router";
const App: React.FC=()=>{
return(
<Router/>
)
};
export default App;
2.2 路由设计
xworld 的是单页面应用,但会通过 react router 实现对应于不同路径的多个视图,使得在用户视角下看起来像是完成了页面切换的效果.
其中共包含四个视图:登录页——英雄帖(/yingxiongtie)、页面A——思过崖(/siguoya)、页面B——藏经阁(/cangjingge)、页面C——开悟坡(/kaiwupo)
视图流转示意
在登录页中,通过登录可以跳转前往页面A——思过崖:
登录页->正式页
在页面 A/B/C 中:
• 通过打开右侧地图,可以完成页面切换
• 通过左上角,可以注销回到登录页
正式页切换&正式页->登录页
在 ./src/router/router.tsx 文件中,对上述路由关系进行了定义:
• 路径 /yingxiongtie 对应为登录页面
• 路径 / 对应为页面 A——思过崖
• 子路径 /cangjingge 对应为页面 B——藏经阁
• 子路径 /kaiwupo 对应为页面 C——开悟坡
• 除了上述以外的其他路径,统一导航到 /
/**
四个页面视图:英雄帖、思过崖、藏经阁、开悟坡
*/
import YingXiongTie from"../pages/yingxiongtie";
import SiGuoYa from"../pages/siguoya";
import CangJingGe from"../pages/cangjingge";
import KaiWuPo from"../pages/kaiwupo";
import {Info,Action} from"../services/msg";
/**
* 路由器声明
* - /yingxiongtie -> 英雄帖
* - / -> 思过崖
* - /cangjingge -> 藏经阁
* - /kaiwupo -> 开悟坡
* - * 重定向到 /
*
*/
const routes :RouteObject[]=[
{
path:"*",
element:<Navigate to="/"/>
},
{
path:"/yingxiongtie",
element:<YingXiongTie/>
},
{
path:"/",
element:<SiGuoYa/>,
children:[
{
path:"/cangjingge",
element:<CangJingGe/>
},
{
path:"/kaiwupo",
element:<KaiWuPo/>
}
]
}
];
/**
* 路由组件
*/
const Router :FC=()=>{
// outlet——路由渲染组件. 依据路由关系,基于 url 映射到对应组件. (方法来自 react-router-dom)
const outlet =useRoutes(routes);
// 获取路由位置信息. (方法来自 react-router-dom)
const location =useLocation();
// 获取登录用户信息
const user = sessionStorage.getItem("user");
// 已登录则跳转至正式页面 - 思过崖
if(user && location.pathname=="/yingxiongtie"){
return(<ToSiGuoYa/>);
}
// 未登录则跳转至登录页面 - 英雄帖
if(!user && location.pathname!="/yingxiongtie"){
return(<ToYingXiongTie/>);
}
return outlet;
};
export default Router;
此外,在 Router 组件中,完成了路由守卫逻辑:
• 对于已完成登录的用户(sessionStorage 存储了用户名),若访问登录页会被导航到页面A
• 对于未登录的用户(sessionStorage 未存储用户名),若访问登录页以外的页面,会被导航到登录页
为了配合上述逻辑,在后续实现的登录和注销流程中,会遵循下述步骤:
• 用户于登录页完成登录时,会将用户名设置到 sessionStorage 中
• 用户在正式页注销时,会将用户名从 sessionStorage 中清除
3 登录页面——英雄帖
3.0 相关知识点
• React FC(function component):通过函数风格实现的 react 组件类型.
react 组件有两大核心功能:1)props:由父组件单向传递给子组件的属性,实现组件通信;2)state:组件内部状态数据,当发生变化时会触发组件重新渲染.
在 FC 中,通过函数入参实现 props;在 react 16 版本后,通过 useState api 实现 state 功能
• promise:应用于异步逻辑的工具,代表了一个可能还未就绪的执行结果
promise 有三种状态:pending——未就绪;fullfilled——成功;rejected——失败,后两者代表了明确的结果
• await:仅限于 async 函数内使用,等待 promise 直至获取其最终结果
• axios:基于 promise 的 http 客户端库,用于浏览器和 node.js 环境
• 前后端跨域问题:在浏览器中,当一个请求的源(协议、域名、端口)与目标资源的源不一致时,浏览器出于安全考虑默认阻止这些跨域请求
本文中将采取代理的方式,通过改写 ip:port 以及 api 路径前缀的方式,解决跨域问题
• antd(ant-design):蚂蚁金服实现的 react ui 组件库,本项目中复用了 antd 的许多高质量组件,并通过修改 css 样式的方式来使其契合 xworld 项目的主题风格
• module.scss 文件:对于仅限局部生效的样式,通过 module.scss 支持其模块化管理,避免与全局样式冲突;对于需要全局生效的样式,定义在 global.scss 中,并于入口文件中统一导入
3.1 页面实现
登录页面——英雄帖
登录页面——英雄帖的代码位于 ./src/pages/yingxiongtie/index.tsx,其中:
• 将整个页面封装成一个 React 函数式组件(FC)的类型
• 与上图相对应,页面中依赖了 logincard 和 musiccard 两个组件
• 通过 yingxiongtie.module.scss 文件控制该页面的样式
// FC——react 函数式组件
import {FC} from"react";
// 音乐播放器组件
import MusicCard from"../../components/musiccard";
// 登录盒子组件
import LoginCard from"../../components/logincard";
// 英雄帖模块的专属样式
import styles from"./yingxiongtie.module.scss";
/**
* 登录页面——英雄帖
*/
const Page: FC=()=>{
return(
<div className={styles.page}>
{/* 组件一:登录盒子 */}
<LoginCard />
{/* 组件二:音乐播放器 */}
<MusicCard src="/audio/yingxiongtie.mp3"/>
</div>)
};
export default Page;
3.2 登录组件实现
选取登录盒子 logincard 组件,介绍其实现细节. 文件位于 ./component/logincard/index.tsx:
/**
* FC——react 函数式组件
* useState——创建 state 属性
*/
import React,{ FC, useState } from"react";
// react router 编程式路由导航组件
import { useNavigate } from"react-router-dom";
// antd 卡片组件
import { Card } from"antd";
// 向服务端发出登录请求的业务方法
import { Login } from"../../services/login";
// 自制的提示信息输出
import { Info,Action } from"../../services/msg";
// 登录盒子模块的专属样式
import styles from"./logincard.module.scss";
/**
* 登录卡组件
*/
const Comp :FC=()=>{
// 基于编程方式进行路由导航
const navigateTo =useNavigate();
// 将用户名与密码记录到 state 中
const[user, setUser]= useState<string>("");
const[passwd, setPasswd]= useState<string>("");
// 更新用户名
const recordUser=(e: React.ChangeEvent<HTMLInputElement>)=>{
setUser(e.target.value);
};
// 更新密码
const recordPasswd=(e: React.ChangeEvent<HTMLInputElement>)=>{
setPasswd(e.target.value);
};
// 登录处理
const toLogin=async()=>{
// 向服务端发起登录请求
const{errno, errmsg}=await Login(user,passwd);
if(errno !=0){
Info(Action.error,errmsg);
return;
}
// 登录成功,设置用户信息
sessionStorage.setItem("user",user);
Info(Action.navigate,"启程成功~");
navigateTo("/");
};
return(
<Card
hoverable
className={styles.logincard}
>
<form>
<h2>英雄帖</h2>
<p>相逢意气为君饮,系马高楼垂柳边</p>
{/**input 框内容变化时,执行回调函数将内容更新到 state 中 */}
<input type="text" placeholder="输入姓名" onChange={recordUser}/>
<input type="password" placeholder="输入暗号" onChange={recordPasswd}/>
{/**
* 按钮被点击时,执行 toLogin 方法:
* 1)向服务端发送请求,校验用户信息合法性
* 2)若校验通过使用 navigate 导航到正式页面
*/}
<input type="button" value="启程" onClick={toLogin}/>
</form>
</Card>
);
};
export default Comp;
其中核心点包括:
• 使用 react router 下的 useNavigate 实现编程时导航能力,当用户登录成功后,导航至页面 A——思过崖
• 使用 react 的 useState 实现 FC state 管理能力,注册监听输入内容的回调函数,将用户输入的用户名和密码存储到 state 中
• 监听用户点击登录按钮事件,通过 services/login 模块向服务端发起登录校验请求,若通过,则使用 react router 下的 useNavigate 实现编程导航能力,导致至页面A——思过崖
3.3 登录请求实现
前端代理流程示意
在项目 ./vite.config.ts 文件中,添加代理配置,将发送至 localhost:3000/api 的请求代理到服务端真实地址 localhost:8080
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins:[react()],
server:{
proxy:{
'/api':{
target:'http://localhost:8080',
changeOrigin:true,
rewrite:(path) => path.replace(/^/api/,''),
},
},
},
})
在 ./src/services/index.ts 文件中:
• 创建 axios 实例,并将服务端请求路径设置为 localhost:3000/api
• 由于 localhost:3000 与浏览器同源,不会被浏览器执行跨域限制
• 通过 ./vite.config.ts 代理配置,实现将 localhost:3000/api 改写为 localhost:8080 的效果
import axios from "axios";
// 创建 axios 实例
const Request = axios.create({
// 服务端请求路径
baseURL:"http://localhost:3000/api",
// 超时时间 单位 ms
timeout:5000
});
// 请求参数 filter
Request.interceptors.request.use(
config =>{
// ... 针对 get 请求参数去转义
return config;
},
err =>{
Promise.reject(err);
}
);
// 响应参数 filter
Request.interceptors.response.use(
res =>{
return res.data;
},
err=>{
return Promise.reject(err);
}
);
export default Request;
向服务端发送登录校验请求(post)的代码位于 ./src/services/login/index.ts,其中依赖到 ./src/services/index.ts 导出的 axios 实例:
import Request from"..";
// 登录响应参数
export interface LoginResp{
// 错误码 0——成功 其他——失败
errno:number
// 错误信息
errmsg:string
};
/**
* @brief 向服务端发送登录请求
* @param user ——用户名
* @param passwd ——密码
* @returns LoginResp
*/
export const Login = async(user:string,passwd:string):Promise<LoginResp>=>{
return Request.post("/xworld/login",{"user": user,"passwd": passwd});
};
4 页面一——思过崖
4.0 相关知识点
• react useEffect:react 提供的组件生命周期回调函数,默认执行时机包含 componentDidMount、componentDidUpdate和componentWillUnmount 几个节点
react 组件生命周期与回调函数
react-spring:用于构建交互式、数据驱动和动画 UI 组件的库,xworld 中使用到了 react-spring 提供的平滑过渡实现组件渲染的效果.
4.1 页面实现
思过崖页面样式
页面A——思过崖的代码位于 ./src/pages/siguoya/index.tsx
// FC——react 函数式组件
import {FC} from "react";
// outlet——渲染工具:路由->组件 useLocation——编程式路由导航组件
import { Outlet, useLocation }from"react-router-dom";
// 组件一
import UserCard from "../../components/usercard";
// 组件二
import MusicCard from "../../components/musiccard";
// 组件三
import MapCard from "../../components/mapcard";
// 组件四
import PracticeHeat from "../../components/practiceheat";
// 组件五
import ViewFlips from "../../components/viewflip";
// 组件六
import FootPrints from "../../components/footprint";
// 专属样式
import styles from"./siguoya.module.scss"
/**
* 思过崖视图
*/
const Page :FC=()=>{
const location =useLocation();
return(
<div>
{/** 组件一 */}
<UserCard/>
{/** 组件二 */}
<MusicCard src="/audio/siguoya.mp3"/>
{/** 组件三 */}
<MapCard/>
<Outlet/>
{
location.pathname =="/"&&
<div className={styles.page}>
{/** 组件四 */}
<PracticeHeat/>
{/** 组件五 */}
<ViewFlips/>
{/** 组件六 */}
<FootPrints/>
</div>
}
</div>)
};
export default Page;
其中:
• 藏经阁 /cangjingge 和开悟坡 /kaiwupo 都是当前页的子路由(见2.2小节路由器部分代码),因此组件内容采用 react outlet 组件展示
• 思过崖 /siguoya 将 usercard、musiccard、mapcard 作为与 outlet 并列平级的组件,因此这部分内容将无需在藏经阁与开悟坡中重复处理,因为它们属于并列关系而非从属关系
• 使用 react useLocation 能力查看当前路径,如果为 / 则正常渲染思过崖下的 practiceheat、viewflip、footprint 三个组件;否则通过 outlet 渲染路径对应的页面
4.2 热点图组件实现
修行热点图 practiceheat 组件位于 ./src/components/practiceheat/index.tsx
// FC——react 函数式组件 useEffect——组件生命周期回调函数 useState——组件 state 属性
import { FC, useEffect, useState } from"react";
// 热点图第三方组件
import HeatMap from "@uiw/react-heat-map";
// antd 卡片组件
import { Card } from "antd";
// 通用信息提示方法
import { Info,Action } from"../../services/msg";
// 向服务端请求获取热点记录方法
import { GetPracticeHeats,PracticeHeat } from"../../services/user";
// 组件专属样式
import styles from"./practiceheat.module.scss";
/**
* 用户修行热点图组件
*/
const Comp :FC=()=>{
// state属性:practiceHeats——热点记录
const[practiceHeats, setPracticeHeats]= useState<PracticeHeat[]>([]);
// 组件渲染、更新、卸载环节执行的回调钩子函数
useEffect(()=>{
// 请求服务端获取到该用户对应的热点记录,并设置到 state 中
const getPracticeHeats=async()=>{
const{errno, errmsg, data}=await GetPracticeHeats(sessionStorage.getItem("user")asstring);
if(errno !=0){
Info(Action.error, errmsg);
return;
}
setPracticeHeats(data);
}
getPracticeHeats();
},[]);
return(
<Card
className={styles.card}
>
<h2>修行热点图</h2>
{/** 读取 state:practiceHeats 渲染到热力图中*/}
<HeatMap
className={styles.heatmap}
value={practiceHeats}
weekLabels={['日曜','月曜','火曜','水曜','木曜','金曜','土曜']}
monthLabels={['小寒','立春','惊蛰','清明','立夏','芒种','小暑','立秋','白露','寒霜','立冬','大雪']}
startDate={new Date('2024/01/01')}
/>
</Card>
);
};
export default Comp;
其中:
• 通过 useEffect 实现在组件挂载和更新时,通过 services/user 向服务端请求获取到用户热点数据,并设置到 state:practiceHeats 中
• 使用 uiw/react-heat-map 库的能力,实现热点图的构建,并将 practiceHeats 作为数据源注入其中
• 通过文件 practiceheat.module.scss 实现组件的模块化样式管理
4.3 风景动画组件实现
思过崖风景动画 viewflip 组件位于 ./src/components/viewflip/index.tsx
// FC——react 函数式组件 useEffect——组件生命周期回调函数 useState——组件 state 属性
import { FC, useState, useEffect } from"react";
// antd 卡片、图片组件
import { Card,Image } from "antd";
// react-spring 动画组件
import { useSpring, animated } from"@react-spring/web";
// 模块专属样式
import styles from "./viewflip.module.scss";
/**
* 思过崖四季风景图
*/
const views :JSX.Element[]=[
(// ... 组件:春季风景图),
(// ...组件:夏季风景图),
(// ...组件:秋季风景图),
(// ...组件:风景风景图)
]
/**
* 思过崖风景动图组件
* 1)实现四级风景动态轮换
* 2)实现图片平滑切换效果
*/
const Comp :FC=()=>{
// state 记录当前展示图片 index
const[page, setPage]= useState<number>(0);
/**
* 组件渲染、更新、卸载环节执行的回调钩子函数
* 1)创建定时器,控制每 6 秒切换一次 page index,实现图片切换
* 2)组件卸载前,回收定时器
*/
useEffect(()=>{
const interval =setInterval(()=>{
setPage((prev)=>(prev +1)%4);
},6000);
// 退出前清理定时器
return()=>{
clearInterval(interval);
};
},[]);
// 设置动画平滑过渡效果
const animation =useSpring({
to:{opacity:1},
from:{opacity:0},
config:{duration:3500},// 平滑过渡时长 3.5s
reset:true// 重复执行
});
return(
<Card
className={styles.card}
>
{
views.map((view: JSX.Element)=>(
// 根据 state 记录的 index 控制应该渲染的风景图
view.key == page.toString()&&
<animated.div
key={view.key}
style={animation}
>
{view}
</animated.div>
))}
</Card>
);
};
export default Comp;
其中:
• 将思过崖四季风景图制作成一个 list,并使用 state:page 作为当前展示的风景图索引
• 在 useEffect 方法中,设置定时器每 6 秒更新一次 state:page,并在组件卸载前完成定时器资源的回收
• 使用 react-spring 的能力,实现风景切换时的平滑过渡效果
5 页面二——藏经阁
5.0 相关知识点
antd table:是 antdesign 实现的一个功能丰富、用于数据展示的表格组件. 用于表格类组件在日常工作中比较常用,且该组件在结构和样式控制上都比较复杂,所以藏经阁页面中特别引入该组件,提供一次实操机会.
5.1 页面实现
藏经阁页面样式
页面B——藏经阁的代码位于 ./src/pages/cangjingge/index.tsx,与上图相对应,其中引入了 titlecard 和 booktable 两个组件,其中 booktable 是核心内容.
// react 函数式组件
import {FC} from "react";
// 组件一
import TitleCard from "../../components/cangjinggetitle";
// 组件二
import BookTable from "../../components/booktable";
// 模块专属样式
import styles from "./cangjingge.module.scss";
/**
* 藏经阁视图
*/
const Page :FC=()=>{
return(
<div className={styles.page}>
{/** 组件一 */}
<TitleCard/>
{/** 组件二 */}
<BookTable/>
</div>);
};
export default Page;
5.2 书籍表格组件实现
藏经阁书籍表格 booktable 组件位于 ./src/components/booktable/index.tsx
// FC——react 函数式组件 useEffect——组件生命周期回调函数 useState——组件 state 属性
import { FC, useState, useEffect } from"react";
// Table——表格组件 Tag——标签组件 Popconfirm——确认弹窗组件 Select——选择器组件
import { Table,Tag,Popconfirm,Select } from"antd";
// 项目内实现的提示组件
import {Info,Action} from "../../services/msg";
// 向服务端请求获取书籍列表的业务方法
import {GetBooks,Book,BookCategory} from "../../services/book";
// 模块专属样式
import styles from "./booktable.module.scss";
// 表格中的列
const {Column} = Table;
/**
* 书籍表格组件
*/
const Comp :FC=()=>{
// state 数据:books——查询得到的书籍列表
const[books, setBooks]= useState<Book[]>([]);
// state 数据:用户检索前设置的书籍品类查询条件
const[category, setCategory]= useState<BookCategory>(BookCategory.all);
// state 数据:用户检索前输入的书籍名称查询条件
const[name, setName]= useState<string>("");
// 组件渲染、更新、卸载环节执行的回调钩子函数
useEffect(()=>{
getBooks();
},[]);
// 请求服务端获取到书籍列表,并设置到 state 中
constgetBooks:()=>void=async()=>{
const{errno, errmsg, data}=await GetBooks({name,category});
if(errno !=0){
Info(Action.error, errmsg);
return;
}
setBooks(data);
};
// ...
// 响应用户借阅操作的回调函数
const subscribeBook=(book: Book)=>{
Info(Action.jieyue,"借阅「"+book.name+"」失败,借阅数量已达上限!");
};
return(
<div>
{/** 表格 */}
<div className={styles.booktable}>
<Table<Book>
scroll={{ x:720, y:500}}
dataSource={books}
// 分页设置
pagination={{
pageSize:4,
showSizeChanger:false,
className:"ant-table-pager",
showLessItems:true
}}
locale={{ emptyText:'目标书籍不存在'}}
>
{/** 表格中的列
* 默认以 dataIndex 获取字段进行展示
* 可以通过 render 方法改写渲染逻辑
*/}
{/**... */}
<Column
title="品类"
dataIndex="bookTypes"
key="bookTypes"
width={140}
render={(bookTypes: string[])=>{
return(
// 品类渲染逻辑改写
);
}}
/>
<Column
title="操作"
key="action"
width={100}
render={(_: any, book: Book)=>{
return(
// 渲染借阅按钮,并注册对应的交互行为
<Popconfirm
className={styles.popconfirm}
overlayClassName={styles.popconfirmOverlay}
placement="leftTop"
title={"确认借阅「"+ book.name + "」吗?"}
onConfirm={()=>{subscribeBook(book)}}
okText="是"
cancelText="否"
>
<button className={styles.button}>借阅</button>
</Popconfirm>
);
}}
/>
</Table>
</div>
</div>
);
};
export default Comp;
其中:
• 设置了三个 state 字段,分别为检索到的书籍列表、用户输入的书籍名称以及书籍品类
• 在 useEffect 中以及用户点击检索按钮时,向服务端发送请求,获取书籍数据,并设置到 state 中
• 以 state 中存储的书籍数据作为 table 的数据源进行展示
• 针对 table 中的列 column,可以通过实现 render 方法,来控制展示的内容以及样式
• 针对 table 可以通过 pagination 来控制分页格式
6 页面三——开悟坡
6.0 相关知识点
• props + currying(函数柯里化) 实现父子双向通信:由于父子组件只能通过 props 实现父 -> 子的单向通信,可以采用函数柯里化的思路,在父组件向子组件传入的 props 中注入闭包函数,使得父组件有机会从闭包中获取到子组件更新的内容,实现逻辑意义上的子 -> 父通信
函数柯里化是函数式编程中的概念,指的是将一个多参数的函数转换成一系列使用一个或多个参数的函数的过程
• react-monaco-editor:基于 react typescript 实现的开源库,主要用于在应用程序中继承代码编辑器组件
6.1 页面实现
开悟坡页面样式
页面C——开悟坡的代码位于 ./src/pages/kaiwupo.tsx,与上图相对应,其中引入了 titlecard 和 bookbag 两个组件,其中 bookbag 是核心内容,随后会展开介绍
// FC——react 函数式组件
import {FC} from "react";
// 组件一
import TitleCard from "../../components/kaiwupotitle";
// 组件二
import BookBag from "../../components/bookbag";
// 模块专属样式
import styles from "./kaiwupo.module.scss";
/**
* 开悟坡视图
*/
const Page :FC=()=>{
return(
<div className={styles.page}>
{/** 组件一 */}
<TitleCard/>
{/** 组件二 */}
<BookBag
name1="六脉神剑"
name2="九阴真经"
name3="计算广告"
name4="经济学原理"
/>
</div>
);
};
export default Page;
6.2 书籍行囊组件实现
开悟坡书籍行囊 bookbag 组件位于 ./src/components/bookbag/index.tsx,其中:
• 通过 props 接收来自父组件的传参,对应为展示的四本书籍的名称
• 引入更底层的 BookContent(书籍页)以及 BookCard(书籍悬浮卡)两个组件
• 通过 state:openBook 来控制 BookContent 组件是否展示以及展示的书籍内容 1)如果为 null 则关闭书籍页;2)否则书籍页依照数据渲染对应的内容
• 渲染 BookCard 组件时,以 props.name 传入书籍名称,并以 props.onClick 传入闭包函数,使得当作为子组件的某个 BookCard 被点击时,会将对应的书籍内容更新到父组件的 state:openBook 属性中
• 仅当 openBook 不为 null 时渲染 BookContent 组件,将 openBook 内容作为 props 传入 BookContent 中,并以 props.onClose 传入闭包函数,使得当作为子组件的 BookContent 被点击关闭时,会将父组件的 state: openBook 设置为 null,从而停止 BookContent 组件的渲染
/**
* FC——react 函数式组件
* useState——创建 state 属性
*/
import { FC, useState } from "react";
import { BookContent as Book } from "../../services/book";
// 书籍悬浮卡组件
import BookCard from "../bookcard";
// 点击书籍悬浮卡后,展开进行阅读的书籍内容组件
import BookContent from "../bookcontent";
// 模块专属样式
import styles from "./bookbag.module.scss";
// 定义 bookbag 的 properties. 要求使用方传入对应参数
interface Props{
// 四本书籍名称
name1:string
name2:string
name3:string
name4:string
}
/**
* 书籍行囊组件
*/
const Comp :FC<Props>=({name1, name2, name3, name4}:Props)=>{
// state:记录当前打开阅读的书籍
const[openBook, setOpenBook]= useState<Book|null>(null);
return(
<div>
{/** 依次排列四个书籍悬浮卡组件 */}
<div className={styles.book1}>
<BookCard
// 设置书籍名称
name={name1}
// 设置点击动作回调函数
onClick={(book: Book)=>{setOpenBook(book)}}
/>
</div>
{/** 依次展开其余三个书籍悬浮卡*/}
{/** ... */}
{/** 正在阅读的书籍内容组件 */}
<div className={styles.bookcontent}>
{openBook &&(
<BookContent
// 设置书籍内容
content={openBook.content}
// 设置书籍语言
language={openBook.language}
// 设置可见性
visible={openBook!=null}
// 设置关闭动作回调函数
onClose={()=>{setOpenBook(null)}}
/>
)}
</div>
</div>
);
};
export default Comp;
BookCard 组件的代码位于 ./src/component/bookcard/index.tsx:
// FC——react 函数式组件 useEffect——组件生命周期回调函数 useState——组件 state 属性
import { FC, useEffect, useState } from "react";
import { Card,Image } from 'antd';
import { GetBook,BookContent as Book } from "../../services/book";
import { Info,Action } from"../../services/msg";
import styles from"./book.module.scss";
// bookcard 的 properties
export interface Props{
// 书籍名称
name:string
// 点击书籍时执行的回调函数
onClick:(book: Book)=>void
}
/**
* 秘籍组件
*/
const Comp :FC<Props>=({name, onClick}: Props)=>{
// state 缓存查询得到的书籍
const[book, setBook]= useState<Book|null>(null);
/** 生命周期 hook 函数
* 根据传入的 props.name 查询得到书籍信息,并设置到 state:book 中
*/
useEffect(()=>{
const getBook=async()=>{
const {errno, errmsg, data}=await GetBook(name);
if(errno !=0){
Info(Action.error,errmsg);
return;
}
setBook(data);
};
getBook();
},[]);
return(
book &&(
<Card
hoverable
className={styles.card}
// 用户点击卡片时,执行 props.onClick 函数,并将缓存的 state:book 传入
onClick={()=>onClick(book)}
>
<Image className={styles.image} src={book.img} preview={false}/>
<h3>{name}</h3>
<p>{book.description}</p>
<button>开始修习</button>
</Card>
)
);
};
export default Comp;
BookContent 的代码位于 ./src/components/bookcontent/index.tsx,其中应用到 react-monaco-editor,以代码编辑器组件作为承载书籍内容的容器
import { FC } from "react";
import { Card } from "antd";
// 代码编辑器
import MonacoEditor from "react-monaco-editor";
import styles from"./bookcontent.module.scss";
// bookcontent properties
export interface Props{
// 书籍内容
content:string
// 是否可见
visible:boolean
// 语言
language:string
// 用户点击关闭时的回调函数
onClose:()=>void
}
/**
* 正在阅读的书籍内容组件
*/
const Comp :FC<Props>=({content, visible,language, onClose} : Props) =>{
return(
// 根据 visible 控制该组件是否可见
visible &&
<Card
className={styles.card}
>
<MonacoEditor
width="620px"
height="720px"
// 根据传入的 props.language 设置书籍语言
language={language}
// 根据传入的 props.content 设置书籍内容
value={content}
/>
{/** */}
</Card>
);
};
export default Comp;
7 总结
本文介绍了我在近期初学前端知识后,基于 react-typescript+vite 实现的网页前端项目——xworld:
页面地址已开放,欢迎来体验(需要用电脑浏览器):
地址:X-World (http://106.14.133.57:3000/)
账号:令狐冲 密码:xajh
本文重点探讨了:
• 从零到一搭建运行 react typescript 项目的环境
• 基于 react-router-dom 技术实现单页面应用下视图的导航切换
• 应用 react 函数式组件完成页面下各模块的拆解,利用到 state 和 props 两个核心特性,满足一系列交互功能的诉求
• 使用到 axios 作为 http 请求客户端,通过设置代理打通前后端交互流程
• 从 ant-design ui 库中引入了一系列现成的高质量组件,并通过 scss 文件完成样式的调整,使得其能够契合 xworld 项目的主题风格