文章目录
- 一、项目起航:项目初始化与配置
- 二、React 与 Hook 应用:实现项目列表
- 三、TS 应用:JS神助攻 - 强类型
- 四、JWT、用户认证与异步请求
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
- 六、用户体验优化 - 加载中和错误状态处理
- 七、Hook,路由,与 URL 状态管理
- 八、用户选择器与项目编辑功能
- 九、深入React 状态管理与Redux机制
- 十、用 react-query 获取数据,管理缓存
- 十一、看板页面及任务组页面开发
- 1~3
- 4~6
- 7&8
- 9&10
- 11&12
- 13.任务组页面 (下)
- 14.完成 popover
- 15.开发完成,部署页面
学习内容来源: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 应用:实现项目列表
- 二、React 与 Hook 应用:实现项目列表
三、TS 应用:JS神助攻 - 强类型
- 三、 TS 应用:JS神助攻 - 强类型
四、JWT、用户认证与异步请求
- 四、 JWT、用户认证与异步请求(上)
- 四、 JWT、用户认证与异步请求(下)
五、CSS 其实很简单 - 用 CSS-in-JS 添加样式
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(上)
- 五、CSS 其实很简单 - 用 CSS-in-JS 添加样式(下)
六、用户体验优化 - 加载中和错误状态处理
- 六、用户体验优化 - 加载中和错误状态处理(上)
- 六、用户体验优化 - 加载中和错误状态处理(中)
- 六、用户体验优化 - 加载中和错误状态处理(下)
七、Hook,路由,与 URL 状态管理
- 七、Hook,路由,与 URL 状态管理(上)
- 七、Hook,路由,与 URL 状态管理(中)
- 七、Hook,路由,与 URL 状态管理(下)
八、用户选择器与项目编辑功能
- 八、用户选择器与项目编辑功能(上)
- 八、用户选择器与项目编辑功能(下)
九、深入React 状态管理与Redux机制
- 九、深入React 状态管理与Redux机制(一)
- 九、深入React 状态管理与Redux机制(二)
- 九、深入React 状态管理与Redux机制(三)
- 九、深入React 状态管理与Redux机制(四)
- 九、深入React 状态管理与Redux机制(五)
十、用 react-query 获取数据,管理缓存
- 十、用 react-query 获取数据,管理缓存(上)
- 十、用 react-query 获取数据,管理缓存(下)
十一、看板页面及任务组页面开发
1~3
- 十一、看板页面及任务组页面开发(一)
4~6
- 十一、看板页面及任务组页面开发(二)
7&8
- 十一、看板页面及任务组页面开发(三)
9&10
- 十一、看板页面及任务组页面开发(四)
11&12
- 十一、看板页面及任务组页面开发(五)
13.任务组页面 (下)
新建 src\screens\TaskGroup\components\createTaskGroup.js
:
import React, { useEffect } from "react";
import { Button, Drawer, Form, Input, Spin } from "antd";
import { DrawerProps } from "antd/es/drawer";
import styled from "@emotion/styled";
import { ErrorBox } from "components/lib";
import { useAddTaskGroup } from "utils/taskGroup";
import { useTaskGroupsQueryKey } from "screens/TaskGroup/utils";
import { useForm } from "antd/es/form/Form";
import { useProjectIdInUrl } from "screens/ViewBoard/utils";
export const CreateTaskGroup = (
props: Pick<DrawerProps, "visible"> & { onClose: () => void }
) => {
const { mutate: addEpic, isLoading, error } = useAddTaskGroup(useTaskGroupsQueryKey());
const [form] = useForm();
const projectId = useProjectIdInUrl();
const onFinish = async (values: any) => {
await addEpic({ ...values, projectId });
props.onClose();
};
useEffect(() => {
form.resetFields();
}, [form, props.visible]);
return (
<Drawer
visible={props.visible}
onClose={props.onClose}
forceRender={true}
destroyOnClose={true}
width={"100%"}
>
<Container>
{isLoading ? (
<Spin size={"large"} />
) : (
<>
<h1>创建任务组</h1>
<ErrorBox error={error} />
<Form
form={form}
layout={"vertical"}
style={{ width: "40rem" }}
onFinish={onFinish}
>
<Form.Item
label={"名称"}
name={"name"}
rules={[{ required: true, message: "请输入任务组名" }]}
>
<Input placeholder={"请输入任务组名称"} />
</Form.Item>
<Form.Item style={{ textAlign: "right" }}>
<Button
loading={isLoading}
type={"primary"}
htmlType={"submit"}
>
提交
</Button>
</Form.Item>
</Form>
</>
)}
</Container>
</Drawer>
);
};
const Container = styled.div`
height: 80vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;
编辑 src\screens\TaskGroup\index.tsx
(新增创建任务组功能):
...
import { CreateTaskGroup } from "./components/createTaskGroup";
export const TaskGroupIndex = () => {
...
const [epicCreateOpen, setEpicCreateOpen] = useState(false);
...
return (
<ViewContainer>
<Row between={true}>
<h1>{currentProject?.name}任务组</h1>
<Button onClick={() => setEpicCreateOpen(true)} type={"link"}>
创建任务组
</Button>
</Row>
<List
style={{ overflow: "scroll" }}
dataSource={taskGroups}
itemLayout={"vertical"}
renderItem={...}
/>
<CreateTaskGroup
onClose={() => setEpicCreateOpen(false)}
visible={epicCreateOpen}
/>
</ViewContainer>
);
};
查看页面, 尝试新增任务组,功能正常!
注意,若是发现新增或删除后页面数据没有变化,那说明新增或删除时使用的
queryKey
和useQuery
中入参不一致,导致无法正常更新缓存数据
14.完成 popover
功能实现的差不多了,接下来查漏补缺
从图中可以看到,在项目列表页中,取消或新增收藏项目,页面左上角收藏夹不会自动更新,查看代码并结合控制台发现 ProjectPopover
中没有给 useProjects
传入参数作为 queryKey
,因此在缓存中 这两个地方(收藏夹和项目列表并不一致)
编辑 src\screens\ProjectList\components\ProjectPopover.tsx
(使用 refetch
,在 onOpenChange
中调用):
...
export const ProjectPopover = () => {
...
const { data: projects, refetch } = useProjects();
...
return (
<Popover onOpenChange={() => refetch()} placement="bottom" content={content}>
项目
</Popover>
);
};
...
查看页面,这样功能正常啦!
编辑 src\utils\use-users.ts
(改为 react-query
的方式):
import { useHttp } from "utils/http";
import { User } from "types/User";
import { useQuery } from "react-query";
export const useUsers = (param?: Partial<User>) => {
const client = useHttp();
return useQuery<User[]>(["users", param], () =>
client("users", { data: param })
);
};
新建 src\screens\ProjectList\components\UserPopover.tsx
(页面布局有一部分与 ProjectPopover
相同,可以拿过来 src\screens\ProjectList\components\ProjectPopover.tsx
):
import React from "react";
import { Divider, List, Popover, Typography } from "antd";
import styled from "@emotion/styled";
import { useUsers } from "utils/use-users";
export const UserPopover = () => {
const { data: users, refetch } = useUsers();
const content = (
<ContentContainer>
<Typography.Text type={"secondary"}>组员列表</Typography.Text>
<List>
{users?.map((user) => (
<List.Item key={user.id}>
<List.Item.Meta title={user.name} />
</List.Item>
))}
</List>
<Divider />
</ContentContainer>
);
return (
<Popover
onVisibleChange={() => refetch()}
placement={"bottom"}
content={content}
>
<span>组员</span>
</Popover>
);
};
const ContentContainer = styled.div`
min-width: 30rem;
`;
编辑 src\authenticated-app.tsx
(使用 UserPopover
组件)
...
import { UserPopover } from "screens/ProjectList/components/UserPopover";
export const AuthenticatedApp = () => {
...
};
const PageHeader = () => {
...
return (
<Header between={true}>
<HeaderLeft gap={true}>
...
<ProjectPopover />
<UserPopover />
</HeaderLeft>
<HeaderRight>...</HeaderRight>
</Header>
);
};
查看页面,显示正常!
15.开发完成,部署页面
按视频教程是部署到 github pages
在 github
上新建一个仓库,仓库名必须为 username.github.io
,其中 username
是 github
用户名。
安装依赖
npm i gh-pages -D
npm i deploy
修改 package.json
(配置部署脚本)
{
"scripts": {
...
"predeploy": "npm run build",
"deploy": "gh-pages -d build -r git@github.com:sindu12jun/sindu12jun.github.io.git -b main"
},
}
注意要将
git
链接替换成自己的,并指定分支(main
替代之前master
作为github
默认主分支)
安装好后执行:npm run deploy
(predeploy
也会自动执行),打包后的文件会自动上传到 github 并生成提交记录
打开部署后的页面,成功!
但是还有个问题,github 如何判断路由是前端路由还是后端路由?
目前点到其他子页面,刷新后 显示404
接下来解决这个问题,参考 spa-github-pages: 使用GitHub Pages 托管单页应用程序
大概步骤如下:
- 自定义 404 页面,将当前的路由信息记录下来
- 携带路由信息跳转到 index
- 进入 index 后检查路由信息,进行还原
新建 public\404.html
(配置404页面,解决强制刷新页面时,github pages当做后端路由,从而报错的问题)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Single Page Apps for GitHub Pages</title>
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// MIT License
// https://github.com/rafgraph/spa-github-pages
// This script takes the current url and converts the path and query
// string into just a query string, and then redirects the browser
// to the new url with only a query string and hash fragment,
// e.g. https://www.foo.tld/one/two?a=b&c=d#qwe, becomes
// https://www.foo.tld/?/one/two&a=b~and~c=d#qwe
// Note: this 404.html file must be at least 512 bytes for it to work
// with Internet Explorer (it is currently > 512 bytes)
// If you're creating a Project Pages site and NOT using a custom domain,
// then set pathSegmentsToKeep to 1 (enterprise users may need to set it to > 1).
// This way the code will only replace the route part of the path, and not
// the real directory in which the app resides, for example:
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
// https://username.github.io/repo-name/?/one/two&a=b~and~c=d#qwe
// Otherwise, leave pathSegmentsToKeep as 0.
var pathSegmentsToKeep = 0;
var l = window.location;
l.replace(
l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
l.pathname.split('/').slice(0, 1 + pathSegmentsToKeep).join('/') + '/?/' +
l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') +
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);
</script>
</head>
<body>
</body>
</html>
编辑 public\index.html
(将路由信息完整的带到 index 中还原,从而实现 SPA 的效果,当然,会看到浏览器地址栏中路由的跳转,体验上稍微差了点,不过功能上已经基本无异。)
<!DOCTYPE html>
<html lang="en">
<head>
...
<title>Jira任务管理系统</title>
<!-- Start Single Page Apps for GitHub Pages -->
<script type="text/javascript">
// Single Page Apps for GitHub Pages
// MIT License
// https://github.com/rafgraph/spa-github-pages
// This script checks to see if a redirect is present in the query string,
// converts it back into the correct url and adds it to the
// browser's history using window.history.replaceState(...),
// which won't cause the browser to attempt to load the new url.
// When the single page app is loaded further down in this file,
// the correct url will be waiting in the browser's history for
// the single page app to route accordingly.
(function(l) {
if (l.search[1] === '/' ) {
var decoded = l.search.slice(1).split('&').map(function(s) {
return s.replace(/~and~/g, '&')
}).join('?');
window.history.replaceState(null, null,
l.pathname.slice(0, -1) + decoded + l.hash
);
}
}(window.location))
</script>
<!-- End Single Page Apps for GitHub Pages -->
</head>
<body>
...
</body>
</html>
英文注释译文:该脚本检查查询字符串中是否存在重定向,将其转换回正确的
url
,并使用window.history.replaceState(…)
将其添加到浏览器的历史记录中,这不会导致浏览器尝试加载新url。当单页应用程序在这个文件中被进一步加载时,正确的url
将在浏览器的历史记录中等待单页应用程序相应地路由。
解决方案原地址:https://github.com/rafgraph/spa-github-pages
- 为方便访问,博主在gitee上 fork 了一份 https://gitee.com/OliverDaDa_admin/spa-github-pages
这样部署就完成啦!
部分引用笔记还在草稿阶段,敬请期待。。。