学习内容来源:React + React Hook + TS 最佳实践-慕课网
相对原教程,我在学习开始时(2023.03)采用的是当前最新版本:
项 | 版本 |
---|---|
react & react-dom | ^18.2.0 |
react-router & react-router-dom | ^6.11.2 |
antd | ^4.24.8 |
@commitlint/cli & @commitlint/config-conventional | ^17.4.4 |
eslint-config-prettier | ^8.6.0 |
husky | ^8.0.3 |
lint-staged | ^13.1.2 |
prettier | 2.8.4 |
json-server | 0.17.2 |
craco-less | ^2.0.0 |
@craco/craco | ^7.1.0 |
qs | ^6.11.0 |
dayjs | ^1.11.7 |
react-helmet | ^6.1.0 |
@types/react-helmet | ^6.1.6 |
react-query | ^6.1.0 |
@welldone-software/why-did-you-render | ^7.0.1 |
@emotion/react & @emotion/styled | ^11.10.6 |
具体配置、操作和内容会有差异,“坑”也会有所不同。。。
- 一、项目起航:项目初始化与配置
- 二、React 与 Hook 应用:实现项目列表
- 三、 TS 应用:JS神助攻 - 强类型
- 四、 JWT、用户认证与异步请求(上)
- 四、 JWT、用户认证与异步请求(下)
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(上)
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(下)
- 六、用户体验优化 - 加载中和错误状态处理(上)
- 六、用户体验优化 - 加载中和错误状态处理(中)
- 六、用户体验优化 - 加载中和错误状态处理(下)
- 七、Hook,路由,与 URL 状态管理(上)
- 七、Hook,路由,与 URL 状态管理(中)
- 七、Hook,路由,与 URL 状态管理(下)
- 八、用户选择器与项目编辑功能(上)
- 八、用户选择器与项目编辑功能(下)
- 九、深入React 状态管理与Redux机制(一)
- 九、深入React 状态管理与Redux机制(二)
- 九、深入React 状态管理与Redux机制(三)
- 九、深入React 状态管理与Redux机制(四)
- 九、深入React 状态管理与Redux机制(五)
- 十、用 react-query 获取数据,管理缓存(上)
- 十、用 react-query 获取数据,管理缓存(下)
之前的项目详情进入看板页的路由有个小问题,点击浏览器返回按钮回不去,原因如下:
push
一个新路由进去,当点击返回,就会将上一个路由置于栈顶;而进入项目详情页(从'projects'
到'projects/1'
)默认重定向子路由是看板页(projects/1/viewboard
),返回上一个路由时,默认又会重定向到看板页路由。列表栈示例如下:['projects', 'projects/1', 'projects/1/viewboard']
接下来解决一下这个问题,编辑 src\screens\ProjectDetail\index.tsx
(重定向标签新增属性 replace
,在重定向时直接替换原路由):
...
export const ProjectDetail = () => {
return (
<div>
...
<Routes>
...
<Route index element={<Navigate to="viewboard" replace/>} />
</Routes>
</div>
);
};
为了方便后续类型统一调用,将 src\screens\ProjectList\components\List.tsx
中 interface Project
提取到 src\types
目录下
视频中 是用 WebStorm ,博主用的是 VSCode:
Move to a new file
,默认同变量名的文件会创建在当前文件所在同一级目录下,其他引用位置也相应改变,涉及引用位置:
src\utils\project.ts
src\screens\ProjectList\components\SearchPanel.tsx
src\screens\ProjectList\components\List.tsx
src\types
目录下,可以看到其他引用位置也相应改变
- 相关功能文档:TypeScript Programming with Visual Studio Code
src\screens\ProjectList\components\SearchPanel.tsx
中 interface User
也执行同样操作,涉及引用位置:
src\screens\ProjectList\components\SearchPanel.tsx
src\screens\ProjectList\components\List.tsx
src\auth-provider.ts
src\context\auth-context.tsx
src\utils\use-users.ts
看板页还需要以下两个类型,新建一下:
src\types\Viewboard.ts
:export interface Viewboard {
id: number;
name: string;
projectId: number;
}
src\types\Task.ts
export interface Task {
id: number;
name: string;
projectId: number;
processorId: number; // 经办人
taskGroupId: number; // 任务组
kanbanId: number;
typeId: number; // bug or task
note: string;
}
接下来创建数据请求的 hook:
src\utils\viewboard.ts
:
import { cleanObject } from "utils";
import { useHttp } from "./http";
import { Viewboard } from "types/Viewboard";
import { useQuery } from "react-query";
export const useViewboards = (param?: Partial<Viewboard>) => {
const client = useHttp();
return useQuery<Viewboard[]>(["viewboards", param], () =>
client("kanbans", { data: cleanObject(param || {}) })
);
};
src\utils\task.ts
:
import { cleanObject } from "utils";
import { useHttp } from "./http";
import { Task } from "types/Task";
import { useQuery } from "react-query";
export const useTasks = (param?: Partial<Task>) => {
const client = useHttp();
return useQuery<Task[]>(["tasks", param], () =>
client("tasks", { data: cleanObject(param || {}) })
);
};
接下来开始开发看板列表,展示需要用到项目数据,可以提取一个从 url
获取 projectId
,再用 id
获取项目数据的 hook
新建 src\screens\ViewBoard\utils.ts
:
import { useLocation } from "react-router"
import { useProject } from "utils/project"
export const useProjectIdInUrl = () => {
const { pathname } = useLocation()
const id = pathname.match(/projects\/(\d+)/)?.[1]
return Number(id)
}
export const useProjectInUrl = () => useProject(useProjectIdInUrl())
export const useViewBoardSearchParams = () => ({projectId: useProjectIdInUrl()})
export const useViewBoardQueryKey = () => ['viewboards', useViewBoardSearchParams()]
export const useTasksSearchParams = () => ({projectId: useProjectIdInUrl()})
export const useTasksQueryKey = () => ['tasks', useTasksSearchParams()]
注意:每一个
useXXXQueryKey
都要确保返回值第一项 与后续列表请求useXXX
中useQuery
的第一个参数保持一致,否则后续增删改都无法正常自动重新请求列表,问题排查比较困难
为看板定制一个展示列组件(任务列表),供每个类型来使用
新建 src\screens\ViewBoard\components\ViewboardCloumn.tsx
:
import { Viewboard } from "types/Viewboard";
import { useTasks } from "utils/task";
import { useTasksSearchParams } from "../utils";
export const ViewboardColumn = ({viewboard}:{viewboard: Viewboard}) => {
const { data: allTasks } = useTasks(useTasksSearchParams())
const tasks = allTasks?.filter(task => task.kanbanId === viewboard.id)
return <div>
<h3>{viewboard.name}</h3>
{
tasks?.map(task => <div key={task.id}>{task.name}</div>)
}
</div>
}
编辑 src\screens\ViewBoard\index.tsx
:
import { useDocumentTitle } from "utils";
import { useViewboards } from "utils/viewboard";
import { useProjectInUrl, useViewBoardSearchParams } from "./utils";
import { ViewboardColumn } from "./components/ViewboardCloumn"
import styled from "@emotion/styled";
export const ViewBoard = () => {
useDocumentTitle('看板列表')
const {data: currentProject} = useProjectInUrl()
const {data: viewboards, } = useViewboards(useViewBoardSearchParams())
return <div>
<h1>{currentProject?.name}看板</h1>
<ColumnsContainer>
{
viewboards?.map(vbd => <ViewboardColumn viewboard={vbd} key={vbd.id}/>)
}
</ColumnsContainer>
</div>;
};
const ColumnsContainer = styled.div`
display: flex;
overflow: hidden;
margin-right: 2rem;
`
通过代码可知:viewboards.map 后 ViewboardColumn 渲染多次,其中 useTasks 也同时执行多次,但是仔细看浏览器开发者工具可发现,相应请求并没有执行多次,而是只执行了一次,这是因为 react-query 的缓存机制(默认两秒内发送的多个key相同且的参数相同的请求只执行最后一次)
访问看板列表可看到如下内容且三种状态任务横向排列即为正常:
待完成
管理登录界面开发
开发中
管理注册界面开发
权限管理界面开发
UI开发
自测
已完成
单元测试
性能优化
任务的类型接口并不直接返回,而是只返回一个 typeId,并不能明确标识任务类型,需要单独访问接口来获取具体任务类型
新建 src\types\TaskType.ts
:
export interface TaskType {
id: number;
name: string;
}
新建 src\utils\task-type.ts
:
import { useHttp } from "./http";
import { useQuery } from "react-query";
import { TaskType } from "types/TaskType";
export const useTaskTypes = () => {
const client = useHttp();
return useQuery<TaskType[]>(["taskTypes"], () =>
client("tasks")
);
};
将以下两个 svg 文件拷贝到 src\assets
bug.svg
<svg xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xlinkHref="http://www.w3.org/1999/xlink">
<title>bugtitle>
<desc>Created with Sketch.desc>
<defs/>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="bug" sketch:type="MSArtboardGroup">
<g id="Bug" sketch:type="MSLayerGroup" transform="translate(1.000000, 1.000000)">
<rect id="Rectangle-36" fill="#E5493A" sketch:type="MSShapeGroup" x="0" y="0" width="14" height="14" rx="2"/>
<path d="M10,7 C10,8.657 8.657,10 7,10 C5.343,10 4,8.657 4,7 C4,5.343 5.343,4 7,4 C8.657,4 10,5.343 10,7" id="Fill-2" fill="#FFFFFF" sketch:type="MSShapeGroup"/>
g>
g>
g>
svg>
task.svg
<svg xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" width="16px" height="16px" viewBox="0 0 16 16" version="1.1">
<title>tasktitle>
<desc>Created with Sketch.desc>
<defs/>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="task" sketch:type="MSArtboardGroup">
<g id="Task" sketch:type="MSLayerGroup" transform="translate(1.000000, 1.000000)">
<rect id="Rectangle-36" fill="#4BADE8" sketch:type="MSShapeGroup" x="0" y="0" width="14" height="14" rx="2"/>
<g id="Page-1" transform="translate(4.000000, 4.500000)" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" sketch:type="MSShapeGroup">
<path d="M2,5 L6,0" id="Stroke-1"/>
<path d="M2,5 L0,3" id="Stroke-3"/>
g>
g>
g>
g>
svg>
直接使用可能会有如下报错:
Compiled with problems:X
ERROR in ./src/assets/task.svg
Module build failed (from ./node_modules/@svgr/webpack/lib/index.js):
SyntaxError: unknown file: Namespace tags are not supported by default. React's JSX doesn't support namespace tags. You can set `throwIfNamespace: false` to bypass this warning.
把
skety:type
这种类型的标签属性改成sketchType
驼峰这样才能被JSX
接受。
- 编译有问题: ./src/assets/bug.svg 中的错误-慕课网
- reactjs - SyntaxError: unknown: Namespace tags are not supported by default - Stack Overflow
源 svg
文件 修改后的源码如下:
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xlinkHref="http://www.w3.org/1999/xlink" xmlnsSketch="http://www.bohemiancoding.com/sketch/ns">
<title>bugtitle>
<desc>Created with Sketch.desc>
<defs>defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketchType="MSPage">
<g id="bug" sketchType="MSArtboardGroup">
<g id="Bug" sketchType="MSLayerGroup" transform="translate(1.000000, 1.000000)">
<rect id="Rectangle-36" fill="#E5493A" sketchType="MSShapeGroup" x="0" y="0" width="14" height="14" rx="2">rect>
<path d="M10,7 C10,8.657 8.657,10 7,10 C5.343,10 4,8.657 4,7 C4,5.343 5.343,4 7,4 C8.657,4 10,5.343 10,7" id="Fill-2" fill="#FFFFFF" sketchType="MSShapeGroup">path>
g>
g>
g>
svg>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlnsSketch="http://www.bohemiancoding.com/sketch/ns">
<title>tasktitle>
<desc>Created with Sketch.desc>
<defs>defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketchType="MSPage">
<g id="task" sketchType="MSArtboardGroup">
<g id="Task" sketchType="MSLayerGroup" transform="translate(1.000000, 1.000000)">
<rect id="Rectangle-36" fill="#4BADE8" sketchType="MSShapeGroup" x="0" y="0" width="14" height="14" rx="2">rect>
<g id="Page-1" transform="translate(4.000000, 4.500000)" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" sketchType="MSShapeGroup">
<path d="M2,5 L6,0" id="Stroke-1">path>
<path d="M2,5 L0,3" id="Stroke-3">path>
g>
g>
g>
g>
svg>
编辑 src\screens\ViewBoard\components\ViewboardCloumn.tsx
(引入图标,并美化):
import { Viewboard } from "types/Viewboard";
import { useTasks } from "utils/task";
import { useTasksSearchParams } from "../utils";
import { useTaskTypes } from "utils/task-type";
import taskIcon from "assets/task.svg";
import bugIcon from "assets/bug.svg";
import styled from "@emotion/styled";
import { Card } from "antd";
const TaskTypeIcon = ({ id }: { id: number }) => {
const { data: taskTypes } = useTaskTypes();
const name = taskTypes?.find((taskType) => taskType.id === id)?.name;
if (!name) {
return null;
}
return <img alt='task-icon' src={name === "task" ? taskIcon : bugIcon} />;
};
export const ViewboardColumn = ({ viewboard }: { viewboard: Viewboard }) => {
const { data: allTasks } = useTasks(useTasksSearchParams());
const tasks = allTasks?.filter((task) => task.kanbanId === viewboard.id);
return (
<Container>
<h3>{viewboard.name}</h3>
<TasksContainer>
{tasks?.map((task) => (
<Card style={{marginBottom: '0.5rem'}} key={task.id}>
<div>{task.name}</div>
<TaskTypeIcon id={task.id} />
</Card>
))}
</TasksContainer>
</Container>
);
};
export const Container = styled.div`
min-width: 27rem;
border-radius: 6px;
background-color: rgb(244, 245, 247);
display: flex;
flex-direction: column;
padding: .7rem .7rem 1rem;
margin-right: 1.5rem;
`
const TasksContainer = styled.div`
overflow: scroll;
flex: 1;
::-webkit-scrollbar {
display: none;
}
`
部分引用笔记还在草稿阶段,敬请期待。。。