文章目錄
- 一、項目起航:項目初始化與配置
- 二、React 與 Hook 應用:實現項目列表
- 三、TS 應用:JS神助攻 - 強類型
- 四、JWT、用戶認證與異步請求
- 五、CSS 其實很簡單 - 用 CSS-in-JS 添加樣式
- 六、用戶體驗優化 - 加載中和錯誤狀態處理
- 七、Hook,路由,與 URL 狀態管理
- 八、用戶選擇器與項目編輯功能
- 九、深入React 狀態管理與Redux機制
- 十、用 react-query 獲取數據,管理緩存
- 十一、看板頁面及任務組頁面開發
- 1.看板列表開發準備工作
- 2.看板列表初步開發
- 3.添加task, bug 圖標
學習內容來源: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.看板列表開發準備工作
之前的項目詳情進入看板頁的路由有個小問題,點擊瀏覽器返回按鈕回不去,原因如下:
- 路由列表是棧結構,每訪問一個路由都會
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:
- 在需要重構的變量上右擊,選擇重構(快捷鍵 Ctrl + Shift + R),選擇
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 tasknote: 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 || {}) }));
};
2.看板列表初步開發
接下來開始開發看板列表,展示需要用到項目數據,可以提取一個從 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開發
自測已完成
單元測試
性能優化
3.添加task, bug 圖標
任務的類型接口并不直接返回,而是只返回一個 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"><!-- Generator: Sketch 3.5.2 (25235) - http://www.bohemiancoding.com/sketch --><title>bug</title><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"><!-- Generator: Sketch 3.5.2 (25235) - http://www.bohemiancoding.com/sketch --><title>task</title><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:XERROR in ./src/assets/task.svgModule 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
文件 修改后的源碼如下:
- bug.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"><!-- Generator: Sketch 3.5.2 (25235) - http://www.bohemiancoding.com/sketch --><title>bug</title><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>
- task.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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"><!-- Generator: Sketch 3.5.2 (25235) - http://www.bohemiancoding.com/sketch --><title>task</title><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;}
`
查看效果:
部分引用筆記還在草稿階段,敬請期待。。。