├── README.md
├── React Hooks 项目
├── (一)登录注册页面.md
├── (七)任务组页面实现.md
├── (三)项目列表功能模块.md
├── (二)项目列表展示.md
├── (五)路由跳转页面.md
├── (八)拖拽功能实现.md
├── (六)看板页面展示.md
├── (四) 搜索功能实现.md
└── (终)项目总结.md
└── React 入门学习
├── React 入门学习(一)-- 基础知识以及 jsx语法.md
├── React 入门学习(七)-- 脚手架配置代理.md
├── React 入门学习(三) -- 组件的生命周期.md
├── React 入门学习(九)-- 消息订阅发布.md
├── React 入门学习(二)-- 面向组件编程.md
├── React 入门学习(五)-- 初始化脚手架.md
├── React 入门学习(八)-- GitHub 搜索案例.md
├── React 入门学习(六)-- TodoList 案例.md
├── React 入门学习(十一)-- React 路由传参.md
├── React 入门学习(十七)-- React 扩展.md
├── React 入门学习(十三)-- antd 的基本使用.md
├── React 入门学习(十二)-- React 路由跳转.md
├── React 入门学习(十五)-- React-Redux 基本使用.md
├── React 入门学习(十六)-- 数据共享.md
├── React 入门学习(十四)-- redux 基本使用.md
├── React 入门学习(十)-- React 路由.md
├── React 入门学习(四)-- diffing 算法.md
└── React核心 -- React-Hooks.md
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
👋 这里是 React 学习天堂
3 | 希望你能有所收获, 期待你的 Star
4 |
5 |
6 |
7 |
8 |
9 |
10 | If you like this series or learn something from it, please★ this repository to show your support! 🤩
11 |
12 |
本仓库非常适合 React 的学习者,持续收集 React 相关的技术好文,如果有什么问题、错误的地方或者有什么想要添加的内容,欢迎与我联系!
13 |
14 |
15 |
16 | 
17 |
18 | ## React 基础学习
19 |
20 | - [基础知识以及 jsx 语法][1-1]
21 | - [面向组件编程][1-2]
22 | - [生命周期 LifeCycle][1-3]
23 | - [diffing 算法][1-4]
24 | - [认识脚手架][1-5]
25 | - [TodoList 案例][1-6]
26 | - [脚手架配置代理][1-7]
27 | - [GitHub 搜索案例][1-8]
28 | - [消息订阅发布][1-9]
29 | - [React 路由][1-10]
30 | - [React 路由传参][1-11]
31 | - [React 路由跳转][1-12]
32 | - [antd 组件库的基本使用][1-13]
33 | - [redux 基本使用][1-14]
34 | - [React-Redux 基本使用][1-15]
35 | - [数据共享][1-16]
36 | - [React 扩展][1-17]
37 | - [React Hooks][1-18]
38 |
39 | ## React 源码解析
40 |
41 | - [专栏介绍][3-1]
42 | - [React 设计理念][3-2]
43 | - [React Fiber 架构][3-3]
44 |
45 | **Render 阶段**
46 |
47 | - [Render 阶段 - beginWork][3-4]
48 | - [Render 阶段 - completeWork][3-5]
49 |
50 | **commit 阶段**
51 | - [commit 阶段流程概览][3-6]
52 | - [BeforeMutation 阶段][3-7]
53 | - [Mutation 阶段][3-8]
54 | - [Layout 阶段][3-9]
55 |
56 | **Diff 算法**
57 |
58 | - [Diff 算法概览][3-10]
59 | - [单节点 Diff][3-11]
60 | - [多节点的 Diff][3-12]
61 |
62 | **状态更新**
63 | - [状态更新流程概览][3-13]
64 | - [优先级更新][3-14]
65 | - [状态更新调度源码解析][3-15]
66 |
67 | **Scheduler 模块**
68 | - [Scheduler 实现原理][Scheduler-origin]
69 | - [Scheduler 源码解析][Scheduler-code]
70 |
71 | **Hooks 实现**
72 | - [React Hooks 源码概览][hooks-pre]
73 | - [React Hooks useState 源码][hooks-useState]
74 | - [useReducer 源码解析][hooks-useReducer]
75 | - [useContext 源码解析][hooks-useContext]
76 | - [useEffect 源码解析][hooks-useEffect]
77 | - [useLayoutEffect 源码解析][hooks-useLayoutEffect]
78 | - [useRef 源码解析][hooks-useRef]
79 | - [useCallback & useMemo 源码解析][hooks-useMemo]
80 | - [useId 源码解析][hooks-useId]
81 | - [useTransition 源码解析][hooks-useTransition]
82 | - [Q & A][hooks-qa]
83 |
84 | ## React 进阶电子书
85 |
86 | - [React 技术揭秘][5-1]
87 | - [人人都能读懂的 react 源码解析][5-2]
88 | - [React 源码解析][5-3]
89 | - [React 进阶实践指南][5-4]
90 | - [reactExplain][5-5]
91 | - [图解React原理系列][5-6]
92 | - [React 进阶专栏][5-7]
93 | - [React 源码解析 -- 基于v18][5-8]
94 |
95 | ## React 精选文章
96 |
97 | - [走进React Fiber 架构][4-1]
98 | - [这可能是最通俗的 React Fiber(时间分片) 打开方式][4-2]
99 | - [走进React Fiber的世界][4-3]
100 | - [详解 react diff][4-4]
101 | - [一文吃透react事件系统原理][4-5]
102 | - [React 事件系统工作原理][4-6]
103 | - [react-router v6 通关指南][4-7]
104 | - [一文吃透react-hooks原理][4-8]
105 | - [React 18 超全升级指南][4-9]
106 | - [从React源码分析渲染更新流程][4-10]
107 | - [React小技巧汇总][4-11]
108 | - [React 全部 Hooks 使用大全 (包含 React v18 版本][4-12]
109 | - [React Hooks 源码学习][4-13]
110 | - [2022 的 React 生态][4-14]
111 | - [React 原理 -- 浅析 React Fiber 架构][4-15]
112 |
113 | ## React Hooks 实战项目
114 |
115 | - [项目介绍&登录注册][2-1]
116 | - [项目列表展示][2-2]
117 | - [项目列表功能模块][2-3]
118 | - [搜索功能实现][2-4]
119 | - [路由跳转页面][2-5]
120 | - [看板页面展示][2-6]
121 | - [任务组页面实现][2-7]
122 | - [拖拽功能实现][2-8]
123 | - [项目总结][2-9]
124 |
125 | If you like this series or learn something from it, please★ this repository to show your support! 🤩
126 |
127 |
以上就是这个仓库的全部内容了,祝愿大家有个美好的未来如果有什么问题、错误的地方或者有什么想要添加的内容,欢迎与我联系!
128 |
129 |
130 |
131 | [1-1]: https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E4%B8%80%EF%BC%89--%20%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%E4%BB%A5%E5%8F%8A%20jsx%E8%AF%AD%E6%B3%95.md
132 | [1-2]: https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E4%BA%8C%EF%BC%89--%20%E9%9D%A2%E5%90%91%E7%BB%84%E4%BB%B6%E7%BC%96%E7%A8%8B.md
133 | [1-3]: https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E4%B8%89%EF%BC%89%20--%20%E7%BB%84%E4%BB%B6%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.md
134 | [1-4]: https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%9B%9B%EF%BC%89--%20diffing%20%E7%AE%97%E6%B3%95.md
135 | [1-5]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E4%BA%94%EF%BC%89--%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%84%9A%E6%89%8B%E6%9E%B6.md
136 | [1-6]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%85%AD%EF%BC%89--%20TodoList%20%E6%A1%88%E4%BE%8B.md
137 | [1-7]: https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E4%B8%83%EF%BC%89--%20%E8%84%9A%E6%89%8B%E6%9E%B6%E9%85%8D%E7%BD%AE%E4%BB%A3%E7%90%86.md
138 | [1-8]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%85%AB%EF%BC%89--%20GitHub%20%E6%90%9C%E7%B4%A2%E6%A1%88%E4%BE%8B.md
139 | [1-9]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E4%B9%9D%EF%BC%89--%20%E6%B6%88%E6%81%AF%E8%AE%A2%E9%98%85%E5%8F%91%E5%B8%83.md
140 | [1-10]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%8D%81%EF%BC%89--%20React%20%E8%B7%AF%E7%94%B1.md
141 | [1-11]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%8D%81%E4%B8%80%EF%BC%89--%20React%20%E8%B7%AF%E7%94%B1%E4%BC%A0%E5%8F%82.md
142 | [1-12]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%8D%81%E4%BA%8C%EF%BC%89--%20React%20%E8%B7%AF%E7%94%B1%E8%B7%B3%E8%BD%AC.md
143 | [1-13]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%8D%81%E4%B8%89%EF%BC%89--%20antd%20%E7%9A%84%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8.md
144 | [1-14]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%8D%81%E5%9B%9B%EF%BC%89--%20redux%20%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8.md
145 | [1-15]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%8D%81%E4%BA%94%EF%BC%89--%20React-Redux%20%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8.md
146 | [1-16]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%8D%81%E5%85%AD%EF%BC%89--%20%E6%95%B0%E6%8D%AE%E5%85%B1%E4%BA%AB.md
147 | [1-17]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0%EF%BC%88%E5%8D%81%E4%B8%83%EF%BC%89--%20React%20%E6%89%A9%E5%B1%95.md
148 | [1-18]:https://github.com/linjunc/react-study/blob/main/React%20%E5%85%A5%E9%97%A8%E5%AD%A6%E4%B9%A0/React%E6%A0%B8%E5%BF%83%20--%20React-Hooks.md
149 |
150 | [2-1]:https://github.com/linjunc/react-study/blob/main/React%20Hooks%20%E9%A1%B9%E7%9B%AE/%EF%BC%88%E4%B8%80%EF%BC%89%E7%99%BB%E5%BD%95%E6%B3%A8%E5%86%8C%E9%A1%B5%E9%9D%A2.md
151 | [2-2]:https://github.com/linjunc/react-study/blob/main/React%20Hooks%20%E9%A1%B9%E7%9B%AE/%EF%BC%88%E4%BA%8C%EF%BC%89%E9%A1%B9%E7%9B%AE%E5%88%97%E8%A1%A8%E5%B1%95%E7%A4%BA.md
152 | [2-3]:https://github.com/linjunc/react-study/blob/main/React%20Hooks%20%E9%A1%B9%E7%9B%AE/%EF%BC%88%E4%B8%89%EF%BC%89%E9%A1%B9%E7%9B%AE%E5%88%97%E8%A1%A8%E5%8A%9F%E8%83%BD%E6%A8%A1%E5%9D%97.md
153 | [2-4]:https://github.com/linjunc/react-study/blob/main/React%20Hooks%20%E9%A1%B9%E7%9B%AE/%EF%BC%88%E5%9B%9B%EF%BC%89%20%E6%90%9C%E7%B4%A2%E5%8A%9F%E8%83%BD%E5%AE%9E%E7%8E%B0.md
154 | [2-5]:https://github.com/linjunc/react-study/blob/main/React%20Hooks%20%E9%A1%B9%E7%9B%AE/%EF%BC%88%E4%BA%94%EF%BC%89%E8%B7%AF%E7%94%B1%E8%B7%B3%E8%BD%AC%E9%A1%B5%E9%9D%A2.md
155 | [2-6]:https://github.com/linjunc/react-study/blob/main/React%20Hooks%20%E9%A1%B9%E7%9B%AE/%EF%BC%88%E5%85%AD%EF%BC%89%E7%9C%8B%E6%9D%BF%E9%A1%B5%E9%9D%A2%E5%B1%95%E7%A4%BA.md
156 | [2-7]:https://github.com/linjunc/react-study/blob/main/React%20Hooks%20%E9%A1%B9%E7%9B%AE/%EF%BC%88%E4%B8%83%EF%BC%89%E4%BB%BB%E5%8A%A1%E7%BB%84%E9%A1%B5%E9%9D%A2%E5%AE%9E%E7%8E%B0.md
157 | [2-8]:https://github.com/linjunc/react-study/blob/main/React%20Hooks%20%E9%A1%B9%E7%9B%AE/%EF%BC%88%E5%85%AB%EF%BC%89%E6%8B%96%E6%8B%BD%E5%8A%9F%E8%83%BD%E5%AE%9E%E7%8E%B0.md
158 | [2-9]:https://github.com/linjunc/react-study/blob/main/React%20Hooks%20%E9%A1%B9%E7%9B%AE/%EF%BC%88%E7%BB%88%EF%BC%89%E9%A1%B9%E7%9B%AE%E6%80%BB%E7%BB%93.md
159 |
160 | [3-1]: https://linjuncheng.cn/pages/react/hard/readme.html
161 | [3-2]: https://linjuncheng.cn/pages/react/hard/fiberidea.html
162 | [3-3]: https://linjuncheng.cn/pages/react/hard/constructure.html
163 | [3-4]: https://linjuncheng.cn/pages/react/hard/render/beginwork.html
164 | [3-5]: https://linjuncheng.cn/pages/react/hard/render/completework.html
165 | [3-6]: https://linjuncheng.cn/pages/react/hard/commit/commit.html
166 | [3-7]: https://linjuncheng.cn/pages/react/hard/commit/beforemutation.html
167 | [3-8]: https://linjuncheng.cn/pages/react/hard/commit/mutation.html
168 | [3-9]: https://linjuncheng.cn/pages/react/hard/commit/layout.html
169 | [3-10]: https://linjuncheng.cn/pages/react/hard/diff/diffpre.html
170 | [3-11]: https://linjuncheng.cn/pages/react/hard/diff/singlediff.html
171 | [3-12]: https://linjuncheng.cn/pages/react/hard/diff/arraydiff.html
172 | [3-13]: https://linjuncheng.cn/pages/react/hard/update/update.html
173 | [3-14]: https://linjuncheng.cn/pages/react/hard/update/priority.html
174 | [3-15]: https://linjuncheng.cn/pages/react/hard/update/updatecode.html
175 | [Scheduler-origin]: https://linjuncheng.cn/pages/react/hard/scheduler/scheduler-origin.html
176 | [Scheduler-code]: https://linjuncheng.cn/pages/react/hard/scheduler/scheduler.html
177 | [hooks-pre]: https://linjuncheng.cn/pages/react/hard/hooks/hooks.html
178 | [hooks-useState]: https://linjuncheng.cn/pages/react/hard/hooks/useState.html
179 | [hooks-useReducer]: https://linjuncheng.cn/pages/react/hard/hooks/usereducer.html
180 | [hooks-useContext]: https://linjuncheng.cn/pages/react/hard/hooks/usecontext.html
181 | [hooks-useEffect]: https://linjuncheng.cn/pages/react/hard/hooks/useeffect.html
182 | [hooks-useLayoutEffect]: https://linjuncheng.cn/pages/react/hard/hooks/uselayouteffect.html
183 | [hooks-useRef]: https://linjuncheng.cn/pages/react/hard/hooks/useref.html
184 | [hooks-useMemo]: https://linjuncheng.cn/pages/react/hard/hooks/usememo-callback.html
185 | [hooks-useid]: https://linjuncheng.cn/pages/react/hard/hooks/useId.html
186 | [hooks-useTransition]: https://linjuncheng.cn/pages/react/hard/hooks/usetransition.html
187 | [hooks-qa]: https://linjuncheng.cn/pages/react/hard/hooks/qa.html
188 |
189 | [4-1]: https://juejin.cn/post/6844904019660537869
190 | [4-2]: https://juejin.cn/post/6844903975112671239
191 | [4-3]: https://juejin.cn/post/6943896410987659277
192 | [4-4]: https://juejin.cn/post/6844903973585944589
193 | [4-5]: https://juejin.cn/post/6955636911214067720
194 | [4-6]: https://juejin.cn/post/6909271104440205326
195 | [4-7]: https://juejin.cn/post/7069555976717729805
196 | [4-8]: https://juejin.cn/post/6944863057000529933
197 | [4-9]: https://juejin.cn/post/7078511027091931167
198 | [4-10]: https://juejin.cn/post/6844904200824946696
199 | [4-11]: https://juejin.cn/post/6844903890467454989
200 | [4-12]: https://juejin.cn/post/7118937685653192735
201 | [4-13]: https://juejin.cn/post/7114491826694389768
202 | [4-14]: https://juejin.cn/post/7085542534943883301
203 | [4-15]: https://juejin.cn/post/7118752985068339237
204 |
205 | [5-1]: https://react.iamkasong.com/
206 | [5-2]: https://xiaochen1024.com/article_item/600ac4384bf83f002edaf54a
207 | [5-3]: https://react.jokcy.me/
208 | [5-4]: https://juejin.cn/book/6945998773818490884
209 | [5-5]: https://github.com/AttackXiaoJinJin/reactExplain
210 | [5-6]: https://7kms.github.io/react-illustration-series/
211 | [5-7]: https://juejin.cn/column/6961274930306482206
212 | [5-8]: https://linjuncheng.cn/pages/react/hard/readme.html
213 |
--------------------------------------------------------------------------------
/React Hooks 项目/(一)登录注册页面.md:
--------------------------------------------------------------------------------
1 | # Hooks + TS 搭建一个任务管理系统(一)-- 登录注册页面
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**大二的前端爱好者**
6 | >
7 | > 📢 这个系列文章是实战 jira 任务管理系统的一个学习总结
8 | >
9 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
10 | >
11 | > 📢 **愿你忠于自己,热爱生活**
12 |
13 | ## 💌 前言
14 |
15 | 这篇文章是这个专栏中的第一篇文章,因此就写点前言吧~,简单的介绍一下吧
16 |
17 | 最近刚学完 React 的一些基本内容,教学视频已经看完了,然后也学习了一下 TS 这门强类型的语言,对前端开发简直就是利器。同时也了解了一下 Hooks 的一些内容,但是对这部分掌握的不是很好,因此跟着视频利用 `Hooks + TS4 + Router6 `做了一个任务管理系统练练手。在做这个 hooks 的项目之前,也有跟着做过一个基于 `React 16.4 版本 + Redux` 实现的简书博客平台,对 `Redux` 也有一定的了解。
18 |
19 | 扯这么多,来说说这个项目吧!
20 |
21 | > 这个项目是跟着视频做的并不是完全由我创新的 😥,因此**如果文章有侵权行为的话,麻烦联系一下删除**(应该不会吧,毕竟文章是我自己写的)
22 |
23 | 这个项目采用的技术栈是 `React Hooks + TS4`
24 |
25 | 主要实现的功能有 :用户登录注册,项目列表的展示,项目的 CRUD,项目详情展示,看板及任务组管理...
26 |
27 | 接下来的系列更文,将会围绕实现这些功能,以及在项目中遇到的难题,提出一些问题和解决方案。
28 |
29 | 强迫自己开启这个专栏是想要更加深入的理解,自己写的代码是什么意思,能够如何优化,了解更多代码上的细节,而不是跟着老师敲完代码就算了...,因此这个专栏会尽我所能将知识点囊括齐全!
30 |
31 | 高能预警:本项目采用了很多的 `custom hook` ,真的非常不错
32 |
33 | 下面开始今天的主题,实现登录注册页面
34 |
35 | 
36 |
37 | ## 一、用状态驱动页面更新
38 |
39 | 为什么第一个要讲“用状态驱动页面更新”呢?
40 |
41 | 我们需要通过当前的**登录状态**,来展示不一样的页面。通过状态来做很多的事情...
42 |
43 | 首先我们需要通过 `useState` ,来创建两个状态,一个是 `isRegister` 用来标识是展示登录界面还是注册界面,当 `isRegister` 为 `true` 时展示注册页面
44 |
45 | 第二个状态是**错误状态**,用来接收登录页面的错误信息,当有错误发生时,都会丢到这个变量当中
46 |
47 | ```tsx
48 | // 标识当前是注册还是登录,false 表示当前是登录状态
49 | const [isRegister, setIsRegister] = useState(false);
50 | const [error, setError] = useState(null)
51 | ```
52 |
53 | 在上面的两行代码中,值得注意的是,通过 `useState` 创建的变量类型默认会是**初始化**时的类型
54 |
55 | 也就是说 `isRegister` 的类型会因为我们初始化时传的 `false` 变成 `boolean` 类型
56 |
57 | 而对于 `error` 而已,在不加泛型的情况下,它默认会是 `null` 类型,因此,在后面对它赋值 `Error` 对象类型时,会发生错误,因此在这里我们需要定义泛型 `Error | null` 这样 `error` 就能接收 `Error` 类型了~
58 |
59 | 现在我们的状态设置好了,接下来看看如何驱动页面更新呢,那一个例子讲讲
60 |
61 | ```tsx
62 |
63 | ```
64 |
65 | 这个是登录和注册切换的按钮,当点击这个按钮时,会触发 `setIsRegister` 改变 `isRegister` 的值,我们通过这个值的 `true or false` 来判断展示的内容
66 |
67 | ```tsx
68 | {/* 判断展示登录页面还是注册页面 */}
69 | {
70 | isRegister ? :
71 | }
72 | ```
73 |
74 | 当为 `true` 的时候展示注册页面,在这里我们将两个页面抽象出了两个组件,将逻辑分开来,我们通过 `props` 向这两个组件传递了 `onError` 方法,在组件中可以通过调用这个方法来设置 `error` 状态的值,再展示到页面上
75 |
76 | 在这里值得我们注意的是,和类式组件不同,函数式组件会默认的接收 `props` 参数,因此我们不需要显式的去使用 `props` 我们可以直接在参数列表中**解构**出来,这样我们整个项目开发完成都不会见到一个 `props`
77 |
78 | ## 二、通过 Antd 布局页面
79 |
80 | 关于布局方面采用的是 `flex` 布局,主要是通过 Antd 组件来实现的
81 |
82 | ```tsx
83 |
84 |
85 | {
86 | isRegister ? '请注册' : "请登录"
87 | }
88 |
89 |
90 | {/* 判断展示登录页面还是注册页面 */}
91 | {
92 | isRegister ? :
93 | }
94 |
95 | {/* 点击切换状态 */}
96 |
97 |
98 | ```
99 |
100 | 这里的 `ShadowCard` 其实是对 `Antd` 中的 `Card` 组件进行了**加工**,让它有了一些**阴影**,同时对它进行了一定的布局
101 |
102 | ```tsx
103 | // 组件加样式,给Card组件更改样式
104 | const ShadowCard = styled(Card)`
105 | width: 40rem;
106 | min-height: 56rem;
107 | padding: 3.2rem 4rem;
108 | box-shadow: rgba(0,0,0,0.1) 0 0 10px;
109 | text-align: center;
110 | `
111 | ```
112 |
113 | 在 `emotion` 中,想要个 Antd 组件添加样式,我们只需要用 `styled(组件名)` 即可
114 |
115 | 对于登录和注册页面,采用的是 Antd 中的 `Form` 表单实现的,在控制好盒子大小后,基本不需要过多的布局
116 |
117 | ```tsx
118 |
120 |
121 |
122 |
123 |
124 |
125 | 登录
126 |
127 | ```
128 |
129 | 对于登录注册的关键就是,**通过前台认证之后,发送请求开启认证即可**,关键就在于这个认证如何实现,当然如果只是简单的发请求是非常简单的,但是往后想想,我们会有**很多个请求**,如果我们每次都写一遍那串代码,那代码的冗余程度可想而知。
130 |
131 | 因此我们想在这里抽象出两个 `custom hook` ,**一个用来获取数据,一个用来处理异步请求**,写这两个之前,我们先写一个专门用来发送请求的文件,我们将我们关于登录注册的请求全部写在这个文件当中,再暴露出去,这样代码看起来思路更加清晰
132 |
133 | ## 三、编写 auth-provider 文件
134 |
135 | 我们在这个文件中来处理我们需要发送的相关请求,首先,由于我们需要实现刷新后仍保持登录状态的效果,我们需要设置 `token` ,并且对于 `token` 数据我们是放在本地存储当中的
136 |
137 | ```tsx
138 | // 保存本地存储中的 token 键
139 | const localStorageKey = "__auth_provider_token__";
140 | // 获取 token 的值
141 | export const getToken = () => window.localStorage.getItem(localStorageKey);
142 | ```
143 |
144 | 通过封装一个函数用来获取我们本地的 `token` 值
145 |
146 | ```tsx
147 | export const handleUserResponse = ({ user }: { user: User }) => {
148 | window.localStorage.setItem(localStorageKey, user.token || "");
149 | return user;
150 | };
151 | ```
152 |
153 | 通过这个函数来设置本地 `token` ,在登录注册后调用
154 |
155 | **处理登录请求**
156 |
157 | ```tsx
158 | export const login = (data: { username: string; password: string }) => {
159 | return fetch(`${apiUrl}/login`, {
160 | method: "POST",
161 | headers: {
162 | "Content-Type": "application/json",
163 | },
164 | body: JSON.stringify(data),
165 | }).then(async (response) => {
166 | if (response.ok) {
167 | return handleUserResponse(await response.json());
168 | } else {
169 | throw Promise.reject(await response.json());
170 | }
171 | });
172 | };
173 | ```
174 |
175 | 当我们在其他文件中调用这个 `login` 时就会返回这个 `fetch` 能够发送登录的请求,当成功返回结果时,就会调用前面的函数来设置一个本地的 `token` 值,用来保存用户的登录状态
176 |
177 | 这里有个比较重要的点:由于我们的请求都是异步的因此我们在 `then` 中需要采用 `async await` 的方式,优雅的解决这个由于异步造成的 undefined 的问题,对于其他注册和登出的请求也是如此
178 |
179 | 在编写好几个请求函数之后,我们需要编写一个 `useAsync` 函数用来专门处理异步请求
180 |
181 | ## 四、编写 useAsync 发送异步请求
182 |
183 | 我们已经能够发送请求获取**登录信息**了,为什么我们还需要再编写一个这样的 `custom hook` 呢?
184 |
185 | 首先,我们在上面确实是能够满足我们最基本的业务需求了,我们编写这个 `custom hook` 能够帮我们将这个异步函数给具体化,什么是具体化呢?
186 |
187 | 我们先来看看这个 `custom hook` 返回的结果
188 |
189 | ```tsx
190 | return {
191 | // 请求状态
192 | isIdle: state.stat === 'idle',
193 | isLoading: state.stat === 'loading',
194 | isError: state.stat === 'error',
195 | isSuccess: state.stat === 'success',
196 | // run 接收一个promise 对象,返回执行结果
197 | run,
198 | setData,
199 | setError,
200 | // retry 被调用重新执行 run,让state 更新
201 | retry,
202 | ...state
203 | }
204 | ```
205 |
206 | 看到这些返回的结果,相信已经有了一定的想法,我们可以通过这个 `hook` 来直视到**异步函数的执行过程**,而且又能将过程**抽象**在这个 hook 当中,在外部,我们只需要 `run` 一下,就能得到结果,这不正是我们想要的吗?
207 |
208 | 我们**不想关注异步的细节**,什么 `then` 啊,`async` 啊,这些我们都不想关心,我们想要的是,**执行后的结果**,因此这个 hook 需要帮我们解决这些问题!这在优化我们代码中起着非常重要的作用
209 |
210 | 对于这个 hook 的实现,比较复杂,类型复杂,
211 |
212 | ```tsx
213 | interface State {
214 | error: Error | null;
215 | // 返回的数据
216 | data: D | null;
217 | // 请求过程状态
218 | stat: 'idle' | 'loading' | 'error' | 'success'
219 | }
220 | ```
221 |
222 | 首先我们定义**初始化状态的接口**
223 |
224 | 初始化我们的初始状态
225 |
226 | ```tsx
227 | // 初始状态
228 | const defaultInitialState: State = {
229 | stat: 'idle',
230 | data: null,
231 | error: null
232 | }
233 | ```
234 |
235 | 我们先写一个 hook 来帮我们判断组件**是否卸载**
236 |
237 | ```tsx
238 | // 用这个dispatch 会帮我们判断 mountedRef 组件是否被卸载
239 | const useSafeDispatch = (dispatch: (...args: T[]) => void) => {
240 | const mountedRef = useMountedRef()
241 | return useCallback((...args: T[]) => (mountedRef.current ? dispatch(...args) : void 0), [dispatch, mountedRef])
242 | }
243 | ```
244 |
245 | 当我们使用这个 hook 时,将会接收到当前组件的状态,当组件被卸载后,我们就不需要再将数据返回了,如果返回的话,就会造成数据无法渲染的情况从而报错,因此,我们编写这个 hook 也是出于这样的考虑
246 |
247 | 我们通过监听 `safeDispatch` 的变化来该判断当前的状态,同时我们可以通过 `setData` 来传递返回的数据,再通过 `safeDispatch` 来发送 `dispatch` 设置响应
248 |
249 | ```tsx
250 | const safeDispatch = useSafeDispatch(dispatch)
251 | // 正常响应时的数据处理
252 | const setData = useCallback((data: D) => safeDispatch({
253 | data,
254 | stat: 'success',
255 | error: null
256 | }), [safeDispatch])
257 | // 发生错误时的错误处理
258 | const setError = useCallback((error: Error) => safeDispatch({
259 | error,
260 | stat: 'error',
261 | data: null
262 | }), [safeDispatch])
263 | ```
264 |
265 | 当然还有一些其他的状态也需要这样编写,基本一致
266 |
267 | 在这里我们开始编写我们的 `run` 函数,这个函数是主入口,**用于触发异步请求**,首先从我们的调用上来看 `run(login(values))` 我们只想传递一个 `promise` 对象就能获得所有的结果,
268 |
269 | 首先我们需要先判断一下,传入的对象是不是 `promise` 对象,如果不是则直接抛出错误
270 |
271 | 当进入 `run` 函数后,我们需要将 `stat` 状态置为 `loading` 状态,这样我们可以通过这个值来实现请求 loading 的效果,
272 |
273 | 最后我们返回一个 `promise` 对象的执行结果,在这个返回当中有很多值得探讨的地方
274 |
275 | 为了获取到传入的 `promise` 对象**抛出的错误**,我需要使用 `then` 中的第二个参数来接收这 **错误对象**,再返回这个错误,才能使用 `catch` 获取,正常情况下,`catch` 获取不到这个错误
276 |
277 | ```tsx
278 | // run是主入口,触发异步请求
279 | // 采用useCallback,只有依赖中的数据发生变化的时候,run才会被重新定义
280 | const run = useCallback((promise: Promise, runConfig?: { retry: () => Promise }) => {
281 | // 如果传入的不是 promise,直接 throw
282 | if (!promise || !promise.then) {
283 | throw new Error('请传入 Promise 类型数据')
284 | }
285 | // 定义重新刷新一次,返回一个有上一次 run 执行时的函数
286 | setRetry(() => () => {
287 | if (runConfig?.retry) {
288 | run(runConfig?.retry(), runConfig)
289 | }
290 | })
291 | // 如果是 promise 则设置状态,开始 loading
292 | safeDispatch({ stat: 'loading' })
293 | // 返回一个promise对象处理数据
294 | return promise
295 | .then(data => {
296 | // 成功则处理stat
297 | // 判断组件状态
298 | setData(data)
299 | return data
300 | }, async (err) => {
301 | // 接收到扔来的错误,再扔一下
302 | return Promise.reject(await err)
303 | })
304 | .catch(error => {
305 | // 错误抛出了,但是接不住
306 | setError(error)
307 | if (config.throwOnError) {
308 | return Promise.reject(error)
309 | }
310 | return Promise.reject(error)
311 | })
312 | }, [config.throwOnError, safeDispatch, setData, setError])
313 | ```
314 |
315 | 在这个 `hook` 中有太多值得我们学习的地方
316 |
317 | 首先当我们的 `custom hook` 返回的值是一个函数时,我们最好用 `useCallback` 来包一下,这样能解决无限循环的问题
318 |
319 | 在我们的请求当中需要对异步情况做出特别的处理,利用 `async` 来解决这些问题
320 |
321 | 对于数据的类型,需要我们对泛型有很清晰的认识
322 |
323 | ## 五、编写 useAuth 获取用户信息
324 |
325 | 在编写好 `useAsync` hook 后,我们需要 通过 `useAuth` 来**获取用户的信息**,主要是依赖于 `useAsync` ,这也能体现出 `useAsync` 的巨大威力
326 |
327 | 在这个 `custom hook` 当中,我们会采用 `useAsync` 暴露的方法,同时也会采用到 `react-query` 处理缓存,利用 `context` 来实现**数据共享**
328 |
329 | ```tsx
330 | export const useAuth = () => {
331 | // 由于在使用 context 时,需要在子节点中声明一下这个 context
332 | const context = React.useContext(AuthContext)
333 | // 如果这个 context 不存在
334 | if (!context) {
335 | throw new Error('useAuth必须在 context 中使用')
336 | }
337 | // 返回这个 context 数据中心
338 | return context
339 | }
340 | ```
341 |
342 | 当我们调用这个 hook 的时候,就会**返回这个 context 对象** ,`AuthContext` ,当然不会这么简单,关键在于我们如何将这些数据存储在 `context` 当中
343 |
344 | 我们编写一个 `AuthProvider` 方法
345 |
346 | ```tsx
347 | export const AuthProvider = ({ children }: { children: ReactNode }) => {
348 | // 设置一个user变量 ,由于user 的类型由初始化的类型而定,但不能是 null ,我们需要进行类型断言
349 | // const [user, setUser] = useState(null)
350 | const { data: user, error, isLoading, isIdle, isError, run, setData: setUser } = useAsync()
351 | const queryClient = useQueryClient()
352 | // 设置三个函数 登录 注册 登出
353 | // setUser 是一个简写的方式 原先是:user => setUser(user)
354 | const login = (form: AuthForm) => auth.login(form).then(setUser)
355 | const register = (form: AuthForm) => auth.register(form).then(setUser)
356 | const logout = () => auth.logout().then(() => {
357 | setUser(null)
358 | // 清除数据缓存
359 | queryClient.clear()
360 | })
361 | // 当组件挂载时,初始化 user
362 | useMount(() => {
363 | run(bootstrapUser())
364 | })
365 | // 当初始化和加载中的时候显示loading
366 | if (isIdle || isLoading) {
367 | return
368 | }
369 | if (isError) {
370 | return
371 | }
372 | // 返回一个 context 容器
373 | return
374 | }
375 | ```
376 |
377 | 当我们这个方法返回了一个 `provider` 容器,这需要我们对 `context` 有一定的了解,我们需要使用 `provider` 来包裹数据共享的范围,只有在这个范围内的元素才能使用这些数据
378 |
379 | 这里的意思是,所有的子元素都能够使用这个 `context` 容器 ,我们在使用的时候
380 |
381 | ```tsx
382 |
383 | {children}
384 |
385 | ```
386 |
387 | 这样**所有的子元素都能共享**它的 `context` 容器
388 |
389 | 接下来我们看看这个函数都写了什么,首先我们调用 `useAsync` 解构出了它的部分返回结果,这些都是我们后面可能会用到的
390 |
391 | 在这里我们对**当前的状态进行了判断**
392 |
393 | ```tsx
394 | // 当初始化和加载中的时候显示loading
395 | if (isIdle || isLoading) {
396 | return
397 | }
398 | if (isError) {
399 | return
400 | }
401 | ```
402 |
403 | 当状态为 `loading` 时我们展示一个加载框,当 `error` 时,展示一个**错误提示框**
404 |
405 | ```tsx
406 | // 当组件挂载时,初始化 user
407 | useMount(() => {
408 | run(bootstrapUser())
409 | })
410 | ```
411 |
412 | 在组件刚挂载时,我们先检查是否存在 `token` 如果有,我们就对他进行**自动登录**
413 |
414 | ```tsx
415 | // 保持用户登录状态,在组件挂载的时候就调用
416 | const bootstrapUser = async () => {
417 | let user = null
418 | // 从本地取出 token
419 | const token = auth.getToken()
420 | if (token) {
421 | // 如果有值,就去发送请求获得 user 信息
422 | const data = await http('me', { token })
423 | user = data.user
424 | }
425 | // 返回 user
426 | return user
427 | }
428 | ```
429 |
430 | 同时我们还将 `auth-provider` 中编写的三个方法,一同存放到了 `context` 容器当中,这样我们可以在外部调用
431 |
432 | ```tsx
433 |
434 | ```
435 |
436 | 这里的 value 设置的就是它的 context 容器中的值
437 |
438 | 通过编写这个 custom hook 我们对 `useAsync` 有了更好的理解,同时也学会了如何使用 `context` 来进行数据的共享
439 |
440 | ## 六、按钮触发函数执行
441 |
442 | 在编写完了前面的几个 `custom hook` 之后,我们已经将数据接口转到了 `context` 当中,因此我们在调用里面的内容时,只需要调用 `useAuth` 来解构出对应的数据即可
443 |
444 | ```tsx
445 | // login.tsx
446 | const { login } = useAuth()
447 | // 采用 useAsync 来封装异步请求,添加loading
448 | const { run, isLoading } = useAsync(undefined, { throwOnError: true })
449 | ```
450 |
451 | 我们得到了 login 函数,同时也得到了 `isLoading` 的状态
452 |
453 | 当表单提交时,会触发 `Form` 组件中的 `onFinish` 事件,我们给他绑定了一个 `handleSubmit` 方法,用于发送请求
454 |
455 | ```tsx
456 | const handleSubmit = async (values: { username: string, password: string }) => {
457 | // 采用 antd 组件库后代码优化
458 | // 这里的catch 会捕获错误,调用 onError 这个函数相当于是 error => onError(error)
459 | // 由于在index中传入的props是,onError={setError} 因此就相当于 setError(error)
460 | run(login(values)).catch(onError)
461 | }
462 | ```
463 |
464 | 就这样我们就能够成功的发送请求,并且返回结果,当有错误发生时,会触发 `catch` 中的 `onError` 设置 `index` 中的 `error` 状态,显示在页面当中
465 |
466 | ## 📌 总结
467 |
468 | 在这个登录注册页面当中,我们可以**学到以下几点**
469 |
470 | 1. context 状态管理
471 | 2. custom hook 在 react 中的强大威力
472 | 3. 当 custom hook 返回函数时,需要使用 useCallback 包裹
473 | 4. 多利用解构赋值,来优化代码
474 | 5. useState 设置的变量,类型会跟随初始值的类型
475 | 6. 对于不同的事务,我们最好能分离出来写,这样我们的主文件思路会非常清晰
476 | 7. 利用 CSS in JS 解决样式混乱的问题
477 |
478 | > 最后,可能在很多地方讲诉的不够清晰,请见谅
479 | >
480 | > 💌 如果文章有什么错误的地方,或者有什么疑问,欢迎留言,也欢迎私信交流
481 |
482 |
--------------------------------------------------------------------------------
/React Hooks 项目/(七)任务组页面实现.md:
--------------------------------------------------------------------------------
1 | # Hooks + TS 搭建一个任务管理系统(七)-- 任务组页面实现
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**大二的前端爱好者**
6 | >
7 | > 📢 这个系列文章是实战 jira 任务管理系统的一个学习总结
8 | >
9 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
10 | >
11 | > 📢 **愿你忠于自己,热爱生活**
12 |
13 | 在上一篇文章中,我们处理了看板页面的布局,以及它的逻辑功能,基础功能已经基本实现,项目、任务的增删改查,搜索功能的实现,在这一篇我们就对任务组页面进行最后的布局,和功能实现,写到这里,大部分的功能 `hook` 已经实现了,对于增删改查我们也已经非常了解了。
14 |
15 | ## 💡 知识点抢先看
16 |
17 | - 增删任务组功能
18 | - 路由跳转
19 |
20 | 
21 |
22 | ## 一、页面布局
23 |
24 | 这部分已经写过好几次了,速战速决
25 |
26 | ### 1. 布局的简单介绍
27 |
28 | 这里我们采用的是 `antd` 中的 `List` 组件,顶部左右两侧采用的是自己封装的 `Row` 组件,让它们排列在两侧,链接跳转部分采用的 `Link` 组件,通过遍历数据的方式实现渲染
29 |
30 | 
31 |
32 | ### 2. 数据的获取
33 |
34 | 在这里我们需要获取到我们的任务数据,在这里我们需要写一个获取数据的 `custom hook`: `useEpics` ,和其他获取数据的 `hook` 一样
35 |
36 | 我们接收一个 `param` 数据对象,通过 `useQuery` 发送请求
37 |
38 | > 再复习一下,它的第二个参数是一个异步事件,第一个参数是**元组**,当依赖项 `param` 发生改变时,会重新发送请求,更新缓存中的 `epics` 数据内容
39 |
40 | ```tsx
41 | export const useEpics = (param?: Partial) => {
42 | const client = useHttp()
43 | return useQuery(['epics', param], () => client('epics', { data: param }))
44 | }
45 | ```
46 |
47 | 我们在 `epic/index.ts` 中使用 ,获取任务组数据 `epics` 以及用于跳转链接的 `tasks` 数据
48 |
49 | ```tsx
50 | // 关于任务的信息
51 | const { data: epics } = useEpics(useEpicSearchParams())
52 | // 获取任务组中的任务列表
53 | const { data: tasks } = useTasks({ projectId: currentProject?.id })
54 | ```
55 |
56 | 这样我们就实现了**数据的获取**
57 |
58 | 接下来我们来看看如何在组件中使用这两个数据的
59 |
60 | 对于 `epics` 它作为我们需要渲染的主内容,需要通过 `List.Item` 进行渲染
61 |
62 | 在 `List` 组件中,我们可以传入我们的数据源 `dataSource` ,通过 `renderItem` 属性,对 `epics` 数据进行遍历
63 |
64 | ```tsx
65 | />
66 | ```
67 |
68 | 这样我们的 `epic` 就是每一个任务数据通过**对象取值**方式就能获取需要的数据
69 |
70 | 在这里主要提一下对于时间的渲染
71 |
72 | 
73 |
74 | **后端给我们返回的数据格式是时间戳,我们需要将她转变成这种格式便于阅读**
75 |
76 | 在这里我们采用了一个 `dayjs` 的库,通过 `format` 方法确定了她输出的时间格式 `YYYY-MM-DD` ,只需要传入它的时间即可
77 |
78 | ```tsx
79 | 开始时间:{dayjs(epic.start).format("YYYY-MM-DD")}
80 | 结束时间:{dayjs(epic.end).format("YYYY-MM-DD")}
81 | ```
82 |
83 | ## 二、增删任务组功能
84 |
85 | 首先我们先来实现删除任务组的功能
86 |
87 | ### 1. 删除任务组
88 |
89 | **实现思路**如下
90 |
91 | 1. 点击删除按钮,弹出提示框
92 | 2. 确认删除
93 | 3. 调用接口删除缓存
94 |
95 | **代码实现**
96 |
97 | 当我们点击删除时,我们调用 `confirmDeleteEpic` 函数,进行删除确认
98 |
99 | 这个函数封装的是一个 `Modal.config` 组件
100 |
101 | ```tsx
102 | // 删除时的提示框
103 | const confirmDeleteEpic = (epic: Epic) => {
104 | Modal.confirm({
105 | title: `你确定删除项目组${epic.name}吗?`,
106 | content: '点击确定删除',
107 | okText: '确定',
108 | onOk() {
109 | // 确认时调用删除
110 | deleteEpic({ id: epic.id })
111 | }
112 | })
113 | }
114 | ```
115 |
116 | 当我们在点击确认时,正式调用删除接口 `deleteEpic` ,传入我们删除的任务组 `id` ,即可删除
117 |
118 | 我们来看看如何实现这个 `deleteEpic`
119 |
120 | 首先我们还是需要封装一个 `useDeleteEpic` 的 `hook` 用来**处理删除请求**,这里采用 `useMutation` 来处理,传入当前的 `id` ,**配置删除的 `config` 对象**
121 |
122 | > 写到这里自己也对 `useMutation` 有了进一步的认识,它可以接收两个参数,第一个参数我们**传入我们的异步请求**,第二个参数来配置 `config` **如何处理缓存中的数据**
123 |
124 | ```tsx
125 | // 删除看板
126 | export const useDeleteEpic = (queryKey: QueryKey) => {
127 | const client = useHttp()
128 | return useMutation(
129 | // 这里我没有出现问题,视频出现了问题
130 | // 直接(id:number)
131 | ({ id }: { id: number }) => client(`epics/${id}`, {
132 | method: "DELETE",
133 | }),
134 | useDeleteConfig(queryKey)
135 | )
136 | }
137 | ```
138 |
139 | 这样我们的删除功能就实现了
140 |
141 | ### 2. 添加任务组功能
142 |
143 | 实现思路
144 |
145 | 1. 写一个 `create-epic` 页面
146 | 2. 写入新增任务组信息
147 | 3. 提交创建请求
148 |
149 | **代码实现**
150 |
151 | 首先我们需要在 `epic` 文件夹目录下创建一个 `create-epic` 文件,用来编写创建任务页面
152 |
153 | > 这样做的好处是能够将复杂部分分离出来,使得主文件中的代码量减少,阅读性更佳
154 |
155 | 新增任务组页面,我们同样采用的是 `Drawer` 组件来实现
156 |
157 | 值得注意的是我们必须要添加 `forceRender={true}` 组件,否则在页面第一次加载时会报错
158 |
159 | 在 `Drawer` 组件中同样的我们采用了 `Form` 组件,**当表单提交时自动调用 `onFinish` 方法,处理添加请求**
160 |
161 | ```tsx
162 | const onFinish = async (values: any) => {
163 | // 仅仅传一个values 不够,需要传入 projectid
164 | await addEpic({ ...values, projectId })
165 | props.onClose()
166 | }
167 | ```
168 |
169 | 在这里我们采用的时一个 `async`、`await` 的组合,等待接口返回结果后我们再关闭窗口,但是由于我们采用了乐观更新,这里其实只要写入缓存中就会关闭窗口了
170 |
171 | 同时为了让 `Form` 表单在窗口关闭时自动清空,这里我们采用了 `useEffect` 来实现,在依赖项中写入 `visible` 监听变化
172 |
173 | ```tsx
174 | useEffect(() => {
175 | form.resetFields()
176 | }, [form, props.visible])
177 | ```
178 |
179 | 这样我们的创建功能也实现了,最后我们再稍微讲讲任务组 `item` 中的路由跳转
180 |
181 | ## 三、路由跳转
182 |
183 | 当我们点击下面的任务时,需要跳转到看板页面对应任务的编辑窗口,我们来看看效果图
184 |
185 | 
186 |
187 | 其实这只要我们的路由地址配置好了就没有问题了
188 |
189 | 我们来看看如何配置这个跳转的**路由地址**
190 |
191 | 指定到对应的 `editingTaskId` 页面,这样窗口就会弹出来了,这样是我们采用 `url` 进行状态管理的好处
192 |
193 | ```tsx
194 | to={`/projects/${currentProject?.id}/kanban?editingTaskId=${task.id}`}
195 | ```
196 |
197 | **那么我们如何将对应的任务绑定到对应的任务组下呢?**
198 |
199 | 这里我们采用 `filter` 来实现,当 `task` 下的 `epicId` 和 `epic` 下的 `id` 一致时说明是这个任务组下的,我们遍历渲染即可
200 |
201 | ```tsx
202 | {
203 | tasks?.filter(task => task.epicId === epic.id)
204 | .map(task =>
209 | {task.name}
210 | )
211 | }
212 | ```
213 |
214 | > 注意:采用 `map` 是一定要注意 `key` 唯一噢~
215 |
216 | ---
217 |
218 | ## 📌 总结
219 |
220 | 1. 能够熟练的实现了增删功能
221 | 2. 认识到了 `url` 状态管理的好处
222 | 3. 采用合适的数组的方法可以极好的帮助我们实现功能
223 |
224 | > 最后,可能在很多地方讲诉的不够清晰,请见谅
225 | >
226 | > 💌 如果文章有什么错误的地方,或者有什么疑问,欢迎留言,也欢迎私信交流
227 |
--------------------------------------------------------------------------------
/React Hooks 项目/(三)项目列表功能模块.md:
--------------------------------------------------------------------------------
1 | # Hooks + TS 搭建一个任务管理系统(三)-- 项目列表功能模块
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**大二的前端爱好者**
6 | >
7 | > 📢 这个系列文章是实战 jira 任务管理系统的一个学习总结
8 | >
9 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
10 | >
11 | > 📢 **愿你忠于自己,热爱生活**
12 |
13 | 在上一篇中,我们封装好了一些 `custom hook` 例如,用于操作 `url` 的 `useUrlQueryParam` 以及 `useSetUrlSearchParam` 同时我们封装了专门在 `project` 列表中使用的 `hook` ,搭建好了基本的框架,这一篇我们来使用这些 `hook` 来实现我们的功能,同时我们也会引出几个 `custom hook`
14 |
15 | ## 💡 知识点抢先看
16 |
17 | - 实现对项目的增删改查
18 | - 收藏功能的实现
19 | - 利用乐观更新来优化用户体验
20 |
21 | ## 一、实现对项目的增删改查
22 |
23 | ### 1. 模态框的实现
24 |
25 | 首先我们先理顺现在的思路,我们现在的单页面都已经布局好了,还有几个功能没有实现,创建项目、编辑项目、删除项目、收藏项目、查找项目(这个在下一篇讲)
26 |
27 | 先来看看我们的效果图
28 |
29 | 
30 |
31 | 我们的创建项目和编辑项目**都是在一个弹出的模态框**内实现的,仔细观察,会发现我们的项目列表并没有消失,效果看起来是**叠加**的。这样,我们接下来就可以**先写创建项目和编辑项目的模态框**,我们只需要将被编辑的**项目数据**传递给模态框就可以了,对于创建项目,我们给一个**空白**的即可
32 |
33 | 这里我们的抽拉效果,采用的是 `antd` 中的 `Drawer` 组件实现的,对这个组件不熟悉的可以看看:[Drawer](https://ant.design/components/drawer-cn/#header)
34 |
35 | 
36 |
37 | 从描述上来看,它会**覆盖住父窗体的内容**,正符合我们的想法,我们只需要将 `Form` 表单丢进这个 `Drawer` 组件中即可,
38 |
39 | ```tsx
40 |
45 | {
46 | isLoading ? :
47 | {title}
48 |
49 |
52 | {/* 点击提交触发onFinish方法 */}
53 |
54 |
55 |
56 |
57 | }
58 |
59 | ```
60 |
61 | 在这个组件中我们设置了 `forceRender` 属性,这个属性可以**控制是否强制渲染**,这也是为了解决,我们在刚打开时,组件未渲染导致**报错**的问题
62 |
63 | 同时我们也可以发现,我们在当中设置了**三元判断**,这样是为了优化我们的用户体验,前面也提过了,我们整个项目采用的是 `react-query` 进行 `url` 管理,在它的 `API` 中有能够返回 `isLoading` 状态的 `hook` 也就是我们的**数据请求的完成状态**,这也让我们可以利用这个 `isLoading` 去实现这个 `Spin` 的**加载效果**
64 |
65 | ```tsx
66 | isLoading ? :
67 | ```
68 |
69 | 这样其实我们的 `modal` 就已经做好了,接下来我们来完善一下这个 `modal` 的**周边措施**,当我们创建完成或者编辑完成时,我们需要关闭 `modal` ,在我们的 `useProjectModel` 中已经暴露了 `close` 方法,我们只需要在 `onFinish` 中调用即可
70 |
71 | > 当 `form` 表单成功提交时,会自动调用 `onFinish` 方法,同时会将 `form` 表单中的数据作为参数,因此我们采用 `useMutateProject` 这个 `hook` 来将数据维护到 `url` 中
72 |
73 | ```tsx
74 | const useMutateProject = editingProject ? useEditProject : useAddProject
75 | const { mutateAsync, error, isLoading: mutateLoading } = useMutateProject(useProjectsQueryKey())
76 | const onFinish = (values: any) => {
77 | mutateAsync({ ...editingProject, ...values }).then(() => {
78 | form.resetFields()
79 | close()
80 | })
81 | }
82 | ```
83 |
84 | 在这里我们采用了 2 个 `custom hook` ,`useEditProject ` 和 `useAddProject`,接下来我们就讲讲这两个 hook
85 |
86 | > **tips**:`form.resetFields` 方法可以重置表单,也就是一个清空表单的效果
87 |
88 | ### 2. 封装增删改查 hook引出
89 |
90 | 在上一小节中,我们也看到了这些 `hook` 的使用,我们在使用的时候只需要传递一个 `queryKey` ,就能够返回一个 `mutate` 以及一些相关的配置,这些我们并没有手动的去写,那它是怎么实现的呢?
91 |
92 | 这其实利用的是 `useMutation` 这个 `react-query` 中的原生 `hook`
93 |
94 | ```tsx
95 | // 示例
96 | return useMutation(
97 | (params: Partial) => client(`projects`, {
98 | method: "POST",
99 | data: params
100 | }),
101 | useAddConfig(queryKey)
102 | )
103 | ```
104 |
105 | 在这里我们传递了两个参数,**第一个参数是异步请求,第二个参数是相关的配置项**
106 |
107 | 这样它就能返回一个用来实现乐观更新的 `mutate` 和 `mutateAsync` 我们可以自己选用,一个是同步的一个是异步的
108 |
109 | 在我们使用的时候,只需要要像发送请求一样,传递我们的数据即可
110 |
111 | ```tsx
112 | // 示例
113 | mutateAsync({ ...editingProject, ...values }).then(() => {
114 | form.resetFields()
115 | close()
116 | })
117 | ```
118 |
119 | 例如我们的编辑效果就采用了异步的方法
120 |
121 | 下面我们来编写这些 `hook`
122 |
123 | ### 3. useEditProject
124 |
125 | 这是在编辑项目时需要调用的 `hook` 当我们编辑完之后,我们就可以调用这个 `hook` 暴露 `mutate` ,接着调用 `mutate` 来发送数据请求
126 |
127 | 首先我们还是逃不开我们的 `http` 这个 `hook` 所有的**异步请求都是通过这里来发送的**
128 |
129 | 我们先返回我们的 `fetch` 方法封装的 `client` 函数 ,最后返回一个 `useMutation` 函数**调用的返回值**,这个函数接收 2 个参数,**一个是我们需要发的请求,一个是配置项**
130 |
131 | 我们通过 `client` 封装我们需要发送的请求,在编辑情况下,我们需要**传递** `id` 来获取需要编辑的项目,`data` 则是整个传递过来的 `params` 这里面将包括了我们需要的数据,**为什么可以看出来呢?**
132 |
133 | 我们在给 `params` 限定类型时,采用了 `Partial` 这表明了 `params` 的变量和变量类型,必须**来自**于 `Project` 这个封装好的项目信息**接口**
134 |
135 | ```tsx
136 | // type/project.ts
137 | export interface Project {
138 | id: number;
139 | name: string;
140 | personId: number;
141 | pin: boolean;
142 | organization: string;
143 | created: number
144 | }
145 |
146 | // utils/project.ts
147 | export const useEditProject = (queryKey: QueryKey) => {
148 | const client = useHttp()
149 | // 实现乐观更新
150 | return useMutation(
151 | (params: Partial) => client(`projects/${params.id}`, {
152 | method: "PATCH",
153 | data: params
154 | }),
155 | useEditConfig(queryKey)
156 | )
157 | }
158 | ```
159 |
160 | 关于 `config` 这些 `hook` 的配置,在乐观更新中会讲到
161 |
162 | 接下来我们再来处理添加请求
163 |
164 | ### 4. useAddProject
165 |
166 | 这几个 `hook` 的相似度非常高,都是一个套路,写习惯了 `custom hook` 真的可以轻松拿捏的
167 |
168 | ```tsx
169 | export const useAddProject = (queryKey: QueryKey) => {
170 | const client = useHttp()
171 | return useMutation(
172 | (params: Partial) => client(`projects`, {
173 | method: "POST",
174 | data: params
175 | }),
176 | useAddConfig(queryKey)
177 | )
178 | }
179 | ```
180 |
181 | 在这里我们同样的方式传递我们的 `params` 参数,使用 `useMutation` 来处理我们的请求
182 |
183 | ### 5. useDeleteProject
184 |
185 | 处理删除的请求,对于删除项目只需要传递 `id` 就可以了,删除指定 `id` 的项目
186 |
187 | ```tsx
188 | export const useDeleteProject = (queryKey: QueryKey) => {
189 | const client = useHttp()
190 | return useMutation(
191 | ({ id }: { id: number }) => client(`projects/${id}`, {
192 | method: "DELETE",
193 | }),
194 | useDeleteConfig(queryKey)
195 | )
196 | }
197 | ```
198 |
199 | > 对于 `useMutation` 的一点理解
200 | >
201 | > 从上面的代码中我们可以可以发现,它都是用来处理我们的请求,我们传递一个异步请求,它也能返回一个请求的函数 (`mutate`),因此可以理解为,使用这个 `hook` 包装我们的异步请求,让它具有能够乐观更新的功能,其他的功能和我们自己封装的 `client` 方法一致
202 |
203 | ### 6. 实现编辑,创建功能
204 |
205 | 我们在点击编辑按钮时,首先需要弹出 `modal` 编辑信息点击保存后,才需要调用发送请求
206 |
207 | **上代码**
208 |
209 | 首先先处理 `modal` 的显示和关闭
210 |
211 | (截取下拉框的关键代码)我们在点击编辑按钮时,会触发 `editProject` 方法,这个方法会触发 `startEdit` ,它是 `useProjectModel` 这个 `custom hook` 暴露出来的 用来开启 `modal`
212 |
213 | ```tsx
214 | const editProject = (id: number) => () => startEdit(id)
215 |
216 | ```
217 |
218 | `modal` 开启来,现在我们需要将视线聚焦在 `project-modal` 这个文件当中,来处理在这个页面上的请求了!
219 |
220 | 在我们调用 `startEdit` 时,会将页面的 `url` 设置成 `editingProjectId` ,因此我们需要在 `modal` 中先**判断一下这个页面开启的请求是来自于编辑还是创建,**
221 |
222 | ```tsx
223 | const useMutateProject = editingProject ? useEditProject : useAddProject
224 | ```
225 |
226 | 这样我们的 `useMutateProject` 就是这两个中的一个,在后面就没有什么担忧了
227 |
228 | 然后我们直接传递当前的 `queryKey` 给这个 `hook` 暴露出我们需要的 `mutate` 这个请求函数,以及错误状态和请求状态
229 |
230 | ```tsx
231 | const { mutateAsync, error, isLoading: mutateLoading } = useMutateProject(useProjectsQueryKey())
232 | ```
233 |
234 | 当我们的 `form` 表单被提交时,我们调用这个方法传递我们 `params` 发送请求
235 |
236 | ```tsx
237 | const onFinish = (values: any) => {
238 | mutateAsync({ ...editingProject, ...values }).then(() => {
239 | form.resetFields()
240 | close()
241 | })
242 | }
243 | ```
244 |
245 | 这样我们的创建编辑功能就实现了
246 |
247 | ### 7. 删除功能
248 |
249 | 这里有一个比较好玩的东西,当我们点击删除时,不能立即执行,我们需要用户确认后才能发送请求,因此我们需要再多封装一层函数 `confirmDeleteProject` ,用来提示用户是否确定删除
250 |
251 | ```tsx
252 | confirmDeleteProject(project.id)} key={'delete'}>
253 | ```
254 |
255 | 再这里我们采用了 `antd` 组件中的 `Modal` 组件下的 `confirm` 框
256 |
257 | ```tsx
258 | const confirmDeleteProject = (id: number) => {
259 | Modal.confirm({
260 | title: '你确定删除项目吗?',
261 | content: '点击确定删除',
262 | okText: '确定',
263 | onOk() {
264 | deleteProject({id})
265 | }
266 | })
267 | }
268 | ```
269 |
270 | 在这里配置我们的提示框的相关信息,以及确定后执行的操作,这里当 `onOk` 时会调用 `deleteProject` 来发送请求,从这个命名也知道它调用的是 `useDeleteProject` ,这也是命名规范的好处之一
271 |
272 | 
273 |
274 | 这样,我们的删除功能就也实现了,关于增删改就写到这里,在这里我们又写了大量的 `custom hook`,自己提升还是很大的
275 |
276 | ## 二、收藏功能的实现
277 |
278 | 对于这个小星星的样式,我们采用的是 `Antd` 中而定 `Rate` 组件
279 |
280 | 
281 |
282 | 它大概长这个样子它可以通过 `count` 来控制星星的个数,因此我们重新封装一个 `Pin` 组件
283 |
284 | ```tsx
285 | interface PinProps extends React.ComponentProps {
286 | checked: boolean;
287 | onCheckedChange?: (checked: boolean) => void
288 | }
289 | export const Pin = ({ checked, onCheckedChange, ...restProps }: PinProps) => {
290 | return onCheckedChange?.(!!num)}
295 | {...restProps}
296 | />
297 | }
298 | ```
299 |
300 | 由于我们新封装的 `Pin` 组件也需要拥有 `Rate` 组件的属性,因此我们采用了一个**继承**的操作 ,我们可以通过 `React.ComponentProps ` 来获取 `Rate` 中的**所有 `props` 类型**,也就是**接收参数**的类型,我们将我们的 `Pin` 组件的 `props` 参数用上这个类型就可以了
301 |
302 | > 这里采用了一个 `!!num` 的高端操作,其实就是一个转化成 `boolean` 类型的方法
303 |
304 | 接着我们就可以在 `columns` 中使用这个 `Pin` 组件了,在星星状态改变时调用编辑方法,改变数据中的 `pin` 状态
305 |
306 | ```tsx
307 | {
308 | title: ,
309 | render(value, project) {
310 | return
317 | }
318 | },
319 | ```
320 |
321 | 在这里我们采用柯里化的方式优化了这段代码,我们在编写 `pinProject` 时,采用了柯里化的方式,一次接收一个参数,返回一个函数,最后执行 `mutate`
322 |
323 | ```tsx
324 | const { mutate } = useEditProject(useProjectsQueryKey())
325 | // 指定修改的pin id 即可
326 | const pinProject = (id: number) => (pin: boolean) => mutate({ id, pin })
327 | ```
328 |
329 | 这样我们的收藏功能就成功的实现了
330 |
331 | ## 三、实现乐观更新
332 |
333 | 接下来我们来谈谈这个乐观更新,可能很多人都不太知道乐观更新是什么东西,我们先来科普一下
334 |
335 | > 采用乐观更新,用户界面的行为就像在从服务器收到实际确认之前成功完成更改一样 ,它乐观地认为它最终会得到确认而不是错误。这可以提供更快速的用户体验。
336 | >
337 | > 简单的说,我们的页面信息会在服务器请求结果返回之前去更新,例如收藏按钮,如果我们的请求时间为 5s,那么不采用乐观更新,收藏的按钮就会在 `5s` 之后采用亮起,而采用乐观更新,则会默认的认为服务器返回的结果必然成功,我们先做去预判,先在用户点击的时候直接亮起按钮,请求让它慢慢请求去吧
338 |
339 | 现在我们就来编写一下乐观更新的代码吧~,在前面的 `hook` 中我们的第二个参数 `config` 没有讲,它就是实现乐观更新的关键
340 |
341 | 首先我们需要编写一个 `useConfig` ,这个在几个 `hook` 中都必须使用到,因为利用 `useMutation` 这个 API 来实现乐观更新,会牵扯到 `useMutation` 生命周期的问题,我们封装一个 `useConfig` 来编写这些生命周期函数
342 |
343 | 在这个 hook 中我们使用了大量的 `any` ,无关大雅
344 |
345 | 我们在成功、提交、失败中设置了相应的回调,来处理不同的请求情况
346 |
347 | ```tsx
348 | // 乐观更新,用来生产代码的 hook
349 | // 这里的类型非常的复杂采用了很多的any ,代价是可以接受的
350 | export const useConfig = (queryKey: QueryKey, callback: (target: any, old?: any[]) => any[]) => {
351 | const queryClient = useQueryClient()
352 | return {
353 | // 生命周期函数
354 | onSuccess: () => queryClient.invalidateQueries(queryKey),
355 | // 提交请求
356 | async onMutate(target: any) {
357 | // 数据列表
358 | const previousItems = queryClient.getQueryData(queryKey)
359 | queryClient.setQueryData(queryKey, (old?: any[]) => {
360 | return callback(target, old)
361 | })
362 | return { previousItems }
363 | },
364 | onError(error: any, newItem: any, context: any) {
365 | // 发生错误继续缓存旧的值
366 | queryClient.setQueryData(queryKey, context.previousItems)
367 | }
368 | }
369 | }
370 | ```
371 |
372 | 我们来简单讲讲这些 API 吧
373 |
374 | 1. `queryClient.invalidateQueries`: 在提交成功/失败之后都进行重新查询更新状态
375 | 2. `queryClient.getQueryData`:获取缓存的旧值
376 | 3. `queryClient.setQueryData`:设置值
377 |
378 | 接下来我们来编写相应的 `config` ,那 `delete` 来讲
379 |
380 | ```tsx
381 | export const useDeleteConfig = (queryKey: QueryKey) => useConfig(queryKey, (target, old) => old?.filter(item => item.id !== target.id) || [])
382 | ```
383 |
384 | 这段代码它其实就只是传入了我们删除项目的数据,然后通过 `filter` 整理了一下数据传递给了 `useConfig` ,因此,这几个都是类似的只是传递的参数不一样
385 |
386 | `useConfig` 接收 2 个参数,一个是 `queryKey` ,一个是新值旧值的函数
387 |
388 | 因此我们通过 `filter` 从旧数据中过滤掉被删除的项目,这样返回的数据就是我们所要的新数据了
389 |
390 | ```tsx
391 | export const useEditConfig = (queryKey: QueryKey) => useConfig(queryKey, (target, old) => old?.map(item => item.id === target.id ? { ...item, ...target } : item) || [])
392 | export const useAddConfig = (queryKey: QueryKey) => useConfig(queryKey, (target, old) => old ? [...old, target] : [])
393 | ```
394 |
395 | 同理这两个 `hook` 也这么写,通过数组的方法筛选出新的数据即可
396 |
397 | 这样我们的乐观更新的逻辑就完成了!
398 |
399 | > 对于底层的实现原理,还不是很熟悉,所以表诉的可能不大清楚
400 |
401 | ---
402 |
403 | 那么这部分的内容就到这里了,下一篇将会讲关于搜索部分的实现~
404 |
405 | ## 📌 总结
406 |
407 | 通过这篇文章我们可以学会以下这些内容
408 |
409 | 1. 在 antd 组件的基础上封装新的组件
410 | 2. 采用乐观更新优化体验
411 | 3. 项目的增删查功能
412 | 4. 采用 `react-query` 进行状态管理
413 | 5. 柯里化解决实际问题
414 |
415 | > 最后,可能在很多地方讲诉的不够清晰,请见谅
416 | >
417 | > 💌 如果文章有什么错误的地方,或者有什么疑问,欢迎留言,也欢迎私信交流
418 |
419 |
--------------------------------------------------------------------------------
/React Hooks 项目/(二)项目列表展示.md:
--------------------------------------------------------------------------------
1 | # Hooks + TS 搭建一个任务管理系统(二)-- 项目列表展示
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**大二的前端爱好者**
6 | >
7 | > 📢 这个系列文章是实战 jira 任务管理系统的一个学习总结
8 | >
9 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
10 | >
11 | > 📢 **愿你忠于自己,热爱生活**
12 |
13 | 在我们写好登录注册界面后,我们需要开始解决登录后的项目列表展示页,这也是我们在自动登录后显示的页面
14 |
15 | ## 💡 知识点抢先看
16 |
17 | 这篇文章将讲到以下几个知识点
18 |
19 | - antd 组件库渲染项目列表
20 | - `...` **更多**按钮的实现
21 | - 通过 **URL 进行状态管理**
22 | - 封装项目列表中的 `url` 操作
23 |
24 | 
25 |
26 | ## 一、antd 组件库渲染项目列表
27 |
28 | 首先我们先来讲讲页面中最重要的列表,这里采用的是 Antd 组件库中的 `Table` 组件为基础架构,我们在它的基础上重新创建了一个 `List` 组件表示我们的项目列表
29 |
30 | 大概的结构如下
31 |
32 | ```tsx
33 | export const List = ({ users, ...props }: ListProps) => {
34 | return
37 | }
38 | ```
39 |
40 | 我们需要向 `columns` 中注入数据,在这里我们的 `List` 组件接收了需要使用的数据,用户数据以及相关配置项
41 |
42 | 这里利用的是一个**类型的继承**
43 |
44 | ```tsx
45 | interface ListProps extends TableProps {
46 | users: User[];
47 | refresh?: () => void;
48 | }
49 | ```
50 |
51 | 我们通过这个接口继承了 `Table` 组件原先的所有 `props` 参数的类型的基础上,又添加了几个类型,这样我们的数据既能符合需求,也能顺利的**穿透**到 `Table` 组件中。同时我们需要给 `Table` 组件指定数据源 `dataSource` ,在这样处理后,我们直接可以使用 `{...props}` 即可
52 |
53 | > 在这里我们使用的 `Project` 泛型,其实也指定了 `dataSource` 的类型,也是 `columns` 中的获取数据类型
54 |
55 | 根据我们 UI 图,这里一共需要有6个数据:收**藏情况、名称、部门、负责人、创建时间、更多按钮**
56 |
57 | 这里将从三个问题来讲解如何渲染数据
58 |
59 | 1. **如何分列渲染数据?**
60 |
61 | 我们通过 `Table` 组件的 `columns` 属性添加对象的方式来实现 `List` 中的每一列,简单的说就是组件自带的属性,直接配置就好,这里的 `title` 也就是用来设置列头的标题
62 |
63 | ```tsx
64 | {
65 | title: '名称',
66 | //其他配置
67 | },
68 | // 其他5列
69 | ```
70 |
71 | 
72 |
73 | 不用标题的话可以不设置 `title` 属性
74 |
75 | 2. **如何显示数据呢?**
76 |
77 | 我们可以使用 `dataIndex` 以及 `render` 来实现
78 |
79 | **首先** `dataIndex` 这个是 `columns` 中的一个 `API` ,我们可以通过它来指定**列数据的来源**
80 |
81 | > `dataIndex` : 列数据在数据项中对应的路径,支持通过数组查询嵌套路径
82 |
83 | 对于部门的数据展示
84 |
85 | ```tsx
86 | {
87 | title: '部门',
88 | dataIndex: 'organization',
89 | sorter: (a, b) => a.name.localeCompare(b.name)
90 | }
91 | ```
92 |
93 | **它可以指定从哪里来获取这些数据,这里就是指定从 `project` 内直接获取数据**
94 |
95 | 我们这里采用的就是这种方法,这样就能直接的对数据进行**列渲染**
96 |
97 | **同时**我们还可以采用 `render` 方法
98 |
99 | > 生成复杂数据的渲染函数,参数分别为当前行的值,当前行数据,行索引
100 |
101 | 一般用来处理一些比较难的逻辑,比如 名称
102 |
103 | 我们采用的就是 `render` 来渲染
104 |
105 | ```tsx
106 | {
107 | title: '名称',
108 | sorter: (a, b) => a.name.localeCompare(b.name),
109 | render(value, project) {
110 | return {project.name}
111 | }
112 | }
113 | ```
114 |
115 | 首先值得注意的是,这里的 `render` 和其他的 `render` 不同,这里的 `render` 更像是一个函数,我们通过传递参数,然后返回结构,就能渲染在页面上
116 |
117 | > function(text, record, index) {}
118 |
119 | 它接收三个参数,都是可选的,分别是当前行的值,**当前行数据**,行索引
120 |
121 | 这里特别注意的是当前行数据,我们可以直接使用 `props` 中的数据,这里我们传入的是 `project` ,最后返回一个 `Link` 元素,这样渲染到页面上的就是一个 `Link` 标签
122 |
123 | 3. **如何实现列排序呢?**
124 |
125 | 在 `columns` 中有一个 `sorter` API,我们可以通过它来实现排序
126 |
127 | ```tsx
128 | sorter: (a, b) => a.name.localeCompare(b.name)
129 | ```
130 |
131 | 通过名字大小写来排序
132 |
133 | > 其实这里讲的都是 `Table` 组件的用法而已,查看文档也能实现
134 |
135 | 在这里有一些列中渲染的是**一个组件**,在后面会讲到
136 |
137 | ## 二、更多按钮的实现
138 |
139 | 在 `Table` 列表的 `columns` 属性中我们的最后一列(更多),采用的是一个封装的组件,这样可以减少我们 `Table` 组件的代码,同时实现组件复用(这次没有用到)
140 |
141 | 更多按钮的实现也是利用了一个 Antd 库中的 `Dropdown` 和 `Menu` 组件,实现一个下拉框的效果
142 |
143 | ```tsx
144 |
145 |
146 | 编辑
147 |
148 | confirmDeleteProject(project.id)} key={'delete'}>
149 | 删除
150 |
151 | }>
152 |
153 | ...
154 |
155 |
156 | ```
157 |
158 | 利用 `overlay` 配置一个 `Menu` 组件,在 `Menu` 中配置**下拉显示的内容** ,`Dropdown` 中直接配置 **当前显示的内容**
159 |
160 | 
161 |
162 | 这个就是实现的效果,这里封装了一个 `ButtonNoPadding` 组件,是一个 Antd 中去除 `padding` 的 `Button` 组件
163 |
164 | > 关于删改的实现后面会讲解
165 |
166 | 关于布局就涉及这么多,接下来才是重头戏
167 |
168 | ## 三、通过 URL 进行状态管理
169 |
170 | 这里有很多的问题!!!
171 |
172 | 在这里我们就讲几个 `custom hook` 吧
173 |
174 | - `useUrlQueryParam`
175 | - `useSetUrlSearchParam`
176 |
177 | 这两个 `hook` 分别是用与返回页面 `url` 中的 `query` 和设置当前的 `URL` 地址的
178 |
179 | 知道了它们的作用,我们来一步步实现它
180 |
181 | 首先在这里有人可能会有疑惑我们为什么不将这两个 `hook` 写成一个呢?
182 |
183 | > 这里一开始实现的时候是写的一个 `hook` ,但是到后面逻辑复杂了之后,就会出现无限循环的情况,同时造成 `url` 的重复跳转,难以实现我们的逻辑,因此我们将两个逻辑分离开来,让它的功能更加具体化
184 |
185 | 这里我们先来写 `useSetUrlSearchParam` ,因为在我们的查看逻辑中使用了这部分的代码
186 |
187 | ### 1. useSetUrlSearchParam
188 |
189 | **首先**我们使用 `react-router-dom` 中的 `useSearchParams` 这个 `hook` ,它返回一个 `searchParams` 和 ` setSearchParams`,从用法上来看有点像 `useState` ,通过这个 `hook` 可以来处理我们的**查询字符串**
190 |
191 | 在这里我们接收一个参数 `params` ,也就是查询字符串,用来设置我们的 `url`,例如我们的编辑页面的 `url`
192 |
193 | 
194 |
195 | 是通过拼接了一个 `editingProjectId=id` 实现的,**转化成代码**的话就是我们这里的 `params` ,在传递的时候是以对象**键值对**的方式来传递的,因此在这里我们对 `params` 的类型的定义应该符合这个规则
196 |
197 | ```tsx
198 | params: { [key in string]: unknown }
199 | ```
200 |
201 | 对于初学 TS 的来说,**如何理解这样的类型定义**呢?
202 |
203 | 我们指定 `params` 的类型是一个对象 `{}` ,它的 `:` 左侧也就是 `key` 被指定为 `string` ,右侧 `unknown` 指定 `value` 的类型
204 |
205 | 在我们成功接收到这个 `params` 时,我们将这个数据解构出来,与原先 `url` 中存在的 `query` 一同经过清理之后,将得到的对象传递给 `setSearchParams` 来设置当前的 `url`
206 |
207 | ```tsx
208 | // 通过这个单独得 hook 来 set search param
209 | // 把输入框的内容映射到url地址上
210 | export const useSetUrlSearchParam = () => {
211 | const [searchParams, setSearchParams] = useSearchParams()
212 | return (params: { [key in string]: unknown }) => {
213 | const o = cleanObject({
214 | ...Object.fromEntries(searchParams),
215 | ...params
216 | }) as URLSearchParamsInit
217 | return setSearchParams(o)
218 | }
219 | }
220 | ```
221 |
222 | **讲讲我自己对这里的理解吧**
223 |
224 | 由于我们这部分采用的是 SPA
225 |
226 | 一方面我们需要实现打开网址时,显示对应的页面,另一方面我们需要实现我们的跳转
227 |
228 | 我们在这里采用的这样的方式:**在我们点击创建或者编辑时,我们将当前的项目列表组件切换成编辑组件**,同时我们通过我们封装的 `custom hook` 来**手动的更改**当前的 `url`,从而实现了 `url` **与数据与页面相匹配**
229 |
230 | ### 2. useUrlQueryParam
231 |
232 | 首先再次明确我们这个 hook 的功能:返回页面 `url` 中的 `query` ,同时利用 `useSetUrlSearchParam` 返回的方法来设置 `url`
233 |
234 | 我们先来明确以下这个 `hook` **接收的参数和返回的值**
235 |
236 | > 接收一个 keys 的数组,也就是 `query` 中的键名的数组,返回一个数组,第一个元素是一个对象保存着 `key-value` ,第二个元素是一个方法,也就是修改 `url` 的方法
237 |
238 | 接下来我们再来确定以下**接收参数的类型**
239 |
240 | > 这里我们接收一个泛型 `K` 的数组,同时由于这是 `key` ,这个 `K` 应当继承 `string`
241 |
242 | ```tsx
243 | (keys: K[])
244 | ```
245 |
246 | 接下来我们来引入一些我们需要用到的方法,查询和设置
247 |
248 | ```tsx
249 | // 定义了一些实用的方法来处理 URL 的查询字符串
250 | const [searchParams] = useSearchParams()
251 | const setSearchParams = useSetUrlSearchParam() // 引入这个自定义的方法,不使用原生自带的
252 | ```
253 |
254 | 我们再来研究以下如何返回当前 `url` 的 `query` 对象
255 |
256 | ```tsx
257 | useMemo(
258 | () => keys.reduce((prev, key) => {
259 | // 解决当get 的值是null 时的默认值
260 | return { ...prev, [key]: searchParams.get(key) || '' }
261 | // 传入的是一个 key 类型在 K 中值为 string 的对象
262 | }, {} as { [key in K]: string }),
263 | [keys, searchParams]
264 | )
265 | ```
266 |
267 | 首先我们通过 `reduce` **遍历**传入的 `keys` 数组,每一次遍历都将使用 `searchParams` 方法去**查找对应的** `value` 值,遍历完成后会**返回整个对象**,利用 `reduce` 将每次的 `key-value` 添加到 `{}` 中,最后全部返回
268 |
269 | 这里我们给 `reduce` 传入了第二个参数,**指定了我们传入的函数的初始值**
270 |
271 | 同时在这里我们采用了 `useMemo` 这个 `hook` 来优化我们的代码,只有在依赖项改变的时候才会重新计算,这样可以**解决无限循环**的问题(**todo**: 关于无限循环的问题之后出一篇文)
272 |
273 | 接下来我们来研究返回数组的第二个值
274 |
275 | ```tsx
276 | // 键值限定在我们设置的范围之内
277 | (params: Partial<{ [key in K]: unknown }>) => {
278 | // 把 fromEntries 转化为一个对象
279 | return setSearchParams(params)
280 | }
281 | ```
282 |
283 | 这个很简单,直接将传入的 `params` 传递给 `setSearchParams` 中添加就可以了~
284 |
285 | 在这里我们采用了一个 `Partial` 方法,它是 `TS ` 联合类型中的一个点,它可以把指定的泛型中的类型都**变成可选**的
286 |
287 | **底层实现**
288 |
289 | ```tsx
290 | type Partial = {
291 | [P in keyof T]?: T[P];
292 | };
293 | ```
294 |
295 | 最后一个非常重要的点是 `as const` ,这个也是 `TS` 中比较高级的用法,也叫做 **const 断言**,否则会错乱
296 |
297 | 关于 const 断言,做个简单的解释,如果没有使用 `as const` 的话,会默认的进行类型推断,`return` 返回的是一个函数类型的数组,但是它完全忘记了有两个元素,因此会丢失返回数组中元素的类型,采用 `const` 断言,就能指示使表达式的字面类型不被扩展
298 |
299 | 未采用 `const` 断言
300 |
301 | 
302 |
303 | 采用 `const` 断言
304 |
305 | 
306 |
307 | 能明显感受出来它们的不同
308 |
309 | 以下是 `return` 的完整代码
310 |
311 | ```tsx
312 | return [
313 | useMemo(
314 | () => keys.reduce((prev, key) => {
315 | return { ...prev, [key]: searchParams.get(key) || '' }
316 | }, {} as { [key in K]: string }),
317 | [keys, searchParams]
318 | ),
319 | (params: Partial<{ [key in K]: unknown }>) => {
320 | return setSearchParams(params)
321 | }
322 | ] as const
323 | ```
324 |
325 | 为了给下一篇文章搭建好梯子,接下来我们写一下我们这两个 `custom hook` 在 `project` 列表中的应用
326 |
327 | ## 四、封装项目列表中的 url 操作
328 |
329 | 由于我们在 `project` 列表中会大量使用到 `url` 操作,为了能将我们的代码更加简洁,我们利用 `useUrlQueryParam` 这个轮子来造车,在这个基础上将 `project` 的特定 `keys` 传入即可,这样我们在 `project` 中使用时,就可以直接调用对应的 `searchParams` 方法
330 |
331 | 这里我们讲 3 个 `custom hook`
332 |
333 | - useProjectsSearchParams
334 | - useProjectsQueryKey
335 | - useProjectModel
336 |
337 | ### 1. useProjectsSearchParams
338 |
339 | 这一个 `hook` 就是 `useUrlQueryParam` 的作用,只是将它具体到了 `project` 中使用
340 |
341 | 返回的是一个数组,第一个元素是查找的数据,第二个是修改的方法
342 |
343 | ```tsx
344 | export const useProjectsSearchParams = () => {
345 | // 要搜索的数据
346 | // 返回的是一个新的对象,造成地址不断改变,不断的渲染
347 | // 用这个方法来设置路由地址跟随输入框变化
348 | // 服务器返回的都是 string 类型
349 | const [param, setParam] = useUrlQueryParam(['name', 'personId'])
350 | return [
351 | // 采用 useMemo 解决 重复调用的问题
352 | useMemo(() => ({ ...param, personId: Number(param.personId) || undefined }), [param]),
353 | setParam
354 | ] as const
355 | }
356 | ```
357 |
358 | 我们在使用这个 `hook` 的时候,**直接调用**即可,因为我们已经指定了它的 `keys` 数组为 `['name', 'personId']`,这个是在 **搜索模块** 中使用的 `hook`
359 |
360 | ### 2. useProjectsQueryKey
361 |
362 | 这个 `hook` 用来返回 `query` 的键值对,返回的是 `{name: '', personId: undefined}` 样式
363 |
364 | ```tsx
365 | export const useProjectsQueryKey = () => {
366 | const [params] = useProjectsSearchParams()
367 | // {name: '', personId: undefined}
368 | return ['projects', params]
369 | }
370 | ```
371 |
372 | 我们在使用的时候也是直接调用即可返回数据
373 |
374 | ### 3. useProjectModel
375 |
376 | 我们通过这个 `hook` 来判断当前的状态**是不是在创建、编辑**,如果是的话我们就显示出我们对应的页面
377 |
378 | 首先我们先从利用 `useUrlQueryParams` 来获取到页面的 `query` 对象
379 |
380 | 再**通过对象解构的方式,解构出对应的数据**,例如这里我们解构出 `query` 中的 `projectCreate` 字段
381 |
382 | 那第一个来说就是利用 `useUrlQueryParam` 传入 `projectCreate` 来在 `url` 中查找有没有这个字段,**返回查找的结果**,同时返回一个可以修改它的函数 `setProjectCreate` ,这就是我们的 `url custom hook` 发挥的作用了
383 |
384 | ```tsx
385 | const [{ projectCreate }, setProjectCreate] = useUrlQueryParam([
386 | 'projectCreate'
387 | ])
388 | // 判断当前是不是在编辑,解构出当前编辑项目的 id
389 | const [{ editingProjectId }, setEditingProjectId] = useUrlQueryParam([
390 | 'editingProjectId'
391 | ])
392 | ```
393 |
394 | 在接下来的代码中就是**封装一些更改它们的方法**,暴露出去给外部直接调用,例如**控制 modal 页面的开关**,`open` 和 `close` 方法,**控制编辑页面**开启的 `startEdit` 方法
395 |
396 | 代码逻辑非常简单,我们只需要调用对应的 `set...` 方法来改变 `url` 中的对应键值对的值就可以了
397 |
398 | ```tsx
399 | const open = () => setProjectCreate({ projectCreate: true })
400 | const startEdit = (id: number) => setEditingProjectId({ editingProjectId: id })
401 | const close = () => setUrlParams({
402 | editingProjectId: undefined, projectCreate: undefined
403 | })
404 | ```
405 |
406 | 例如 `open` 我们通过 `setProjectCreate({ projectCreate: true })` 将 `projectCreate` 改成 `true` 表示当前正在创建的页面
407 |
408 | 关于这个 `editingProjectId` 我们可以通过 `useProject` 这个 `custom hook` 来获取(或许在下一篇会讲到,这里不展开),采用的是 `react-query` , 它返回的是一个 `data` 数 据
409 |
410 | 最后我们暴露这些方法
411 |
412 | ```tsx
413 | return {
414 | // 采用 id才是最佳选择,这样不用等待数据返回就能打开编辑框
415 | projectModelOpen: projectCreate === 'true' || Boolean(editingProjectId),
416 | open,
417 | close,
418 | startEdit,
419 | editingProject,
420 | isLoading
421 | }
422 | ```
423 |
424 | 这样我们的 `project` 列表下的 `url` 控制操作 `hook` 就全部完成了
425 |
426 | 那么这篇文章就到这里结束了,在接下来的文章中,会利用这些封装好的 `hook` **去实现项目列表的增删改查以及乐观更新等功能**
427 |
428 | ## 📌 总结
429 |
430 | 1. 在这篇文章中我们写了大量的 `custom hook` ,也更加的熟练了它的写法和好处
431 | 2. 对 `const` 断言有了一定的了解
432 | 3. 学会了如何使用 `Table` 、`Dropdown` 等组件
433 | 4. 大致的认识了 `useMemo` 的用法
434 | 5. 对 `useSearchParams` 有了一定的了解
435 | 6. `TS` 中的联合类型有了更深的理解
436 |
437 | > 最后,可能在很多地方讲诉的不够清晰,请见谅
438 | >
439 | > 💌 如果文章有什么错误的地方,或者有什么疑问,欢迎留言,也欢迎私信交流
440 |
441 |
--------------------------------------------------------------------------------
/React Hooks 项目/(五)路由跳转页面.md:
--------------------------------------------------------------------------------
1 | # Hooks + TS 搭建一个任务管理系统(五)-- 路由跳转页面
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**大二的前端爱好者**
6 | >
7 | > 📢 这个系列文章是实战 jira 任务管理系统的一个学习总结
8 | >
9 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
10 | >
11 | > 📢 **愿你忠于自己,热爱生活**
12 |
13 | 在上一篇文章中我们已经写完了**首页项目列表的展示部分**,利用了大量的 `custom hook` 来处理对 `url` 进行操作,实现了将 `query` 映射到 `url` 的操作,同时利用 `react-query` 中的 `useMutation` 搭配实现了乐观更新的效果,同时利用 `useDebounce` 来减少请求,优化性能
14 |
15 | 接下来我们将处理一下其他的页面,在开发其他页面之前,我们先树立好骨架,先将页面的跳转以及 `title` 变化这些基本的独立于业务之外的东西写好
16 |
17 | ## 💡 知识点抢先看
18 |
19 | - 利用 `router 6` 实现路由跳转
20 | - 封装 `useDocumentTitle` 来**设置文档的标题**
21 |
22 | 实现效果
23 |
24 | 
25 |
26 | ## 一、利用 router 实现路由跳转
27 |
28 | 实现跳转我们先把视线放到点击的链接上,在这里我们给项目利用了 `Link` 组件进行包裹,同时采用 `to` 属性实现了 `url` 的转变
29 |
30 | ```tsx
31 | {
32 | title: '名称',
33 | sorter: (a, b) => a.name.localeCompare(b.name),
34 | render(value, project) {
35 | return {project.name}
36 | }
37 | }
38 | ```
39 |
40 | 现在当我们点击第一个项目时,会将路由跳转到了 `projects/1` 地址下,这样显然是不能找到对应的页面的,它缺少了页面的标识
41 |
42 | 我们在 `project/index.tsx` 文件中,编写侧边栏的样式,以及设置路由的跳转,这里我们需要采用 `react-router` ,以及 `antd` 配合实现
43 |
44 | ```tsx
45 |
55 | ```
56 |
57 | 在这里我们采用了 `Menu` 菜单标签,利用了 `react-router-dom` 中的 `Link` 组件来实现地址的跳转,侧边栏对地址的操作,会导致右侧,看板和任务组的切换,因此我们需要给右侧配置相应的 `Route` 连接组件
58 |
59 | ```tsx
60 |
61 |
62 | } />
63 | } />
64 | {/* 默认路由是push,相当于又成为了栈顶,也就是当前页面被push了两次,第一次的值不匹配第二次才匹配 */}
65 | {/* 采用replace这样就能替换掉传入的栈顶元素,下面的路由成为了栈顶*/}
66 |
67 |
68 |
69 | ```
70 |
71 | 在这里我们需要设置一个 `Navigate` 用来做路由跳转的兜底方案,当上面**两个都没有匹配**上时,我们将它的地址**拼接**上 `/kanban` 强制的跳转到 `/kanban` 页面,这也是实现我们**从项目列表点击跳转后显示看板页面的原因**
72 |
73 | 在这里有很多值得注意的地方,我们在这里采用了 `replace` 来**替换路由,这是有原因的!**
74 |
75 | 浏览器的历史记录就像一个**栈的数据结构**,当我们采用 `to` 跳转时,实际上是向栈中 `push` 了一个路由地址,这里我们采用 `Navigate` 来进行**设置默认路由**,它的操作也是 `push`,也就是说,我们为了跳转到当前页面被 `push` 了**两次**
76 |
77 | 因此当我们点击返回上一页时,又会跳转到当前的 `kanban` 页面,又向栈中 `push` 了**两个地址**,这样我们的返回就永远在这里不断地循环,**永远返回不去上一页。**
78 |
79 | 因此在这里我们需要采用 `repalce` 去**替换栈顶那个匹配不上的路由地址!**
80 |
81 | **Q&A**
82 |
83 | **在实现这部分的时候,遇到了一些问题,稍微提及一下,给后人乘凉**
84 |
85 | 由于使用的是最新版的 `router` 在安装的时候,会让你选择版本,目前应该是更新到了 `react-router6 - beta4` 版本了,在这个版本中使用 `Navigate` 会有问题,这个 `Navigate` 的默认路由不会生效,具体原因不是很清楚,遇到这种情况可以降低一下版本到 `beta0`
86 |
87 | 这个版本中是没有问题的
88 |
89 | ## 二、封装 useDocumentTitle 来设置文档的标题
90 |
91 | 在上面我们已经顺利的实现了路由跳转,对 `Router` 有了一定的理解,接下来我们来做一个好玩的 `hook` ,它用来控制文档的标题
92 |
93 | 
94 |
95 | 大概的效果是这样,这个 `hook` 我们可以**迁移到其他的项目中使用,复用性很高**
96 |
97 | 我们先理一下思路
98 |
99 | 1. 首先需要获取到当前的 `title`
100 | 2. 在调用 `hook` 的时候需要接收一个 `title` ,并设置一个 `title`
101 | 3. 会不会有时候设置 `title` 一样 ,不需要重新设置呢
102 |
103 | 我们先来看看我们实现好的 `useDocumentTitle` 是如何使用的
104 |
105 | ```tsx
106 | useDocumentTitle('项目列表', false)
107 | ```
108 |
109 | 第一个参数传递的是需要设置的 `title` ,第二个参数用来配置 `title` 在组件卸载时是不是需要变化
110 |
111 | 首先我们先来设置一下它接收的参数类型
112 |
113 | ```tsx
114 | export const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
115 | }
116 | ```
117 |
118 | 这里我们接收传来的 `title` 和 配置选项
119 |
120 | 首先我们先让 `title` 能够驱动页面 `title` 的更新
121 |
122 | 我们利用 `useEffect` 来实现在 `title` 变化时,修改文档标题
123 |
124 | ```tsx
125 | useEffect(() => {
126 | document.title = title
127 | }, [title])
128 | ```
129 |
130 | 接下来我们来处理,组件在卸载时不变化的情况,为什么需要添加这个逻辑呢?
131 |
132 | 如果我们不添加这个逻辑的话,需要每个页面都指定 `title` 如果未指定就会显示默认的 `title` ,因此我们增加了这个可选配置项
133 |
134 | ```tsx
135 | // 利用 useRef 自定义 hook 它会一直帮我们保存好这个 title值,不会改变,
136 | const oldTitle = useRef(document.title).current
137 | ```
138 |
139 | 首先我们采用 `useRef` 来保存当前的 `title`,也就是更改前的 `title`
140 |
141 | 接着我们采用 `useEffect` 来处理在组件卸载时的 `title` 变化
142 |
143 | ```tsx
144 | useEffect(() => {
145 | // 利用闭包不指定依赖得到的永远是旧title ,是代码初次运行时的 oldTitle
146 | // 不利于别人阅读
147 | return () => {
148 | if (!keepOnUnmount) {
149 | document.title = oldTitle
150 | }
151 | }
152 | }, [keepOnUnmount, oldTitle])
153 | ```
154 |
155 | 这里我们利用到了闭包的知识, `oldTitle` 一直是初次运行的 `title`
156 |
157 | ## 📌 总结
158 |
159 | 这篇文章没有太多的内容,写了一个简单的 `hook` ,稍稍总结一下
160 |
161 | 1. 利用 `Router` 实现路由跳转
162 | 2. 避免 `react-router` 版本问题,产生的错误
163 | 3. 封装高复用性的 `hook` `useDocumentTitle`
164 |
165 | > 最后,可能在很多地方讲诉的不够清晰,请见谅
166 | >
167 | > 💌 如果文章有什么错误的地方,或者有什么疑问,欢迎留言,也欢迎私信交流
168 |
169 |
--------------------------------------------------------------------------------
/React Hooks 项目/(八)拖拽功能实现.md:
--------------------------------------------------------------------------------
1 | # Hooks + TS 搭建一个任务管理系统(八)-- 拖拽功能实现
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**大二的前端爱好者**
6 | >
7 | > 📢 这个系列文章是实战 jira 任务管理系统的一个学习总结
8 | >
9 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
10 | >
11 | > 📢 **愿你忠于自己,热爱生活**
12 |
13 | 在上一篇文章中,我们写好了任务组页面,就现在来说我们的项目已经基本完成了,所有的 CRUD 操作、路由跳转、页面布局都已经实现了。在这一篇文章中,我们再来优化一下我们的项目,我们给我的看板页面添加一个**拖拽功能**
14 |
15 | **这篇内容不是很懂,有点水,弄懂再来改**
16 |
17 | ## 💡 知识点抢先看
18 |
19 | - 给看板添加拖拽功能
20 | - 讲解 HTML5 中的 `drop` 和 `drag`
21 |
22 | ## 一、给看板添加拖拽功能
23 |
24 | 这一篇文章就只讲一个部分,正如标题所说,添加一个**拖拽功能**
25 |
26 | 实现效果像这样
27 |
28 | 
29 |
30 | 我们实现这个功能采用了一个 `react-beautiful-dnd` 的库,关于这个库可以查看 : [npm官网](https://www.npmjs.com/package/react-beautiful-dnd)
31 |
32 | 关于这个库的使用呢,我们简单的介绍一下,首先我们需要定义一个 `Droppable` 组件来包裹我们的拖拽的元素,表示这块区域的内容我们能够拖拽,其次需要对**放的地方**,也就是我们的元素添加一个 `Draggable` 组件包裹,用来表示这块区域是能够放下的区域
33 |
34 | 在这里是重写了自带的 `Drop` 和 `Drag` 组件
35 |
36 | **这部分比较难,搞得不是很懂,提几个点吧**
37 |
38 | - 在这里我们想要抽离出一个 `children` 属性,不使用原生的 `children` 属性
39 | - 由于 API 的要求,我们需要预留接收 `ref`,这里我们采用转发的方式来实现,通过 `forwardRef` 的方式来实现
40 |
41 | ```tsx
42 | export const DropChild = React.forwardRef(({ children, ...props }, ref) =>
43 |
44 | {children}
45 | {/* api要求加的 */}
46 | {props.provided?.placeholder}
47 |
48 | )
49 | ```
50 |
51 | ### 1. 实现 `Drop` 组件
52 |
53 | ```tsx
54 | // 这个文件相当于重构了 drop 原生组件
55 | // 定义一个类型,不想用 自带的 children ,采用自己的
56 | type DropProps = Omit & { children: ReactNode }
57 | export const Drop = ({ children, ...props }: DropProps) => {
58 | return
59 | {
60 | (provided => {
61 | if (React.isValidElement(children)) {
62 | // 给所有的子元素都加上props属性
63 | return React.cloneElement(children, {
64 | ...provided.droppableProps,
65 | ref: provided.innerRef,
66 | provided
67 | })
68 | }
69 | return
70 | })
71 | }
72 |
73 | }
74 | ```
75 |
76 | ### 2. 实现 `Drag` 组件
77 |
78 | ```tsx
79 | type DragProps = Omit & { children: ReactNode }
80 | export const Drag = ({ children, ...props }: DragProps) => {
81 | return
82 | {
83 | provided => {
84 | if (React.isValidElement(children)) {
85 | return React.cloneElement(children, {
86 | ...provided.draggableProps,
87 | ...provided.dragHandleProps,
88 | ref: provided.innerRef
89 | })
90 | }
91 | return
92 | }
93 | }
94 |
95 | }
96 | ```
97 |
98 | ### 3. 拖拽持久化
99 |
100 | 写好了两个组件,虽然很难,可以直接 `cv` 一下这部分的代码。
101 |
102 | - 理解起来还是挺可以的,使用 `Drop` 组件包裹拖得位置,用 `Drag` 组件包裹放的位置
103 | - 最后我们需要持久化我们的状态,这里采用的是原生组件中自带的 `onDragEnd` 方法来实现
104 |
105 | 我们在这里需要再实现一个 `hook` 来实现这个功能,很难
106 |
107 | 这里我们通过 `if` 判断它当前**是拖的看板还是任务**,判断一下是**左右**还是**上下**拖拽,通过组件中自带的方法计算出放下的 `id` 和拿起来的 `id` 将它插入到这个 `kanban` 任务中即可
108 |
109 | > 当我们拖拽完成时,会返回 `source` 和 `destination` 对象,这里面有我们拖拽的相关信息
110 |
111 | 如果是 `column` 的话就是看板之间的拖拽,我们需要调用我们新封装的一个 `useReorderKanban` 方法进行持久化
112 |
113 | 如果是 `row` 则调用任务之间的持久化方法 `useRecordTask` 方法进行持久化
114 |
115 | ```tsx
116 | export const useDragEnd = () => {
117 | // 先取到看板
118 | const { data: kanbans } = useKanbans(useKanbanSearchParams())
119 | const { mutate: reorderKanban } = useReorderKanban(useKanbansQueryKey())
120 | // 获取task信息
121 | const { data: allTasks = []} = useTasks(useTasksSearchParams())
122 | const { mutate: reorderTask } = useReorderTask(useTasksQueryKey())
123 | return useCallback(({ source, destination, type }: DropResult) => {
124 | if (!destination) {
125 | return
126 | }
127 | // 看板排序
128 | if (type === 'COLUMN') {
129 | const fromId = kanbans?.[source.index].id
130 | const toId = kanbans?.[destination.index].id
131 | // 如果没变化的时候直接return
132 | if (!fromId || !toId || fromId === toId) {
133 | return
134 | }
135 | // 判断放下的位置在目标的什么方位
136 | const type = destination.index > source.index ? 'after' : 'before'
137 | reorderKanban({ fromId, referenceId: toId, type })
138 | }
139 | if (type === 'ROW') {
140 | // 通过 + 转变为数字
141 | const fromKanbanId = +source.droppableId
142 | const toKanbanId = +destination.droppableId
143 | // 不允许跨版排序
144 | if (fromKanbanId !== toKanbanId) {
145 | return
146 | }
147 | // 获取拖拽的元素
148 | const fromTask = allTasks.filter(task => task.kanbanId === fromKanbanId)[source.index]
149 | const toTask = allTasks.filter(task => task.kanbanId === fromKanbanId)[destination.index]
150 | //
151 | if (fromTask?.id === toTask?.id) {
152 | return
153 | }
154 | reorderTask({
155 | fromId: fromTask?.id,
156 | referenceId: toTask?.id,
157 | fromKanbanId,
158 | toKanbanId,
159 | type: fromKanbanId === toKanbanId && destination.index > source.index ? 'after' : 'before'
160 | })
161 | }
162 | }, [allTasks, kanbans, reorderKanban, reorderTask])
163 | }
164 | ```
165 |
166 | ### 4. useReorderKanban
167 |
168 | 通过传入一组数据,包括起始位置,插入位置,在插入位置的前面还是后面,这些数据,进行后台接口的判断,来进行持久化,这里采用的 `useMutation` 就是前面讲的,使用方法都很熟练了
169 |
170 | ```tsx
171 | // 持久化数据接口
172 | export const useReorderKanban = (queryKey:QueryKey) => {
173 | const client = useHttp()
174 | return useMutation(
175 | (params: SortProps) => {
176 | return client('kanbans/reorder', {
177 | data: params,
178 | method: "POST"
179 | })
180 | },
181 | useReorderKanbanConfig(queryKey)
182 | )
183 | }
184 | ```
185 |
186 | ### 5. 在 HTML5 中新增的 Drop 和 Drag
187 |
188 | 当我们需要设置某个元素可拖放时,只需要 `draggable` 设置为 `true`
189 |
190 | ```html
191 |
192 | ```
193 |
194 | 当拖放执行时,会发生 `ondragstart` 和 `setData()`
195 |
196 | 执行 `ondragstart` 会调用一个函数 `drag` 函数,它规定了被拖拽的数据
197 |
198 | ```js
199 | function drag(event)
200 | {
201 | event.dataTransfer.setData("Text",ev.target.id);
202 | }
203 | ```
204 |
205 | > 这里的 `Text` 时我们需要添加到 `drag object` 中的数据类型
206 |
207 | 在何处放置被拖动的数据
208 |
209 | > 默认地,无法将数据/元素放置到其他元素中。如果需要设置允许放置,我们必须阻止对元素的默认处理方式。
210 | >
211 | > 这要通过调用 `ondragover` 事件的 `event.preventDefault()` 方法:
212 |
213 | ```js
214 | event.preventDefault()
215 | ```
216 |
217 | 当防止时会发生 `drop` 事件
218 |
219 | ```js
220 | function drop(ev)
221 | {
222 | ev.preventDefault();
223 | var data=ev.dataTransfer.getData("Text");
224 | ev.target.appendChild(document.getElementById(data));
225 | }
226 | ```
227 |
228 | 代码解释:
229 |
230 | - 调用 `preventDefault()` 来避免浏览器对数据的默认处理(`drop` 事件的默认行为是以链接形式打开)
231 | - 通过 `dataTransfer.getData("Text")` 方法获得被拖的数据。该方法将返回在 `setData()` 方法中设置为相同类型的任何数据。
232 | - 被拖数据是被拖元素的 `id ("drag1")`
233 | - 把被拖元素追加到放置元素(目标元素)中
234 |
235 | (参考于[菜鸟教程](https://www.runoob.com/html/html5-draganddrop.html))
236 |
237 | 可以亲自试一试:[在线演示](https://codepen.io/linjc55/pen/ZEJWBrN)
238 |
239 | ## 📌 总结
240 |
241 | 1. 大概了解了一下如何使用 `react-beautiful-dnd`
242 | 2. 关于拖拽持久化有了大概的认识
243 | 3. 了解了 HTML5 中的 `drop` 和 `drag`
244 |
245 | > 最后,可能在很多地方讲诉的不够清晰,请见谅
246 | >
247 | > 💌 如果文章有什么错误的地方,或者有什么疑问,欢迎留言,也欢迎私信交流
248 |
--------------------------------------------------------------------------------
/React Hooks 项目/(六)看板页面展示.md:
--------------------------------------------------------------------------------
1 | # Hooks + TS 搭建一个任务管理系统(六)-- 看板页面展示
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**大二的前端爱好者**
6 | >
7 | > 📢 这个系列文章是实战 jira 任务管理系统的一个学习总结
8 | >
9 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
10 | >
11 | > 📢 **愿你忠于自己,热爱生活**
12 |
13 | 在上一篇文章中,我们实现了路由的跳转,实现了对应项目跳转到显示对应内容的看板页面,在这当中,我们编写了 `useDocumentTitle` 、`useDebounce` 这两个给 `custom hook` 。接下来我们将来处理看板部分的展示
14 |
15 | ## 💡 知识点抢先看
16 |
17 | - 封装 `KanbanColumn` 来布局页面
18 | - 编写大量的 `custom hook` 来处理看板数据
19 | - 对 `useQuery` 有进一步的了解
20 | - 利用 `filter` 实现数据的统一性
21 |
22 | 
23 |
24 | ## 一、处理看板数据的 custom hook
25 |
26 | 在这里我们需要先解决以下**获取看板数据**的问题,有了数据我们才能更好的驱动视图
27 |
28 | 我们将这些 `hook` 单独写在一个 `kanban.ts` 写在 `util` 文件夹内,这个文件夹中的 `hook` 都是一些**复用性高的,和页面关系不大**的 `hook`
29 |
30 | ### 1. useKanbans
31 |
32 | 这里获取数据的方法和前面获取项目数据的方法一样,我们采用 `useQuery` 来进行缓存看板数据,这里我们需要接收一个 `param` 作为参数,传递当前的 `projectId` 即可,当这个 `id` 变化时,表示切换了其他项目的看板,我们需要**重新请求以下**
33 |
34 | ```tsx
35 | export const useKanbans = (param?: Partial) => {
36 | // 采用 useHttp 来封装请求
37 | const client = useHttp()
38 | // 映射一个 名为 kanbans 的缓存数据,当 param 变化时,重新发送请求,写入缓存
39 | return useQuery(['kanbans', param], () => client('kanbans', { data: param }))
40 | }
41 | ```
42 |
43 | 封装好了 `usekanbans` ,我们已经能够获取项目中的看板数据了,接下来我们在封装一个 `custom hook` 来获取 `projectId` ,以实现 `useKanBans` 的用处
44 |
45 | ### 2. useProjectIdInUrl
46 |
47 | 我们在 `kanban` 文件夹,下的 `util` 中编写这段代码,因为它和项目有着直接的关系
48 |
49 | 首先在我们之前的路由处理中,我们将我们的 `projectId` 映射到了 `url` 上,我们可以通过**解析这个 `url` 地址来得到当前页面请求的项目 `id`**
50 |
51 | 这里我们采用 `react-router` 中的 `hook` 来得到 `pathname`,它的格式是这样的 `/projects/1/kanban`
52 |
53 | 因此我们通过**正则表达式**来获取出当中的数字也就是我们的 `proejctId` ,最后返回这个 `id` 的数字类型即可
54 |
55 | ```tsx
56 | export const useProjectIdInUrl = () => {
57 | const { pathname } = useLocation()
58 | // 返回的是一个数组
59 | const id = pathname.match(/projects\/(\d+)/)?.[1]
60 | return Number(id)
61 | }
62 | ```
63 |
64 | ### 3. useProjectInUrl
65 |
66 | 有了我们的 `projectId` ,我们就可以使用通过它来获取我们的项目数据,这样我们就能获取到我们的**项目的名称**,显示到页面上
67 |
68 | ```tsx
69 | // 通过 id 获取项目信息
70 | export const useProjectInUrl = () => useProject(useProjectIdInUrl())
71 | ```
72 |
73 | 使用
74 |
75 | ```tsx
76 | const { data: currentProject } = useProjectInUrl()
77 | {currentProject?.name}看板
78 | ```
79 |
80 | 写到这里我们已经能够获取到看板数据以及项目信息了,接下来我们需要来获取对应的任务信息
81 |
82 | ### 4. useKanbanSearchParams
83 |
84 | 为了避免我们获取到的看板数据是全部项目中的看板数据,我们需要将 `id` 转为 `key-value` 传递给 `useKanbans` 来获取数据
85 |
86 | ```tsx
87 | export const useKanbanSearchParams = () => ({ projectId: useProjectIdInUrl() })
88 | ```
89 |
90 | ### 5. useTasks
91 |
92 | 接着我们需要来获取 `task` 数据,也就是我们这个项目的**任务数据**
93 |
94 | 和获取 `kanban` 数据一样,我们需要采用 `useQuery` 来处理
95 |
96 | ```tsx
97 | export const useTasks = (param?: Partial) => {
98 | const client = useHttp()
99 | // 搜索框请求在这里触发
100 | return useQuery(['tasks', param], () => client('tasks', { data: param }))
101 | }
102 | ```
103 |
104 | 在这里就讲讲类型吧~
105 |
106 | 在这里我们接收一个可选的参数,`Task` ,`Task` 是我们封装在 `types` 中的一个**共享接口**
107 |
108 | ````tsx
109 | export interface Task {
110 | id: number;
111 | name: string;
112 | // 经办人
113 | processorId: number;
114 | projectId: number;
115 | // 任务组
116 | epicId: number;
117 | kanbanId: number;
118 | // bug or task
119 | typeId: number;
120 | note: string;
121 | }
122 | ````
123 |
124 | 这里定义的都是后端**返回的数据类型**
125 |
126 | `Partial` 的作用是,让接口中的变量都变成**可选的**
127 |
128 | 这样我们就也实现了对看板中的 `task` 获取,接下来同样的我们需要实现获取对应看板中的 `task`
129 |
130 | ### 6. useTasksSearchParams
131 |
132 | 为了让我们获取到的任务数据来自于当前的看板我们也需要封装一个 `searchParams` 来获取相应项目下的看板信息
133 |
134 | ```tsx
135 | export const useTaskSearchParams = () => ({ projectId: useProjectIdInUrl() })
136 | ```
137 |
138 | 在之后,我们会对这个 `hook` 进行改造
139 |
140 | ## 二、封装 KanbanColumn 渲染页面
141 |
142 | ### 1. 看板和任务数据统一
143 |
144 | 明确我们这个组件的作用,我们需要用它来渲染每一列的看板
145 |
146 | 
147 |
148 |
149 |
150 | 大概是这样一个布局,首先,因为我们需要将**任务渲染到对应的看板列表**下,因此首先我们需要解决数据的问题
151 |
152 | 我们在 `KanbanColumn` 中获取数据,在这里我们需要十分明确,这个我们的这个组件它**只是渲染一列**,我们通过遍历实现多列,这个很关键
153 |
154 | 我们在 `column` 中获取所有的 `task` 数据,**通过 `filter` 方法,将它筛选出来**,这样,最后得到的就是和 `kanbanId` 匹配的 `task` 数据
155 |
156 | ```tsx
157 | const { data: allTasks } = useTasks(useTasksSearchParams())
158 | // 对数据进行分类,返回的是三段数据,都是数组
159 | const tasks = allTasks?.filter(task => task.kanbanId === kanban.id)
160 | ```
161 |
162 | **在这里有一个很有意思的问题**
163 |
164 | 我们个每一个 `column` 都绑定了一个 `useTasks` ,按理说它应该会发送**多次的请求** ,我们来看看到底是不是这样
165 |
166 | 
167 |
168 | 在这里我们可以发现它一共发送了 2次请求,但是我启动的这个看板中有三个 `column`
169 |
170 | 不妨我们再多添加几个 `column` ,我们再来看看
171 |
172 | 
173 |
174 | 在这里始终都是只有2个请求,那这是为什么呢?
175 |
176 | 其实在我们在遍历添加 `kanbanColumns` 组件时,只会发起一个请求,即使,我们给每一个 `column` 都绑定了 `useTask`
177 |
178 | 这是因为,我们采用的 `react-query` 的功劳,在我们采用 `useQuery` 时,**如果在 2s 之内有相同的 `queryKey` 发出请求的话,就会合并这些请求,只会发出一个**
179 |
180 | 现在我们已经有了每个看板下的 `Task` 数据了,我们只需要遍历渲染即可,这里我们采用的还是 `Antd` 组件库
181 |
182 | ### 2. useTaskTypes 处理不同类型任务的 icon
183 |
184 | 在我们的任务中又分为 `bug` 和 `task`,我们都会有相应的图标展示
185 |
186 | 在这里我们在 `utils` 下封装一个 `useTaskTypes` 来获取 `task` 的类型
187 |
188 | ```tsx
189 | export const useTaskTypes = () => {
190 | const client = useHttp()
191 | // 获取所有的task type
192 | return useQuery(['taskTypes'], () => client('taskTypes'))
193 | }
194 | ```
195 |
196 | 在这里我们封装一个 `TaskTypeIcon` 小组件,来返回类型对应的 `icon` ,这里我们只需要接收一个 `taskid` 作为参数,用来**判断这个任务是什么类型**
197 |
198 | ```tsx
199 | // 通过type渲染图片
200 | const TaskTypeIcon = ({ id }: { id: number }) => {
201 | const { data: taskTypes } = useTaskTypes()
202 | const name = taskTypes?.find(taskType => taskType.id === id)?.name;
203 | if (!name) {
204 | return null
205 | }
206 | return
207 | }
208 | ```
209 |
210 | ## 三、处理任务的搜索功能
211 |
212 | ### 1. useTasksSearchParams
213 |
214 | 在我们前面已经有用到这个 `hook` 了,现在,我们需要添加一些代码,来实现**搜索框的逻辑**,在之前我们通过这个来返回用户 `id` 的对象,这个功能也不能遗忘噢~
215 |
216 | ```tsx
217 | export const useTasksSearchParams = () => {
218 | // 搜索内容
219 | const [param] = useUrlQueryParam([
220 | 'name',
221 | 'typeId',
222 | 'processorId',
223 | 'tagId'
224 | ])
225 | // 获取当前的项目id用来获取看板数据
226 | const projectId = useProjectIdInUrl()
227 | // 返回的数组,并监听 param变化
228 | return useMemo(() => ({
229 | projectId,
230 | typeId: Number(param.typeId) || undefined,
231 | processId: Number(param.processorId) || undefined,
232 | tagId: Number(param.tagId) || undefined,
233 | name: param.name
234 | }), [projectId, param])
235 | }
236 | ```
237 |
238 | 在这里我们封装的这个方法,用于返回最小的 `task` 列表数据,这里需要实现的搜索功能在前面的项目搜索框也实现过了,采用 `useSetUrlSearchParam` 来修改当前的 `url` 地址,来造成数据的变化,又由于,我们这个 `hook` 返回的数据中的依赖项发生改变,造成了显示内容的改变,从而达到搜索效果
239 |
240 | ### 2. 重置按钮
241 |
242 | 在这里勇个比较有意思的按钮,清楚筛选器,它实现的方法请求非常的简单,我们只需要将所有的数据重置为 `undefined` ,我们的 `clean` 函数,就会讲 `query` **修理为空,这样我们返回的数据就会是全部的数据**
243 |
244 | ```tsx
245 | const reset = () => {
246 | setSearchParams({
247 | typeId: undefined,
248 | processId: undefined,
249 | tagId: undefined,
250 | name: undefined
251 | })
252 | }
253 | ```
254 |
255 | ## 四、看板的增删改查功能
256 |
257 | 这部分的内容和之前的项目列表相似度很高,我们这里就不详细讲了,稍微解释以下这些 `hook` 的作用
258 |
259 | ### 1. useAddKanban
260 |
261 | 接着我们需要处理看板增删的 `hook` ,在这里我们有必要采用乐观更新来实现,不然在服务器请求慢时,造成页面假死过长
262 |
263 | 和前面一样,我们采用 `useMutation` 来封装 `http` 请求,返回一个被处理过的 `mutate` 请求方式或者 `mutateAsync` 异步请求方式
264 |
265 | 在这里我们接收了一个 `queryKey` 作为参数,这里它是一个数组**第一个元素是缓存中的数据名称**,第二个元素是它的重新刷新的依赖
266 |
267 | ```tsx
268 | export const useAddKanban = (queryKey: QueryKey) => {
269 | const client = useHttp()
270 | // 处理 http 请求
271 | return useMutation(
272 | (params: Partial) => client(`kanbans`, {
273 | method: "POST",
274 | data: params
275 | }),
276 | // 配置乐观更新
277 | useAddConfig(queryKey)
278 | )
279 | }
280 | ```
281 |
282 | 在 `config` 配置中,我们将在 `old` 元素中,通过数组解构的方式,将新数据添加到了缓存中,这样我们就实现了对数据的更改
283 |
284 | ```tsx
285 | export const useAddConfig = (queryKey: QueryKey) => useConfig(queryKey, (target, old) => old ? [...old, target] : [])
286 | ```
287 |
288 | ### 2. useDeleteKanban
289 |
290 | **删除看板**的 `hook` ,在这里我们采用同样的方法,采用的 `config` 也是我们之前就封装过的,对于所有的增删改都成立的 `hook`
291 |
292 | ```tsx
293 | // 删除看板
294 | export const useDeleteKanban = (queryKey: QueryKey) => {
295 | const client = useHttp()
296 | return useMutation(
297 | ({ id }: { id: number }) => client(`kanbans/${id}`, {
298 | method: "DELETE",
299 | }),
300 | useDeleteConfig(queryKey)
301 | )
302 | }
303 | ```
304 |
305 | 在这里接收的参数只有 `id` ,删除看板的 `id`
306 |
307 | ## 五、任务的增删改查功能
308 |
309 | 增删改查的功能都差不多,只是传递的参数不一样罢了,在这里,我们就拿一个编辑功能来讲
310 |
311 | 我们首先封装了一个控制 `modal` 开关的 `hook` `useTasksModel`
312 |
313 | ```tsx
314 | const [form] = useForm()
315 | const { editingTaskId, editingTask, close } = useTasksModel()
316 | // 解构一个 task 方法
317 | const { mutateAsync: editTask, isLoading: editLoading } = useEditTask(useTasksQueryKey())
318 | // 添加一个删除任务的方法
319 | const { mutateAsync: deleteTask } = useDeleteTask(useTasksQueryKey())
320 | // 点击取消时,调用close同时清空表单
321 | ```
322 |
323 | 在这里我们暴露出了很多关于任务增删改查的方法,只要调用即可,这里我们在 `modal` 中,绑定了 `onOk` 以及 `onCancel` 方法
324 |
325 | 这里有个值得注意的地方
326 |
327 | 我们这次采用的是 `mutateAsync` 异步执行,因此我们需要采用 `await` 进行等待执行结果
328 |
329 | ```tsx
330 | const onCancel = () => {
331 | close()
332 | form.resetFields()
333 | }
334 | const onOk = async () => {
335 | // 获取表单数据
336 | await editTask({ ...editingTask, ...form.getFieldsValue() })
337 | // 关闭表单
338 | close()
339 | }
340 | ```
341 |
342 | ----
343 |
344 | ## 📌 总结
345 |
346 | 在这篇文章中我们做完了看板页面的制作,我们能学到这些东西
347 |
348 | 1. 熟悉了增删改查的操作
349 | 2. 了解了 `useQuery` 的用法
350 | 3. 对 `modal` 组件有了更多的了解
351 | 4. 了解了 `react-query` 能够优化请求次数
352 |
353 | > 最后,可能在很多地方讲诉的不够清晰,请见谅
354 | >
355 | > 💌 如果文章有什么错误的地方,或者有什么疑问,欢迎留言,也欢迎私信交流
356 |
357 |
--------------------------------------------------------------------------------
/React Hooks 项目/(四) 搜索功能实现.md:
--------------------------------------------------------------------------------
1 | # Hooks + TS 搭建一个任务管理系统(四)-- 搜索功能实现
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**大二的前端爱好者**
6 | >
7 | > 📢 这个系列文章是实战 jira 任务管理系统的一个学习总结
8 | >
9 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
10 | >
11 | > 📢 **愿你忠于自己,热爱生活**
12 |
13 | 在上一篇文章中,我们已经写过了关于**项目列表展示**的部分,通过大量的 `custom hook` 实现了项目的增删改查,也写很多复用性很高的 `hook` ,这样我们可以在后面的代码中复用,优化和缩减我们的开发时间
14 |
15 | ## 💡 知识点抢先看
16 |
17 | - 封装 userSelect 组件
18 | - 将输入框内容映射到 url 上
19 | - 利用防抖优化输入框请求
20 |
21 | 先献上效果图
22 |
23 | 
24 |
25 | ## 一、封装 UserSelect 组件
26 |
27 | 这次的项目采用的是 `Antd` 组件库,在这部分中我们采用 `Form` 表单以及 `Input` 来实现搜索框的样式,对于下拉框,将采用以 `Select` 组件为基础的 `UserSelect` 自定义组件
28 |
29 | 重新封装 `Select` 组件,在这里我们首先是封装了一个 `IdSelect` 组件,再在这个组件的基础上抽象一个 `UserSelect` 组件
30 |
31 | 这样做的目的是为了让 `IdSelect` 组件能够**实现复用**
32 |
33 | 下面我们先来写 `IdSelect` 组件吧,从名字上也可以看出,它是通过 `id` 来选择 `option` 的
34 |
35 | 在前面的文章中,我们也有提到过,利用 `antd` 组件来封装自定义组件,需要**继承它的原先的类型**,来保持它的 `props` 正常工作
36 |
37 | 我们先来看看 `IdSelect` 应当接收的**参数类型**
38 |
39 | ```tsx
40 | // 继承 Select 身上的方法
41 | type SelectProps = React.ComponentProps
42 | // 在 type 中定义公共类型
43 | interface IdSelectProps extends Omit {
44 | value?: Raw | null | undefined,
45 | // onChange 只能传入number
46 | onChange?: (value?: number) => void,
47 | defaultOptionName?: string,
48 | options?: { name: string, id: number }[]
49 | }
50 | ```
51 |
52 | 它的类型还是比较复杂的
53 |
54 | 首先是 `SelectProps` 定义的一个类型等于 `Select` 的类型,再在 `IdSelectProps` 的类型中继承部分的 `SelectProps` 类型
55 |
56 | **为什么说是部分呢?**
57 |
58 | 由于我们原生的 `Select` 组件中对于 `onChange` 属性的类型是采用泛型来定义的,这会导致我们的 `number` 类型数据转化成 `string` ,总之就会导致最后的后端返回数据的类型和 `Select` 中的类型不一致,因此我们需要将 `onChange` 限制为 `number` 类型
59 |
60 | 这个是 `onChange` 的**类型声明**
61 |
62 | ```tsx
63 | onChange?: (value: ValueType, option: OptionsType[number] | OptionsType) => void;
64 | ```
65 |
66 | 同时对于一些类型我们有自己明确的类型,因此我们不需要采用它原生的类型,我们自己重新定义
67 |
68 | 因此我们采用 `Omit` 关键字来除去 `SelectProps` 中的部分类型声明,重新写一份
69 |
70 | ```tsx
71 | Omit
72 | ```
73 |
74 | 这样我们就完成了对 `Select` 数据类型的封装,接着我们需要将一些相关的配置**全部传递**给它们
75 |
76 | 例如,`value` 属性的默认值,`onChange` 的执行时机,以及 `defaultOptionName`
77 |
78 | ```tsx
79 | export const IdSelect = (props: IdSelectProps) => {
80 | const { value, onChange, defaultOptionName, options, ...restProps } = props
81 | return
94 | }
95 | ```
96 |
97 | 代码的思路很简单,当没有 `options` 时,`value` 设置为 `0` ,显示**默认负责人**。同时我们需要对传入的 `value` 进行类型转化,保证它是 `number` 类型
98 |
99 | 这样我们的 `IdSelect` 就封装好了,它相对于 `Select` 有更加严格的类型要求,以确保我们传递的参数类型不会出错
100 |
101 | 接着我们将这个 `IdSelect` 特殊化到 `User` 中,再封装一个 `UserSelect` 给 `project` 中按照人员查找来使用
102 |
103 | ```tsx
104 | export const UserSelect = (props:React.ComponentProps)=>{
105 | const {data:users} = useUsers()
106 | return
107 | }
108 | ```
109 |
110 | 写熟练了真是随便拿捏
111 |
112 | 同样的,我们的数据类型继承自 `IdSelect` ,然后,我们先直接传入我们的 `Users` 数据,实现了一个 `UserSelect`
113 |
114 | **为什么这样就可以了呢?**
115 |
116 | 我们将数据传递下去之后,得到的 `Select` 就是一个人员列表了,这样我们只需要做一些其他配置就可以了,**不需要考虑人员数据的问题**
117 |
118 | 
119 |
120 | **接着**,我们在搜索部分的 `Form` 表单中,使用这个组件
121 |
122 | ```tsx
123 | // search-panel.tsx
124 |
129 | setParam({
130 | ...param,
131 | personId: value
132 | })} />
133 | ```
134 |
135 | 在这里我们配置了默认选型,以及通过 `props` 传递的用户 `id` (`param.personId`),同时在输入框被选择时触发的事件,用来操控我们的页面 `url` 变化
136 |
137 | ## 二、将输入框内容映射到 url 上
138 |
139 | 在上一小节我们最后谈到了 `url` 的变化,确实如此,当我们在输入框中输入内容时,或者时 `Select` 中选择内容时,都应该要**映射到 `url` 中**,这样我们将 `url` 复制在新页面打开,还会**保留同样的信息**,这种功能也是非常常见的,例如**掘金社区**的文章标题,`h1、h2` 标签
140 |
141 | 
142 |
143 | 因此我们有理由,有必要实现这样的功能!
144 |
145 | 想到 `url` 操作,我们很容易想到我们的 `useProjectsQueryKey` 这一类 `hook`,当然这有一定的关系
146 |
147 | 在这里我们需要使用我们之前封装过的 **`useProjectsSearchParams`** 这个 `custom hook` ,
148 |
149 | 我们先再看看这个 `hook` 的源码
150 |
151 | ```tsx
152 | export const useProjectsSearchParams = () => {
153 | // 返回的是一个新的对象,造成地址不断改变,不断的渲染
154 | const [param, setParam] = useUrlQueryParam(['name', 'personId'])
155 | return [
156 | // 采用 useMemo 解决 重复调用的问题
157 | useMemo(() => ({ ...param, personId: Number(param.personId) || undefined }), [param]),
158 | setParam
159 | ] as const
160 | }
161 | ```
162 |
163 | 简单梳理一下,就是通过 `useUrlQueryParam` 来设置和查询相关的`query` 数据,返回的是一个数组,形式类似于 `useState` ,一个是值,一个更改这个值
164 |
165 | 我们可以看到这个 `hook` 监听的 `url query` 是 `name、personId` 也就是**项目名和负责人**,正符合我们的查询需求
166 |
167 | 我们先在 `ProjectListScreen` 这个 `project` 的最外层组件中暴露 `hook` 中返回的两个方法
168 |
169 | ```tsx
170 | const [param, setParam] = useProjectsSearchParams()
171 | ```
172 |
173 | 这样如果我们通过 `setParam` 导致了 `param` 的变化,就会触发 `useUrlQueryParam` **实现页面的 `url` 的更新**
174 |
175 | 例如这里的**搜索模块**,我们通过 `props` 传递 `setParam` 方法给子组件
176 |
177 | ```tsx
178 |
179 | ```
180 |
181 | 在子组件中使用这个方法来控制 `param` 的变化,从而引起 `url` 的变化
182 |
183 | 例如,我们在监听 `input` 框输入时
184 |
185 | ```tsx
186 | setParam({
191 | ...param,
192 | name: e.target.value
193 | })} />
194 | ```
195 |
196 | 我们在 `onChange` 中调用了 `setParam` 设置了新的 `param` 值,在 `UserSelect` 中同样的采用这样的方式修改 `param` 值,触发 `url` 的更新,这样我们的功能就实现了一半了,接下来我们需要**利用当前用户查询的 `param` 去获取数据**
197 |
198 | ```tsx
199 | const { isLoading, error, data: list} = useProjects(param, 200)
200 | ```
201 |
202 | 返回获取到的结果和状态即可,这里采用的 `useProjects` ,是一个封装的 `custom hook` ,它会在 `param` 变化时 ,**通过 `useQuery` 不断的请求数据**,这也是我们返回的数据中能够有 `isLoading、error` 这些的原因
203 |
204 | **在这里提一下 `useQuery`** ,它是 `reacy-query` 中的一个 `api` ,用来做缓存的,接收的第一个参数是用来起名字,第二个参数是异步请求,它会把请求的结果放到缓存中,但是**这个缓存不是浏览器缓存**
205 |
206 | 第一个参数可以是一个数组,类似于 `useEffect` ,当依赖项变化的时候就会触发 `useQuery` 重新执行
207 |
208 | ```tsx
209 | export const useProjects = (param?: Partial) => {
210 | const client = useHttp()
211 | // 当 param 变化的时候触发 useQuery 重新渲染,我们需要在第一个参数中传入一个数组,数组的第二位传入依赖
212 | return useQuery(['projects', param], () => client('projects', { data: param }))
213 | }
214 | ```
215 |
216 | 现在我们的功能也算是基本实现了,但是我们打开控制台会发现有很多很多的请求,这并不是我们想要的,因此我们可以采用防抖,**每隔多少秒,再请求一次**
217 |
218 | 
219 |
220 | ## 三、useDebounce 实现防抖
221 |
222 | 为了减少请求的次数,我们封装了一个 `useDebounce` 方法,用来对数据进行防抖操作
223 |
224 | 关于防抖不必多说了吧,这里我们采用的是 `useState` 来创建这个全局变量,通过 `set...` 来控制它值的变化,也就这一点不一样的地方
225 |
226 | 简单说一说这里的泛型吧,这里我们采用了一个泛型 `V` ,第一个 `` 是用来做泛型声明的,它的类型由我们传入的 `value` 来指定,`value` 是什么就是什么
227 |
228 | ```tsx
229 | export const useDebounce = (value: V, delay?: number): any => {
230 | // 设置一个 debouncedValue 值,用于暂存值,以及监控变化
231 | const [debouncedValue, setDebouncedValue] = useState(value)
232 | useEffect(() => {
233 | // 接收一个定时器,参数为一个函数和延时时间
234 | // 每次value变化,设置一个定时器
235 | const timeout = setTimeout(() => setDebouncedValue(value), delay)
236 | // 每次上一个useEffect 的定时器被清除,相当于上一个定时器被卸载了
237 | return () => clearTimeout(timeout)
238 | // 监听value 和 delay 变化,当参数变化时,重新调用这个函数设置定时器
239 | }, [value, delay])
240 | // 返回值
241 | return debouncedValue
242 | }
243 | ```
244 |
245 | 
246 |
247 | ## 📌 总结
248 |
249 | 在这篇文章中我们做完了项目列表的搜索模块,我们能学到这些东西
250 |
251 | 1. 已有组件封装新的组件参数类型问题
252 | 2. 如何 实现了输入框与 `url` 统一
253 | 3. 采用 `hook` 实现防抖
254 |
255 | > 最后,可能在很多地方讲诉的不够清晰,请见谅
256 | >
257 | > 💌 如果文章有什么错误的地方,或者有什么疑问,欢迎留言,也欢迎私信交流
--------------------------------------------------------------------------------
/React Hooks 项目/(终)项目总结.md:
--------------------------------------------------------------------------------
1 | # Hooks + TS 搭建一个任务管理系统(终)-- 项目总结
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**大二的前端爱好者**
6 | >
7 | > 📢 这个系列文章是实战 jira 任务管理系统的最后一篇文章
8 | >
9 | > 📢 用来**总结项目中遇到的问题,以及解决方法**
10 | >
11 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
12 | >
13 | > 📢 **愿你忠于自己,热爱生活**
14 |
15 | ## 💡 内容抢先看
16 |
17 | - 技术栈
18 | - Q&A 文档
19 |
20 | 整个项目已经学习完了,也做出来了,但是缺少后端服务器,还无法上线,稍做总结吧~
21 |
22 | ## 一、采用技术栈
23 |
24 | 本文采用了以下技术
25 |
26 | - React 17
27 | - React Hook
28 | - TS4
29 | - Hook + Content
30 | - React Query
31 | - CSS in JS
32 | - React Router 6
33 |
34 | 采用 `content` 来做全局状态管理
35 |
36 | 利用 `React Query` 进行 `url` 缓存,实现 `url` 状态管理
37 |
38 | 利用 `CSS in JS` 来替代传统组织式的 CSS 代码,将 HTML 与 CSS 选择器解耦,实现真正的组件化
39 |
40 | 利用 `TS4` 来规范 `JS` 进行类型检查,规范代码
41 |
42 | ## 二、Q&A 文档
43 |
44 | ### 1. 怎么实现页面刷新后仍然是上一次的状态?
45 |
46 | 通过 `token` 以及本地存储实现,我们在登录时,会将token 存储到本地中,这一步不需要我们手动操作,用的老师的库会自动实现。我们在初始化页面的时候,需要挂载一个 `useMount` 方法进行初始化,在这个函数里,主要进行的是 `token` 令牌的判断,如果存在 `token` 我们就,发送一个请求去获取用户数据 `data`
47 |
48 | 然后返回 `user` 数据
49 |
50 | ### 2. 为什么使用 catch 中的 err 会报错呢?
51 |
52 | 在 `TS4.4` 版本中规定了 `catch` 中的 `err` 对象默认类型为 `unknown` ,因此我们不能用它向其他东西赋值,我们可以先进行类型设置
53 |
54 | 那为什么使用连写的方式就可以呢 `login(values).catch(onError)` 原因是,我们的 `login` 调用是异步的,但是一旦调用就会执行 `catch` ,因此获取不到值
55 |
56 | 一方面可以采用 `async` 来解决,也可以连写
57 |
58 | ### 3. 为什么控制台打印 error 总是 null
59 |
60 | 原因是 Hook 中的事件是异步的,例如 `useState` 是异步的,会先执行打印 `error`
61 |
62 | 严重问题,error 无法获取
63 |
64 | 解决!!!!
65 |
66 | 通过 `then` 的第二个参数,获取到返回错误的 `promise` 对象,然后,再通过 `throw` 抛出这个错误
67 |
68 | 被外层的 `catch` 接收,注意!!抛出错误中的 `then` 方法是一个异步事件,需要通过 `async` 来解决
69 |
70 | ```js
71 | .then(data => {
72 | // 成功则处理stat
73 | console.log(data);
74 | setData(data)
75 | // throw new Error('222')
76 | return data
77 | }, async(err) => {
78 | console.log('失败');
79 | // 卧槽,尼玛的,解决了catch 获取不到错误的问题
80 | throw Promise.reject(await err.then())
81 | })
82 | ```
83 |
84 | 其他代码不变
85 |
86 | 同时注意,在 `fetch` 中返回错误,不能用 return 需要用 `throw` ,抛出 promise 错误
87 |
88 | ### 4. 页面的不同 title 是如何实现的?
89 |
90 | 采用自定义的 hook `useDocumentTitle` ,监听title 的变化
91 |
92 | ```ts
93 | export const useDocumentTitle = (title: string) =>{
94 | useEffect(() => {
95 | document.title = title
96 | }, [title])
97 | }
98 | ```
99 |
100 | 但是这不是最优的方案,直接这样使用会造成页面退出时获取标题丢失,我们想要的是,当我们退出登录时,标题会到 `jira 平台...` 字样
101 |
102 | 我们需要将页面中的最开始的那个 `title` 保存起来,也就是 `jira...` 然后,在当前页面被卸载时,改变这个 `title`
103 |
104 | 我们可以利用 `hook` 天然的闭包特性来实现,但是这样会造成的问题是,不利于别人阅读我们的代码,闭包还是一个挺难发现的东西,在 `hook` 中
105 |
106 | 我们可以使用 `useRef` ,它能够帮我们保存变量的最初始状态,也就是 `jira...` ,因此这样也可以解决我们的问题,我们添加多一个 `useEffect` 来监听页面的卸载,当卸载时我们就设置会原先的 `title`
107 |
108 | 最终版 `useDocumentTitle` 自定义 `hook`
109 |
110 | ```ts
111 | // 添加 title 的 hook
112 | export const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
113 | // 利用 useRef 自定义 hook 它会一直帮我们保存好这个 title值,不会改变,
114 | const oldTitle = useRef(document.title).current
115 | // const oldTitle = document.title
116 | useEffect(() => {
117 | document.title = title
118 | }, [title])
119 | // 页面卸载时,重新设置为原来的 title
120 | useEffect(() => {
121 | // 利用闭包不指定依赖得到的永远是旧title ,是代码初次运行时的 oldTitle
122 | // 不利于别人阅读
123 | return () => {
124 | if (!keepOnUnmount) {
125 | document.title = oldTitle
126 | }
127 | }
128 | }, [keepOnUnmount, oldTitle])
129 | }
130 | ```
131 |
132 | ### 5. 为什么采用 Navigate 会无法设置默认跳转呢?
133 |
134 | 盲猜版本迭代
135 |
136 | 艹,不要安装 `beta4` 版本,安装 `beta.0` ,第四版中的 `Navigate` 失效了
137 |
138 | ### 6. 在采用 antd 自定义组件的时候,如何开放更多的类型呢?
139 |
140 | 我们可以利用 `React` 自带的方法,获取到组件身上的全部类型
141 |
142 | ```ts
143 | type SelectProps = React.ComponentProps
144 | ```
145 |
146 | 然后,通过 `extends` 来继承 `SelectProps` 身上的方法
147 |
148 | ```ts
149 | interface IdSelectProps extends SelectProps
150 | ```
151 |
152 | 但是这样会有类型冲突的问题
153 |
154 | 
155 |
156 | 因此我们需要排除掉我们在这里使用过的类型,采用 `Omit` 方法
157 |
158 | ```tsx
159 | interface IdSelectProps extends Omit
160 | ```
161 |
162 | 这样我们定义的类型就能够接收所有的 `props` 了,最后还要解构一下其他的 `props` 噢
163 |
164 | ### 7. 什么时候命名 ts,tsx 文件呢?
165 |
166 | 当包含模板文件的时候采用 `tsx` 文件,不包含模板代码的时候使用 `ts` 文件,不然会引起误会
167 |
168 | ### 8. 在代码中出现的 !! 是什么意思呢
169 |
170 | ```tsx
171 | onCheckedChange?.(!!num)
172 | ```
173 |
174 | 例如这里的 `!!num`
175 |
176 | 它代表的意思是 `Boolean(num)` 将 `num` 转化成 `boolean` 类型 `true or false`
177 |
178 | ### 9. 在组件中我们不能使用 hook,那我们如何更改组件状态呢?
179 |
180 | 我们可以在我们的自定义 hook 中,暴露一个函数,我们通过调用这个函数来实现状态的更新
181 |
182 | ### 10. 在请求数据返回之前如果页面被卸载了,造成报错如何解决
183 |
184 | 这个问题的来源是,我们在请求数据的时候,**我们登出了页面**,当前的 `setData` **还没有结束**,当完成时,需要渲染的页面已经不存在了,因此我们**需要判断一下**,页面是否被卸载再来渲染组件
185 |
186 | 为此我们写了一个自定义的 `hook` 用来判断组件是否被卸载
187 |
188 | ```tsx
189 | export const useMountedRef = () => {
190 | const mountedRef = useRef(false)
191 | // 通过 useEffect hook 来监听组件状态
192 | useEffect(() => {
193 | mountedRef.current = true
194 | return () => {
195 | mountedRef.current = false
196 | }
197 | })
198 | return mountedRef
199 | }
200 | ```
201 |
202 | 主要利用了 `useEffect` 的特性,当组件卸载时执行 `return` ,当我们写自定义 hook 的话,如果返回一个函数,非常大概率是需要使用 `useMemo` 或 `useCallback`
203 |
204 | 非常重要
205 |
206 | ### 11. 怎么理解 component composition 这种透传数据的模式
207 |
208 | 引用官网的一句话
209 |
210 | > Context 主要应用场景在于*很多*不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。
211 | >
212 | > **如果你只是想避免层层传递一些属性,[组件组合(component composition)](https://zh-hans.reactjs.org/docs/composition-vs-inheritance.html)有时候是一个比 context 更好的解决方案。**
213 |
214 | **我们把我们需要用到数据的那个组件直接丢到数据来源的 props 身上** ,然后消费数据,把消费完的组件,也就是要被渲染到页面的内容,通过 `props` 传回来。这就是 `component compositon` ,简单粗暴,我们在原来的地方,直接渲染这个组件即可
215 |
216 | 例如:我们在 `Page` 组件中需要传递个 `Auth` 组件 `user` 信息,它们之间有很多的深层嵌套
217 |
218 | 我们可以这么做 (官网例子)
219 |
220 | ```tsx
221 | function Page(props) {
222 | const user = props.user;
223 | const userLink = (
224 |
225 |
226 |
227 | );
228 | return ;
229 | }
230 |
231 | // 现在,我们有这样的组件:
232 |
233 | // ... 渲染出 ...Page的子组件
234 |
235 | // ... 渲染出 ...PageLayout的子组件
236 |
237 | // ... 渲染出 ...
238 | {props.userLink}
239 | ```
240 |
241 | 这样我们只用传递 `userLink` 即可,
242 |
243 | ### 12. 为什么创建和编辑中的关闭按钮,只有一个起作用?
244 |
245 | 造成这个问题主要原因在于这段代码
246 |
247 | ```tsx
248 | const close = () => {
249 | setEditingProjectId({ editingProjectId: undefined });
250 | setProjectCreate({ projectCreate: undefined });
251 | }
252 | ```
253 |
254 | 测试发现哪条语句在前面,哪个就生效,在前面的那个不会生效,初步判断造成问题的原因是异步操作,但是还没有找到解决的方法
255 |
256 | 更正问题来源:由于后面的那一条会把前面的数据重新设置上去造成的
257 |
258 | 最终将这里的两次调用抽成了一次,将 `seturl...` 函数抽象成两个,一个读取,一个设置
259 |
260 | ### 13. 搜索框的功能是如何实现的?
261 |
262 | 在 `useTask` 中触发,发送请求
263 |
264 | ```tsx
265 | export const useTasks = (param?: Partial) => {
266 | const client = useHttp()
267 | // 搜索框请求在这里触发
268 | return useQuery(['tasks', param], () => client('tasks', { data: param }))
269 | }
270 | ```
271 |
272 | ### 14. 如何部署到 github 上?
273 |
274 | 1. 部署github,创建一个 名字.github.io
275 |
276 | 2. `yarn add gh-pages -D` 安装包
277 |
278 | 3. ```shell
279 | // 在 package.json 中配置一下
280 | "predeploy": "npm run build",
281 | "deploy": "gh-pages -d build -r git@github.com:linjunc/linjunc.github.io.git -b main"
282 | ```
283 |
284 | 4. 执行 `npm run deploy` 命令
285 |
286 | ### 15. useMemo 和 useCallback 有什么区别?
287 |
288 | - `useCallback` :就是返回一个函数,只有在依赖项发生变化的时候才会更新。**一般在函数返回函数时,需要使用 `useCallback` 来包裹**。更多的时**防止子组件重新渲染**
289 |
290 | > `useCallback` 返回一个**函数**,当把它返回的这个函数作为子组件使用时,可以避免每次父组件更新时都重新渲染这个子组件,子组件一般配合 `memo` 使用
291 |
292 | - `useMemo`:传递一个创建函数和依赖项,创建函数会需要返回一个值,只有在**依赖项发生改变的时候,才会重新调用此函数**,返回一个新的值。**这里的改变,不表示地址的改变,只有值得改变**。主要**能够优化当前组件也可以优化子组件**
293 |
294 | > `useMemo` 返回的的是一个**值**,用于避免在每次渲染时都进行高开销的计算
295 |
296 | ---
297 |
298 | ## 📌 总结
299 |
300 | - 持续更新
301 |
302 | > 最后,可能在很多地方讲诉的不够清晰,请见谅
303 | >
304 | > 💌 如果文章有什么错误的地方,或者有什么疑问,欢迎留言,也欢迎私信交流
--------------------------------------------------------------------------------
/React 入门学习/React 入门学习(一)-- 基础知识以及 jsx语法.md:
--------------------------------------------------------------------------------
1 | # React 从入门到入土(一)-- 基础知识以及 jsx 语法
2 |
3 | 
4 |
5 | > 📢 大家好😪 ,我是小丞同学,最近在学习 React、小程序、阅读 JS 高程,以及整理 Node 的笔记,这是关于 React 的第一篇文章,也是我学习的第一个框架,内容如有错误,欢迎大家指正
6 | >
7 | > 📢 愿你生活明朗,万物可爱
8 |
9 | 先附上[React官网](https://zh-hans.reactjs.org/) ,有很多问题都要通过查询官方文档来解决,要学会查文档~
10 |
11 | ## 一、React 简介
12 |
13 | ### 1. 关于 React
14 |
15 | 整几个面试题来认识一下~~
16 |
17 | > 什么是 React ?
18 |
19 | **React** 是一个用于构建用户界面的 JavaScript 库。
20 |
21 | - 是一个将数据渲染为 HTML 视图的开源 JS 库
22 | - 它遵循基于组件的方法,有助于构建可重用的 UI 组件
23 | - 它用于开发复杂的交互式的 web 和移动 UI
24 |
25 | > React 有什么特点?
26 |
27 | 1. 使用虚拟 DOM 而不是真正的 DOM
28 | 2. 它可以用服务器渲染
29 | 3. 它遵循单向数据流或数据绑定
30 | 4. 高效
31 | 5. 声明式编码,组件化编码
32 |
33 | > React 的一些主要优点?
34 |
35 | 1. 它提高了应用的性能
36 | 2. 可以方便在客户端和服务器端使用
37 | 3. 由于使用 JSX,代码的可读性更好
38 | 4. 使用React,编写 UI 测试用例变得非常容易
39 |
40 | ### 2. Hello React
41 |
42 | 首先需要引入几个 react 包,我直接用的是老师下载好的
43 |
44 | - React 核心库、操作 DOM 的 react 扩展库、将 jsx 转为 js 的 babel 库
45 |
46 | 
47 |
48 | ```jsx
49 | const VDOM = Hello,React
50 | ReactDOM.render(VDOM,document.querySelector(".test"))
51 | ```
52 |
53 | ### 3. 虚拟 DOM 和真实 DOM 的两种创建方法
54 |
55 | #### 3.1 JS 创建虚拟 DOM
56 |
57 | ```js
58 | //1.创建虚拟DOM,创建嵌套格式的dom
59 | const VDOM=React.createElement('h1',{id:'title'},React.createElement('span',{},'hello,React'))
60 | //2.渲染虚拟DOM到页面
61 | ReactDOM.render(VDOM,document.querySelector('.test'))
62 | ```
63 |
64 | #### 3.2 Jsx 创建虚拟DOM
65 |
66 | ```jsx
67 | //1.创建虚拟DOM
68 | const VDOM = ( /* 此处一定不要写引号,因为不是字符串 */
69 |
70 | Hello,React
71 |
72 | )
73 | //2.渲染虚拟DOM到页面
74 | ReactDOM.render(VDOM,document.querySelector('.test'))
75 | ```
76 |
77 | > js 的写法并不是常用的,常用jsx来写,毕竟JSX更符合书写的习惯
78 |
79 | ## 二、jsx 语法
80 |
81 | 1. 定义虚拟DOM,不能使用`“”`
82 |
83 | 2. 标签中混入JS表达式的时候使用`{}`
84 |
85 | ```jsx
86 | id = {myId.toUpperCase()}
87 | ```
88 |
89 | 3. 样式的类名指定不能使用class,使用`className`
90 |
91 | 4. 内敛样式要使用`{{}}`包裹
92 |
93 | ```jsx
94 | style={{color:'skyblue',fontSize:'24px'}}
95 | ```
96 |
97 | 5. 不能有多个根标签,只能有一个根标签
98 |
99 | 6. 标签必须闭合,自闭合也行
100 |
101 | 7. 如果小写字母开头,就将标签转化为 html 同名元素,如果 html 中无该标签对应的元素,就报错;如果是大写字母开头,react 就去渲染对应的组件,如果没有就报错
102 |
103 | > 记几个
104 |
105 | #### 1. 注释
106 |
107 | 写在花括号里
108 |
109 | ```jsx
110 | ReactDOM.render(
111 |
112 |
小丞
113 | {/*注释...*/}
114 | ,
115 | document.getElementById('example')
116 | );
117 | ```
118 |
119 | #### 2. 数组
120 |
121 | JSX 允许在模板中插入数组,数组自动展开全部成员
122 |
123 | ```jsx
124 | var arr = [
125 | 小丞
,
126 | 同学
,
127 | ];
128 | ReactDOM.render(
129 | {arr}
,
130 | document.getElementById('example')
131 | );
132 | ```
133 |
134 | ### tip: JSX 小练习
135 |
136 | 根据动态数据生成 `li`
137 |
138 | ```jsx
139 | const data = ['A','B','C']
140 | const VDOM = (
141 |
142 |
143 | {
144 | data.map((item,index)=>{
145 | return - {item}
146 | })
147 | }
148 |
149 |
150 | )
151 | ReactDOM.render(VDOM,document.querySelector('.test'))
152 | ```
153 |
154 |
--------------------------------------------------------------------------------
/React 入门学习/React 入门学习(七)-- 脚手架配置代理.md:
--------------------------------------------------------------------------------
1 | # React 入门学习(七)-- 脚手架配置代理
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**准大二的前端爱好者**
6 | >
7 | > 📢 这篇文章是学习 React 中**脚手架配置代理**的学习笔记
8 | >
9 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
10 | >
11 | > 📢 **愿你忠于自己,热爱生活**
12 |
13 | # 引言
14 |
15 | React 本身只关注于页面,并不包含发送 Ajax 请求的代码,所以一般都是集成第三方的包,或者自己封装的
16 |
17 | 自己封装的话,比较麻烦,而且也可能考虑不全
18 |
19 | 常用的有两个库,一个是JQuery,一个是 axios
20 |
21 | 1. JQuery 这个比较重,因为 Ajax 服务也只是它这个库里的一小块功能,它主要做的还是 DOM 操作,而这不利于 React ,不推荐使用
22 | 2. axios 这个就比较轻,而且采用 Promise 风格,代码的逻辑会相对清晰,**推荐使用**
23 |
24 | 因此我们这里采用 axios 来发送客户端请求
25 |
26 | 以前,我们在发送请求的时候,经常会遇到一个很重要的问题:跨域!
27 |
28 | 
29 |
30 | 在我以前的学习中,基本上都需要操作后端服务器代码才能解决跨域的问题,配置请求头,利用 script,这些都需要后端服务器的配合,因此我们前端需要自己解决这个问题的话,就需要这个技术了:**代理**。
31 |
32 | 在说代理之前,先谈谈为什么会出现跨域?
33 |
34 | 这个应该是源于浏览器的同源策略。所谓同源(即指在同一个域)就是两个页面具有相同的协议,主机和端口号, 当一个请求 URL 的**协议、域名、端口**三者之间任意一个与当前页面 URL 不同即为跨域 。
35 |
36 | 也就是说 `xxx:3000`和 `xxx:4000` 会有跨域问题,`xxx:3000` 与 `abc:3000` 有跨域问题
37 |
38 | 那接下来我们采用**配置代理**的方式去解决这个问题
39 |
40 | > 关于跨域的问题解决方案,在之后的文章会有总结 ~
41 |
42 | ## 1. 全局代理
43 |
44 | 第一种方法,我把它叫做全局代理,因为它直接将代理配置在了配置文件 `package.json` 中
45 |
46 | ```json
47 | "proxy":"http://localhost:5000"
48 | // "proxy":"请求的地址"
49 | ```
50 |
51 | 这样配置代理时,首先会在抓原请求地址上访问,如果访问不到文件,就会转发到这里配置的地址上去请求
52 |
53 | 
54 |
55 | 我们需要做的就是在我们的请求代码中,将请求的地址改到转发的地址,即可
56 |
57 | 但是这样会有一些问题,它会先向我们请求的地址,也就是这里的 `3000` 端口下请求数据,如果在 `3000` 端口中存在我们需要访问的文件,会直接返回,**不会再去转发**
58 |
59 | 因此这就会出现问题,同时因为这种方式采用的是全局配置的关系,导致**只能转发到一个地址**,不能配置多个代理
60 |
61 | ## 2. 单独配置
62 |
63 | 这也是我自己起的名字,这种配置方式,可以给多个请求配置代理,非常不错
64 |
65 | 它的工作原理和全局配置是一样的,但是写法不同
66 |
67 | **首先**我们需要在 `src` 目录下,创建代理配置文件 `setupProxy.js`
68 |
69 | 注意:这个文件只能叫这个名字,脚手架在启动的时候,会自动执行这些文件
70 |
71 | **第二步**
72 |
73 | 配置具体的代理规则,我们大致讲讲这些是什么意思
74 |
75 | 1. 首先我们需要引入这个 `http-proxy-middleware` 中间件,然后需要导出一个对象,这里建议使用函数,使用对象的话兼容性不大好
76 |
77 | 2. 然后我们需要在 `app.use` 中配置,我们的代理规则,首先 `proxy` 接收的第一个参数是需要转发的请求,我的理解是一个标志的作用,当有这个标志的时候,预示着我们需要采用代理,例如 `/api1` ,我们就需要在我们 `axios` 的请求路径中,加上 `/api1` ,这样所有添加了 `/api1` 前缀的请求都会转发到这
78 | 3. 第二个参数接受的是一个对象,用于配置代理。
79 | - `target` 属性用于配置转发目标地址,也就是我们数据的地址
80 | - `changeOrigin` 属性用于控制服务器收到的请求头中 `host` 字段,可以理解为一个伪装效果,为 `true` 时,收到的 `host` 就为请求数据的地址
81 | - `pathRewrite` 属性用于去除请求前缀,因为我们通过代理请求时,需要在请求地址前添加一个标志,但是实际的地址是不存在这个标志的,所以我们**一定要去除**这个前缀,这里采用的有点类似于正则替换的方式
82 |
83 | 配置一个代理的完整代码如下
84 |
85 | ```js
86 | const proxy = require('http-proxy-middleware')
87 | module.exports = function(app) {
88 | app.use(
89 | proxy('/api1', {
90 | target: 'http://localhost:5000', //配置转发目标地址
91 | changeOrigin: true, //控制服务器接收到的请求头中host字段的值
92 | pathRewrite: {'^/api1': ''} //去除请求前缀址(必须配置)
93 | }),
94 | )
95 | }
96 | ```
97 |
98 | ---
99 |
100 | 关于脚手架配置代理的内容就到这里啦!
101 |
102 | > 非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈
103 |
104 |
--------------------------------------------------------------------------------
/React 入门学习/React 入门学习(三) -- 组件的生命周期.md:
--------------------------------------------------------------------------------
1 | # React 入门(三) -- 生命周期 LifeCycle
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,这一篇是关于 React 的学习笔记,关于组件的生命周期
6 | >
7 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
8 | >
9 | > 📢 愿你生活明朗,万物可爱
10 |
11 | ## 引言
12 |
13 | 在 React 中为我们提供了一些生命周期钩子函数,让我们能在 React 执行的重要阶段,在钩子函数中做一些事情。那么在 React 的生命周期中,有哪些钩子函数呢,我们来总结一下
14 |
15 | ## React 生命周期
16 |
17 | React 生命周期主要包括三个阶段:初始化阶段,更新阶段,销毁阶段
18 |
19 | ### 初始化阶段
20 |
21 | #### 1. constructor 执行
22 |
23 | `constructor` 在组件初始化的时候只会执行一次
24 |
25 | 通常它用于做这两件事
26 |
27 | 1. 初始化函数内部 `state`
28 | 2. 绑定函数
29 |
30 | ```js
31 | constructor(props) {
32 | console.log('进入构造器');
33 | super(props)
34 | this.state = { count: 0 }
35 | }
36 | ```
37 |
38 | 现在我们通常不会使用 `constructor` 属性,而是改用类加箭头函数的方法,来替代 `constructor`
39 |
40 | 例如,我们可以这样初始化 `state`
41 |
42 | ```js
43 | state = {
44 | count: 0
45 | };
46 | ```
47 |
48 | #### 2. static getDerivedStateFromProps 执行 (新钩子)
49 |
50 | 这个是 React 新版本中新增的2个钩子之一,据说很少用。
51 |
52 | `getDerivedStateFromProps` 在初始化和更新中都会被调用,并且在 `render` 方法之前调用,它返回一个对象用来更新 `state`
53 |
54 | `getDerivedStateFromProps` 是类上直接绑定的静态(`static`)方法,它接收两个参数 `props` 和 `state`
55 |
56 | `props` 是即将要替代 `state` 的值,而 `state` 是当前未替代前的值
57 |
58 | > 注意:`state` 的值在任何时候都取决于传入的 `props` ,不会再改变
59 |
60 | 如下
61 |
62 | ```js
63 | static getDerivedStateFromProps(props) {
64 | return props
65 | }
66 | ReactDOM.render(,document.querySelector('.test'))
67 | ```
68 |
69 | `count` 的值不会改变,一直是 109
70 |
71 | #### 2. componentWillMount 执行(即将废弃)
72 |
73 | > 如果存在 `getDerivedStateFromProps` 和 `getSnapshotBeforeUpdate` 就不会执行生命周期`componentWillMount`。
74 |
75 | 该方法只在挂载的时候调用一次,表示组件将要被挂载,并且在 `render` 方法之前调用。
76 |
77 | 这个方法在 React 18版本中将要被废弃,官方解释是在 React 异步机制下,如果滥用这个钩子可能会有 Bug
78 |
79 | #### 3. render 执行
80 |
81 | `render()` 方法是组件中必须实现的方法,用于渲染 DOM ,但是它不会真正的操作 DOM,它的作用是把需要的东西返回出去。
82 |
83 | 实现渲染 DOM 操作的是 `ReactDOM.render()`
84 |
85 | > 注意:避免在 `render` 中使用 `setState` ,否则会死循环
86 |
87 | #### 4. componentDidMount 执行
88 |
89 | `componentDidMount` 的执行意味着初始化挂载操作已经基本完成,它主要用于组件挂载完成后做某些操作
90 |
91 | 这个挂载完成指的是:组件插入 DOM tree
92 |
93 | #### 初始化阶段总结
94 |
95 | 执行顺序 `constructor` -> `getDerivedStateFromProps` 或者 `componentWillMount` -> `render` -> `componentDidMount`
96 |
97 | 
98 |
99 | ### 更新阶段
100 |
101 | 
102 |
103 | 这里记录新生命周期的流程
104 |
105 | #### 1. getDerivedStateFromProps 执行
106 |
107 | 执行生命周期`getDerivedStateFromProps`, 返回的值用于合并 `state`,生成新的`state`。
108 |
109 | #### 2. shouldComponentUpdat 执行
110 |
111 | `shouldComponentUpdate()` 在组件更新之前调用,可以通过返回值来控制组件是否更新,允许更新返回 `true` ,反之不更新
112 |
113 | #### 3. render 执行
114 |
115 | 在控制是否更新的函数中,如果返回 `true` 才会执行 `render` ,得到最新的 `React element`
116 |
117 | #### 4. getSnapshotBeforeUpdate 执行
118 |
119 | 在最近一次的渲染输出之前被提交之前调用,也就是即将挂载时调用
120 |
121 | 相当于淘宝购物的快照,会保留下单前的商品内容,在 React 中就相当于是 即将更新前的状态
122 |
123 | > 它可以使组件在 DOM 真正更新之前捕获一些信息(例如滚动位置),此生命周期返回的任何值都会作为参数传递给 `componentDidUpdate()`。如不需要传递任何值,那么请返回 null
124 |
125 | #### 5. componentDidUpdate 执行
126 |
127 | 组件在更新完毕后会立即被调用,首次渲染不会调用
128 |
129 | ---
130 |
131 | 到此更新阶段就结束了,在 React 旧版本中有两个与更新有关的钩子函数 `componentWillReceiveProps` 和 `componentWillUpdate` 都即将废弃
132 |
133 | `componentWillReceiveProps` 我不太懂
134 |
135 | `componentWillUpdate` 在 `render` 之前执行,表示组件将要更新
136 |
137 | ### 销毁阶段
138 |
139 | #### componentWillUnmount 执行
140 |
141 | 在组件即将被卸载或销毁时进行调用。
142 |
143 | ## 总结
144 |
145 | **初始化**
146 |
147 | - constructor()
148 | - static getDerivedStateFromProps()
149 | - render()
150 | - componentDidMount()
151 |
152 | **更新**
153 |
154 | - static getDerivedStateFromProps()
155 | - shouldComponentUpdate()
156 | - render()
157 | - getSnapshotBeforeUpdate()
158 | - componentDidUpdate()
159 |
160 | **销毁**
161 |
162 | - componentWillUnmount()
163 |
164 | ---
165 |
166 | > 初学 React ,对生命周期还没有深入的理解,只能大概知道在什么时候触发哪个钩子,希望各位大佬多多指教,有什么建议可以提一提 🙏
167 |
168 |
--------------------------------------------------------------------------------
/React 入门学习/React 入门学习(九)-- 消息订阅发布.md:
--------------------------------------------------------------------------------
1 | # React 入门学习(九)-- 消息订阅发布
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**大二的前端爱好者**
6 | >
7 | > 📢 这篇文章是学习 React 中 GitHub 搜索案例的学习笔记
8 | >
9 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
10 | >
11 | > 📢 **愿你忠于自己,热爱生活**
12 |
13 | ## 引言
14 |
15 | 在昨天写的 `Github` 案例中,我们采用的是 `axios` 发送请求来获取数据,同时我们需要将数据从 `Search` 中传入给 `App`,再由 `App` 组件再将数据传递给 `List` 组件,这个过程会显得多此一举。同时我们要将 `state` 状态存放在 `App` 组件当中,但是这些 `state` 状态都是在 `List` 组件中使用的,在 `Search` 组件中做的,只是更新这些数据,那这样也会显得很没有必要,我们完全可以将 `state` 状态存放在 `List` 组件中,但是这样我们又会遇到技术难题,兄弟组件间的数据通信。那这里我们就学习一下如何利用消息订阅发布来解决**兄弟组件间的通信**
16 |
17 | ## 消息发布订阅
18 |
19 | 要解决上面的问题,我们可以借助发布订阅的机制,我们可以将 App 文件中的所有状态和方法全部去除,因为本来就不是在 App 组件中直接使用这些方法的,App 组件只是一个中间媒介而已
20 |
21 | 我们先简单的说一下**消息订阅和发布的机制**
22 |
23 | 就拿我们平常订杂志来说,我们和出版社说我们要订一年的足球周刊,那每次有新的足球周刊,它都会寄来给你。
24 |
25 | 换到代码层面上,我们订阅了一个消息假设为 A,当另一个人发布了 A 消息时,因为我们订阅了消息 A ,那么我们就可以拿到 A 消息,并获取数据
26 |
27 | 那我们要怎么实现呢?
28 |
29 | 首先引入 `pubsub-js`
30 |
31 | 我们需要先安装这个库
32 |
33 | ```js
34 | yarn add pubsub-js
35 | ```
36 |
37 | 引入这个库
38 |
39 | ```js
40 | import PubSub from 'pubsub-js'
41 | ```
42 |
43 | 订阅消息
44 |
45 | 我们通过 `subscribe` 来订阅消息,它接收两个参数,第一个参数是消息的名称,第二个是消息成功的回调,回调中也接受两个参数,一个是消息名称,一个是返回的数据
46 |
47 | ```js
48 | PubSub.subscribe('search',(msg,data)=>{
49 | console.log(msg,data);
50 | })
51 | ```
52 |
53 | 发布消息
54 |
55 | 我们采用 `publish` 来发布消息,用法如下
56 |
57 | ```js
58 | PubSub.publish('search',{name:'tom',age:18})
59 | ```
60 |
61 | 有了这些基础,我们可以完善我们昨天写的 GitHub 案例
62 |
63 | 将数据的更新通过 `publish` 来传递,例如在发送请求之前,我们需要出现 loading 字样
64 |
65 | ```js
66 | // 之前的写法
67 | this.props.updateAppState({ isFirst: false, isLoading: true })
68 | // 改为发布订阅方式
69 | PubSub.publish('search',{ isFirst: false, isLoading: true })
70 | ```
71 |
72 | 这样我们就能成功的在请求之前发送消息,我们只需要在 List 组件中订阅一下这个消息即可,并将返回的数据用于更新状态即可
73 |
74 | ```js
75 | PubSub.subscribe('search',(msg,stateObj)=>{
76 | this.setState(stateObj)
77 | })
78 | ```
79 |
80 | 同时上面的代码会返回一个 `token` ,这个就类似于定时器的编号的存在,我们可以通过这个 `token` 值,来取消对应的订阅
81 |
82 | 通过 `unsubscribe` 来取消指定的订阅
83 |
84 | ```js
85 | PubSub.unsubscribe(this.token)
86 | ```
87 |
88 | ## 扩展 -- Fetch
89 |
90 | 首先 fetch 也是一种发送请求的方式,它是在 xhr 之外的一种,我们平常用的 Jquery 和 axios 都是封装了 xhr 的第三方库,而 fetch 是官方自带的库,同时它也采用的是 Promise 的方式,大大简化了写法
91 |
92 | 如何使用呢?
93 |
94 | ```js
95 | fetch('http://xxx')
96 | .then(response => response.json())
97 | .then(json => console.log(json))
98 | .catch(err => console.log('Request Failed', err));
99 | ```
100 |
101 | 它的使用方法和 axios 非常的类似,都是返回 Promise 对象,但是不同的是, fetch 关注分离,它在第一次请求时,不会直接返回数据,会先返回联系服务器的状态,在第二步中才能够获取到数据
102 |
103 | 我们需要在第一次 `then` 中返回 `response.json()` 因为这个是包含数据的 promise 对象,再调用一次 `then` 方法即可实现
104 |
105 | 但是这么多次的调用 `then` 并不是我们所期望的,相信看过之前生成器的文章的伙伴,已经有了想法。
106 |
107 | 我们可以利用 `async` 和 `await` 配合使用,来简化代码
108 |
109 | 可以将 `await` 理解成一个自动执行的 `then` 方法,这样清晰多了
110 |
111 | ```js
112 |
113 | async function getJSON() {
114 | let url = 'https://xxx';
115 | try {
116 | let response = await fetch(url);
117 | return await reasponse.json();
118 | } catch (error) {
119 | console.log('Request Failed', error);
120 | }
121 | }
122 | ```
123 |
124 | 最后关于错误对象的获取可以采用 `try...catch` 来实现
125 |
126 | 关于 fetch 的更多内容
127 |
128 | 强烈推荐阮一峰老师的博文:[fetch](http://www.ruanyifeng.com/blog/2020/12/fetch-tutorial.html)
129 |
130 | ---
131 |
132 | > 非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈
133 |
134 |
--------------------------------------------------------------------------------
/React 入门学习/React 入门学习(二)-- 面向组件编程.md:
--------------------------------------------------------------------------------
1 | # React 从入门到入土(二)-- 面向组件编程
2 |
3 | 
4 |
5 | > 📢 大家好😪 ,我是小丞同学,最近在学习 React、小程序、阅读 JS 高程,以及整理 Node 的笔记,这是关于 React 的**第二篇**文章,也是我学习的第一个框架,内容如有错误,欢迎大家指正
6 | >
7 | > 📢 愿你生活明朗,万物可爱
8 |
9 | ## 一、组件的使用
10 |
11 | 当应用是以多组件的方式实现,这个应用就是一个组件化的应用
12 |
13 | > **注意:**
14 | >
15 | > 1. 组件名必须是首字母大写
16 | >
17 | > 2. 虚拟DOM元素只能有一个根元素
18 | >
19 | > 3. 虚拟DOM元素必须有结束标签 `< />`
20 |
21 | **渲染类组件标签的基本流程**
22 |
23 | 1. React 内部会创建组件实例对象
24 |
25 | 2. 调用`render()`得到虚拟 DOM ,并解析为真实 DOM
26 |
27 | 3. 插入到指定的页面元素内部
28 |
29 | ### 1. 函数式组件
30 |
31 | ```js
32 | //1.先创建函数,函数可以有参数,也可以没有,但是必须要有返回值 返回一个虚拟DOM
33 | function Welcome(props) {
34 | return Hello, {props.name}
;
35 | }
36 | //2.进行渲染
37 | ReactDOM.Render(,document.getElementById("div"));
38 | ```
39 |
40 | 上面的代码经历了以下几步
41 |
42 | 1. 我们调用 `ReactDOM.render()` 函数,并传入 `` 作为参数。
43 | 2. React 调用 `Welcome` 组件,并将 `{name: 'ljc'}` 作为 props 传入。
44 | 3. `Welcome` 组件将 `Hello, ljc` 元素作为返回值。
45 | 4. React DOM 将 DOM 高效地更新为 `Hello,ljc`。
46 |
47 | ### 2. 类式组件
48 |
49 | 
50 |
51 | ```js
52 | class MyComponent extends React.Component {
53 | state = {isHot:false}
54 | render() {
55 | const {isHot} = this.state
56 | return 今天天气很{isHot?'炎热':'凉爽'}
57 | }
58 | changeWeather = ()=>{
59 | const isHot = this.state.isHot
60 | this.setState({isHot:!isHot})
61 | }
62 | }
63 | ReactDOM.render(,document.querySelector('.test'))
64 | ```
65 |
66 | 这玩意底层不简单,`this`的指向真的需要好好学习
67 |
68 | **在优化过程中遇到的问题**
69 |
70 | 1. 组件中的 render 方法中的 this 为组件实例对象
71 | 2. 组件自定义方法中由于开启了严格模式,this 指向 `undefined` 如何解决
72 | 1. 通过 bind 改变 this 指向
73 | 2. 推荐采用箭头函数,箭头函数的 `this` 指向
74 | 3. state 数据不能直接修改或者更新
75 |
76 | ### 3. 其他知识
77 |
78 | 包含表单元素的组件分为非受控租价与受控组件
79 |
80 | - **受控组件**:表单组件的输入组件随着输入并将内容存储到状态中(随时更新)
81 | - **非受控组件**:表单组件的输入组件的内容在有需求的时候才存储到状态中(即用即取)
82 |
83 | ## 二、组件实例三大属性
84 |
85 | ### 1. state
86 |
87 | > React 把组件看成是一个状态机(State Machines)。通过与用户的交互,实现不同状态,然后渲染 UI,让用户界面和数据保持一致。
88 | >
89 | > React 里,只需更新组件的 state,然后根据新的 state 重新渲染用户界面(不要操作 DOM)。
90 |
91 | 简单的说就是组件的状态,也就是该组件所存储的数据
92 |
93 | **类式组件中的使用**
94 |
95 | 
96 |
97 | 使用的时候通过`this.state`调用`state`里的值
98 |
99 | 在类式组件中定义`state`
100 |
101 | - 在构造器中初始化`state`
102 | - 在类中添加属性`state`来初始化
103 |
104 | **修改 state**
105 |
106 | 在**类式组件**的函数中,直接修改`state`值
107 |
108 | ```js
109 | this.state.weather = '凉爽'
110 | ```
111 |
112 | > 页面的渲染靠的是`render`函数
113 |
114 | 这时候会发现页面内容不会改变,原因是 React 中不建议 `state`不允许直接修改,而是通过类的原型对象上的方法 `setState()`
115 |
116 | **setState()**
117 |
118 | ```js
119 | this.setState(partialState, [callback]);
120 | ```
121 |
122 | - `partialState`: 需要更新的状态的部分对象
123 | - `callback`: 更新完状态后的回调函数
124 |
125 | 有两种写法:写法1
126 |
127 | ```js
128 | this.setState({
129 | weather: "凉爽"
130 | })
131 | ```
132 |
133 | 写法2:
134 |
135 | ```js
136 | // 传入一个函数,返回x需要修改成的对象,参数为当前的 state
137 | this.setState(state => ({count: state.count+1});
138 | ```
139 |
140 | `setState`是一种合并操作,不是替换操作
141 |
142 | ---
143 |
144 | - 在执行 `setState`操作后,React 会自动调用一次 `render()`
145 | - `render()` 的执行次数是 1+n (1 为初始化时的自动调用,n 为状态更新的次数)
146 |
147 | ### 2. props
148 |
149 | 与`state`不同,`state`是组件自身的状态,而`props`则是外部传入的数据
150 |
151 | **类式组件中使用**
152 |
153 | 
154 |
155 | 在使用的时候可以通过 `this.props`来获取值 类式组件的 `props`:
156 |
157 | 1. 通过在组件标签上传递值,在组件中就可以获取到所传递的值
158 | 2. 在构造器里的`props`参数里可以获取到 `props`
159 | 3. 可以分别设置 `propTypes` 和 `defaultProps` 两个属性来分别操作 `props`的规范和默认值,两者都是直接添加在类式组件的**原型对象**上的(所以需要添加 `static`)
160 | 4. 同时可以通过`...`运算符来简化
161 |
162 | 
163 |
164 | **函数式组件中的使用**
165 |
166 | > 函数在使用props的时候,是作为参数进行使用的(props)
167 |
168 | 
169 |
170 | 函数组件的 `props`定义:
171 |
172 | 1. 在组件标签中传递 `props`的值
173 | 2. 组件函数的参数为 `props`
174 | 3. 对 `props`的限制和默认值同样设置在原型对象上
175 |
176 | ### 3. refs
177 |
178 | Refs 提供了一种方式,允许我们访问 DOM 节点或在 `render` 方法中创建的 React 元素。
179 |
180 | > 在我们正常的操作节点时,需要采用DOM API 来查找元素,但是这样违背了 React 的理念,因此有了`refs`
181 |
182 | 有三种操作`refs`的方法,分别为:
183 |
184 | - 字符串形式
185 | - 回调形式
186 | - `createRef`形式
187 |
188 | **字符串形式**`refs`
189 |
190 | 
191 |
192 | 虽然这个方法废弃了,但是还能用,还很好用hhh~
193 |
194 | **回调形式的**`refs`
195 |
196 | 组件实例的`ref`属性传递一个回调函数`c => this.input1 = c `(箭头函数简写),这样会在实例的属性中存储对DOM节点的引用,使用时可通过`this.input1`来使用
197 |
198 | **使用方法**
199 |
200 | ```js
201 | this.input1 = c } type="text" placeholder="点击按钮提示数据"/>
202 | ```
203 |
204 | **我的理解**
205 |
206 | `c`会接收到当前节点作为参数,`ref`的值为函数的返回值,也就是`this.input1 = c`,因此是给实例下的`input1`赋值
207 |
208 | **createRef 形式**(推荐写法)
209 |
210 | React 给我们提供了一个相应的API,它会自动的将该 DOM 元素放入实例对象中
211 |
212 | 我们先给DOM元素添加ref属性
213 |
214 | ```html
215 |
216 |
217 | ```
218 |
219 | 通过API,创建React的容器,会将DOM元素赋值给实例对象的名称为容器的属性的`current`,好烦..
220 |
221 | ```js
222 | MyRef = React.createRef();
223 | MyRef1 = React.createRef();
224 | ```
225 |
226 | 注意:专人专用,好烦,一个节点创建一个容器
227 |
228 | ```js
229 | //调用
230 | btnOnClick = () =>{
231 | //创建之后,将自身节点,传入current中
232 | console.log(this.MyRef.current.value);
233 | }
234 | ```
235 |
236 | 注意:我们不要过度的使用 ref,如果发生时间的元素刚好是需要操作的元素,就可以使用事件对象去替代。过度使用有什么问题我也不清楚,可能有 bug 吧
237 |
238 | ### 4. 事件处理
239 |
240 | 1. React 使用的是自定义事件,而不是原生的 DOM 事件
241 |
242 | 2. React 的事件是通过事件委托方式处理的(为了更加的高效)
243 |
244 | 3. 可以通过事件的 `event.target`获取发生的 DOM 元素对象,可以尽量减少 `refs`的使用
245 |
246 | 
247 |
248 | ## 三、高阶函数
249 |
250 | 关于这部分的知识,之前的笔记有记过了,我真是太棒了
251 |
252 | 链接[高阶函数](https://linjc.blog.csdn.net/article/details/116765732),关于AOP,偏函数,柯里化都有不错的记录,感觉还是不错的
253 |
254 |
255 |
256 |
--------------------------------------------------------------------------------
/React 入门学习/React 入门学习(五)-- 初始化脚手架.md:
--------------------------------------------------------------------------------
1 | # React 入门学习(五)-- 认识脚手架
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,这篇文章是学习 React 脚手架的学习笔记
6 | >
7 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
8 | >
9 | > 📢 愿你生活明朗,万物可爱
10 |
11 | ## 简介
12 |
13 | 这篇文章主要围绕 React 中的脚手架,来解决一下几个问题
14 |
15 | **灵魂三问:是什么?为什么?怎么办?**
16 |
17 | 1. 什么是脚手架?
18 | 2. 为什么要用脚手架?
19 | 3. 怎么用脚手架?
20 |
21 | ## 🍕 1. 什么是 React 脚手架?
22 |
23 | 在我们的现实生活中,脚手架最常用的使用场景是在工地,它是为了保证施工顺利的、方便的进行而搭建的,在工地上搭建的脚手架可以帮助工人们高校的去完成工作,同时在大楼建设完成后,拆除脚手架并不会有任何的影响。
24 |
25 | 在我们的 React 项目中,脚手架的作用与之有异曲同工之妙
26 |
27 | React 脚手架其实是一个工具帮我们快速的生成项目的工程化结构,每个项目的结构其实大致都是相同的,所以 React 给我提前的搭建好了,这也是脚手架强大之处之一,也是用 React 创建 SPA 应用的最佳方式
28 |
29 | ## 🍔 2. 为什么要用脚手架?
30 |
31 | 在前面的介绍中,我们也有了一定的认知,脚手架可以帮助我们快速的搭建一个项目结构
32 |
33 | 在我之前学习 `webpack` 的过程中,每次都需要配置 `webpack.config.js` 文件,用于配置我们项目的相关 `loader` 、`plugin`,这些操作比较复杂,但是它的重复性很高,而且在项目打包时又很有必要,那 React 脚手架就帮助我们做了这些,它不需要我们人为的去编写 `webpack` 配置文件,它将这些配置文件全部都已经提前的配置好了。
34 |
35 | 据我猜测是直接输入一行命令就能打包完成。
36 |
37 | > 目前还没有学习到哪,本文主要讲**脚手架的项目目录结构以及安装**
38 |
39 | ## 🍟 3. 怎么用 React 脚手架?
40 |
41 | 这也是这篇文章的重点,如何去安装 React 脚手架,并且理解它其中的相关文件作用
42 |
43 | 首先介绍如何安装脚手架
44 |
45 | ### 1. 安装 React 脚手架
46 |
47 | 首先确保安装了 `npm` 和`Node`,版本不要太古老,具体是多少不大清楚,建议还是用 `npm update` 更新一下
48 |
49 | 然后打开 cmd 命令行工具,全局安装 `create-react-app`
50 |
51 | ```shell
52 | npm i create-react-app -g
53 | ```
54 |
55 | 然后可以**新建**一个文件夹用于存放项目
56 |
57 | 在当前的文件夹下执行
58 |
59 | ```shell
60 | create-react-app hello-react
61 | ```
62 |
63 | **快速搭建项目**
64 |
65 | 再在生成好的 `hello-react` 文件夹中执行
66 |
67 | ```shell
68 | npm start
69 | ```
70 |
71 | **启动项目**
72 |
73 | 接下来我们看看这些文件都有什么作用
74 |
75 | ### 2. 脚手架项目结构
76 |
77 | ```
78 | hello-react
79 | ├─ .gitignore // 自动创建本地仓库
80 | ├─ package.json // 相关配置文件
81 | ├─ public // 公共资源
82 | │ ├─ favicon.ico // 浏览器顶部的icon图标
83 | │ ├─ index.html // 应用的 index.html入口
84 | │ ├─ logo192.png // 在 manifest 中使用的logo图
85 | │ ├─ logo512.png // 同上
86 | │ ├─ manifest.json // 应用加壳的配置文件
87 | │ └─ robots.txt // 爬虫给协议文件
88 | ├─ src // 源码文件夹
89 | │ ├─ App.css // App组件的样式
90 | │ ├─ App.js // App组件
91 | │ ├─ App.test.js // 用于给APP做测试
92 | │ ├─ index.css // 样式
93 | │ ├─ index.js // 入口文件
94 | │ ├─ logo.svg // logo图
95 | │ ├─ reportWebVitals.js // 页面性能分析文件
96 | │ └─ setupTests.js // 组件单元测试文件
97 | └─ yarn.lock
98 | ```
99 |
100 | 再介绍一下public目录下的 `index.html` 文件中的代码意思
101 |
102 | ```html
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
114 |
115 |
116 | React App
117 |
118 |
119 |
120 |
121 |
122 |
123 | ```
124 |
125 | 以上是删除代码注释后的全部代码
126 |
127 | **第5行**
128 |
129 | 指定浏览器图标的路径,这里直接采用 `%PUBLIC_URL%` 原因是 `webpack` 配置好了,它代表的意思就是 `public` 文件夹
130 |
131 | ```html
132 |
133 | ```
134 |
135 | **第6行**
136 |
137 | 用于做移动端网页适配
138 |
139 | ```html
140 |
141 | ```
142 |
143 | **第七行**
144 |
145 | 用于配置安卓手机浏览器顶部颜色,兼容性不大好
146 |
147 | ```html
148 |
149 | ```
150 |
151 | **8到11行**
152 |
153 | 用于描述网站信息
154 |
155 | ```html
156 |
160 | ```
161 |
162 | **第12行**
163 |
164 | 苹果手机触摸版应用图标
165 |
166 | ```html
167 |
168 | ```
169 |
170 | **第13行**
171 |
172 | 应用加壳时的配置文件
173 |
174 | ```html
175 |
176 | ```
177 |
178 | > 以上就是关于 React 脚手架的全部内容了,非常感谢你的阅读💕
179 |
180 |
--------------------------------------------------------------------------------
/React 入门学习/React 入门学习(八)-- GitHub 搜索案例.md:
--------------------------------------------------------------------------------
1 | # React 入门学习(八)-- GitHub 搜索案例
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**准大二的前端爱好者**
6 | >
7 | > 📢 这篇文章是学习 React 中 GitHub 搜索案例的学习笔记
8 | >
9 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
10 | >
11 | > 📢 **愿你忠于自己,热爱生活**
12 |
13 | ## 引言
14 |
15 | 本文主要介绍 React 学习中 Github 搜索案例,这个案例主要涉及到了 Axios 发送请求,数据渲染以及一些中间交替效果的实现
16 |
17 | 个人感觉在做完 TodoList 案例之后,这个案例会很轻松,只是多加了一个 Loading 效果的实现思路,以及一些小细节的完善,感觉练练手还是很不错的
18 |
19 | ## 一、实现静态组件
20 |
21 | 和之前的 TodoList 案例一样,我们需要先实现静态组件,在实现静态组件之前,我们还需要拆分组件,这个页面的组件,我们可以将它拆成以下两个组件,第一个组件是 `Search`,第二个是 `List`
22 |
23 | 
24 |
25 | 接下来我们需要将提前写好的静态页面,对应拆分到组件当中
26 |
27 | 注意:
28 |
29 | 1. class 需要改成 className
30 | 2. style 的值需要使用双花括号的形式
31 |
32 | 最重要的一点就是,`img` 标签,一定要**添加** `alt` 属性表示图片加载失败时的提示。
33 |
34 | 同时,`a` 标签要添加 `rel="noreferrer"`属性,不然会有大量的警告出现
35 |
36 | 
37 |
38 | ## 二、axios 发送请求
39 |
40 | 在实现静态组件之后,我们需要通过向 `github` 发送请求,来获取相应的用户信息
41 |
42 | 但是由于短时间内多次请求,可能会导致请求不返回结果等情况发生,因此我们采用了一个事先搭建好的本地服务器
43 |
44 | 我们启动服务器,向这个地址发送请求即可
45 |
46 | 
47 |
48 | 这个请求类型是 GET 请求,我们需要传递一个搜索的关键字,去请求数据
49 |
50 | 我们首先要获取到用户点击搜索按钮后**输入框中的值**
51 |
52 | 在需要触发事件的 `input` 标签中,添加 `ref` 属性
53 |
54 | ```js
55 | this.keyWordElement = c} type="text" placeholder="输入关键词点击搜索" />
56 | ```
57 |
58 | 我们可以通过 `this.keyWordElement` 属性来获取到这个当前节点,也就是这个 `input` 框
59 |
60 | 我们再通过 `value` 值,即可获取到当前 `input` 框中的值
61 |
62 | ```js
63 | // search 回调
64 | const { keyWordElement: { value: keyWord } } = this
65 | ```
66 |
67 | 这里采用的是连续的解构赋值,最后将 `value` 改为 `keyWord` ,这样好辨别
68 |
69 | 获取到了 `keyWord` 值,接下来我们就需要发送请求了
70 |
71 | ```js
72 | axios.get(`http://localhost:3000/api1/search/users?q=${keyWord}`).then(
73 | response => {
74 | this.props.updateAppState({ isLoading: false, users: response.data.items })
75 | },
76 | error => {
77 | this.props.updateAppState({ isLoading: false, err: error.message })
78 | }
79 | )
80 | ```
81 |
82 | 我们将 `keyWord` 接在请求地址的后面,来传递参数,以获得相关数据
83 |
84 | 这里会存在跨域的问题,因我我们是站在 3000 端口向 5000 端口发送请求的
85 |
86 | 因此我们需要配置代理来解决跨域的问题,我们需要在请求地址前,加上启用代理的标志 `/api1`
87 |
88 | ```js
89 | // setupProxy.js
90 | const proxy = require('http-proxy-middleware')
91 | module.exports = function (app) {
92 | app.use(
93 | proxy('/api1', {
94 | target: 'http://localhost:5000',
95 | changeOrigin: true,
96 | pathRewrite: {
97 | '^/api1': ''
98 | }
99 | })
100 | )
101 | }
102 | ```
103 |
104 | 这样我们就能成功的获取到了数据
105 |
106 | 
107 |
108 | ## 三、渲染数据
109 |
110 | 在获取到了数据之后,我们需要对数据进行分析,并将这些数据渲染到页面上
111 |
112 | 比较重要的一点是,我们获取到的用户个数是动态的,因此我们需要通过遍历的方式去实现
113 |
114 | 同时我们的数据当前存在于 `Search` 组件当中,我们需要在 `List` 组件中使用,所以我们需要个 `Search` 组件传递一个函数,来实现子向父传递数据,再通过 `App` 组件,向`List` 组件传递数据即可得到 `data`
115 |
116 | ```js
117 | users.map((userObj) => {
118 | return (
119 |
125 | )
126 | })
127 | ```
128 |
129 | 这里我们通过 `map` 遍历整个返回的数据,来循环的添加 `card` 的个数
130 |
131 | 同时将一些用户信息添加到其中
132 |
133 | ## 四、增加交互
134 |
135 | 做到这里其实已经完成了一大半了,但是似乎少了点交互
136 |
137 | - 加载时的 loading 效果
138 | - 第一次进入页面时 List 组件中的**欢迎使用字样**
139 | - 在报错时应该提示错误信息
140 |
141 | 这一些都预示着我们不能单纯的将用户数据直接渲染,我们需要添加一些判断,什么时候该渲染数据,什么时候渲染 loading,什么时候渲染 err
142 |
143 | 首先我们需要增加一些状态,来指示我们该渲染什么,比如
144 |
145 | - 采用 `isFrist` 来判断页面是否第一次启动,初始值给 `true`,点击搜索后改为 `false`
146 | - 采用 `isLoading` 来判断是否应该显示 Loading 动画,初始值给 `false`,在点击搜索后改为 `true`,在拿到数据后改为 `false`
147 | - 采用 `err` 来判断是否渲染错误信息,当报错时填入报错信息,初始值**给空**
148 |
149 | ```js
150 | state = { users: [], isFirst: true, isLoading: false, err: '' }
151 | ```
152 |
153 | 这样我们就需要改变我先前采用的数据传递方式,采用更新状态的方式,接收一个状态对象来**更新数据**,这样就不用去指定什么时候更新什么,就可以减少很多**不必要**的函数声明
154 |
155 | 同时在 App 组件给 List 组件传递数据时,我们可以采用解构赋值的方式,这样可以减少代码量
156 |
157 | ```js
158 | // App.jsx
159 | // 接收一个状态对象
160 | updateAppState = (stateObj) => {
161 | this.setState(stateObj)
162 | }
163 |
164 |
165 | ```
166 |
167 | 这样我们只需要在 List 组件中,判断这些状态的值,来显示即可
168 |
169 | ```js
170 | // List/index.jsx
171 | // 对象解构
172 | const { users, isFirst, isLoading, err } = this.props
173 | // 判断
174 | {
175 | isFirst ? 欢迎使用,输入关键字,点击搜索
:
176 | isLoading ? Loading...
:
177 | err ? {err}
:
178 | users.map((userObj) => {
179 | return (
180 | // 渲染数据块
181 | //为了减少代码量,就不贴了
182 | )
183 | })
184 | }
185 | ```
186 |
187 | 我们需要先判断是否第一次,再判断是不是正在加载,再判断有没有报错,最后再渲染数据
188 |
189 | 我们的状态更新是在 Search 组件中实现的,在点击搜索之后数据返回之前,我们需要将 `isFirst` 改为 `false` ,`isLoading` 改为 `true`
190 |
191 | 接收到数据后我们再将 `isLoading` 改为 `false` 即可
192 |
193 | 以上就是 Github 搜索案例的实现过程
194 |
195 | 
196 |
197 | 最终效果图
198 |
199 | ---
200 |
201 | > 前端路还有很长,今天我就大二啦!加油吧!!!
202 |
203 | > 非常感谢您的阅读,欢迎提出你的意见,有什么问题欢迎指出,谢谢!🎈
204 |
205 |
--------------------------------------------------------------------------------
/React 入门学习/React 入门学习(六)-- TodoList 案例.md:
--------------------------------------------------------------------------------
1 | # React 入门学习(六)-- TodoList 案例
2 |
3 | 
4 |
5 | > 📢 大家好,我是小丞同学,一名**准大二的前端爱好者**
6 | >
7 | > 📢 这篇文章是学习 React 练习中 TodoList 案例的操作笔记
8 | >
9 | > 📢 非常感谢你的阅读,不对的地方欢迎指正 🙏
10 | >
11 | > 📢 **愿你忠于自己,热爱生活**
12 |
13 | ## 引言
14 |
15 | TodoList 案例在前端学习中挺重要的,从原生 JavaScript 的增删查改,到现在 React 的组件通信,都是一个不错的案例,这篇文章主要记录,还原一下通过 React 实现 TodoList 的全过程
16 |
17 | 
18 |
19 | ## 一、拆分组件
20 |
21 | 首先第一步需要做的是将这个页面拆分成几个组件
22 |
23 | 首先顶部的输入框,可以完成添加项目的功能,可以拆分成一个 **Header 组件**
24 |
25 | 中间部分可以实现一个渲染列表的功能,可以拆分成一个 **List 组件**
26 |
27 | 在这部分里面,每一个待办事项都可以拆分成一个 **Item 组件**
28 |
29 | 最后底部显示当前完成状态的部分,可以拆分成一个 **Footer 组件**
30 |
31 | 
32 |
33 | 在拆分完组件后,我们下一步要做的就是去实现这些组件的静态效果
34 |
35 | ## 二、实现静态组件
36 |
37 | 首先,我们可以先写好这个页面的静态页面,然后再分离组件,所以这就要求我们
38 |
39 | 以后写静态页面的时候,一定要有明确的规范
40 |
41 | 1. 打好注释
42 | 2. 每个部分的 CSS 要写在一个地方,不要随意写
43 | 3. 命名一定要规范
44 | 4. CSS 选择器不要关联太多层级
45 | 5. 在写 HTML 时就要划分好布局
46 |
47 | 这样有利于我们分离组件
48 |
49 | 首先,我们在 `src` 目录下,新建一个 `Components` 文件夹,用于存放我们的组件,然后在文件夹下,新建 `Header` 、`Item`、`List` 、`Footer` 组件文件夹,再创建其下的 `index.jsx`,`index.css` 文件,用于创建对应组件及其样式文件
50 |
51 | ```markdown
52 | todolist
53 | ├─ package.json
54 | ├─ public
55 | │ ├─ favicon.ico
56 | │ └─ index.html
57 | ├─ src
58 | │ ├─ App.css
59 | │ ├─ App.jsx
60 | │ ├─ Components
61 | │ │ ├─ Footer
62 | │ │ │ ├─ index.css
63 | │ │ │ └─ index.jsx
64 | │ │ ├─ Header
65 | │ │ │ ├─ index.css
66 | │ │ │ └─ index.jsx
67 | │ │ ├─ item
68 | │ │ │ ├─ index.css
69 | │ │ │ └─ index.jsx
70 | │ │ └─ List
71 | │ │ ├─ index.css
72 | │ │ └─ index.jsx
73 | │ └─ index.js
74 | └─ yarn.lock
75 | ```
76 |
77 | 最终目录结构如上
78 |
79 | 然后我们将每个组件,对应的 HTML 结构 CV 到对应组件的 `index.jsx` 文件中 `return` 出来,再将 CSS 样式添加到 `index.css` 文件中
80 |
81 | **记得**,在 `index.jsx` 中一定要引入 `index.css` 文件
82 |
83 | 实现了静态组件后,我们需要添加事件等,来实现动态组件
84 |
85 | ## 三、实现动态组件
86 |
87 | ### 🍎 1. 动态展示列表
88 |
89 | 我们目前实现的列表项是固定的,我们需要它通过**状态**来维护,而不是通过**组件标签**来维护
90 |
91 | 首先我们知道,父子之间传递参数,可以通过 `state` 和 `props` 实现
92 |
93 | 我们通过在父组件也就是 `App.jsx` 中设置状态
94 |
95 | 
96 |
97 | 再将它传递给对应的渲染组件 `List`
98 |
99 | ```jsx
100 | const { todos } = this.state
101 |
102 | ```
103 |
104 | 这样在 `List` 组件中就能通过 `props` 来获取到 `todos`
105 |
106 | 我们通过解构取出 `todos`
107 |
108 | ```jsx
109 | const { todos, updateTodo } = this.props
110 | ```
111 |
112 | 再通过 `map` 遍历渲染 `Item` 数量
113 |
114 | ```js
115 | {
116 | todos.map(todo => {
117 | return
118 | })
119 | }
120 | ```
121 |
122 | 同时由于我们的数据渲染最终是在 `Item` 组件中完成的,所以我们需要将数据传递给 `Item` 组件
123 |
124 | 这里有两个注意点
125 |
126 | 1. 关于 `key` 的作用在 diff 算法的文章中已经有讲过了,需要满足**唯一性**
127 | 2. 这里采用了简写形式 `{...todo}` ,这使得代码更加简洁,它代表的意思是
128 |
129 | ```js
130 | id = {todo.id} name = {todo.name} done = {todo.done}
131 | ```
132 |
133 | 在 `Item` 组件中取出 `props` 即可使用
134 |
135 | ```js
136 | const { id, name, done } = this.props
137 | ```
138 |
139 | 这样我们更改 `APP.jsx` 文件中的 `state` 就能驱动着 `Item` 组件的更新,如图
140 |
141 | 
142 |
143 | 同时这里需要注意的是
144 |
145 | 对于复选框的选中状态,这里采用的是 `defaultChecked = {done}`,相比于 `checked` 属性,这个设定的是默认值,能够更改
146 |
147 | ### 🍍 2. 添加事项功能
148 |
149 | 首先我们需要在 Header 组件中,绑定键盘事件,判断按下的是否为回车,如果为回车,则将当前输入框中的内容传递给 APP 组件
150 |
151 | > 因为,在目前的学习知识中,Header 组件和渲染组件 List 属于兄弟组件,没有办法进行直接的数据传递,因此可以将数据传递给 APP 再由 APP 转发给 List。
152 |
153 | ```jsx
154 | // Header/index.jsx
155 | handleKeyUp = (event) => {
156 | // 结构赋值获取 keyCode,target
157 | const { keyCode, target } = event
158 | // 判断是不是回车
159 | if (keyCode !== 13) return
160 | if(target.value.trim() === '') {
161 | alert('输入不能为空')
162 | }
163 | // 准备一个todo对象
164 | const todoObj = { id: nanoid(), name: target.value, done: false }
165 | // 传递给app
166 | this.props.addTodo(todoObj)
167 | // 清空
168 | target.value = ''
169 | }
170 | ```
171 |
172 | 我们在 `App.jsx` 中添加了事件 `addTodo` ,这样可以将 Header 组件传递的参数,维护到 `App` 的状态中
173 |
174 | ```jsx
175 | // App.jsx
176 | addTodo = (todoObj) => {
177 | const { todos } = this.state
178 | // 追加一个 todo
179 | const newTodos = [todoObj, ...todos]
180 | this.setState({ todos: newTodos })
181 | }
182 | ```
183 |
184 | 在这小部分中,需要我们注意的是,我们新建的 `todo` 对象,一定要保证它的 `id` 的唯一性
185 |
186 | 这里采用的 `nanoid` 库,这个库的每一次调用都会返回一个唯一的值
187 |
188 | ```shell
189 | npm i nanoid
190 | ```
191 |
192 | 安装这个库,然后引入
193 |
194 | 通过 `nanoid()` 即可生成唯一值
195 |
196 | 
197 |
198 | ### 🍋 3. 实现鼠标悬浮效果
199 |
200 | 接下来我们需要实现每个 `Item` 中的小功能
201 |
202 | 首先是鼠标移入时的变色效果
203 |
204 | 我的逻辑是,通过一个状态来维护是否鼠标移入,比如用一个 `mouse` 变量,值给 `false` 当鼠标移入时,重新设定状态为 `true` 当鼠标移出时设为 `false` ,然后我们只需要在 `style` 中用`mouse` 去设定样式即可
205 |
206 | 下面我们来代码实现
207 |
208 | 在 `Item` 组件中,先设定状态
209 |
210 | ```jsx
211 | state = { mouse: false } // 标识鼠标移入,移出
212 | ```
213 |
214 | 给元素绑定上鼠标移入,移出事件
215 |
216 | ```js
217 |
218 | ```
219 |
220 | 当鼠标移入时,会触发 `onMouseEnter` 事件,调用 `handleMouse` 事件传入参数 `true` 表示鼠标进入,更新组件状态
221 |
222 | ```js
223 | handleMouse = flag => {
224 | return () => {
225 | this.setState({ mouse: flag })
226 | }
227 | }
228 | ```
229 |
230 | 再在 `li` 身上添加由 `mouse` 控制的背景颜色
231 |
232 | ```js
233 | style={{ backgroundColor: this.state.mouse ? '#ddd' : 'white' }}
234 | ```
235 |
236 | 同时通过 `mouse` 来控制删除按钮的显示和隐藏,做法和上面一样
237 |
238 | 
239 |
240 | 观察 mouse 的变化
241 |
242 | ### 🍉 4. 复选框状态维护
243 |
244 | 我们需要将当前复选框的状态,维护到 `state` 当中
245 |
246 | 我们的思路是
247 |
248 | 在复选框中添加一个 `onChange` 事件来进行数据的传递,当事件触发时我们执行 `handleCheck` 函数,这个函数可以向 App 组件中传递参数,这样再在 App 中改变状态即可
249 |
250 | 首先绑定事件
251 |
252 | ```js
253 | // Item/index.jsx
254 |
255 | ```
256 |
257 | 事件回调
258 |
259 | ```jsx
260 | handleCheck = (id) => {
261 | return (event) => {
262 | this.props.updateTodo(id, event.target.checked)
263 | }
264 | }
265 | ```
266 |
267 | 由于我们需要传递 `id` 来记录状态更新的对象,因此我们需要采用高阶函数的写法,不然函数会直接执行而报错,复选框的状态我们可以通过 `event.target.checked` 来获取
268 |
269 | 这样我们将我们需要改变状态的 `Item` 的 `id` 和改变后的状态,传递给了 App
270 |
271 | 内定义的`updateTodo` 事件,这样我们可以在 App 组件中操作改变状态
272 |
273 | 我们传递了两个参数 `id` 和 `done`
274 |
275 | 通过遍历找出该 `id` 对应的 `todo` 对象,更改它的 `done` 即可
276 |
277 | ```js
278 | // App.jsx
279 | updateTodo = (id, done) => {
280 | const { todos } = this.state
281 | // 处理
282 | const newTodos = todos.map(todoObj => {
283 | if (todoObj.id === id) {
284 | return { ...todoObj, done }
285 | } else {
286 | return todoObj
287 | }
288 | })
289 | this.setState({ todos: newTodos })
290 | }
291 | ```
292 |
293 | 这里更改的方式是 `{ ...todoObj, done }`,首先会展开 `todoObj` 的每一项,再对 `done` 属性做覆盖
294 |
295 | 
296 |
297 |
298 |
299 | ### 🍏 5. 限制参数类型
300 |
301 | 在我们前面写的东西中,我们并没有对参数的**类型以及必要性**进行限制
302 |
303 | 在前面我们也学过这个,我们需要借助 `propTypes` 这个库
304 |
305 | 首先我们需要引入这个库,然后对 `props` 进行限制
306 |
307 | ```js
308 | // Header
309 | static propTypes = {
310 | addTodo: PropTypes.func.isRequired
311 | }
312 | ```
313 |
314 | 在Header 组件中需要接收一个 `addTodo` 函数,所以我们进行一下限制
315 |
316 | 同时在 List 组件中也需要进行对 `todos` 以及 `updateTodo` 的限制
317 |
318 | 如果传入的参数不符合限制,则会报 **warning**
319 |
320 | ### 🍒 6. 删除按钮
321 |
322 | 现在我们需要实现删除按钮的效果
323 |
324 | 这个和前面的挺像的,首先我们分析一下,我们需要在 `Item` 组件上的按钮绑定点击事件,然后传入被点击事项的 `id` 值,通过 `props` 将它传递给父元素 `List` ,再通过在 `List` 中绑定一个 `App` 组件中的删除回调,将 `id` 传递给 `App` 来改变 `state`
325 |
326 | 首先我们先编写 点击事件
327 |
328 | ```js
329 | // Item/index.jsx
330 | handleDelete = (id) => {
331 | this.props.deleteTodo(id)
332 | }
333 | ```
334 |
335 | 绑定在点击事件的回调上
336 |
337 | 子组件想影响父组件的状态,需要父组件传递一个函数,因此我们在 `App` 中添加一个 `deleteTodo` 函数
338 |
339 | ```js
340 | // app.jsx
341 | deleteTodo = (id) => {
342 | const { todos } = this.state
343 | const newTodos = todos.filter(todoObj => {
344 | return todoObj.id !== id
345 | })
346 | this.setState({ todos: newTodos })
347 | }
348 | ```
349 |
350 | 然后将这个函数传递给 List 组件,再传递给 Item
351 |
352 | 增加一个判断
353 |
354 | ```js
355 | if(window.confirm('确认删除')) {
356 | this.props.deleteTodo(id)
357 | }
358 | ```
359 |
360 | 
361 |
362 | ### 🍓 7. 获取完成数量
363 |
364 | 我们在 App 中向 `Footer` 组件传递 `todos` 数据,再去统计数据
365 |
366 | 统计 `done `为 `true` 的个数
367 |
368 | ```js
369 | const doneCount = todos.reduce((pre, todo) => {
370 | return pre + (todo.done ? 1 : 0)
371 | }, 0)
372 | ```
373 |
374 | 再渲染数据即可
375 |
376 | 
377 |
378 | ### 🍊 8. 全选按钮
379 |
380 | 首先我们需要在按钮上绑定事件,由于子组件需要改变父组件的状态,所以我们的操作和之前的一样,先绑定事件,再在 App 中传一个函数个 Footer ,再在 Footer 中调用这个函数并传入参数即可
381 |
382 | 这里需要特别注意的是
383 |
384 | `defaulChecked` 只有第一次会起作用,所以我们需要将前面写的改成 `checked` 添加 `onChange` 事件即可
385 |
386 | 首先我们先在 App 中给 Footer 传入一个函数 `checkAllTodo`
387 |
388 | ```js
389 | // App.jsx
390 | checkAllTodo = (done) => {
391 | const { todos } = this.state
392 | const newTodos = todos.map((todoObj => {
393 | return { ...todoObj, done: done }
394 | }))
395 | this.setState({ todos: newTodos })
396 | }
397 | // render
398 |