├── .nojekyll ├── 02~组件基础 ├── 03~组件样式 │ ├── CSS-in-JS.md │ ├── 过渡与动画 │ │ ├── README.md │ │ └── TransitionGroup.md │ └── 样式定义与引入.md ├── 01~组件声明 │ ├── 函数式组件 │ │ ├── README.md │ │ ├── React.memo.md │ │ └── 函数式 React 开发.md │ ├── 事件系统 │ │ ├── 拖拽效果.md │ │ ├── 界面事件.md │ │ └── 合成事件.md │ └── 类组件 │ │ └── DOM 操作.md ├── 02~组件数据流 │ ├── Hooks │ │ ├── Hooks 扩展 │ │ │ └── AHook.md │ │ ├── 基础元语 │ │ │ ├── useReducer.md │ │ │ ├── useState & useRef.md │ │ │ ├── useEffect.md │ │ │ ├── useCallback & useMemo.md │ │ │ └── useSubscription.md │ │ ├── 机制解析 │ │ │ └── Hooks Scratch.md │ │ ├── 设计模式 │ │ │ └── 优化数据流.md │ │ └── README.md │ ├── README.md │ ├── Context.md │ ├── Props.md │ └── 不可变操作.md ├── 05~Storybook │ ├── 扩展.md │ ├── 用例.md │ ├── README.md │ └── 配置.md ├── .DS_Store ├── 06~组件库 │ ├── .DS_Store │ ├── Antd │ │ ├── README.md │ │ ├── 表单.md │ │ ├── 文件上传.md │ │ └── 应用配置.md │ ├── Form │ │ ├── .DS_Store │ │ └── React Hook Form.md │ └── 组件范式 │ │ ├── Svg.md │ │ ├── Formik.md │ │ ├── 列表组件.md │ │ └── 表单组件.md └── 04~React Router │ ├── 框架集成.md │ ├── Hooks Api.md │ ├── 配置与匹配.md │ └── 控制与切换.md ├── 04~工程实践 ├── 02~数据加载 │ ├── README.md │ ├── 异步渲染.md │ ├── React Query │ │ ├── 高级查询.md │ │ └── 快速开始.md │ ├── Suspense.md │ ├── 组件间通信.md │ └── 数据请求.md ├── 03~设计模式 │ ├── README.md │ └── 组件划分.md ├── 01~静态类型 │ ├── Flow.md │ └── TypeScript.md ├── 性能优化 │ ├── README.md │ ├── 性能评测与分析.md │ └── 异步碎片化状态更新.md ├── 类 React 库 │ ├── README.md │ ├── Inferno.md │ └── Preact.md ├── .DS_Store ├── 服务端渲染 │ ├── React Router.md │ ├── README.md │ ├── 搭建渲染服务器.md │ └── 服务端渲染性能浅析.md ├── 04~风格指南 │ └── 99~参考资料 │ │ └── 2021-React Philosophies.md ├── 组件测试 │ ├── README.md │ ├── Jest.md │ ├── 99~参考资料 │ │ └── README.md │ └── Enzyme.md └── 服务端组件 │ └── README.md ├── INTRODUCTION.md ├── 03~状态管理 ├── MobX │ ├── 实践调优.md │ ├── 异步事件.md │ ├── 数据存储.md │ ├── 响应式界面.md │ └── README.md ├── GraphQL │ └── README.md ├── .DS_Store ├── Hooks │ ├── README.md │ └── Bistate.md ├── README.md ├── Zustand │ ├── README.md │ ├── 03.状态持久化.md │ ├── 01.状态派生与 Selector.md │ ├── 04.状态重置.md │ ├── 02.状态分形与 Slice.md │ └── 99~参考资料 │ │ └── 2022-精读 zustand 源码.md ├── Redux │ ├── Dva │ │ ├── README.md │ │ └── 快速开始.md │ ├── 99~参考资料 │ │ └── README.md │ ├── redux-form.md │ ├── State 结构设计.md │ ├── Redux Hooks.md │ └── README.md ├── Recoiljs │ └── README.md ├── XState │ ├── README.md │ ├── 有限状态机.md │ └── React.md └── Context │ └── Context.md ├── 05~架构机制 ├── 组件系统 │ ├── 事务机制.md │ ├── 源码概览.md │ ├── 组件系统.md │ ├── VDOM.md │ ├── 99~参考资料 │ │ └── 2023-RSC From Scratch.md │ └── setState.md ├── 从零实现类 React 框架 │ ├── Ueact │ │ ├── 组件系统设计.md │ │ ├── README.md │ │ └── VirtualDOM 算法详解与实现.md │ └── Didact │ │ └── Didact.md ├── 基于 Stack 的调和 │ └── README.md ├── .DS_Store ├── 99~参考资料 │ └── 2020-《图解 React 源码》 │ │ └── README.md ├── React Compiler │ └── 99~参考资料 │ │ └── 2024~Understanding React Compiler.md └── 基于 Fiber 的调和 │ └── README.md ├── 10~Next.js └── README.link ├── .DS_Store ├── .github ├── .DS_Store ├── META │ ├── ABOUT.md │ ├── meta.yml │ └── ROADMAP.md └── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md ├── .gitattributes ├── 01~快速开始 ├── 版本特性 │ ├── React 15 │ │ └── README.md │ ├── React 16 │ │ └── README.md │ └── React 17 │ │ └── README.md ├── create-react-app.md ├── README.md └── TypeScript │ └── 99~参考资料 │ └── README.md ├── LICENSE ├── .gitignore └── README.md /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02~组件基础/03~组件样式/CSS-in-JS.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /04~工程实践/02~数据加载/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /INTRODUCTION.md: -------------------------------------------------------------------------------- 1 | # 本篇导读 2 | -------------------------------------------------------------------------------- /02~组件基础/01~组件声明/函数式组件/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02~组件基础/03~组件样式/过渡与动画/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /03~状态管理/MobX/实践调优.md: -------------------------------------------------------------------------------- 1 | # 实践调优 2 | -------------------------------------------------------------------------------- /03~状态管理/MobX/异步事件.md: -------------------------------------------------------------------------------- 1 | # 异步事件 2 | -------------------------------------------------------------------------------- /03~状态管理/MobX/数据存储.md: -------------------------------------------------------------------------------- 1 | # 数据存储 2 | -------------------------------------------------------------------------------- /04~工程实践/03~设计模式/README.md: -------------------------------------------------------------------------------- 1 | # 组件划分 2 | -------------------------------------------------------------------------------- /05~架构机制/组件系统/事务机制.md: -------------------------------------------------------------------------------- 1 | # React 中事务机制 2 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/Hooks/Hooks 扩展/AHook.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /02~组件基础/03~组件样式/过渡与动画/TransitionGroup.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /04~工程实践/01~静态类型/Flow.md: -------------------------------------------------------------------------------- 1 | # Flow 静态类型检测 2 | -------------------------------------------------------------------------------- /04~工程实践/性能优化/README.md: -------------------------------------------------------------------------------- 1 | # React 性能优化 2 | -------------------------------------------------------------------------------- /04~工程实践/类 React 库/README.md: -------------------------------------------------------------------------------- 1 | # 类 React 库 2 | -------------------------------------------------------------------------------- /03~状态管理/GraphQL/README.md: -------------------------------------------------------------------------------- 1 | # React 中集成使用 GraphQL 2 | -------------------------------------------------------------------------------- /05~架构机制/从零实现类 React 框架/Ueact/组件系统设计.md: -------------------------------------------------------------------------------- 1 | # 组件系统设计 2 | -------------------------------------------------------------------------------- /05~架构机制/基于 Stack 的调和/README.md: -------------------------------------------------------------------------------- 1 | # React Stack 调和 2 | -------------------------------------------------------------------------------- /02~组件基础/01~组件声明/事件系统/拖拽效果.md: -------------------------------------------------------------------------------- 1 | # 拖拽事件 2 | 3 | # 可拖拽的文件视图 4 | -------------------------------------------------------------------------------- /10~Next.js/README.link: -------------------------------------------------------------------------------- 1 | https://github.com/wx-chevalier/Next.js-Notes.git -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/React-Notes/HEAD/.DS_Store -------------------------------------------------------------------------------- /02~组件基础/05~Storybook/扩展.md: -------------------------------------------------------------------------------- 1 | # Storybook 扩展 2 | 3 | 本部分我们讨论 Storybook 中如何添加自定义的扩展。 4 | -------------------------------------------------------------------------------- /.github/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/React-Notes/HEAD/.github/.DS_Store -------------------------------------------------------------------------------- /02~组件基础/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/React-Notes/HEAD/02~组件基础/.DS_Store -------------------------------------------------------------------------------- /03~状态管理/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/React-Notes/HEAD/03~状态管理/.DS_Store -------------------------------------------------------------------------------- /04~工程实践/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/React-Notes/HEAD/04~工程实践/.DS_Store -------------------------------------------------------------------------------- /05~架构机制/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/React-Notes/HEAD/05~架构机制/.DS_Store -------------------------------------------------------------------------------- /02~组件基础/06~组件库/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/React-Notes/HEAD/02~组件基础/06~组件库/.DS_Store -------------------------------------------------------------------------------- /02~组件基础/06~组件库/Antd/README.md: -------------------------------------------------------------------------------- 1 | # Antd 2 | 3 | antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。 4 | -------------------------------------------------------------------------------- /03~状态管理/Hooks/README.md: -------------------------------------------------------------------------------- 1 | # 基于 Hooks 的状态管理 2 | 3 | # Links 4 | 5 | - https://link.medium.com/89xXHkRMzZ 6 | -------------------------------------------------------------------------------- /02~组件基础/06~组件库/Form/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wx-chevalier/React-Notes/HEAD/02~组件基础/06~组件库/Form/.DS_Store -------------------------------------------------------------------------------- /03~状态管理/Hooks/Bistate.md: -------------------------------------------------------------------------------- 1 | # Bistate 2 | 3 | # Links 4 | 5 | - https://mp.weixin.qq.com/s/9i2y_-00P-OTBJLomo_Qwg 6 | -------------------------------------------------------------------------------- /05~架构机制/组件系统/源码概览.md: -------------------------------------------------------------------------------- 1 | # React 源码概览 2 | 3 | # 断点调试 React 应用 4 | 5 | # 目录总览 6 | 7 | # 组件声明 8 | 9 | # DOM 渲染 10 | -------------------------------------------------------------------------------- /05~架构机制/组件系统/组件系统.md: -------------------------------------------------------------------------------- 1 | # React 中的组件体系 2 | 3 | ![](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230513202648.png) 4 | -------------------------------------------------------------------------------- /05~架构机制/组件系统/VDOM.md: -------------------------------------------------------------------------------- 1 | # VDOM 2 | 3 | # Links 4 | 5 | - https://mp.weixin.qq.com/s/lWyqHfHFAstS6AhfaHe7Iw 详解 React 16 的 Diff 策略 6 | -------------------------------------------------------------------------------- /.github/META/ABOUT.md: -------------------------------------------------------------------------------- 1 | # 关于 2 | 3 | ## 规划 4 | 5 | ## 致谢 6 | 7 | 由于笔者平日忙于工作,几乎所有线上的文档都是我夫人帮忙整理,在此特别致谢;同时也感谢我家的布丁安静的趴在脚边,不再那么粪发涂墙。 8 | -------------------------------------------------------------------------------- /02~组件基础/06~组件库/组件范式/Svg.md: -------------------------------------------------------------------------------- 1 | # React 中 Svg 的使用 2 | 3 | # Links 4 | 5 | - https://www.smooth-code.com/open-source/svgr/docs/webpack/ 6 | -------------------------------------------------------------------------------- /05~架构机制/99~参考资料/2020-《图解 React 源码》/README.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://github.com/7kms/react-illustration-series) TODO! 2 | 3 | # 图解 React 源码 4 | -------------------------------------------------------------------------------- /.github/META/meta.yml: -------------------------------------------------------------------------------- 1 | - permlink: [现代 Web 全栈开发与工程架构 https://github.com/wx-chevalier/React-Series](https://github.com/wx-chevalier/React-Series) -------------------------------------------------------------------------------- /04~工程实践/服务端渲染/React Router.md: -------------------------------------------------------------------------------- 1 | # React Router 2 | 3 | # Links 4 | 5 | - https://reacttraining.com/react-router/web/guides/server-rendering 6 | -------------------------------------------------------------------------------- /02~组件基础/06~组件库/组件范式/Formik.md: -------------------------------------------------------------------------------- 1 | # Formik 2 | 3 | # Links 4 | 5 | - https://medium.com/@a.cagarweyne/dynamic-react-form-with-formik-4aad80ad7e5 6 | -------------------------------------------------------------------------------- /04~工程实践/04~风格指南/99~参考资料/2021-React Philosophies.md: -------------------------------------------------------------------------------- 1 | # React Philosophies 2 | 3 | # Links 4 | 5 | - https://github.com/mithi/react-philosophies 6 | -------------------------------------------------------------------------------- /05~架构机制/组件系统/99~参考资料/2023-RSC From Scratch.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://github.com/reactwg/server-components/discussions/5) 2 | 3 | # RSC From Scratch 4 | -------------------------------------------------------------------------------- /05~架构机制/从零实现类 React 框架/Didact/Didact.md: -------------------------------------------------------------------------------- 1 | # Didact 2 | 3 | # Links 4 | 5 | - https://github.com/pomber/didact A DIY guide to build your own React 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.xmind filter=lfs diff=lfs merge=lfs -text 2 | *.zip filter=lfs diff=lfs merge=lfs -text 3 | *.pdf filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /05~架构机制/从零实现类 React 框架/Ueact/README.md: -------------------------------------------------------------------------------- 1 | # Ueact 2 | 3 | # Links 4 | 5 | - https://segmentfault.com/a/119000002003413 从零自己编写一个 React 框架 【中高级前端杀手锏级别技能】 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | -------------------------------------------------------------------------------- /05~架构机制/React Compiler/99~参考资料/2024~Understanding React Compiler.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://tonyalicea.dev/blog/understanding-react-compiler/) 2 | 3 | # Understanding React Compiler 4 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/Hooks/基础元语/useReducer.md: -------------------------------------------------------------------------------- 1 | # useReducer 2 | 3 | # Links 4 | 5 | - https://medium.com/@rossbulat/the-future-of-react-reducers-and-global-state-management-30cda8a3b082 6 | -------------------------------------------------------------------------------- /05~架构机制/从零实现类 React 框架/Ueact/VirtualDOM 算法详解与实现.md: -------------------------------------------------------------------------------- 1 | # VirtualDOM 算法详解与实现 2 | 3 | ![](https://cdn-images-1.medium.com/max/1600/1*ZrzXoRljG5Co5KvEsWJNjA.png) 4 | 5 | # 模型构建 6 | 7 | # 差异比较 8 | 9 | # 补丁应用 10 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/Hooks/机制解析/Hooks Scratch.md: -------------------------------------------------------------------------------- 1 | # 实现简单的 React Hooks 2 | 3 | # Links 4 | 5 | - https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/ Deep dive: How do React hooks really work? 6 | -------------------------------------------------------------------------------- /04~工程实践/02~数据加载/异步渲染.md: -------------------------------------------------------------------------------- 1 | # 异步渲染 2 | 3 | # Loadable 4 | 5 | # Links 6 | 7 | - https://medium.com/front-end-weekly/loading-components-asynchronously-in-react-app-with-an-hoc-61ca27c4fda7 8 | - https://github.com/jamiebuilds/react-loadable 9 | -------------------------------------------------------------------------------- /03~状态管理/README.md: -------------------------------------------------------------------------------- 1 | > 参考 [《Web-Engineering-Notes》](https://github.com/wx-chevalier/Web-Engineering-Notes) 中的状态管理; 2 | 3 | # 状态管理 4 | 5 | 在类组件或者函数式组件中,我们都可以利用类 setState 的方式来进行局部状态管理。本章主要讨论利用 MobX、Redux 等常用的状态管理库来优化 React 的状态管理及代码结构。 6 | 7 | # Links 8 | 9 | - https://mp.weixin.qq.com/s/q1SjrWlh2qvkK8p8Flqt0g 不要再问 React Hooks 能否取代 Redux 了 10 | -------------------------------------------------------------------------------- /03~状态管理/Zustand/README.md: -------------------------------------------------------------------------------- 1 | # Zustand 2 | 3 | Zustand 是一个非常时髦的状态管理库,也是 2021 年 Star 增长最快的 React 状态管理库。它的理念非常函数式,API 设计的很优雅,值得学习。Zustand 可以作为 NPM 上的一个包来使用: 4 | 5 | ```sh 6 | # NPM 7 | npm install zustand 8 | 9 | # Yarn 10 | yarn add zustand 11 | ``` 12 | 13 | # Links 14 | 15 | - https://cloud.tencent.com/developer/article/1956768 精读《zustand 源码》 16 | -------------------------------------------------------------------------------- /01~快速开始/版本特性/React 15/README.md: -------------------------------------------------------------------------------- 1 | # React 15 架构 2 | 3 | React15 架构可以分为两层: 4 | 5 | - Reconciler(协调器)—— 负责找出变化的组件; 6 | - Renderer(渲染器)—— 负责将变化的组件渲染到页面上; 7 | 8 | 在 React15 及以前,Reconciler 采用递归的方式创建虚拟 DOM,**递归过程是不能中断的**。如果组件树的层级很深,递归会占用线程很多时间,递归更新时间超过了 16ms,用户交互就会卡顿。 9 | 10 | 为了解决这个问题,React16 将递归的无法中断的更新重构为**异步的可中断更新**,由于曾经用于递归的虚拟 DOM 数据结构已经无法满足需要。于是,全新的 Fiber 架构应运而生。 11 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/Hooks/设计模式/优化数据流.md: -------------------------------------------------------------------------------- 1 | # Links 2 | 3 | - 换个角度思考 React Hooks https://mp.weixin.qq.com/s?__biz=MzIzOTkwMjM0OQ==&mid=2247490146&idx=1&sn=6c8cf62cb1afa37808132710e8c4df28&chksm=e9225180de55d8965cb9554b0b87d68def18e7a9f9e2c3396d9e025e96ff75a2a5f93b2ee25a&mpshare=1&scene=1&srcid=0730uaR050eM08PYN2Ud0dKS&sharer_sharetime=1627655322017&sharer_shareid=ab27ca96b5bf5b0b51edd5a0f67fd6c7#rd -------------------------------------------------------------------------------- /01~快速开始/版本特性/React 16/README.md: -------------------------------------------------------------------------------- 1 | # React 16 2 | 3 | 为了解决同步更新长时间占用线程导致页面卡顿的问题,也为了探索运行时优化的更多可能,React 开始重构并一直持续至今。重构的目标是实现 Concurrent Mode(并发模式)。 4 | 5 | 从 v15 到 v16,React 团队花了两年时间将源码架构中的 Stack Reconciler 重构为 Fiber Reconciler。 6 | 7 | React16 架构可以分为三层: 8 | 9 | - Scheduler(调度器)—— **调度任务的优先级**,高优任务优先进入 Reconciler; 10 | - Reconciler(协调器)—— 负责找出变化的组件:**更新工作从递归变成了可以中断的循环过程。Reconciler 内部采用了 Fiber 的架构**; 11 | - Renderer(渲染器)—— 负责将变化的组件渲染到页面上。 12 | -------------------------------------------------------------------------------- /01~快速开始/版本特性/React 17/README.md: -------------------------------------------------------------------------------- 1 | # React 17 2 | 3 | React16 的**expirationTimes 模型**只能区分是否`>=expirationTimes`决定节点是否更新。React17 的**lanes 模型**可以选定一个更新区间,并且动态的向区间中增减优先级,可以处理更细粒度的更新。 4 | 5 | > Lane 用**二进制位**表示任务的优先级,方便优先级的计算(位运算),不同优先级占用不同位置的“**赛道**”,而且存在批的概念,优先级越低,“赛道”越多。高优先级打断低优先级,新建的任务需要赋予什么优先级等问题都是 Lane 所要解决的问题。 6 | 7 | Concurrent Mode 的目的是实现一套可中断/恢复的更新机制。其由两部分组成: 8 | 9 | - 一套协程架构:Fiber Reconciler 10 | - 基于协程架构的启发式更新算法:控制协程架构工作方式的算法 11 | -------------------------------------------------------------------------------- /03~状态管理/Redux/Dva/README.md: -------------------------------------------------------------------------------- 1 | # Dva 2 | 3 | Dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,Dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。 4 | 5 | Dva 的官方特性如下: 6 | 7 | - 易学易用,仅有 6 个 api,对 redux 用户尤其友好,配合 umi 使用后更是降低为 0 API。 8 | 9 | - elm 概念,通过 reducers, effects 和 subscriptions 组织 model。 10 | 11 | - 插件机制,比如 dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading。 12 | 13 | - 支持 HMR,基于 babel-plugin-dva-hmr 实现 components、routes 和 models 的 HMR。 14 | -------------------------------------------------------------------------------- /01~快速开始/create-react-app.md: -------------------------------------------------------------------------------- 1 | # 基于 create-react-app 的快速开发与应用调试 2 | 3 | React,或者说 React+Redux+Webpack 形成的一套完整的开发体系与项目结构是笔者目前的选择,但是正如很多人吐槽的,整个 React 社区对初学者实在太不友好了。就好像之前对于 Java 的吐槽,Python 中 HelloWord 就一行,但是 Java 里你要专门去建个类。有时候太多的选择反而只会带来痛苦。所以在这里笔者想先说一句,使用你能够学会的,做到你能力范围内最好的即可,不然只会带来无尽的半成品。 4 | 5 | # Utils 6 | 7 | ## [React Devtools](https://github.com/facebook/react-devtools) 8 | 9 | React Devtools 是 React 官方提供的类似于浏览器调试台的插件,可以允许以查看组件的层次、各个组件的 Props、States 等等信息。使用方式也很简单,直接在 Firefox 或者 Chrome 的加载项仓库中搜索下载即可。 10 | -------------------------------------------------------------------------------- /03~状态管理/MobX/响应式界面.md: -------------------------------------------------------------------------------- 1 | # 响应式界面 2 | 3 | ```js 4 | import { observer } from "mobx-react"; 5 | import { now } from "mobx-utils"; 6 | 7 | const NEW_YEAR = new Date(2018, 0, 1); 8 | const SECOND = 1000; 9 | const MINUTE = 60 * SECOND; 10 | const HOUR = 60 * MINUTE; 11 | 12 | const CountDown = observer(() => { 13 | const timeLeft = NEW_YEAR - now(); 14 | return ( 15 |

16 | 距离新年还有   17 | {Math.floor(timeLeft / HOUR)}:{Math.floor((timeLeft % HOUR) / MINUTE)}:{Math.floor( 18 | (timeLeft % MINUTE) / SECOND 19 | )}! 20 |

21 | ); 22 | }); 23 | 24 | React.render(, document.body); 25 | ``` 26 | -------------------------------------------------------------------------------- /02~组件基础/05~Storybook/用例.md: -------------------------------------------------------------------------------- 1 | # React Story 2 | 3 | 基础的 Story 的格式如下: 4 | 5 | ```js 6 | // file: src/stories/index.js 7 | 8 | import React from "react"; 9 | import { storiesOf } from "@storybook/react"; 10 | import { action } from "@storybook/addon-actions"; 11 | import Button from "../components/Button"; 12 | 13 | storiesOf("Button", module) 14 | .add("with text", () => ( 15 | 16 | )) 17 | .add("with some emoji", () => ( 18 | 23 | )); 24 | ``` 25 | -------------------------------------------------------------------------------- /02~组件基础/01~组件声明/函数式组件/React.memo.md: -------------------------------------------------------------------------------- 1 | # React.memo 2 | 3 | ```js 4 | import React from "react"; 5 | 6 | // Generates random colours any time it's called 7 | const randomColour = () => "#" + ((Math.random() * 0xffffff) << 0).toString(16); 8 | 9 | // The type of the props 10 | type ButtonProps = React.ButtonHTMLAttributes; 11 | 12 | // A memoized button with a random background colour 13 | const Button = React.memo((props: ButtonProps) => ( 14 | 17 | )); 18 | ``` 19 | 20 | # Links 21 | 22 | - https://dmitripavlutin.com/use-react-memo-wisely/ 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /04~工程实践/02~数据加载/React Query/高级查询.md: -------------------------------------------------------------------------------- 1 | # 高级查询 2 | 3 | # 依赖处理 4 | 5 | 依赖性(或序列性)查询在执行前要依赖先前的查询完成。要做到这一点,就像使用启用选项来告诉查询何时可以运行一样简单。 6 | 7 | ```js 8 | // Get the user 9 | const { data: user } = useQuery(["user", email], getUserByEmail); 10 | 11 | const userId = user?.id; 12 | 13 | // Then get the user's projects 14 | const { isIdle, data: projects } = useQuery( 15 | ["projects", userId], 16 | getProjectsByUser, 17 | { 18 | // The query will not execute until the userId exists 19 | enabled: !!userId, 20 | } 21 | ); 22 | 23 | // isIdle will be `true` until `enabled` is true and the query begins to fetch. 24 | // It will then go to the `isLoading` stage and hopefully the `isSuccess` stage :) 25 | ``` 26 | -------------------------------------------------------------------------------- /04~工程实践/性能优化/性能评测与分析.md: -------------------------------------------------------------------------------- 1 | # 性能评测与分析 2 | 3 | Browser 4 | 5 | - DOM elements and mutations 6 | - Repaints and reflows 7 | - Garbage collection 8 | 9 | React 10 | 11 | - Unnecessary renders 12 | - Development build of React 13 | 14 | ```js 15 | componentDidUpdate(prevProps, prevState) { 16 | Object.entries(this.props || {}).forEach( 17 | ([key, val]) => 18 | prevProps[key] !== val && console.log(`Prop '${key}' changed`) 19 | ); 20 | Object.entries(this.state || {}).forEach( 21 | ([key, val]) => 22 | prevState[key] !== val && console.log(`State '${key}' changed`) 23 | ); 24 | } 25 | ``` 26 | 27 | 在类组件中,我们可以通过如上代码来判断某个 Props 或者 State 是否发生变化。 28 | 29 | ``` 30 | __REACT_DEVTOOLS_GLOBAL_HOOK__.on('update', (e) => { console.log('Updated', e) }) 31 | ``` 32 | -------------------------------------------------------------------------------- /03~状态管理/Redux/99~参考资料/README.md: -------------------------------------------------------------------------------- 1 | # React Redux 参考资料 2 | 3 | - [2017~Quick Redux tips for connecting your React components](https://medium.com/dailyjs/quick-redux-tips-for-connecting-your-react-components-e08da72f5b3) 4 | 5 | - [2017~From Zero to Redux in 3 Minutes A simplified guide to using Redux in a React application.](https://medium.com/@christiannaths/from-zero-to-redux-8db779b6ed01#.1j80ztr5q) 6 | 7 | - [2017~React Redux Cheat Sheet on Workflow & Concept](https://github.com/uanders/react-redux-cheatsheet): This article contains a graphical cheat sheet for the workflow and concept of Redux. 8 | 9 | - [React and Redux Sagas Authentication App Tutorial](http://start.jcolemorrison.com/react-and-redux-sagas-authentication-app-tutorial/) 10 | 11 | - [中文的 React+Redux 系列教程](https://github.com/lewis617/react-redux-tutorial) 12 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/README.md: -------------------------------------------------------------------------------- 1 | # 组件数据流 2 | 3 | 组件的主要职责是将原始数据转化为 HTML 中的富文本格式,而 Props 与 State 协作完成这件事,换言之,Props 与 State 的并集即是全部的原始数据。Props 与 State 之间也是有很多交集的,譬如: 4 | 5 | - Props 与 State 都是 JS 对象。 6 | - Props 与 State 的值的改变都会触发界面的重新渲染。 7 | - Props 与 State 都是确定性的,即在确定的 Props 或者 State 的值的情况下都会得出相同的界面。 8 | 9 | 不过 Props 顾名思义,更多的是作为 Component 的配置项存在。Props 往往是由父元素指定并且传递给自己的子元素,不过自身往往不会去改变 Props 的值。另一方面,State 在组件被挂载时才会被赋予一个默认值,而常常在与用户的交互中发生更改。往往一个组件独立地维护它的整个状态机,可以认为 State 是一个私有属性。他们的对比如下: 10 | 11 | | 描述              | Props  | State  | 12 | | ---------------------------- | ------ | ------ | 13 | | 是否可以从父元素获取初始值   | Yes    | Yes    | 14 | | 是否可以被父元素改变      | Yes    | No     | 15 | | 是否可以设置默认值       | Yes    | Yes    | 16 | | 是否可以在组件内改变      | No     | Yes    | 17 | | 是否可以设置为子元素的初始值 | Yes    | Yes    | 18 | | 是否可以在子元素中改变     | Yes    | No     | 19 | -------------------------------------------------------------------------------- /02~组件基础/01~组件声明/函数式组件/函数式 React 开发.md: -------------------------------------------------------------------------------- 1 | # 函数式 React 开发 2 | 3 | # 函数式 setState 4 | 5 | 参考 React 官方文档中的描述,`setState` 并不是立刻改变 `this.state` 的值,而是创建挂起的状态事务;如果直接在 `setState` 之后访问状态对象只会获得之前的值。譬如下述的代码就会存在某些错误或者预判差异: 6 | 7 | ``` 8 | updateState({target}) { 9 |  this.setState({user: {...this.state.user, [target.name]: target.value}}); 10 |  doSomething(this.state.user) // Uh oh, setState merely schedules a state change, so this.state.user may still have old value 11 | } 12 | ``` 13 | 14 | 如果我们希望去在某个状态实际更新完毕之后,执行某些操作,那么可以以如下方式使用自定义的新状态: 15 | 16 | ```js 17 | updateState({target}) { 18 |  this.setState(prevState => { 19 |  const updatedUser = {...prevState.user, [target.name]: target.value}; // use previous value in state to build new state... 20 |  doSomething(updatedUser); // Now I can safely utilize the new state I've created to call other funcs... 21 |  return { user: updatedUser }; // And what I return here will be set as the new state 22 |  }); 23 |  } 24 | ``` 25 | 26 | # 高阶函数 27 | -------------------------------------------------------------------------------- /05~架构机制/组件系统/setState.md: -------------------------------------------------------------------------------- 1 | # React setState 2 | 3 | 在代码中调用 `setState` 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。 4 | 5 | # setState 调用流程 6 | 7 | # 事务 8 | 9 | 事务(Transaction)源于数据库理论,是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。React 中将多个操作封装到某个事务中,将操作与执行相剥离,下面的伪代码简述事务的基本流程: 10 | 11 | ```js 12 | // 代码只是为了理解 transaction 和 react 无关 13 | transactionManager = new TransactionManager(); 14 | transactionManager.add( 15 | new Transaction(function () { 16 | // 修改数据的操作1 17 | }) 18 | ); 19 | transactionManager.add( 20 | new Transaction(function () { 21 | // 修改数据的操作2 22 | }) 23 | ); 24 | 25 | transactionManager.perform(); 26 | ``` 27 | 28 | React 和许多现代前端框架都借鉴了这一设计模式,将操作和执行分离。这样的好处是可以做到极大的性能优化,举个例子,我们知道 DOM 操作是极其耗时的,为了优化性能,我们可以将 DOM 操作合并一起执行。React 的 DOM 更新也是合并执行的,这得益于事务设计。 29 | 30 | # 批次更新 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /02~组件基础/01~组件声明/事件系统/界面事件.md: -------------------------------------------------------------------------------- 1 | # 事件应用实践 2 | 3 | # 表单事件与输入校验 4 | 5 | # 鼠标事件与悬浮反馈 6 | 7 | # 触摸事件与 onTouchTap 8 | 9 | # 浏览器监听 10 | 11 | ## 缩放监听 12 | 13 | ```js 14 | class WindowWidth extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { width: 0 }; 18 | } 19 | 20 | componentDidMount() { 21 | this.setState({ width: window.innerWidth }); 22 | window.addEventListener("resize", ({ target }) => { 23 | this.setState({ width: target.innerWidth }); 24 | }); 25 | } 26 | 27 | render() { 28 | const { width } = this.state; 29 | const { Width } = this.props; 30 | return ; 31 | } 32 | } 33 | 34 | ; 35 | 36 | const DisplayDevice = ({ width }) => { 37 | let device = null; 38 | if (width <= 480) { 39 | device = "mobile"; 40 | } else if (width <= 768) { 41 | device = "tablet"; 42 | } else { 43 | device = "desktop"; 44 | } 45 | return
you are using a {device}
; 46 | }; 47 | ``` 48 | -------------------------------------------------------------------------------- /02~组件基础/06~组件库/Antd/表单.md: -------------------------------------------------------------------------------- 1 | # Antd 中表单的使用 2 | 3 | # 表单数据 4 | 5 | ## 数据存储外化 6 | 7 | 通过使用 onFieldsChange 与 mapPropsToFields,可以把表单的数据存储到上层组件或者 Redux、Dva 中。注意,mapPropsToFields 里面返回的表单域数据必须使用 Form.createFormField 包装。 8 | 9 | ```tsx 10 | const CustomizedForm = Form.create({ 11 | name: "global_state", 12 | onFieldsChange(props, changedFields) { 13 | props.onChange(changedFields); 14 | }, 15 | mapPropsToFields(props) { 16 | return { 17 | username: Form.createFormField({ 18 | ...props.username, 19 | value: props.username.value 20 | }) 21 | }; 22 | }, 23 | onValuesChange(_, values) { 24 | console.log(values); 25 | } 26 | })(props => { 27 | const { getFieldDecorator } = props.form; 28 | return ( 29 |
30 | 31 | {getFieldDecorator("username", { 32 | rules: [{ required: true, message: "Username is required!" }] 33 | })()} 34 | 35 |
36 | ); 37 | }); 38 | ``` 39 | 40 | # 表单校验 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 王下邀月熊 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/META/ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Web Series Roadmap 2 | 3 | # MileStone V1 4 | 5 | ## 导论 6 | 7 | - [ ] 数据流驱动的界面:[from-imperative-to-functional-javascript](https://codeburst.io/from-imperative-to-functional-javascript-5dc9e16d9184), [imperative-vs-declarative-programming](https://tylermcginnis.com/imperative-vs-declarative-programming/) 8 | 9 | ## 工程实践 10 | 11 | - [ ] 重新整理当前目录中的文档,重新划分文档结构与内容。 12 | 13 | - [ ] 调试 14 | 15 | - [ ] JavaScript 代码调试 https://parg.co/Upj 这篇文章中的性能调试的图片合并到 Chrome DevTools 文档中 https://parg.co/Uvo 16 | 17 | - [ ] Web 性能优化 18 | 19 | - [ ] 整理 http://harttle.land/2017/04/04/using-http-cache.html 这篇文章中的 HTTP 缓存相关内容 20 | - [ ] 完成基于 IntersectionObserver 的图片加载优化一文 21 | 22 | ## React 23 | 24 | - [ ] React 数据流 25 | 26 | - [ ] React Context https://parg.co/UXl https://github.com/thejameskyle/unstated https://parg.co/UX4 https://parg.co/YG6 27 | 28 | - [ ] React 性能优化 29 | 30 | - [ ] 优先完成 React 应用性能优化:render 函数优化 31 | 32 | - [ ] 异步 React 33 | 34 | - [ ] React Suspense https://parg.co/UWE https://parg.co/UWr https://codesandbox.io/s/github/jaredpalmer/react-suspense-playground https://www.zhihu.com/question/268028123 https://parg.co/Ugq 35 | -------------------------------------------------------------------------------- /02~组件基础/05~Storybook/README.md: -------------------------------------------------------------------------------- 1 | # Storybook 2 | 3 | Storybook 是用户界面开发环境和 UI 组件的游乐场。该工具使开发人员能够独立创建组件,并在隔离的开发环境中以交互方式展示组件。Storybook 在主应用程序之外运行,因此用户可以独立开发 UI 组件,而无需担心应用程序特定的依赖关系和要求。 4 | 5 | ![Storybook 基础示例](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230516214532.png) 6 | 7 | Storybook 还支持很多插件,并附带灵活的 API,可根据需要自定义 Storybook。还可以构建静态版本的 Storybook 并将其部署到 HTTP 服务器。 8 | 9 | # 环境配置 10 | 11 | 我们可以通过如下方式自动地安装 Storybook 环境: 12 | 13 | ```sh 14 | $ npx -p @storybook/cli sb init --type react 15 | ``` 16 | 17 | 或者也可以进行手动安装: 18 | 19 | ```sh 20 | $ npm install @storybook/react --save-dev 21 | $ npm install react react-dom --save 22 | $ npm install babel-loader @babel/core --save-dev 23 | ``` 24 | 25 | 然后在 package.json 中添加运行脚本: 26 | 27 | ```json 28 | { 29 | "scripts": { 30 | "storybook": "start-storybook" 31 | } 32 | } 33 | ``` 34 | 35 | 接下来在 `.storybook/config.js` 中添加配置文件: 36 | 37 | ```js 38 | import { configure } from "@storybook/react"; 39 | 40 | function loadStories() { 41 | require("../stories/index.js"); 42 | // You can require as many stories as you need. 43 | } 44 | 45 | configure(loadStories, module); 46 | ``` 47 | 48 | 然后就可以开始编写我们自己的测试用例啦。 49 | -------------------------------------------------------------------------------- /01~快速开始/README.md: -------------------------------------------------------------------------------- 1 | # 导论 2 | 3 | 基本的 React 的页面形式如下所示: 4 | 5 | ```html 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 17 | 18 | 19 | ``` 20 | 21 | React 独创了一种 JS、CSS 和 HTML 混写的 JSX 格式,可以通过在页面中引入 JSXTransformer 这个文件进行客户端的编译,不过还是推荐在 服务端编译。 22 | 23 | ```js 24 | const HelloMessage = React.createClass({ 25 | render: function() { 26 | return
Hello {this.props.name}
; 27 | } 28 | }); 29 | React.render( 30 | , 31 | document.getElementById("container") 32 | ); 33 | ``` 34 | 35 | React.render 是 React 的最基本方法,用于将模板转为 HTML 语言,并插入指定的 DOM 节点。要注意的是,React 的渲染函数并不是简单地把 HTML 元素复制到页面上,而是维护了一张 Virtual Dom 映射表。 36 | 37 | ```js 38 | class ExampleComponent extends React.Component { 39 | constructor() { 40 | super(); 41 | this._handleClick = this._handleClick.bind(this); 42 | this.state = Store.getState(); 43 | } // ... 44 | } 45 | ``` 46 | 47 | 在对于 React 的基本语法有了了解之后,下面我们会开始进行快速地环境搭建与实验。 48 | -------------------------------------------------------------------------------- /03~状态管理/Zustand/03.状态持久化.md: -------------------------------------------------------------------------------- 1 | # 状态持久化 2 | 3 | # 与 URL hash 同步 4 | 5 | ```ts 6 | import { create } from "zustand"; 7 | import { persist, StateStorage, createJSONStorage } from "zustand/middleware"; 8 | 9 | const hashStorage: StateStorage = { 10 | getItem: (key): string => { 11 | const searchParams = new URLSearchParams(location.hash.slice(1)); 12 | const storedValue = searchParams.get(key) ?? ""; 13 | return storedValue; 14 | }, 15 | setItem: (key, newValue): void => { 16 | const searchParams = new URLSearchParams(location.hash.slice(1)); 17 | searchParams.set(key, JSON.stringify(newValue)); 18 | location.hash = searchParams.toString(); 19 | }, 20 | removeItem: (key): void => { 21 | const searchParams = new URLSearchParams(location.hash.slice(1)); 22 | searchParams.delete(key); 23 | location.hash = searchParams.toString(); 24 | }, 25 | }; 26 | 27 | export const useBoundStore = create( 28 | persist( 29 | (set, get) => ({ 30 | fishes: 0, 31 | addAFish: () => set({ fishes: get().fishes + 1 }), 32 | }), 33 | { 34 | name: "food-storage", // unique name 35 | storage: createJSONStorage(() => hashStorage), 36 | } 37 | ) 38 | ); 39 | ``` 40 | -------------------------------------------------------------------------------- /04~工程实践/组件测试/README.md: -------------------------------------------------------------------------------- 1 | # React 组件测试 2 | 3 | # Shallow rendering 4 | 5 | 浅渲染指的是将一个组件渲染成虚拟 DOM 对象,但是只渲染第一层,不渲染所有子组件。所以即使你对子组件做了一下改动却不会影响浅渲染的输出结果。或者是引入的子组件中发生了错误,也不会对父组件的浅渲染结果产生影响。浅渲染是不依赖 DOM 环境的。 6 | 7 | 譬如: 8 | 9 | ```js 10 | const ButtonWithIcon = ({ icon, children }) => ( 11 | 15 | ); 16 | ``` 17 | 18 | 在 React 中将会被渲染成如下: 19 | 20 | ```js 21 | 25 | ``` 26 | 27 | 但是在浅渲染中只会被渲染成如下结果: 28 | 29 | ```js 30 | 34 | ``` 35 | 36 | 需要注意的是 Icon 组件并未被渲染出来。 37 | 38 | # 快照测试 39 | 40 | Jest 快照就像那些带有由文本字符组合而成表达窗口和按钮的静态 UI:它是存储在文本文件中的组件的渲染输出。我们可以告诉 Jest 哪些组件输出的 UI 不会有意外的改变,那么 Jest 在运行时会将其保存到如下所示的文件中: 41 | 42 | ```js 43 | exports[`test should render a label 1`] = ` 44 | 48 | `; 49 | 50 | exports[`test should render a small label 1`] = ` 51 | 55 | `; 56 | ``` 57 | 58 | 每次更改组件时,Jest 都会与当前测试的值进行比较并显示差异,并且会在你做出修改是要求你更新快照。除了测试之外,Jest 将快照存储在类似 snapshots/Label.spec.js.snap 这样的文件中,同时你需要提交这些文件。 59 | -------------------------------------------------------------------------------- /01~快速开始/TypeScript/99~参考资料/README.md: -------------------------------------------------------------------------------- 1 | # 参考资料 2 | 3 | - [2017~React Higher Order Components in TypeScript made simple](https://parg.co/mWg): When refactoring a Higher-Order Component (HOC) in a TypeScript project at work, there was some confusion regarding how to write them properly. 4 | 5 | - [2017~React Typescript by sample](https://github.com/Lemoncode/react-typescript-samples): The goal of this project is to provide a set of simple samples, providing and step by step guide to start working with React and Typescript. 6 | 7 | - [2018~TypeScript and React using create-react-app](https://parg.co/U15): A step-by-step guide to setting up your first app. 8 | 9 | - [2018~Ultimate React Component Patterns with Typescript 2.8](https://parg.co/UDE): Stateful, Stateless, Default Props, Render Callbacks, Component Injection, Generic Components, High Order Components, Controlled Components. 10 | 11 | - [2018~React & Redux in TypeScript - Static Typing Guide](https://github.com/piotrwitek/react-redux-typescript-guide): The complete guide to static typing in "React & Redux" apps using TypeScript 12 | 13 | - [2018~TypeScript and React #Series#](https://fettblog.eu/typescript-react/): With that, TypeScript and React are a perfect fit. You will enjoy combining both technologies together to get huge productivity boost when writing your applications! 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all 2 | * 3 | 4 | # Unignore all with extensions 5 | !*.* 6 | 7 | # Unignore all dirs 8 | !*/ 9 | 10 | .DS_Store 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | 71 | # next.js build output 72 | .next 73 | -------------------------------------------------------------------------------- /02~组件基础/06~组件库/组件范式/列表组件.md: -------------------------------------------------------------------------------- 1 | # 列表组件 2 | 3 | # 列表渲染 4 | 5 | # Keys 6 | 7 | ## Keyed Fragment 8 | 9 | 在大部分情况下,我们可以使用`key`来标识列表中的元素,不过如果我们希望将多个子列表的顺序互换,就无法直接为某个列表添加`key`属性了,譬如下面这个函数式组件中: 10 | 11 | ``` 12 | function Swapper(props) { 13 |   let children; 14 |   if (props.swapped) { 15 |   children = [props.rightChildren, props.leftChildren]; 16 |   } else { 17 |   children = [props.leftChildren, props.rightChildren]; 18 |   } 19 |   return
{children}
; 20 | } 21 | ``` 22 | 23 | 当我们修改该组件的`swapped`属性时,两个子列表中的元素都会先被卸载再被挂载,而理想环境下我们应该只是将这两个子列表交换下顺序就好了。简单粗暴的方式是我们为两个子列表各包裹一层`div`,然后为这两个`div`添加`key`属性,不过 React 提供了 [Keyed Fragments](https://facebook.github.io/react/docs/create-fragment.html) 特性: 24 | 25 | ``` 26 | import createFragment from 'react-addons-create-fragment' 27 | 28 | 29 | function Swapper(props) { 30 |   let children; 31 |   if (props.swapped) { 32 |   children = createFragment({ 33 |   right: props.rightChildren, 34 |   left: props.leftChildren 35 |   }); 36 |   } else { 37 |   children = createFragment({ 38 |   left: props.leftChildren, 39 |   right: props.rightChildren 40 |   }); 41 |   } 42 |   return
{children}
; 43 | 44 | } 45 | ``` 46 | 47 | 在`createFragment`函数中传入的对象中的键会自动作为两个子列表的键使用,即 React 在进行渲染时会先判断是否有相同键的元素存在,如果存在则直接更改顺序,而不会经过卸载、挂载这相对繁杂的步骤。而`createFragment`函数的返回值则可以当做正常的对象进行使用,我们可以使用`React.Children`系列函数来对其进行操作。 48 | 49 | # React Canvas 50 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/Hooks/README.md: -------------------------------------------------------------------------------- 1 | # React Hooks 2 | 3 | React Hooks 是 React 16.8 引入的新特性,允许我们在不使用 Class 的前提下使用 state 和其他特性。React Hooks 要解决的问题是状态共享,是继 render-props 和 higher-order components 之后的第三种状态逻辑复用方案,不会产生 JSX 嵌套地狱问题。 4 | 5 | Hooks 的优势在于: 6 | 7 | - 多个状态不会产生嵌套,写法还是平铺的 8 | - 允许函数组件使用 state 和部分生命周期 9 | - 更容易将组件的 UI 与状态分离 10 | 11 | ```ts 12 | function useWindowWidth() { 13 | const [width, setWidth] = React.useState(window.innerWidth); 14 | 15 | useEffect(() => { 16 | const onResize = () => { 17 | setWidth(window.innerWidth); 18 | }; 19 | 20 | window.addEventListener("resize", onResize); 21 | 22 | return () => { 23 | window.removeEventListener("resize", onResize); 24 | }; 25 | }, [width]); 26 | 27 | return width; 28 | } 29 | ``` 30 | 31 | 不过 Hooks 也并非全无代价,函数式组件本身会导致大量的临时函数被创建。 32 | 33 | # Links 34 | 35 | - https://reactjs.org/docs/hooks-faq.html 36 | - https://reactjs.org/blog/2019/02/06/react-v16.8.0.html 37 | - https://fettblog.eu/typescript-react/hooks/ 38 | - https://mp.weixin.qq.com/s/P5XZO5j494rGczXqKIza5w 39 | - https://mp.weixin.qq.com/s/LrwPFDK2YCjcxAfw79N9Tg 40 | - https://mp.weixin.qq.com/s/968ukIjEhhEOeLD5SQoKaw 41 | - https://www.zhihu.com/question/338443007/answer/773530095 42 | - https://blog.csdn.net/qq_41384351/article/details/90048454 43 | - https://mp.weixin.qq.com/s/YEs5nH4aOAxOPYuW8oVlBA 44 | - https://segmentfault.com/a/1190000020120456 45 | - https://itnext.io/optimizing-react-code-with-hooks-3eaaf5978351 46 | -------------------------------------------------------------------------------- /05~架构机制/基于 Fiber 的调和/README.md: -------------------------------------------------------------------------------- 1 | # Fiber 2 | 3 | 将整个更新划分为多个原子性的任务,这就保证了原本完整的组件的更新流程可以被中断与恢复。在浏览器的空闲期执行这些任务并且区别高优先级与低优先级的任务 4 | 5 | ``` 6 | In React 15, if A is replaced by B, we unmount A, and then create and mount B: 7 | 8 | A.componentWillUnmount 9 | B.constructor 10 | B.componentWillMount 11 | B.componentDidMount 12 | 13 | In Fiber, we create B first, and only later unmount A and mount B: 14 | 15 | B.constructor 16 | B.componentWillMount 17 | A.componentWillUnmount 18 | B.componentDidMount 19 | 20 | Cooperative Scheduling -> Stack Reconciler -> Work-in-Progress Tree 21 | ``` 22 | 23 | 将当前界面树上的指针指向 Work-in-Progress 树中的对应节点,从而通过简单的键值复制来实现对象复用;这种技术也就是所谓的 Double Buffering,其能够在内存分配与垃圾回收等多个方面进行性能优化。 24 | 25 | - Synchronous 26 | - Task 27 | - Animation 28 | 29 | * High Priority: 动画与用户交互 30 | * Low Priority: 网络请求 31 | * OffScreen: 任何的隐藏对象 32 | 33 | 在 Fiber 的设计中,另一个需要考虑的情形就是所谓的饥饿(Starvation),如果我们持续性地存在大量的高优先级的更新请求,那么是否低优先级的更新请求就一直无法执行? 34 | 35 | React 另一个存在的问题就是初次渲染缓慢,目前 React 在渲染之前需要抓取到完整的代码文件(不考虑异步加载的情形),而利用 Streaming Rendering 技术,React 允许 36 | 37 | # Links 38 | 39 | - https://medium.com/@chang_yan/get-started-with-react-fiber-ea30e597aad0 40 | - https://medium.com/react-in-depth/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react-e1c04700ef6e 41 | - https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7 42 | - https://medium.com/edge-coders/react-16-features-and-fiber-explanation-e779544bb1b7 43 | -------------------------------------------------------------------------------- /04~工程实践/服务端渲染/README.md: -------------------------------------------------------------------------------- 1 | # 服务端渲染 2 | 3 | 服务端渲染(Server Side Rendering)顾名思义即是在服务端将载入数据的组件渲染为 HTML 然后返回给客户端以直接显示,我们也可以通过类似于插入`window.APP_DATA = data`的方式将数据传入到 HTML 文档中。总体而言,服务端渲染可能会造成一定的带宽浪费,不过其能够优化用户的首屏加载时间与搜索引擎解析。在正式介绍服务端渲染之前我们还需要明晰下前后端分离与服务端渲染之间概念的差异。早期的网页是数据、模板与样式的混合,即以经典的 APS.NET、PHP 与 JSP 为例,是由服务端的模板提供一系列的标签完成从业务逻辑代码到页面的流动。彼时,前端只是用来展示数据,而随着 Ajax 技术的流行,将 WebAPP 也视作 CS 架构;抽象来说,会认为 CS 是客户端与服务器之间的双向通信,而 BS 是客户端与服务端之间的单向通信。从初始打开这个网页到最终关闭,网页本身也有了一套自己的状态,而拥有这种变化的状态的基础就是 AJAX,即从单向通信变成了双向通信。在这个衍化的过程中我们发现前后端未分离的一个重要特性就是页面是在服务端生成的,而服务端渲染虽然也是提倡在服务端即生成界面,但是其整体的技术栈却已是围绕富客户端框架的一系列新兴技术。我们现在称之为服务端渲染的技术并非传统的以 JSP、PHP 为代表的服务端模板数据填充,更准确的服务端渲染作用的描述是对于客户端应用的预启动与预加载。引入服务端渲染带来的优势主要在于以下三个方面: 4 | 5 | -  对浏览器兼容性的提升,目前 React、Angular、Vue 等现代 Web 框架纷纷放弃了对于旧版本浏览器的支持,引入服务端渲染之后至少对于使用旧版本浏览器的用户能够提供更加友好的首屏展示,虽然后续功能依然不能使用。 6 | 7 | -  对搜索引擎更加友好,客户端渲染意味着整体的渲染用脚本完成,这一点对于爬虫并不友好。虽然现代爬虫往往也会通过内置自动化浏览器等方式支持脚本执行,但是这样无形会加重很多爬虫服务器的负载,因此 Google 这样的大型搜索引擎在进行网页索引的时候还是依赖于文档本身。如果你希望提升在搜索引擎上的排行,让你的网站更方便地被搜索到,那么支持服务端渲染是个不错的选择。 8 | 9 | -  整体加载速度与用户体验优化,在首屏渲染的时候,服务端渲染的性能是远快于客户端渲染的。不过在后续的页面响应更新与子视图渲染时,受限于网络带宽与重渲染的范畴,服务端渲染是会弱于客户端渲染。另外在服务端渲染的同时,我们也会在服务端抓取部分应用数据附加到文档中,在目前 HTTP/1.1 仍为主流的情况下可以减少客户端的请求连接数与时延,让用户更快地接触到所需要的应用数据。 10 | 11 | 总结而言,服务端渲染与客户端渲染是相辅相成的,在 React 等框架的协助下我们也可以很方便地为开发阶段的纯客户端渲染应用添加服务端渲染支持。 12 | 13 | # Links 14 | 15 | - https://zhuanlan.zhihu.com/p/76967335 16 | - 提取最后的对比表格 https://dev.to/ahoy/messages/pmrF7ZcvQR0aCGcy81L4O1BBSb6kLSiy/click?signature=ea4086a4f3854a99f89606fe5739a276708af9b7&url=https%3A%2F%2Fdev.to%2Fkefranabg%2Fdemystifying-ssr-csr-universal-and-static-rendering-with-animations-m7d%3Futm_source%3Ddigest_mailer%26utm_medium%3Demail%26utm_campaign%3Ddigest_email 17 | -------------------------------------------------------------------------------- /04~工程实践/类 React 库/Inferno.md: -------------------------------------------------------------------------------- 1 | # Inferno 2 | 3 | Inferno 是一个快速,类似于 React 的库,用于在客户端和服务器上构建高性能的用户界面。Inferno 故意在组件方面保留与 React 相同的设计思想:单向数据流和关注点分离。在这些示例中,通过 Inferno JSX Babel 插件使用 JSX,以提供表达 Inferno 虚拟 DOM 的简单方法。您不需要使用 JSX,它是完全可选的,您可以使用 hyperscript 或 createElement(就像 React 一样)。请记住,编译时间优化仅适用于 JSX。 4 | 5 | ```js 6 | import { render } from "inferno"; 7 | 8 | const message = "Hello world"; 9 | 10 | render(, document.getElementById("app")); 11 | ``` 12 | 13 | Inferno 也支持类似 React 的类组件: 14 | 15 | ```js 16 | import { render, Component } from "inferno"; 17 | 18 | class MyComponent extends Component { 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | counter: 0, 23 | }; 24 | } 25 | render() { 26 | return ( 27 |
28 |

Header!

29 | Counter is at: {this.state.counter} 30 |
31 | ); 32 | } 33 | } 34 | 35 | render(, document.getElementById("app")); 36 | ``` 37 | 38 | 由于性能是该库的重要方面,因此我们想向您展示如何进一步优化应用程序。在下面的示例中,我们通过使用 `JSX$HasVNodeChildren` 预定义子形状的编译时间来优化差异过程。然后,我们使用 Inferno.createTextVNode 创建文本 vNode。 39 | 40 | ```js 41 | import { createTextVNode, render, Component } from "inferno"; 42 | 43 | class MyComponent extends Component { 44 | constructor(props) { 45 | super(props); 46 | this.state = { 47 | counter: 0, 48 | }; 49 | } 50 | render() { 51 | return ( 52 |
53 |

Header!

54 | 55 | {createTextVNode("Counter is at: " + this.state.counter)} 56 | 57 |
58 | ); 59 | } 60 | } 61 | 62 | render(, document.getElementById("app")); 63 | ``` 64 | -------------------------------------------------------------------------------- /04~工程实践/01~静态类型/TypeScript.md: -------------------------------------------------------------------------------- 1 | # React & TypeScript 2 | 3 | 如果对 TypeScript 尚不了解的同学可以参考 [TypeScript CheatSheet]()。 4 | 5 | # 组件基础 6 | 7 | React 的 TypeScript 类型声明可以参考 [types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react),[antd](https://github.com/ant-design/ant-design) 也是非常不错的使用 TypeScript 开发的大型 React 项目。 8 | 9 | ```ts 10 | import * as React from "react"; 11 | import formatPrice from "../utils/formatPrice"; 12 | 13 | export interface IPriceProps { 14 | num: number; 15 | symbol: "$" | "€" | "£"; 16 | } 17 | 18 | const Price: React.SFC = ({ num, symbol }: IPriceProps) => ( 19 |
20 |

{formatPrice(num, symbol)}

21 |
22 | ); 23 | ``` 24 | 25 | ```ts 26 | export function positionStyle( 27 | Component: React.ComponentType 28 | ): React.StatelessComponent { 29 | return (props: any) => { 30 | const { top, left, ...rest } = props; 31 | return ( 32 |
33 | 34 |
35 | ); 36 | }; 37 | } 38 | ``` 39 | 40 | ## 事件处理 41 | 42 | # 设计模式 43 | 44 | ## 高阶组件 45 | 46 | 譬如在 [types/react-redux](https://parg.co/o47) 中,connect 函数的类型声明可以 interface 来声明多个重载: 47 | 48 | ```ts 49 | export interface Connect { 50 | ... 51 | ( 52 | mapStateToProps: MapStateToPropsParam, 53 | mapDispatchToProps: MapDispatchToPropsParam 54 | ): InferableComponentEnhancerWithProps; 55 | ... 56 | } 57 | 58 | export declare const connect: Connect; 59 | ``` 60 | 61 | # 状态管理 62 | 63 | # Links 64 | 65 | - https://medium.com/@rossbulat/how-to-use-typescript-with-react-and-redux-a118b1e02b76 66 | -------------------------------------------------------------------------------- /02~组件基础/03~组件样式/样式定义与引入.md: -------------------------------------------------------------------------------- 1 | # 样式定义与引入 2 | 3 | # CSS 样式 4 | 5 | # 引入 CSS 文件 6 | 7 | # 动态样式类名 8 | 9 | # SCSS 10 | 11 | # Style 12 | 13 | ## Inline Style:行内样式 14 | 15 | 在 React 中,如果要使用行内元素,不可以直接使用 style="”这种方式,可以有: 16 | 17 | ```js 18 | import React from 'react'; 19 | 20 | var style = { 21 |   backgroundColor: '#EEE' 22 | }; 23 | 24 | export default React.createClass({ 25 |   render: function () { 26 |     return ( 27 |        28 |       //或者 29 |         

Hello world

30 |        31 |     ) 32 |   } 33 | }); 34 | ``` 35 | 36 | 可以看出,React 的 style 属性接收的也是一个 JavaScript 对象。 37 | 38 | ## Styled Component 39 | 40 | # Class 41 | 42 | 你可以根据这个策略为每个组件创建 CSS  文件,可以让组件名和 CSS  中的 class  使用一个命名空间,来避免一个组件中的一些 class  干扰到另外一些组件的 class。 43 | 44 | _app/components/MyComponent.css_ 45 | 46 | ```css 47 | .MyComponent-wrapper  { 48 |   background-color: #eee; 49 | } 50 | ``` 51 | 52 | _app/components/MyComponent.jsx_ 53 | 54 | ```js 55 | import "./MyComponent.css"; 56 | import React from "react"; 57 | 58 | export default React.createClass({ 59 | render: function () { 60 | return ( 61 |
62 |         

Hello world

63 |        64 |
65 | ); 66 | }, 67 | }); 68 | ``` 69 | 70 | ## Multiple Class 71 | 72 | 上文中提及的利用 className 方式赋值,如果在存在多个类名的情况下: 73 | 74 | ```js 75 | render: function() { 76 |   var cx = React.addons.classSet; 77 |   var classes = cx({ 78 |     'message': true, 79 |     'message-important': this.props.isImportant, 80 |     'message-read': this.props.isRead 81 |   }); 82 |   // same final string, but much cleaner 83 |   return Great, I'll be there.; 84 | } 85 | ``` 86 | 87 | 这里的 classSet 只是起到了一个帮助进行类名合成的功能,React 官方已经弃用了,改为了[这个](https://github.com/JedWatson/classnames)。 88 | 89 | # SCSS 90 | -------------------------------------------------------------------------------- /04~工程实践/类 React 库/Preact.md: -------------------------------------------------------------------------------- 1 | # Preact 2 | 3 | Preact,它是 React 的 3KB 轻量替代方案,拥有同样的 ES6 API。高性能,轻量,即时生产是 Preact 关注的核心。基于这些主题,Preact 关注于 React 的核心功能,实现了一套简单可预测的 diff 算法使它成为最快的虚拟 DOM 框架之一,同时 preact-compat 为兼容性提供了保证,使得 Preact 可以无缝对接 React 生态中的大量组件,同时也补充了很多 Preact 没有实现的功能。 4 | 5 | ![Performance Comparison](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230516214017.png) 6 | 7 | ## 工作流程 8 | 9 | 简单介绍了 Preact 的前生今世以后,接下来说下 Preact 的工作流程,主要包含五个模块:component、h 函数、render、diff 算法、回收机制。首先是我们定义好的组件,在渲染开始的时候,首先会进入 h 函数生成对应的 virtual node(如果是 JSX 编写,之前还需要一步转码)。每一个 vnode 中包含自身节点的信息,以及子节点的信息,由此而连结成为一棵 virtual dom 树。基于生成的 vnode,render 模块会结合当前 dom 树的情况进行流程控制,并为后续的 diff 操作做一些准备工作。 10 | 11 | ![Preact 工作流程](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230516214039.png) 12 | 13 | Preact 的 diff 算法实现有别于 react 基于双 virtual dom 树的思路,Preact 只维持一棵新的 virtual dom 树,diff 过程中会基于 dom 树还原出旧的 virtual dom 树,再将两者进行比较,并在比较过程中实时对 dom 树进行 patch 操作,最终生成新的 dom 树。与此同时,diff 过程中被卸载的组件和节点不会被直接删除,而是被分别放入回收池中缓存,当再次有同类型的组件或节点被构建时,可以在回收池中找到同名元素进行改造,避免从零构建的开销。 14 | 15 | # 快速开始 16 | 17 | ```js 18 | import { h, render, Component } from "preact"; 19 | 20 | class Clock extends Component { 21 | state = { time: Date.now() }; 22 | 23 | // Called whenever our component is created 24 | componentDidMount() { 25 | // update time every second 26 | this.timer = setInterval(() => { 27 | this.setState({ time: Date.now() }); 28 | }, 1000); 29 | } 30 | 31 | // Called just before our component will be destroyed 32 | componentWillUnmount() { 33 | // stop when not renderable 34 | clearInterval(this.timer); 35 | } 36 | 37 | render() { 38 | let time = new Date(this.state.time).toLocaleTimeString(); 39 | return {time}; 40 | } 41 | } 42 | 43 | render(, document.body); 44 | ``` 45 | 46 | # Links 47 | 48 | — https://www.axihe.com/react/preact/home.html#linkstate Preact 学习笔记 49 | -------------------------------------------------------------------------------- /04~工程实践/组件测试/Jest.md: -------------------------------------------------------------------------------- 1 | # 基于 Jest 的 React 组件测试 2 | 3 | # JSX 4 | 5 | TypeScript 具有三种 JSX 模式:preserve,react 和 react-native。这些模式只在代码生成阶段起作用,类型检查并不受影响。 6 | 7 | - 在 preserve 模式下生成代码中会保留 JSX 以供后续的转换操作使用(比如:Babel)。另外,输出文件会带有.jsx 扩展名。 8 | 9 | - react 模式会生成 React.createElement,在使用前不需要再进行转换操作了,输出文件的扩展名为.js。 10 | 11 | - react-native 相当于 preserve,它也保留了所有的 JSX,但是输出文件的扩展名是.js。 12 | 13 | 假设有这样一个 JSX 表达式``,expr 可能引用环境自带的某些东西(比如,在 DOM 环境里的 div 或 span)或者是你自定义的组件。这是非常重要的,原因有如下两点: 14 | 15 | - 对于 React,固有元素会生成字符串`(React.createElement("div"))`,然而由你自定义的组件却不会生成(React.createElement(MyComponent))。 16 | - 传入 JSX 元素里的属性类型的查找方式不同。固有元素属性本身就支持,然而自定义的组件会自己去指定它们具有哪个属性。 17 | 18 | TypeScript 使用与 React 相同的规范 来区别它们。固有元素总是以一个小写字母开头,基于值的元素总是以一个大写字母开头。 19 | 20 | # 固有元素 21 | 22 | 固有元素使用特殊的接口 JSX.IntrinsicElements 来查找。默认地,如果这个接口没有指定,会全部通过,不对固有元素进行类型检查。然而,如果这个接口存在,那么固有元素的名字需要在 JSX.IntrinsicElements 接口的属性里查找。例如: 23 | 24 | ```jsx 25 | declare namespace JSX { 26 | interface IntrinsicElements { 27 | foo: any 28 | } 29 | } 30 | 31 | ; // 正确 32 | ; // 错误 33 | ``` 34 | 35 | 在上例中,`` 没有问题,但是 `` 会报错,因为它没在 JSX.IntrinsicElements 里指定。 36 | 37 | ## 基于值的元素 38 | 39 | 基于值的元素会简单的在它所在的作用域里按标识符查找。 40 | 41 | ```jsx 42 | import MyComponent from "./myComponent"; 43 | 44 | ; // 正确 45 | ; // 错误 46 | ``` 47 | 48 | ## 工厂函数 49 | 50 | `jsx: react`编译选项使用的工厂函数是可以配置的。可以使用 jsxFactory 命令行选项,或内联的@jsx 注释指令在每个文件上设置。比如,给 createElement 设置 jsxFactory,`
` 会使用 `createElement("div")` 来生成,而不是 React.createElement("div")。 51 | 52 | 注释指令可以像下面这样使用(在 TypeScript 2.8 里): 53 | 54 | ```js 55 | import preact = require("preact"); 56 | /* @jsx preact.h */ 57 | const x =
; 58 | ``` 59 | 60 | 生成: 61 | 62 | ```js 63 | const preact = require("preact"); 64 | const x = preact.h("div", null); 65 | ``` 66 | 67 | 工厂函数的选择同样会影响 JSX 命名空间的查找(类型检查)。如果工厂函数使用 React.createElement 定义(默认),编译器会先检查 React.JSX,之后才检查全局的 JSX。如果工厂函数定义为 h,那么在检查全局的 JSX 之前先检查 h.JSX。 68 | -------------------------------------------------------------------------------- /04~工程实践/服务端渲染/搭建渲染服务器.md: -------------------------------------------------------------------------------- 1 | # 基于 Express 的渲染服务器 2 | 3 | ## renderToString 4 | 5 | React 提供了两个方法 `renderToString` 和 `renderToStaticMarkup` 用来将组件(Virtual DOM)输出成 HTML 字符串,这是 React 服务器端渲染的基础,它移除了服务器端对于浏览器环境的依赖,所以让服务器端渲染变成了一件有吸引力的事情。这两个方法被包含在了 react-dom 仓库中,可以通过如下方式引入与使用: 6 | 7 | ```js 8 | import ReactDOMServer from "react-dom/server"; 9 | 10 | var ReactDOMServer = require("react-dom/server"); 11 | 12 | ReactDOMServer.renderToString(element); 13 | ``` 14 | 15 | 我们可以在服务端即使用`renderToString`将组件转化为 HTML 标签然后传递给客户端,这里 React 会自动为标签进行校验和计算;这样我们在客户端调用 `ReactDOM.render()` 渲染某个组件时,如果 React 发现已经存在了服务端渲染好的标签,则会直接使用这些标签来节约渲染时间。ReactDOMServer 中提供的另一个渲染函数是`renderToStaticMarkup`,其很类似于`renderToString`,不过其忽略了额外的譬如`data-reactid`这样的 React 内部使用的非 HTML 标准属性;如果你只想把 React 作为简单的静态网页生成器,那么推荐使用这种方式,会帮你避免额外的带宽消耗。 16 | 17 | 服务器端渲染除了要解决对浏览器环境的依赖,还要解决两个问题: 18 | 19 | - 前后端可以共享状态 20 | 21 | - 前后端路由可以统一处理 22 | 23 | ## 状态传递 24 | 25 | ## 路由权限控制 26 | 27 | ```js 28 | import { renderToString } from "react-dom/server"; 29 | import { match, RouterContext } from "react-router"; 30 | import routes from "./routes"; 31 | 32 | serve((req, res) => { 33 | // Note that req.url here should be the full URL path from 34 | // the original request, including the query string. 35 | match( 36 | { routes, location: req.url }, 37 | (error, redirectLocation, renderProps) => { 38 | if (error) { 39 | res.status(500).send(error.message); 40 | } else if (redirectLocation) { 41 | res.redirect(302, redirectLocation.pathname + redirectLocation.search); 42 | } else if (renderProps) { 43 | // You can also check renderProps.components or renderProps.routes for 44 | // your "not found" component or route respectively, and send a 404 as 45 | // below, if you're using a catch-all route. 46 | res 47 | .status(200) 48 | .send(renderToString()); 49 | } else { 50 | res.status(404).send("Not found"); 51 | } 52 | } 53 | ); 54 | }); 55 | ``` 56 | 57 | # 基于 Next.js 快速搭建渲染服务器 58 | -------------------------------------------------------------------------------- /04~工程实践/组件测试/99~参考资料/README.md: -------------------------------------------------------------------------------- 1 | # 参考资料 2 | 3 | # Test | 测试 4 | 5 | - [2017~Testing React Applications #Series#](https://blog.logrocket.com/testing-react-applications-part-1-of-3-ebd8397917f3):With React and the ecosystem of testing tools that have emerged around it, it’s finally possible to build robust, scalable tests that provide strong guarantees on code correctness. 6 | 7 | - [Snapshot Testing React Components with Jest](https://semaphoreci.com/community/tutorials/snapshot-testing-react-components-with-jest) 8 | 9 | - [Testing in React: best practices, tips and tricks](https://parg.co/bsP) 10 | 11 | - [2017~Testing React components with Jest and Enzyme](https://hackernoon.com/testing-react-components-with-jest-and-enzyme-41d592c174f#.yfpuy4eip) 12 | 13 | - [Testing a React & Redux Codebase](http://silvenon.com/testing-react-and-redux/) 14 | 15 | - [Testing in React: Getting Off The Ground](https://medium.com/javascript-inside/testing-in-react-getting-off-the-ground-5f569f3088a#.6ip96uul5) 16 | 17 | - [a-step-by-step-tdd-approach-on-testing-react-components-using-enzyme](http://thereignn.ghost.io/a-step-by-step-tdd-approach-on-testing-react-components-using-enzyme/) 18 | 19 | - [enzyme-javascript-testing-utilities-for-react](https://medium.com/airbnb-engineering/enzyme-javascript-testing-utilities-for-react-a417e5e5090f#.huj3rtv24) 20 | 21 | - [Testing React Apps With Jest](https://facebook.github.io/jest/docs/tutorial-react.html) 22 | 23 | - [2017~Front-end (React) Snapshot Testing with Jest: What is it for?](https://parg.co/bRQ) 24 | 25 | - [2017~Jest Testing patterns in React-Redux applications](https://parg.co/U1G): Jest provides a complete ecosystem for testing. There is no need of extra libraries - Mocha, Sinon, Istanbul, Chai, proxyquire etc. as all are present in Jest itself. 26 | 27 | ## Component Test | 组件测试 28 | 29 | ## E2E Test | 端到端测试 30 | 31 | - [2018~End-to-end testing React apps with Puppeteer and Jest](https://blog.logrocket.com/end-to-end-testing-react-apps-with-puppeteer-and-jest-ce2f414b4fd7): In this tutorial, we’ll see how to write tests for a React app using Jest and Puppeteer. 32 | -------------------------------------------------------------------------------- /03~状态管理/Recoiljs/README.md: -------------------------------------------------------------------------------- 1 | # Rcoiljs 2 | 3 | Rcoiljs 是 Facebook 针对 React 推出的新的状态管理框架,它更小,更加地 Reactive,不会破坏 Code Splitting。 4 | 5 | # Atoms 和 Selectors 6 | 7 | Atoms 跟名字一样,就是原子化,提供一个 state,如下,设置唯一的 key 和 默认值: 8 | 9 | ```js 10 | const todoListState = atom({ 11 | key: "todoListState", 12 | default: [], 13 | }); 14 | ``` 15 | 16 | 当我们在 app 里面使用的时候,从官网的 todo list 项目来看,有三种使用方式 17 | 18 | - 单纯去使用它的值 `const todoList = useRecoilValue(todoListState);`, 如下 19 | 20 | ```js 21 | {todoList.map((todoItem) => ( 22 | 23 | ))} 24 | ``` 25 | 26 | - 单纯想去更新值 `const setTodoList = useSetRecoilState(todoListState);`, 如下 27 | 28 | ```js 29 | const addItem = () => { 30 | setTodoList((oldTodoList) => [ 31 | ...oldTodoList, 32 | { 33 | id: getId(), 34 | text: inputValue, 35 | isComplete: false, 36 | }, 37 | ]); 38 | }; 39 | ``` 40 | 41 | - 想同时获取值和可以更新值 `const [todoList, setTodoList] = useRecoilState(todoListState);`,类似 react useState,其中 todolist 是 state 值,这个没什么好说,setTodoList 也是直接把值设置进去,注意跟上面 useSetRecoilState 产出的 setTodoList 的区别, 42 | 43 | # Selectors 44 | 45 | ```js 46 | const todoListFilterState = atom({ 47 | key: "todoListFilterState", 48 | default: "Show All", 49 | }); 50 | 51 | const filteredTodoListState = selector({ 52 | key: "filteredTodoListState", 53 | get: ({ get }) => { 54 | const filter = get(todoListFilterState); 55 | const list = get(todoListState); 56 | 57 | switch (filter) { 58 | case "Show Completed": 59 | return list.filter((item) => item.isComplete); 60 | case "Show Uncompleted": 61 | return list.filter((item) => !item.isComplete); 62 | default: 63 | return list; 64 | } 65 | }, 66 | }); 67 | ``` 68 | 69 | 同时 selector 也支持 set 操作,类似官网对华氏度和摄氏度的转化, 当我们对摄氏度的 selector 进行赋值的时候,也会更新华氏度 tempFahrenheit 的值: 70 | 71 | ```js 72 | const tempFahrenheit = atom({ 73 | key: 'tempFahrenheit', 74 | default: 32, 75 | }); 76 | 77 | const tempCelcius = selector({ 78 | key: 'tempCelcius', 79 | get: ({get}) => ((get(tempFahrenheit) - 32) 5) / 9, 80 | set: ({set}, newValue) => set(tempFahrenheit, (newValue 9) / 5 + 32), 81 | }); 82 | ``` 83 | -------------------------------------------------------------------------------- /02~组件基础/04~React Router/框架集成.md: -------------------------------------------------------------------------------- 1 | # 框架集成 2 | 3 | # Redux 4 | 5 | - 安装方式: 6 | 7 | ```jsx 8 | npm install --save react-router-redux 9 | ``` 10 | 11 | ## 简单示例 12 | 13 | ```jsx 14 | import React from "react"; 15 | import ReactDOM from "react-dom"; 16 | import { createStore, combineReducers, applyMiddleware } from "redux"; 17 | import { Provider } from "react-redux"; 18 | import { Router, Route, browserHistory } from "react-router"; 19 | import { syncHistoryWithStore, routerReducer } from "react-router-redux"; 20 | 21 | import reducers from "/reducers"; 22 | 23 | // Add the reducer to your store on the `routing` key 24 | const store = createStore( 25 | combineReducers({ 26 | ...reducers, 27 | routing: routerReducer, 28 | }) 29 | ); 30 | 31 | // Create an enhanced history that syncs navigation events with the store 32 | const history = syncHistoryWithStore(browserHistory, store); 33 | 34 | ReactDOM.render( 35 | 36 | {/* Tell the Router to use our enhanced history */} 37 | 38 | 39 | 40 | 41 | 42 | 43 | , 44 | document.getElementById("mount") 45 | ); 46 | ``` 47 | 48 | ## Params | Router 的参数 49 | 50 | 在 React Router 中可以通过本身组件的 Props 来传递路由参数,而在 React-Redux 中因为是采用了`connect()`方法将 State 映射到了 Props 中,因此需要采用`mapStateToProps`中的第二个参数进行路由映射: 51 | 52 | ```js 53 | function mapStateToProps(state, ownProps) { 54 | return { 55 | id: ownProps.params.id, 56 | filter: ownProps.location.query.filter, 57 | }; 58 | } 59 | ``` 60 | 61 | ## History 62 | 63 | 如果有时候需要对于你的路由的历史进行监控的话,可以采用如下的方案: 64 | 65 | ```js 66 | const history = syncHistoryWithStore(browserHistory, store); 67 | 68 | history.listen((location) => analyticsService.track(location.pathname)); 69 | ``` 70 | 71 | ## Navigation Control 72 | 73 | - issue navigation events via Redux actions 74 | 75 | ```js 76 | import { routerMiddleware, push } from "react-router-redux"; 77 | 78 | // Apply the middleware to the store 79 | const middleware = routerMiddleware(browserHistory); 80 | const store = createStore(reducers, applyMiddleware(middleware)); 81 | 82 | // Dispatch from anywhere like normal. 83 | store.dispatch(push("/foo")); 84 | ``` 85 | -------------------------------------------------------------------------------- /03~状态管理/Redux/redux-form.md: -------------------------------------------------------------------------------- 1 | # redux-form 2 | 3 | ## Simple Form 4 | 5 | 如果在 Redux Form 中需要手动地设置值,应该在 Field 的 `onChange` 方法中进行修改,譬如: 6 | 7 | ```jsx 8 | 21 | ``` 22 | 23 | 这一特性常常用于在自定义组件中进行值设置, 24 | 25 | ## Initial Form Values 26 | 27 | ```jsx 28 | import { React, Component } from "react"; 29 | import { bindActionCreators } from "redux"; 30 | import { connect } from "react-redux"; 31 | 32 | import { reduxForm } from "redux-form"; 33 | import { registerPerson } from "actions/coolStuff"; 34 | 35 | @connect(null, (dispatch) => ({ 36 | registerPerson: bindActionCreators(registerPerson, dispatch), 37 | })) 38 | export default class ExampleComponent extends Component { 39 | render() { 40 | const myInitialValues = { 41 | initialValues: { 42 | name: "John Doe", 43 | age: 42, 44 | fruitPreference: "apples", 45 | }, 46 | }; 47 | return ( 48 |
49 |

Check out my cool form!

50 | { 53 | this.props.registerPerson(fields); 54 | }} 55 | /> 56 |
57 | ); 58 | } 59 | } 60 | 61 | @reduxForm({ 62 | form: "exampleForm", 63 | fields: ["name", "age", "fruitPreference"], 64 | }) 65 | class CoolForm extends Component { 66 | render() { 67 | const { 68 | fields: { name, age, fruitPreference }, 69 | handleSubmit, 70 | } = this.props; 71 | return ( 72 |
73 | 74 | 75 | 76 | 77 | 78 | 82 | 85 |
86 | ); 87 | } 88 | } 89 | ``` 90 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/Hooks/基础元语/useState & useRef.md: -------------------------------------------------------------------------------- 1 | # 组件内状态 2 | 3 | # useState 4 | 5 | useState 接收一个初始值,返回一个数组,数组里面分别是当前值和修改这个值的方法(类似 state 和 setState)。useState 接收一个函数,返回一个数组。setCount 可以接收新值,也可以接收一个返回新值的函数。 6 | 7 | ```ts 8 | const [count1, setCount1] = useState(0); 9 | const [count2, setCount2] = useState(() => 0); 10 | setCount1(1); // 修改 state 11 | ``` 12 | 13 | 函数式状态的粒度会比类中状态更细,函数式状态保存的是快照,类状态保存的是最新值。引用类型的情况下,类状态不需要传入新的引用,而函数式状态必须保证是个新的引用。 14 | 15 | ## 快照(闭包)与最新值(引用) 16 | 17 | 在函数式组件中,我们有时候会发现所谓闭包冻结的现象,譬如在如下代码中: 18 | 19 | ```ts 20 | function App() { 21 | const [count, setCount] = useState(0); 22 | const inc = React.useCallback(() => { 23 | setTimeout(() => { 24 | setCount(count + 1); 25 | }); 26 | }, []); 27 | 28 | return

{count}

; 29 | } 30 | ``` 31 | 32 | 类组件里面可以通过 this.state 引用到 count,所以每次 setTimeout 的时候都能通过引用拿到上一次的最新 count,所以点击多少次最后就加了多少。在函数式组件里面每次更新都是重新执行当前函数,也就是说 setTimeout 里面读取到的 count 是通过闭包获取的,而这个 count 实际上只是初始值,并不是上次执行完成后的最新值,所以最后只加了 1 次。 33 | 34 | 想要解决这个问题,那就涉及到另一个新的 Hook 方法 useRef。useRef 是一个对象,它拥有一个 current 属性,并且不管函数组件执行多少次,而 useRef 返回的对象永远都是原来那一个。 35 | 36 | ```ts 37 | export default function App() { 38 | const [count, setCount] = React.useState(0); 39 | const ref = useRef(0); 40 | 41 | const inc = React.useCallback(() => { 42 | setTimeout(() => { 43 | setCount((ref.current += 1)); 44 | }); 45 | }, []); 46 | 47 | return ( 48 |

49 | {count},{ref.current} 50 |

51 | ); 52 | } 53 | ``` 54 | 55 | # useRef 56 | 57 | ```js 58 | export function useRef(initialValue: T): { current: T } { 59 | currentlyRenderingFiber = resolveCurrentlyRenderingFiber(); 60 | workInProgressHook = createWorkInProgressHook(); 61 | let ref; 62 | 63 | if (workInProgressHook.memoizedState === null) { 64 | ref = { current: initialValue }; 65 | // ... 66 | workInProgressHook.memoizedState = ref; 67 | } else { 68 | ref = workInProgressHook.memoizedState; 69 | } 70 | return ref; 71 | } 72 | ``` 73 | 74 | 对于函数式组件,如果我们需要获取该组件子元素的 Ref,可以使用 forwardRef 来进行 Ref 转发: 75 | 76 | ```js 77 | const FancyButton = React.forwardRef((props, ref) => ( 78 | 81 | )); 82 | 83 | // You can now get a ref directly to the DOM button: 84 | const ref = React.createRef(); 85 | Click me!; 86 | ``` 87 | -------------------------------------------------------------------------------- /02~组件基础/01~组件声明/类组件/DOM 操作.md: -------------------------------------------------------------------------------- 1 | # React 组件中 DOM 操作 2 | 3 | `React.findDOMNode()`方法能够帮我们根据`refs`获取某个子组件的 DOM 对象,不过需要注意的是组件并不是真实的 DOM  节点,而是存在于内存之中的一种数据结构,叫做虚拟 DOM (virtual DOM)。只有当它插入文档以后,才会变成真实的 DOM 。根据 React  的设计,所有的 DOM  变动,都先在虚拟 DOM 上发生,然后再将实际发生变动的部分,反映在真实 DOM 上,这种算法叫做 DOM diff ,它可以极大提高网页的性能表现。但是,有时需要从组件获取真实 DOM  的节点,这时就要用到 React.findDOMNode  方法。 4 | 5 | ```js 6 | var MyComponent = React.createClass({ 7 | handleClick: function() { 8 | React.findDOMNode(this.refs.myTextInput).focus(); 9 | }, 10 | render: function() { 11 | return ( 12 |
13 | 14 | 19 |
20 | ); 21 | } 22 | }); 23 | 24 | React.render(, document.getElementById("example")); 25 | ``` 26 | 27 | 需要注意的是,由于 React.findDOMNode  方法获取的是真实 DOM ,所以必须等到虚拟 DOM  插入文档以后,才能使用这个方法,否则会返回 null 。上面代码中,通过为组件指定 Click  事件的回调函数,确保了只有等到真实 DOM  发生 Click  事件之后,才会调用 React.findDOMNode  方法。 28 | 29 | # 组件渲染到 DOM 30 | 31 | React 的初衷是构建相对独立的界面开发库, 32 | 33 | 在源代码中,组件定义相关代码与渲染相关代码是相分离的。当我们声明了某个组件之后,可以通过`ReactDOM`的`render`函数将 React 组件渲染到 DOM 元素中: 34 | 35 | ```js 36 | const RootElement = ( 37 |  
38 |  

The world is yours

39 |  

Say hello to my little friend

40 |  
41 | ) 42 | 43 | ReactDOM.render(RootElement, document.getElementById('app')) 44 | ``` 45 | 46 | # Refs 47 | 48 | # 整合非 React 类库 49 | 50 | ## jQuery Integration 51 | 52 | 目前,我们项目中不可避免的还会存在大量的基于 jQuery 的插件,这些插件也确实非常的好用呦,通常我们会采取将 jQuery 插件封装成一个 React 组件的方式来进行调用,譬如我们需要调用一个用于播放的 jQuery 插件 JPlayer,那么可以以如下方式使用: 53 | 54 | ```js 55 | // JPlayer component 56 | class JPlayer extends React.Component { 57 | static propTypes = { 58 | sources: React.PropTypes.array.isRequired 59 | }; 60 | componentDidMount() { 61 | $(this.refs.jplayer).jPlayer({ 62 | ready: () => { 63 | $(this.refs.jplayer).jPlayer("setMedia", this.props.sources); 64 | }, 65 | swfPath: "/js", 66 | supplied: _.keys(this.props.sources) 67 | }); 68 | } 69 | componentWillUmount() { 70 | // I don't know jPlayer API but if you need to destroy it do it here. 71 | } 72 | render() { 73 | return
; 74 | } 75 | } 76 | 77 | // Use it in another component... 78 | ; 79 | ``` 80 | -------------------------------------------------------------------------------- /02~组件基础/06~组件库/组件范式/表单组件.md: -------------------------------------------------------------------------------- 1 | # 表单组件 2 | 3 | # 受控组件与非受控组件 4 | 5 | # 常用组件 6 | 7 | ## Text 8 | 9 | ## Select 10 | 11 | # 表单验证 12 | 13 | 在我们真实的表单组件开发中,我们不可避免地需要对于用户输入的内容进行验证并且根据验证结果给予用户相关反馈提示。实际开发中,我们会使用受控组件,这样所有的表单数据都会存放于组件状态中(暂时不考虑状态存放于组件外),我们也可以很方便地根据内部状态进行计算。譬如产品需求是唯有用户在输入有效的邮箱地址时才允许点击注册按钮,否则注册按钮处于不可点击状态,图示如下: 14 | 15 | 我们可以使用简单的表达式来推导是否应该允许点击按钮,即仅当邮箱长度大于 0 并且密码长度大于 0 的时候才允许点击。 16 | 17 | ```js 18 | const { email, password } = this.state; 19 | const isEnabled = email.length > 0 && password.length > 0; 20 | 21 | ; 22 | ``` 23 | 24 | 到这里我们已经完成了最简单基础的表单验证,不过在真实工程开发中我们会遇到的最头痛的问题反而不是验证本身,而在于如何进行合适的错误反馈。常见的输入框错误反馈模式有如下几种: 25 | 26 | 不同的产品经理、不同的产品对于这些错误反馈有不同的喜好,不过从工程的角度上我们希望尽可能地将逻辑与界面表示相分离,并且能够根据产品经理的要求迅速改变错误反馈模式与具体的样式。首先,我们需要考虑下组件内的存储错误信息的可独立于界面层的数据结构。基本的数据结构如下所示: 27 | 28 | ``` 29 | errors: { 30 |   name: false, 31 |   email: true, 32 | } 33 | ``` 34 | 35 | 这里的`false`代表某个域是验证通过的,而`true`则代表某个域是有问题的。在构建了存储错误信息的数据结构之后,我们要接着讲验证的过程独立于渲染函数以使其符合单一职责原则: 36 | 37 | ``` 38 | function validate(email, password) { 39 |   // true means invalid, so our conditions got reversed 40 |   return { 41 |   email: email.length === 0, 42 |   password: password.length === 0, 43 |   }; 44 | } 45 | ``` 46 | 47 | 这里的`validate`函数是典型的纯函数,我们可以方便地对其进行单元测试或者重构。下面我们需要在渲染函数中调用该验证函数: 48 | 49 | ```js 50 | const errors = validate(this.state.email, this.state.password); 51 | ``` 52 | 53 | 在获取了错误信息后,我们还需要在按钮的属性上引用错误信息,完整的注册表单代码为: 54 | 55 | ```jsx 56 | 57 | class SignUpForm extends React.Component { 58 |   constructor() { 59 |   super(); 60 |   this.state = { 61 |   email: '', 62 |   password: '', 63 |   touched: { 64 |   email: false, 65 |   password: false, 66 |   }, 67 |   }; 68 |   } 69 | 70 | // ... 71 | 72 | handleBlur = (field) => (evt) => { 73 |   this.setState({ 74 |   touched: { ...this.state.touched, [field]: true }, 75 |   }); 76 |   } 77 | 78 | render() 79 |   const shouldMarkError = (field) => { 80 |   const hasError = errors[field]; 81 |   const shouldShow = this.state.touched[field]; 82 | 83 | return hasError ? shouldShow : false; 84 |   };} 85 | 86 | // ... 87 | /** 88 | 89 | 98 |   99 | */ 100 | } 101 | 102 | ``` 103 | -------------------------------------------------------------------------------- /02~组件基础/04~React Router/Hooks Api.md: -------------------------------------------------------------------------------- 1 | # Hooks Api 2 | 3 | React Router 附带了一些 Hooks Api,可让您访问路由器的状态并从组件内部执行导航。 4 | 5 | # useHistory 6 | 7 | useHistory Hook 使您可以访问可用于导航的历史记录实例。 8 | 9 | ```js 10 | import { useHistory } from "react-router"; 11 | 12 | function HomeButton() { 13 | let history = useHistory(); 14 | 15 | function handleClick() { 16 | history.push("/home"); 17 | } 18 | 19 | return ( 20 | 23 | ); 24 | } 25 | ``` 26 | 27 | # useLocation 28 | 29 | useLocation Hook 返回代表当前 URL 的位置对象。您可以将其想像为 useState,它会在 URL 发生更改时返回一个新位置。在您希望每次加载新页面时都使用 Web 分析工具触发新的“页面浏览”事件的情况下,如以下示例所示: 30 | 31 | ```js 32 | import React from "react"; 33 | import ReactDOM from "react-dom"; 34 | import { BrowserRouter as Router, Switch, useLocation } from "react-router"; 35 | 36 | function usePageViews() { 37 | let location = useLocation(); 38 | React.useEffect(() => { 39 | ga.send(["pageview", location.pathname]); 40 | }, [location]); 41 | } 42 | 43 | function App() { 44 | usePageViews(); 45 | return ...; 46 | } 47 | 48 | ReactDOM.render( 49 | 50 | 51 | , 52 | node 53 | ); 54 | ``` 55 | 56 | # useParams 57 | 58 | useParams 返回 URL 参数的键/值对的对象。使用它来访问当前 `` 的 match.params。 59 | 60 | ```js 61 | import React from "react"; 62 | import ReactDOM from "react-dom"; 63 | import { 64 | BrowserRouter as Router, 65 | Switch, 66 | Route, 67 | useParams 68 | } from "react-router"; 69 | 70 | function BlogPost() { 71 | let { slug } = useParams(); 72 | return
Now showing post {slug}
; 73 | } 74 | 75 | ReactDOM.render( 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | , 86 | node 87 | ); 88 | ``` 89 | 90 | # useRouteMatch 91 | 92 | useRouteMatch Hook 尝试以与 `` 相同的方式匹配当前 URL。在无需实际呈现`` 的情况下访问匹配数据最有用。 93 | 94 | ```js 95 | function BlogPost() { 96 | return ( 97 | { 100 | // Do whatever you want with the match ... 101 | return
; 102 | }} 103 | /> 104 | ); 105 | } 106 | 107 | function BlogPost() { 108 | let match = useMatch("/blog/:slug"); 109 | // Do whatever you want with the match... 110 | } 111 | ``` 112 | -------------------------------------------------------------------------------- /03~状态管理/XState/README.md: -------------------------------------------------------------------------------- 1 | # xstate 2 | 3 | XState 是一个状态管理(State Management)的 Library,负责储存及描述各种状态与各种状态间的转换,有点类似于 Redux、Flux,不同的地方在于 XState 整个核心都源自于 Statecharts,也就是我们需要定义好整个应用,程序会有哪些状态,和每个状态下能转换到哪些状态(顺序性)以及他们之间如何转换。 4 | 5 | ## Statecharts 6 | 7 | 其实 Statecharts 并不是什麽新技术或新概念,早在 1984 年 David HAREL 的论文就提出了 Statechart,是由早期的状态图(state diagrams)所拓展而来的,在该篇论文中对状态图加入了三个元素分别处理了层级(hierarchy)、併发(concurrency)和通讯(communication)。让原先的状态图变的高度结构化且能更有效地描述各种状态。 8 | 9 | ![Statecharts 来描述 Fetch](https://imgchr.com/i/sd9FHJ) 10 | 11 | # 为何需要 xstate? 12 | 13 | 其实用过 Angular.js 的开发者都会知道状态管理的重要性,当应用,程序状态分散在不同地方时,就会使得状态难以管理,并且容易出现 Bug,直到 Flux 出现提出了单一资料源(Single Source of Truth)及单向资料流(unidirectional data flow)等概念后,状态管理的问题才得到了缓解。而 Redux 的出现利用 Funtional Programming 的手法大幅度的降低原先 Flux 的複杂度以及学习成本,如果我们依照 Redux 的架构已经可以把因为状态複杂度而陡然上升的维护成本控制得很好,那如今为什麽我们还需要一个新的状态管理工具呢? 14 | 15 | ## 缺乏清晰的状态描述 16 | 17 | 不管是使用 Redux 或其他相关的 Library 都会有状态难以清晰描述的问题,最主要原因有两个,第一个是我们完全混合了状态(state)跟资料(context),所有东西都直接往 Reducer 裡面塞导致我们无法分清楚哪些是资料、哪些是状态。这裡的资料(context)指的是显示在页面上的内容,通常这些资料会存储在后端并透过 API 取得,在 XState 称之为 context,在 UML State Mechine 裡面称为 Extended states;而状态(state)则是指应用,程序当前的状态,比如说是否已登入或者 Menu 是否展开等等状态。 18 | 19 | 另一个因素是我们通常都使用 flag 来表达某个小状态,再由多个 flags 来表达某个状态,当这种 flag 越来越多时,我们的,程序就会很容易出现 Bug,,程序码会长的像下面这样: 20 | 21 | ```js 22 | if (isLogin && isYYY && isXXX) 23 | ``` 24 | 25 | 这样的,程序码其实就是所谓的 bottom-up code,通常是我们先有一个小状态比如说 isLogin 然后后面又加了其他各种状态,当我们这种小状态一多,就会让,程序容出现难以察觉的 Bug。 26 | 27 | ## 过于自由的状态转换 28 | 29 | 如上面我们提到的,过去我们的状态是由多个 flags 所组成,这导致了我们无法明确的定义各种状态之间的关係,最后就会变成我们无法确定状态之间的切换是否正确,比如说 isAdmin 为 true 时 isLogin 应该必定为 true。像这样用 flag 储存小状态就会有可能出现状态转换出错的情况,比如说 isAdmin 设定成 true 了,却忘记把 isLogin 也设定为 true;而实际上状态的複杂度会比这裡举的例子複杂许多,这样的,程序码大到一定程度就会变成我们再也无法真正理解,程序有哪些状态,以及哪些可能的状态应该被处理(除非你再从头跟 PM 及 Designer 完整的过一次流程与画面,但如果专案够大很有可能他们也不会很清楚)。 30 | 31 | ## 难以与工程师之外的人讨论 32 | 33 | 同样的当我们今天用各种 flags 的方式去描述整个应用,程序的状态时,其实是很难跟工程师之外的人沟通或讨论的,就算是工程师也要追 Code 花时间理解当前的,程序到底是如何运作并且在哪个状态下出现的 Bug,这会让我们很难快速地发现问题也很难跟 PM 讨论需求设计是否存在逻辑上的矛盾,或是有未处理的状态。 34 | 35 | # XState 有什麽优势? 36 | 37 | ## ,程序码即 UI Spec 38 | 39 | 当我们今天用 XState 定义好各种状态之后,就可以直接利用 XState 提供的图像化工具(Visualizer)把,程序码转换成图片,如下: 40 | 41 | ![xstate 状态逻辑图](https://s3.ax1x.com/2021/01/15/sws4SS.png) 42 | 43 | 当我们有这张图之后,就可以把这个当作 UI Spec 跟 PM 及设计师讨论哪方面流程有问题,或是还有哪些没有明确订定的状态。 44 | 45 | ## 写更少的测试 46 | 47 | 由于我们已经明确定义出各个状态以及每个状态之间的关係,这让我们可以更轻鬆的撰写测试,也不需要测试那些根本不可能出现的状态,并透过 Model-based Testing 我们只需要写各个状态下的断言(assertion)就可以自动把各种状态切换的路径都测试完!XState 在这方面也提供了 xstate-test 有完整的范例跟教学。 48 | 49 | ## 更快速的路径优化 50 | 51 | 当我们完成一个应用,程序时,最需要做的通常就是使用者体验(User Experience)的优化,我们常常需要利用各种服务来收集各个页面间的转化率或是哪些状态让使用者最快跳过等等的数据。透过这些数据来优化我们应用,程序的流程,让使用者体验进一步的提升。而如果使用了 XState 我们就可以在各个状态转换之间送 log 到数据收集的服务(如 GA, MIXpanel 等等),就可以进一步分析哪些状态可能是不必要的,来优化我们的 User Flow。 52 | -------------------------------------------------------------------------------- /03~状态管理/Zustand/01.状态派生与 Selector.md: -------------------------------------------------------------------------------- 1 | # Computed State 2 | 3 | ```ts 4 | import computed from "zustand-computed"; 5 | 6 | type Store = { 7 | count: number; 8 | inc: () => void; 9 | dec: () => void; 10 | }; 11 | 12 | type ComputedStore = { 13 | countSq: number; 14 | }; 15 | 16 | const computeState = (state: Store): ComputedStore => ({ 17 | countSq: state.count ** 2, 18 | }); 19 | 20 | // use curried create 21 | const useStore = create()( 22 | computed( 23 | (set) => ({ 24 | count: 1, 25 | inc: () => set((state) => ({ count: state.count + 1 })), 26 | dec: () => set((state) => ({ count: state.count - 1 })), 27 | }), 28 | computeState 29 | ) 30 | ); 31 | 32 | const useStore = create()( 33 | devtools( 34 | computed( 35 | immer((set) => ({ 36 | count: 1, 37 | inc: () => 38 | set((state) => { 39 | // example with Immer middleware 40 | state.count += 1; 41 | }), 42 | dec: () => set((state) => ({ count: state.count - 1 })), 43 | })), 44 | computeState 45 | ) 46 | ) 47 | ); 48 | ``` 49 | 50 | # 自动生成 Selector 51 | 52 | 我们建议在使用 store 的属性或动作时使用选择器。你可以像这样访问 store 里的值: 53 | 54 | ```ts 55 | const bears = useBearStore((state) => state.bears); 56 | ``` 57 | 58 | 然而,写这些东西可能很乏味。如果你是这种情况,你可以自动生成你的选择器。 59 | 60 | ```ts 61 | import { StoreApi, UseBoundStore } from "zustand"; 62 | 63 | type WithSelectors = S extends { getState: () => infer T } 64 | ? S & { use: { [K in keyof T]: () => T[K] } } 65 | : never; 66 | 67 | const createSelectors = >>( 68 | _store: S 69 | ) => { 70 | let store = _store as WithSelectors; 71 | store.use = {}; 72 | for (let k of Object.keys(store.getState())) { 73 | (store.use as any)[k] = () => store((s) => s[k as keyof typeof s]); 74 | } 75 | 76 | return store; 77 | }; 78 | ``` 79 | 80 | 如果有如下的 Store: 81 | 82 | ```ts 83 | interface BearState { 84 | bears: number; 85 | increase: (by: number) => void; 86 | increment: () => void; 87 | } 88 | 89 | const useBearStoreBase = create()((set) => ({ 90 | bears: 0, 91 | increase: (by) => set((state) => ({ bears: state.bears + by })), 92 | increment: () => set((state) => ({ bears: state.bears + 1 })), 93 | })); 94 | ``` 95 | 96 | 即可生成如下可直接访问的 store: 97 | 98 | ```ts 99 | const useBearStore = createSelectors(useBearStoreBase); 100 | 101 | // get the property 102 | const bears = useBearStore.use.bears(); 103 | 104 | // get the action 105 | const increase = useBearStore.use.increment(); 106 | ``` 107 | -------------------------------------------------------------------------------- /03~状态管理/Redux/State 结构设计.md: -------------------------------------------------------------------------------- 1 | # State 结构设计 2 | 3 | 接下来我们一起讨论下 State 的结构设计问题,在复杂组件中,我们可能需要在但单组件内维持复杂的具有项目依赖关系的状态数据,譬如在经典的 TODOList 组件中,我们首先需要保存当前全部的待做事项数据: 4 | 5 | ```js 6 | const initialState = [ 7 | { id: 1, text: "laundry" }, 8 | { id: 2, text: "shopping" }, // ... 9 | ]; 10 | 11 | class List extends React.Component { 12 | state = { 13 | todos: initialState, 14 | }; 15 | 16 | render() { 17 | return ( 18 |
19 |               20 |
    21 | {this.state.todos.map((todo) => ( 22 |
  • {todo.text}
  • 23 | ))} 24 |
      25 |
26 | ); 27 | } 28 | } 29 | ``` 30 | 31 | 当你以为你大功告成的时候,产品经理让你添加一个搜索框,可以根据用户输入的内容动态地过滤列表显示内容。估计很多开发者会根据直觉写出如下代码: 32 | 33 | ```js 34 | class List extends React.Component { 35 | state = { 36 | todos: initialState, 37 | filteredTodos: null, 38 | }; 39 | 40 | search(searchText) { 41 | const filteredTodos = this.state.todos.filter( 42 | (todo) => todo.text.indexOf(searchText) > 0 43 | ); 44 | this.setState({ filteredTodos: filteredTodos }); 45 | } 46 | 47 | render() { 48 | // get todos from state 49 | const { filteredTodos, todos } = this.state; // if there are filtered todos use them 50 | 51 | const list = filteredTodos === null ? todos : filteredTodos; 52 | 53 | return ( 54 |
55 |                  56 | 57 |                  58 |
    59 |      60 | {list.map((todo) => ( 61 |
  • {todo.text}
  • 62 | ))} 63 |
64 |              65 |
66 | ); 67 | } 68 | } 69 | ``` 70 | 71 | 从功能上来说,这段代码没有问题,但是其将 Todos 数据保存到了两个不同的列表中,这就导致了相同的数据被保存到了两个地方,不仅造成了数据存储的冗余,还会为我们未来的修改造成不便。譬如用户可能需要在过滤之后修改某个 Todo 项目的属性,那么此时便需要同时改变`todos`与`filteredTodos`这两个属性的数据方可。在 State 的结构设计时,我们应该遵循尽可能地保证其最小化原则,在此考虑下我们的组件可以修改为: 72 | 73 | ```js 74 | class List extends React.Component { 75 | state = { 76 | todos: initialState, 77 | searchText: null, 78 | }; 79 | 80 | search(searchText) { 81 | this.setState({ searchText: searchText }); 82 | } 83 | 84 | filter(todos) { 85 | if (!this.state.searchText) { 86 | return todos; 87 | } 88 | 89 | return todos.filter((todo) => todo.text.indexOf(this.state.searchText) > 0); 90 | } 91 | 92 | render() { 93 | const { todos } = this.state; 94 | 95 | return ( 96 |
97 |                  98 | 99 |                  100 |
    101 |      102 | {this.filter(todos).map((todo) => ( 103 |
  • {todo.text}
  • 104 | ))} 105 |
106 |              107 |
108 | ); 109 | } 110 | } 111 | ``` 112 | -------------------------------------------------------------------------------- /02~组件基础/01~组件声明/事件系统/合成事件.md: -------------------------------------------------------------------------------- 1 | # SyntheticEvent | 合成事件详解 2 | 3 | # Event pooling 4 | 5 | 如上图所示,在 JavaScript 中,事件的触发实质上是要经过三个阶段:事件捕获、目标对象本身的事件处理和事件冒泡,假设在 div 中触发了 click 事件,实际上首先经历捕获阶段会由父级元素将事件一直传递到事件发生的元素,执行完目标事件本身的处理事件后,然后经历冒泡阶段,将事件从子元素向父元素冒泡。正因为事件在 DOM 的传递经历这样一个过程,从而为行为委托提供了可能。通俗地讲,行为委托的实质就是将子元素事件的处理委托给父级元素处理。 6 | 7 | React 会将所有的事件都绑定在最外层(document),使用统一的事件监听,并在冒泡阶段处理事件,当挂载或者卸载组件时,只需要在通过的在统一的事件监听位置增加或者删除对象,因此可以提高效率。并且 React 并没有使用原生的浏览器事件,而是在基于 Virtual DOM 的基础上实现了合成事件(SyntheticEvent),事件处理程序接收到的是 SyntheticEvent 的实例。SyntheticEvent 完全符合 W3C 的标准,因此在事件层次上具有浏览器兼容性,与原生的浏览器事件一样拥有同样的接口,可以通过 stopPropagation()和 preventDefault() 相应的中断。如果需要访问当原生的事件对象,可以通过引用 nativeEvent 获得。 8 | 9 | ![](https://segmentfault.com/img/remote/1460000008782648?w=407&h=356) 10 | 11 | ![](https://segmentfault.com/img/remote/1460000008782649?w=885&h=518) 12 | 13 | 上图为大致的 React 事件机制的流程图,React 中的事件机制分为两个阶段:事件注册和事件触发: 14 | 15 | - 事件注册:React 在组件加载(mount)和更新(update)时,其中的 ReactDOMComponent 会对传入的事件属性进行处理,对相关事件进行注册和存储。document 中注册的事件不处理具体的事件,仅对事件进行分发。ReactBrowserEventEmitter 作为事件注册入口,担负着事件注册和事件触发。注册事件的回调函数由 EventPluginHub 来统一管理,根据事件的类型(type)和组件标识(`_rootNodeID`)为 key 唯一标识事件并进行存储。 16 | 17 | - 事件执行:事件执行时,document 上绑定事件 ReactEventListener.dispatchEvent 会对事件进行分发,根据之前存储的类型(type)和组件标识(`_rootNodeID`)找到触发事件的组件。ReactEventEmitter 利用 EventPluginHub 中注入(inject)的 plugins(例如:SimpleEventPlugin、EnterLeaveEventPlugin)会将原生的 DOM 事件转化成合成的事件,然后批量执行存储的回调函,回调函数的执行分为两步,第一步是将所有的合成事件放到事件队列里面,第二步是逐个执行。需要注意的是,浏览器原生会为每个事件的每个 listener 创建一个事件对象,可以从这个事件对象获取到事件的引用。这会造成高额的内存分配,React 在启动时就会为每种对象分配内存池,用到某一个事件对象时就可以从这个内存池进行复用,节省内存。 18 | 19 | # 外部事件触发 20 | 21 | ## 外部触发关闭 22 | 23 | 点击事件是 Web 开发中常见的事件之一,我们在上文中介绍的基本事件绑定也是以点击事件为例。这里我们想讨论下另一个常见的与点击相关的需求,即点击组件外部分触发事件处理。典型的用例譬如在弹出窗中,我们希望点击弹出窗外的部分自动关闭弹出窗,或者某个下拉菜单打开状态下,点击其他部分自动关闭该菜单。这种需求有一种解决思路是为组件设置一个全局浮层,这样可以将组件外的点击事件绑定到浮层上,直接监听浮层的点击即可。不过很多产品经理在提需求的时候是不喜欢这种方式的,因此我们可以使用另一种方法,直接在 React 根容器中监听点击事件: 24 | 25 | ```js 26 | window.__myapp_container = document.getElementById("app"); 27 | 28 | React.render(, window.__myapp_container); 29 | ``` 30 | 31 | 然后在组件中我们动态的设置对于根元素的监听: 32 | 33 | ```js 34 | export default class ClickListener extends Component { 35 | static propTypes = { 36 | children: PropTypes.node.isRequired, 37 | onClickOutside: PropTypes.func.isRequired 38 | }; 39 | 40 | componentDidMount() { 41 | window.__myapp_container.addEventListener( 42 | "click", 43 | this.handleDocumentClick 44 | ); 45 | } 46 | 47 | componentWillUnmount() { 48 | window.__myapp_container.removeEventListener( 49 | "click", 50 | this.handleDocumentClick 51 | ); 52 | } /* using fat arrow to bind to instance */ 53 | 54 | handleDocumentClick = evt => { 55 | const area = ReactDOM.findDOMNode(this.refs.area); 56 | 57 | if (!area.contains(evt.target)) { 58 | this.props.onClickOutside(evt); 59 | } 60 | }; 61 | 62 | render() { 63 | return ( 64 |
65 |      {this.props.children} 66 |      67 |
68 | ); 69 | } 70 | } 71 | ``` 72 | 73 | ## 自定义事件分发 74 | -------------------------------------------------------------------------------- /03~状态管理/Zustand/04.状态重置.md: -------------------------------------------------------------------------------- 1 | # 状态重置 2 | 3 | 以下模式可用于将状态重置为其初始值。 4 | 5 | ```ts 6 | import { create } from "zustand"; 7 | 8 | // define types for state values and actions separately 9 | type State = { 10 | salmon: number; 11 | tuna: number; 12 | }; 13 | 14 | type Actions = { 15 | addSalmon: (qty: number) => void; 16 | addTuna: (qty: number) => void; 17 | reset: () => void; 18 | }; 19 | 20 | // define the initial state 21 | const initialState: State = { 22 | salmon: 0, 23 | tuna: 0, 24 | }; 25 | 26 | // create store 27 | const useSlice = create()((set, get) => ({ 28 | ...initialState, 29 | 30 | addSalmon: (qty: number) => { 31 | set({ salmon: get().salmon + qty }); 32 | }, 33 | 34 | addTuna: (qty: number) => { 35 | set({ tuna: get().tuna + qty }); 36 | }, 37 | 38 | reset: () => { 39 | set(initialState); 40 | }, 41 | })); 42 | ``` 43 | 44 | 一次重置多个 store: 45 | 46 | ```ts 47 | import { create: _create, StateCreator } from 'zustand' 48 | 49 | const resetters: (() => void)[] = [] 50 | 51 | export const create = ((f: StateCreator | undefined) => { 52 | if (f === undefined) return create 53 | const store = _create(f) 54 | const initialState = store.getState() 55 | resetters.push(() => { 56 | store.setState(initialState, true) 57 | }) 58 | return store 59 | }) as typeof _create 60 | 61 | export const resetAllStores = () => { 62 | for (const resetter of resetters) { 63 | resetter() 64 | } 65 | } 66 | ``` 67 | 68 | 使用切片模式重置绑定的 store: 69 | 70 | ```ts 71 | import create, { StateCreator } from "zustand"; 72 | 73 | const resetters: (() => void)[] = []; 74 | 75 | const initialBearState = { bears: 0 }; 76 | 77 | interface BearSlice { 78 | bears: number; 79 | addBear: () => void; 80 | eatFish: () => void; 81 | } 82 | 83 | const createBearSlice: StateCreator< 84 | BearSlice & FishSlice, 85 | [], 86 | [], 87 | BearSlice 88 | > = (set) => { 89 | resetters.push(() => set(initialBearState)); 90 | return { 91 | ...initialBearState, 92 | addBear: () => set((state) => ({ bears: state.bears + 1 })), 93 | eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), 94 | }; 95 | }; 96 | 97 | const initialStateFish = { fishes: 0 }; 98 | 99 | interface FishSlice { 100 | fishes: number; 101 | addFish: () => void; 102 | } 103 | 104 | const createFishSlice: StateCreator< 105 | BearSlice & FishSlice, 106 | [], 107 | [], 108 | FishSlice 109 | > = (set) => { 110 | resetters.push(() => set(initialStateFish)); 111 | return { 112 | ...initialStateFish, 113 | addFish: () => set((state) => ({ fishes: state.fishes + 1 })), 114 | }; 115 | }; 116 | 117 | const useBoundStore = create()((...a) => ({ 118 | ...createBearSlice(...a), 119 | ...createFishSlice(...a), 120 | })); 121 | 122 | export const resetAllSlices = () => resetters.forEach((resetter) => resetter()); 123 | 124 | export default useBoundStore; 125 | ``` 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Contributors][contributors-shield]][contributors-url] 2 | [![Forks][forks-shield]][forks-url] 3 | [![Stargazers][stars-shield]][stars-url] 4 | [![Issues][issues-shield]][issues-url] 5 | [![license: CC BY-NC-SA 4.0](https://img.shields.io/badge/license-CC%20BY--NC--SA%204.0-lightgrey.svg)][license-url] 6 | 7 | 8 |
9 |

10 | 11 | Logo 12 | 13 | 14 |

15 | 在线阅读 >> 16 |
17 |
18 | 代码案例 19 | · 20 | 参考资料 21 | 22 |

23 |

24 | 25 | # React Series - React 入门与工程实践篇 26 | 27 | 本篇归属于 [Web Series,Web 开发入门与工程实践](https://github.com/wx-chevalier/Web-Notes),其主要以 React 为核心的技术体系为主线,为读者构建完整的前端技术知识体系,探讨前端工程化的思想,并且能使不同技术水准的读者都有所得。传统上,Web 应用的 UI 是使用模板或 HTML 构建的。这些模板就是你可以用来构建 UI 的全套抽象。React 将用户界面分解为各个组件,发展出了构建 UI 的全新途径。构建可以管理自己状态的封装组件,然后将它们组合起来制作成复杂的 UI。 28 | 29 | ![Think React](https://ngte-superbed.oss-cn-beijing.aliyuncs.com/item/20230417210044.png) 30 | 31 | # Nav | 关联导航 32 | 33 | - 如果你是颇有经验的开发者,想要了解前端工程化与架构方面的知识,那么建议阅读[《Web-Notes](https://github.com/wx-chevalier/Web-Notes)》一文。 34 | 35 | - 如果你希望了解 Node.js 全栈开发,可以参阅 [《Node-Notes](https://github.com/wx-chevalier/Node-Notes)》。 36 | 37 | # About 38 | 39 | ## Copyright & More | 延伸阅读 40 | 41 | ![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg) ![](https://parg.co/bDm) 42 | 43 | 笔者所有文章遵循[知识共享 署名 - 非商业性使用 - 禁止演绎 4.0 国际许可协议](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh),欢迎转载,尊重版权。您还可以前往 [NGTE Books](https://ng-tech.icu/books-gallery/) 主页浏览包含知识体系、编程语言、软件工程、模式与架构、Web 与大前端、服务端开发实践与工程架构、分布式基础架构、人工智能与深度学习、产品运营与创业等多类目的书籍列表: 44 | 45 | [![NGTE Books](https://s2.ax1x.com/2020/01/18/19uXtI.png)](https://ng-tech.icu/books-gallery/) 46 | 47 | 48 | 49 | 50 | [contributors-shield]: https://img.shields.io/github/contributors/wx-chevalier/repo.svg?style=flat-square 51 | [contributors-url]: https://github.com/wx-chevalier/repo/graphs/contributors 52 | [forks-shield]: https://img.shields.io/github/forks/wx-chevalier/repo.svg?style=flat-square 53 | [forks-url]: https://github.com/wx-chevalier/repo/network/members 54 | [stars-shield]: https://img.shields.io/github/stars/wx-chevalier/repo.svg?style=flat-square 55 | [stars-url]: https://github.com/wx-chevalier/repo/stargazers 56 | [issues-shield]: https://img.shields.io/github/issues/wx-chevalier/repo.svg?style=flat-square 57 | [issues-url]: https://github.com/wx-chevalier/repo/issues 58 | [license-shield]: https://img.shields.io/github/license/wx-chevalier/repo.svg?style=flat-square 59 | [license-url]: https://github.com/wx-chevalier/repo/blob/master/LICENSE.txt 60 | -------------------------------------------------------------------------------- /03~状态管理/Context/Context.md: -------------------------------------------------------------------------------- 1 | # Context 用于状态管理 2 | 3 | 如前文所述,React 单组件中允许使用 setState 进行状态管理,而对于组件树,我们可以使用 Props 逐层传递状态值与回调函数。不过这种方式无形会导致多层组件间的强耦合,并且会导致大量的冗余代码。像 Redux, MobX 这样的状态管理框架,它们的作用之一就是将状态代码独立于组件,并未多层组件提供直接的数据流通通道;实际上我们也可以利用 Context API 进行跨层组件间的数据传递,来构建简单的状态管理工具。[Unstated](https://github.com/jamiebuilds/unstated) 就是对于 Context API 进行简要封装形成的状态管理库,它并不需要开发者学习额外的 API 或者库用法,而只需要使用普通的 React 组件中的 setState 方法操作 state,并利用 Context 将其传递到子组件中。Unstated 中主要包含了 Container,Subscribe 以及 Provider 三个组件,其中 Provider 负责存放所有的内部状态实例,类似于 Redux 或者 Apollo 中的 Provider: 4 | 5 | ```js 6 | const App = () => ( 7 | 8 |
9 | 10 | ); 11 | ``` 12 | 13 | Unstated 会在内部创建 Context 对象,并在 Provider 中包裹 Context.Provider 对象: 14 | 15 | ```js 16 | const StateContext = createReactContext(null); 17 | 18 | ... 19 | 20 | export function Provider(props: ProviderProps) { 21 | return ( 22 | // 集成父组件中的 Provider 23 | 24 | { 25 | ... 26 | return ( 27 | 28 | {props.children} 29 | 30 | ); 31 | }} 32 | 33 | ); 34 | } 35 | ``` 36 | 37 | Container 是朴素的拥有 setState 方法的 JavaScript 类,其仅负责进行状态操作,其用法如下: 38 | 39 | ```js 40 | // BookContainer.js 41 | import { Container } from "unstated"; 42 | class BookContainer extends Container { 43 | state = { 44 | books: [], 45 | booksVisible: false 46 | }; 47 | addBook = book => { 48 | const books = [...this.state.books]; 49 | books.push(book); 50 | this.setState({ books }); 51 | }; 52 | toggleVisibility = () => { 53 | this.setState({ 54 | booksVisible: !this.state.booksVisible 55 | }); 56 | }; 57 | } 58 | export { BookContainer }; 59 | ``` 60 | 61 | 参考 Container 的源代码,可以发现其主要是对 setState 进行了复写: 62 | 63 | ```js 64 | // ... 65 | setState(state: $Shape) { 66 | this.state = Object.assign({}, this.state, state); 67 | this._listeners.forEach(fn => fn()); 68 | } 69 | // ... 70 | ``` 71 | 72 | Subscribe 组件则提供了将 Container 实例传递给自定义组件的媒介,当状态变化时,组件会进行自动渲染: 73 | 74 | ```js 75 | 76 | {(bookStore, counterStore) => { 77 | const { state: { books, booksVisible } } = bookStore 78 | { 79 | booksVisible && books.map((book, index) => ( 80 |
81 |

{book.name}

82 |

{book.author}

83 |
84 | ) 85 | } 86 | }} 87 |
88 | ``` 89 | 90 | Subscribe 在组件内提供了 Context.Consumer 包裹,并且自动创建 Container/Store 实例: 91 | 92 | ```js 93 | instance = new Container(); 94 | safeMap.set(Container, instance); 95 | 96 | ... 97 | 98 | render() { 99 | return ( 100 | 101 | {map => 102 | this.props.children.apply( 103 | null, 104 | this._createInstances(map, this.props.to) 105 | ) 106 | } 107 | 108 | ); 109 | } 110 | ``` 111 | -------------------------------------------------------------------------------- /04~工程实践/服务端组件/README.md: -------------------------------------------------------------------------------- 1 | # React Server Components 2 | 3 | 服务端组件允许开发人员构建跨越服务器和客户端的应用程序,将客户端应用程序的丰富交互性与传统服务器渲染的改进性能相结合。 4 | 5 | - 服务端组件只在服务器上运行,对捆绑包大小没有影响。它们的代码永远不会下载到客户端,有助于减少捆绑包大小,改善启动时间。 6 | - 服务端组件可以访问服务器端的数据源,如数据库、文件系统或(微)服务。 7 | - 服务端组件可与客户端组件(即传统的 React 组件)无缝集成。服务端组件可以在服务器上加载数据,并将其作为 Props 传递给客户端组件,允许客户端处理渲染页面的交互部分。 8 | - 服务端组件可以动态地选择要渲染的客户端组件,允许客户端只下载渲染页面所需的最少代码。 9 | - 服务端组件在重新加载时保留客户端状态。这意味着当服务端组件树被重新加载时,客户端的状态、焦点、甚至正在进行的动画都不会被中断或重置。 10 | - 服务端组件是以渐进式和增量式的方式将 UI 的渲染单元流向客户端。结合 Suspense,这使得开发人员能够制作有意的加载状态,并在等待页面剩余部分加载时快速显示重要内容。 11 | - 开发人员还可以在服务器和客户端之间共享代码,允许用一个组件在一个路由上渲染服务器上某些内容的静态版本,并在不同的路由上渲染客户端上该内容的可编辑版本。 12 | 13 | # 基础示例 14 | 15 | 这个例子渲染了一个带有标题和正文的简单 Note。它使用服务器组件渲染 Note 的不可编辑视图,并使用客户端组件(传统的 React 组件)选择性地渲染 Note 的编辑器。首先,我们在服务器上渲染 Note。我们的工作惯例是用.server.js 后缀(或.server.jsx、.server.tsx 等)来命名服务器组件。 16 | 17 | ```js 18 | // Note.server.js - Server Component 19 | 20 | import db from "db.server"; 21 | // (A1) We import from NoteEditor.client.js - a Client Component. 22 | import NoteEditor from "NoteEditor.client"; 23 | 24 | function Note(props) { 25 | const { id, isEditing } = props; 26 | // (B) Can directly access server data sources during render, e.g. databases 27 | const note = db.posts.get(id); 28 | 29 | return ( 30 |
31 |

{note.title}

32 |
{note.body}
33 | {/* (A2) Dynamically render the editor only if necessary */} 34 | {isEditing ? : null} 35 |
36 | ); 37 | } 38 | ``` 39 | 40 | 这个例子说明了几个关键点。 41 | 42 | - 这 "只是" 一个 React 组件:它接收道具并渲染一个视图。服务器组件有一些限制--例如,它们不能使用状态或效果--但总的来说,它们的工作与你所期望的一样。更多的细节在下面的 Capabilities & Constraints of Server and Client Components 中提供。 43 | - 服务器组件可以直接访问服务器数据源,如数据库,如(B)所示。这是通过一个通用机制实现的,允许社区创建兼容的 API,与各种数据源一起工作。 44 | - 服务器组件可以通过导入和渲染一个 "客户端" 组件将渲染工作交给客户端,如(A1)和(A2)中分别说明。客户端组件使用 .client.js 后缀(或 .client.jsx、.client.tsx 等)。捆绑程序会将这些导入与其他动态导入进行类似的处理,有可能根据各种启发式方法将它们拆分到另一个捆绑程序中。在这个例子中,只有当 props.isEditing 为真时,NodeEditor.client.js 才会被加载到客户端。 45 | 46 | 相比之下,客户端组件是你已经习惯的典型组件。它们可以访问 React 的全部功能:状态、效果、访问 DOM 等。"客户端组件" 这个名字并没有任何新的含义,它只是为了将这些组件与服务器组件区分开来。继续我们的例子,下面是我们如何实现 Note 编辑器。 47 | 48 | ```js 49 | // NodeEditor.client.js - Client Component 50 | 51 | export default function NoteEditor(props) { 52 | const note = props.note; 53 | const [title, setTitle] = useState(note.title); 54 | const [body, setBody] = useState(note.body); 55 | const updateTitle = (event) => { 56 | setTitle(event.target.value); 57 | }; 58 | const updateBody = (event) => { 59 | setTitle(event.target.value); 60 | }; 61 | const submit = () => { 62 | // ...save note... 63 | }; 64 | return ( 65 |
66 | 67 | 70 |
71 | ); 72 | } 73 | ``` 74 | 75 | 这看起来像一个普通的 React 组件,因为它就是。客户端组件只是普通的组件。一个重要的考虑因素是,当 React 在客户端上渲染 Server Components 的结果时,它保留了之前可能已经渲染的 Client Components 的状态。具体来说,React 会将从服务器传递过来的新道具合并到现有的 Client Components 中,维护这些组件的状态(和 DOM),以保留焦点、状态和任何正在进行的动画。 76 | -------------------------------------------------------------------------------- /02~组件基础/06~组件库/Antd/文件上传.md: -------------------------------------------------------------------------------- 1 | # 文件上传 2 | 3 | # Upload 4 | 5 | ## 图片上传 6 | 7 | ```js 8 | class UploadThumb extends PureComponent { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | loading: false, 13 | imageUrl: "" 14 | }; 15 | } 16 | 17 | handleChange(info) { 18 | if (info.file.status === "uploading") { 19 | this.setState({ loading: true }); 20 | return; 21 | } 22 | if (info.file.status === "done") { 23 | // Get this url from response in real world. 24 | this.getBase64(info.file.originFileObj, imageUrl => 25 | this.setState({ imageUrl, loading: false }) 26 | ); 27 | } 28 | } 29 | 30 | getBase64(img, callback) { 31 | const reader = new FileReader(); 32 | reader.addEventListener("load", () => callback(reader.result)); 33 | reader.readAsDataURL(img); 34 | } 35 | 36 | beforeUpload(file) { 37 | const isJPG = file.type === "image/jpeg"; 38 | if (!isJPG) { 39 | message.error("You can only upload JPG file!"); 40 | } 41 | const isLt2M = file.size / 1024 / 1024 < 2; 42 | if (!isLt2M) { 43 | message.error("Image must smaller than 2MB!"); 44 | } 45 | return isJPG && isLt2M; 46 | } 47 | 48 | render() { 49 | const uploadButton = ( 50 |
51 | 52 |
Upload
53 |
54 | ); 55 | 56 | return ( 57 | 66 | {this.state.imageUrl ? ( 67 | 68 | ) : ( 69 | uploadButton 70 | )} 71 | 72 | ); 73 | } 74 | } 75 | ``` 76 | 77 | ## 文件转换 78 | 79 | ```js 80 | const props = { 81 | action: "https://www.mocky.io/v2/5cc8019d300000980a055e76", 82 | transformFile(file) { 83 | return new Promise(resolve => { 84 | const reader = new FileReader(); 85 | reader.readAsDataURL(file); 86 | reader.onload = () => { 87 | const canvas = document.createElement("canvas"); 88 | const img = document.createElement("img"); 89 | img.src = reader.result; 90 | img.onload = () => { 91 | const ctx = canvas.getContext("2d"); 92 | ctx.drawImage(img, 0, 0); 93 | ctx.fillStyle = "red"; 94 | ctx.textBaseline = "middle"; 95 | ctx.fillText("Ant Design", 20, 20); 96 | canvas.toBlob(resolve); 97 | }; 98 | }; 99 | }); 100 | } 101 | }; 102 | 103 | ReactDOM.render( 104 |
105 | 106 | 109 | 110 |
, 111 | mountNode 112 | ); 113 | ``` 114 | -------------------------------------------------------------------------------- /04~工程实践/02~数据加载/Suspense.md: -------------------------------------------------------------------------------- 1 | # Suspense 2 | 3 | React Suspense 全部涉及处理具有异步数据需求的视图之间的转换: 4 | 5 | ```js 6 | import { createCache } from "react-cache"; 7 | import { createResource } from "react-cache"; 8 | 9 | export let cache = createCache(); 10 | 11 | export let InvoiceResource = createResource((id) => { 12 | return fetch(`/invoices/${id}`).then((response) => { 13 | return response.json(); 14 | }); 15 | }); 16 | ``` 17 | 18 | ```js 19 | import cache from "./cache"; 20 | import InvoiceResource from "./InvoiceResource"; 21 | 22 | let Invoice = ({ invoiceId }) => { 23 | let invoice = InvoiceResource.read(cache, invoiceId); 24 | return

{invoice.number}

; 25 | }; 26 | ``` 27 | 28 | React 开始渲染(在内存中)。它打到了 InvoicesResource.read() 调用。该键的缓存(id 为键)将为空,因此它将调用我们提供给 createResource 的函数,从而触发异步获取。然后缓存将抛出我们返回的承诺(是的,我也从未考虑过抛出任何错误,但也有错误,但是如果需要可以抛出窗口。)抛出之后,不再执行任何代码。React 等待承诺解决。诺言解决。React 尝试再次渲染发票(在内存中)。它再次点击 InvoicesResource.read() 。这次数据位于缓存中,因此可以从 ApiResource.read() 同步返回我们的数据。React 将页面呈现到 DOM: 29 | 30 | ```js 31 | // the store and reducer 32 | import { createStore } from "redux"; 33 | import { connect } from "react-redux"; 34 | 35 | let reducer = (state, action) => { 36 | if (action.type === "LOADED_INVOICE") { 37 | return { 38 | ...state, 39 | invoice: action.data, 40 | }; 41 | } 42 | return state; 43 | }; 44 | 45 | let store = createStore(reducer); 46 | 47 | ///////////////////////////////////////////// 48 | // the action 49 | function fetchInvoice(dispatch, id) { 50 | fetch(`/invoices/${id}`).then((response) => { 51 | dispatch({ 52 | type: "LOADED_INVOICE", 53 | data: response.json(), 54 | }); 55 | }); 56 | } 57 | 58 | ///////////////////////////////////////////// 59 | // the component, all connected up 60 | class Invoice extends React.Component { 61 | componentDidMount() { 62 | fetchInvoice(this.props.dispatch, this.props.invoiceId); 63 | } 64 | 65 | componentDidUpdate(prevProps) { 66 | if (prevProps.invoiceId !== this.props.invoiceId) { 67 | fetchInvoice(this.props.dispatch, this.props.invoiceId); 68 | } 69 | } 70 | 71 | render() { 72 | if (!this.props.invoice) { 73 | return null; 74 | } 75 | return

{invoice.number}

; 76 | } 77 | } 78 | 79 | export default connect((state) => { 80 | return { invoices: state.invoices }; 81 | })(Invoices); 82 | ``` 83 | 84 | ```js 85 | import React, { Suspense, Fragment, memo } from "react"; 86 | import { unstable_createResource } from "react-cache"; 87 | 88 | const Fetcher = unstable_createResource(() => 89 | fetch("https://jsonplaceholder.typicode.com/todos").then((r) => r.json()) 90 | ); 91 | 92 | const List = () => { 93 | const data = Fetcher.read(); 94 | return ( 95 |
    96 | {data.map((item) => ( 97 |
  • 98 | {item.title} 99 |
  • 100 | ))} 101 |
102 | ); 103 | }; 104 | 105 | const App = () => ( 106 | 107 |

{`React: ${React.version} Demo`}

108 | Loading...
}> 109 | 110 | 111 | 112 | ); 113 | 114 | const MemoApp = memo(App); 115 | 116 | export default MemoApp; 117 | ``` 118 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/Context.md: -------------------------------------------------------------------------------- 1 | # React Context 详解 2 | 3 | # Context 基本使用 4 | 5 | [CodeSandbox/Context](https://codesandbox.io/embed/1yx4kl1jz7) 6 | 7 | ```js 8 | const ThemeContext = React.createContext("dark"); 9 | 10 | export const { Consumer, Provider } = ThemeContext; 11 | 12 | // This is a HOC function. 13 | // It takes a component... 14 | export default function withTheme(Component) { 15 | // ...and returns another component... 16 | return function ThemedComponent(props) { 17 | // ... and renders the wrapped component with the context theme! 18 | // Notice that we pass through any additional props as well 19 | return ( 20 | {(theme) => } 21 | ); 22 | }; 23 | } 24 | ``` 25 | 26 | ```js 27 | function Header({ children, theme }) { 28 | return

{children}

; 29 | } 30 | 31 | // Use the withTheme HOC to inject the context theme, 32 | // Without having to bloat our component to reference it: 33 | export default withTheme(Header); 34 | ``` 35 | 36 | # HoC 封装 37 | 38 | # Hooks 封装 39 | 40 | # 老版本的 Context API 41 | 42 | 16.3 版本之前的 React 中的 Context 一直是实验特性,其使用也比较麻烦,这里我们简单回顾下,以方便使用老版本 React 的开发者。如果希望在组件中使用 `Context`,我们需要引入 `contextTypes`、`getChildContext`、`childContextTypes` 这三个属性: 43 | 44 | - getChildContext: 该函数是父组件的类函数之一,它会返回子函数中获取到的 `this.context` 的值内容,因此我们需要在这里设置子函数能够返回的属性信息。 45 | 46 | - childContextTypes: 该对象用于描述 `getChildContext` 返回值的数据结构,其会起到类似于 `propTyps` 这样的类型校验功能。 47 | 48 | - contextTypes: 该对象在子组件中用于描述父组件提供的上下文数据结构,可以将它看做子组件对于父组件的请求,同时也会起到类型检测的作用。 49 | 50 | 参考 React 官方示例,我们可以通过如下方式来使用 Context 跨层传递数据: 51 | 52 | ```js 53 | import PropTypes from "prop-types"; 54 | 55 | class Button extends React.Component { 56 | render() { 57 | return ( 58 | 61 | ); 62 | } 63 | } 64 | 65 | Button.contextTypes = { 66 | color: PropTypes.string, 67 | }; 68 | 69 | class Message extends React.Component { 70 | render() { 71 | return ( 72 |
73 | {this.props.text} 74 |
75 | ); 76 | } 77 | } 78 | 79 | class MessageList extends React.Component { 80 | getChildContext() { 81 | return { color: "purple" }; 82 | } 83 | 84 | render() { 85 | const children = this.props.messages.map((message) => ( 86 | 87 | )); 88 | return
{children}
; 89 | } 90 | } 91 | 92 | MessageList.childContextTypes = { 93 | color: PropTypes.string, 94 | }; 95 | ``` 96 | 97 | 通过为 MessageList 组件添加 childContextTypes 与 getChildContext 属性,React 会自动将 getChildContext 返回的值传递到子组件树中。不过,React 官方并不建议我们大量使用 Context,原因概括为以下几点: 98 | 99 | - 老版本的 Context API 允许以 Props 方式透传,其问题在于破坏了组件本身的可移植性,或者说是分形架构,增强了组件间的耦合度。所谓的分形架构,即组件树中的任一部分能够被独立抽取使用,并且方便移植到其他组件树中。(参考[诚身](https://www.zhihu.com/question/267168180/answer/319754359)的回答) 100 | 101 | - 尽管其可以减少逐层传递带来的冗余代码,尽量的解耦和组件,但是当构造复杂时,我们也会陷入抽象漏洞,无法去判断 `Context` 到底是哪个父组件提供的。此时 `Context` 就像所谓的全局变量一样,大量的全局变量的使用会导致组件的不可以预测性,导致整个系统的鲁棒性降低。 102 | 103 | - `Context` 并不会触发组件重渲染,如果组件树中的某个组件的 `shouldComponentUpdate` 函数返回 `false` 而避免了子层组件的重渲染,那么新的 Context 值也就无法传递到子层组件,而导致目标组件无法保证自己每次都可以接收到更新后的 Context 值。 104 | 105 | # Links 106 | 107 | - https://juejin.im/post/5a90e0545188257a63112977 108 | - https://www.techiediaries.com/react-context-api-tutorial/ 109 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/Props.md: -------------------------------------------------------------------------------- 1 | # React Props 2 | 3 | # Component Properties 4 | 5 | Function as Prop 6 | 7 | ```js 8 | const Foo = ({ hello }) => { 9 | return hello("foo"); 10 | }; 11 | 12 | const hello = name => { 13 | return
`hello from ${name}`
; 14 | }; 15 | 16 | ; 17 | ``` 18 | 19 | Component Injection 20 | 21 | ``` 22 | class WindowWidth extends React.Component { 23 |   constructor(props) { 24 |   super(props); 25 |   this.state = { width: 0 }; 26 |   } 27 | 28 | 29 |  ... 30 | 31 | 32 |   render() { 33 | 34 | 35 |   const { width } = this.state; 36 |     const { Width } = this.props; 37 |   return ; 38 |  } 39 | } 40 | 41 | 42 | 43 | 44 | 45 | 46 | const DisplayDevice = ({ width }) => { 47 |   let device = null; 48 |   if (width <= 480) { 49 |   device = 'mobile'; 50 |   } else if (width <= 768) { 51 |   device = 'tablet'; 52 |   } else { 53 |   device = 'desktop'; 54 |   } 55 |   return
you are using a {device}
; 56 | }; 57 | ``` 58 | 59 | # Prop Validation 60 | 61 | # children 62 | 63 | ## 渲染回调 64 | 65 | 渲染回调(Render Callback)即指那些子元素为某个函数的组件,也就是所谓的 Function-as-Child;我们可以利用这种模式复用有状态组件从而共享部分业务逻辑。如果需要定义渲染回调,则需要在 render 函数中返回对于传入的子元素的调用结果: 66 | 67 | ```js 68 | import { Component } from "react"; 69 | 70 | class SharedThing extends Component { 71 | // ... 72 | 73 | render() { 74 | return this.props.children(thing1, thing2); 75 | } 76 | } 77 | 78 | export default SharedThing; 79 | ``` 80 | 81 | 然后在其他组件中我们可以调用该组件并且获得该组件的执行结果: 82 | 83 | ```js 84 | import React from 'react' 85 | 86 | const AnotherComponent = () => ( 87 |   88 |   {(thing1, thing2) => ( 89 |   // use thing1 and thing2 90 |   )} 91 |   92 | ) 93 | 94 | export default AnotherComponent 95 | ``` 96 | 97 | 一个比较典型的案例就是共享开关逻辑,某个开关组件 Toggle 会在内部存放用来表示当前开关状态的 `toggled` 变量,我们可以通过渲染回调的模式在将控制开关的逻辑提取出来: 98 | 99 | ```js 100 | import { Component } from "react"; 101 | 102 | class Toggle extends Component { 103 | state = { 104 | isOpen: false 105 | }; 106 | 107 | handleToggleClick = () => { 108 | this.setState({ 109 | isOpen: !this.state.isOpen 110 | }); 111 | }; 112 | 113 | render() { 114 | return this.props.children(this.state.isOpen, this.handleToggleClick); 115 | } 116 | } 117 | 118 | export default Toggle; 119 | ``` 120 | 121 | 现在所有使用 Toggle 的组件都能够访问到内部的 `isOpen` 状态并且能够使用 `handleToggleClick` 函数来触发 Toggle 内部状态的变化: 122 | 123 | ```js 124 | import React from "react"; 125 | import Toggle from "./Toggle"; 126 | 127 | const Accordion = ({ teaser, details }) => ( 128 | 129 | {(isOpen, handleToggleClick) => ( 130 |
131 | {`${isOpen ? "-" : "+"} ${teaser}`}  132 | {isOpen && details} 133 |
134 | )} 135 |
136 | ); 137 | 138 | export default Accordion; 139 | ``` 140 | 141 | ```js 142 | const Thumbnail = ({ src, teaser }) => ( 143 | 144 | {(isOpen, handleToggleClick) => ( 145 |
146 |
{teaser}
147 | {teaser} 155 |
156 | )} 157 |
158 | ); 159 | 160 | export default Thumbnail; 161 | ``` 162 | 163 | ## cloneElement 164 | -------------------------------------------------------------------------------- /03~状态管理/MobX/README.md: -------------------------------------------------------------------------------- 1 | # 使用 MobX 存储应用状态 2 | 3 | MobX 遵循透明函数响应式编程 TFRP 的设计理念,允许我们按照惯有的面向对象的思想来编写代码,而不需要去学习很多新的范式或者抽象概念。 4 | 5 | 严格来说,MobX 应该看做数据流(Dataflow)库,而不是一个完整的状态管理框架;MobX 也不限定于双向数据流,在实践中,我们同样推崇单向数据流的机制,即避免在组件中以属性访问的方式进行值修改。Redux 则是有着强约束的 Opinionated 状态管理框架。Redux 为我们提供了完全地可预测性(Predicability)与可测试性(Testability)的同时,也带了流程的碎片化与大量的模板代码,其异步处理的流程也相对复杂。而 Cycle.js 同样学习曲线也较为陡峭,其遵循不同的设计范式,需要使用者充分理解 RxJS 的理念。与基于 React Context 的原生状态管理方案,MobX 又能提供独立于框架的状态管理方案,便于我们分割与重用逻辑代码,并且进行测试用例。 6 | 7 | # MobX 的基础使用 8 | 9 | ## 测试 10 | 11 | ```js 12 | class MessageStore { 13 | // bad 14 | markMessageAsRead = (message) => { 15 | if (message.status === "new") { 16 | fetch({ 17 | method: "GET", 18 | path: `/notification/read/${message.id}`, 19 | }).then(() => (message.status = "read")); 20 | } 21 | }; 22 | // good 23 | markMessageAsRead = (message) => { 24 | if (message.status !== "new") { 25 | return Promise.reject("Message is not new"); 26 | } 27 | // it's now easily mockable 28 | return api.markMessageAsRead(message).then(() => { 29 | // this is a pure function 30 | // you can test it easily 31 | return this.updateMessage(message, { status: " read" }); 32 | }); 33 | }; 34 | } 35 | ``` 36 | 37 | # MobX 与 React 集成范式 38 | 39 | # MST 40 | 41 | # MobX 的设计理念 42 | 43 | ## MobX 与 Redux 的对比 44 | 45 | 本文的最后,我们也讨论下 46 | 47 | First of all, state within Redux is simply represented as plain objects. Nothing more. Redux makes state tangible. You can pick up objects and drop them in a different place. Like in local storage or in the body of a network request. 48 | 49 | Secondly, Redux offers many constraints that make it easy to write generic algorithms that reason about the state without specific domain knowledge (unlike reducers). Such algorithms are typically expressed as middleware. Creating such mechanisms is feasible, partly because the data is immutable, but mostly because the state is tree shaped and preferably serializable. This makes it possible to traverse any Redux state tree in a predictable manner. 50 | 51 | [mobx-state-tree](https://github.com/mobxjs/mobx-state-tree) 52 | 53 | ```js 54 | await this.props.actions.choose({ 55 | workShiftIdList: _.map(items, (d) => d.workShiftId), 56 | }); 57 | this.trace("batch-chose", { size: items.length }); 58 | await this.fetchTableData(); 59 | 60 | actions.chooseWorks = (works) => (dispatch) => { 61 | choose(works).then( 62 | () => { 63 | // 更新结果信息 64 | dispatch(createAction(CHOOSE_WORKS)); 65 | // 执行重新抓取操作 66 | dispatch(actions.fetchChoices()); 67 | // 清空已选 68 | dispatch({ 69 | type: `${prefix}/CLEAR_SELECTED_WORKs`, 70 | }); 71 | }, 72 | () => {} 73 | ); 74 | }; 75 | ``` 76 | 77 | # WebSocket 78 | 79 | ```ts 80 | import { onBecomeObserved, onBecomeUnobserved } from "mobx"; 81 | import { observable, decorate } from "mobx"; 82 | class AutoObservable { 83 | data: T; 84 | 85 | constructor(onObserved: () => void, onUnobserved: () => void) { 86 | onBecomeObserved(this, "data", onObserved); 87 | onBecomeUnobserved(this, "data", onUnobserved); 88 | } 89 | } 90 | decorate(AutoObservable, { 91 | data: observable, 92 | }); 93 | ``` 94 | 95 | ```ts 96 | let autoObservable: AutoObservable; 97 | let socket: Websocket; 98 | const openStream = () => { 99 | socket = new Websocket("ws://localhost:8080"); 100 | socket.on("message", (message) => { 101 | autoObservable.data = message; 102 | }); 103 | }; 104 | const closeStream = () => { 105 | socket.close(); 106 | }; 107 | autoObservable = new AutoObservable(openStream, closeStream); 108 | ``` 109 | -------------------------------------------------------------------------------- /03~状态管理/Redux/Redux Hooks.md: -------------------------------------------------------------------------------- 1 | # Redux Hooks 2 | 3 | 在函数式组件中如果希望使用 Redux,我们可以使用 connect 函数注入 State 与 Action Creator,也可以使用 React Redux 提供的 Hooks Api。 4 | 5 | # useSelector 6 | 7 | ```js 8 | const result : any = useSelector(selector : Function, equalityFn? : Function) 9 | ``` 10 | 11 | useSelector 允许我们通过传入的 selector 函数将 State 中数据提取出来,其相当于 connect 中的 mapStateToProps 的函数。该 selector 会在组件重渲染时候被调用,useSelector 同样会监听 Redux store 的变化,然后在某个 action 分发时调用。 12 | 13 | 当某个 action 被分发时,useSelector 会对之前 selector 返回的结果与当前的结果进行对比;当发现值不同时,该组件会被强制重渲染。useSelector 值会使用严格比较(`===`)来判断值的变化,而 connect 函数会使用浅比较(`==`)来判断是否需要进行重渲染。在 mapState 中,所有指定的返回域会被合并为某个对象,connect 会自动去比较单个属性值是否发生变化。而 useSelector 中则是会直接比较 selector 函数的返回值;。 14 | 15 | ```js 16 | import React from "react"; 17 | import { useSelector } from "react-redux"; 18 | 19 | export const CounterComponent = () => { 20 | const counter = useSelector(state => state.counter); 21 | return
{counter}
; 22 | }; 23 | 24 | // 如果需要引用 Props 中的数据,则以闭包方式传入 25 | export const TodoListItem = props => { 26 | const todo = useSelector(state => state.todos[props.id]); 27 | return
{todo.text}
; 28 | }; 29 | ``` 30 | 31 | 在上述的用法中,每次组件渲染的时候都会创建新的 selector 函数实例;我们可以使用 reselect 来创建可缓存的 selector 函数: 32 | 33 | ```js 34 | import React from "react"; 35 | import { useSelector } from "react-redux"; 36 | import { createSelector } from "reselect"; 37 | 38 | const selectNumOfDoneTodos = createSelector( 39 | state => state.todos, 40 | todos => todos.filter(todo => todo.isDone).length 41 | ); 42 | 43 | export const DoneTodosCounter = () => { 44 | const NumOfDoneTodos = useSelector(selectNumOfDoneTodos); 45 | return
{NumOfDoneTodos}
; 46 | }; 47 | 48 | export const App = () => { 49 | return ( 50 | <> 51 | Number of done todos: 52 | 53 | 54 | ); 55 | }; 56 | ``` 57 | 58 | # useDispatch 59 | 60 | ```js 61 | const dispatch = useDispatch(); 62 | ``` 63 | 64 | 该 Hook 会返回 Redux store 中的 dispatch 函数的引用,可以永安里分发 Action: 65 | 66 | ```js 67 | import React from "react"; 68 | import { useDispatch } from "react-redux"; 69 | 70 | export const CounterComponent = ({ value }) => { 71 | const dispatch = useDispatch(); 72 | 73 | return ( 74 |
75 | {value} 76 | 79 |
80 | ); 81 | }; 82 | ``` 83 | 84 | 当我们在父组件封装某个事件处理函数时,建议是使用 useCallback 来创建缓存的函数,以避免子组件因为事件处理函数的变化而造成的无意义渲染: 85 | 86 | ```js 87 | import React, { useCallback } from "react"; 88 | import { useDispatch } from "react-redux"; 89 | 90 | export const CounterComponent = ({ value }) => { 91 | const dispatch = useDispatch(); 92 | const incrementCounter = useCallback( 93 | () => dispatch({ type: "increment-counter" }), 94 | [dispatch] 95 | ); 96 | 97 | return ( 98 |
99 | {value} 100 | 101 |
102 | ); 103 | }; 104 | 105 | export const MyIncrementButton = React.memo(({ onIncrement }) => ( 106 | 107 | )); 108 | ``` 109 | 110 | # useStore 111 | 112 | ```js 113 | const store = useStore(); 114 | ``` 115 | 116 | 该 Hook 会返回 Provider 中传入的 store 实例: 117 | 118 | ```js 119 | import React from "react"; 120 | import { useStore } from "react-redux"; 121 | 122 | export const CounterComponent = ({ value }) => { 123 | const store = useStore(); 124 | 125 | // EXAMPLE ONLY! Do not do this in a real app. 126 | // The component will not automatically update if the store state changes 127 | return
{store.getState()}
; 128 | }; 129 | ``` 130 | -------------------------------------------------------------------------------- /04~工程实践/02~数据加载/组件间通信.md: -------------------------------------------------------------------------------- 1 | # 组件间通信 2 | 3 | 本部分我们暂时不考虑使用 MobX 或者 Redux 进行状态管理 4 | 5 | # 父组件向子组件通信 6 | 7 | # 子组件向父组件通信 8 | 9 | # 跨级组件通信 10 | 11 | 当我们需要让子组件跨级获取数据时,最简单暴力的方法就是多层嵌套,将信息从顶层组件一级一级的传递下来。不过这样的弊端也是显而易见,中间层的组件被迫处理大量自己本身不需要的数据。我们在前文中讨论过 Context 的概念与用法,虽然 Context 存在潜在问题,但是其涉及初衷就是用来解耦组件,保证数据的跨级传递。譬如我们在设计某个表单时,产品经理要求当用户点击某个输入框时,列表自动滚动到该输入框所在位置。首先,控制表单滚动的事件应该由表单组件处理,并且我们不希望输入组件与表单组件强耦合,因此我们没有在 Form 组件中显式声明输入组件,而是通过`children`属性动态传入: 12 | 13 | ```js 14 | // Parent Component 15 | class Form extends Component { 16 | handleFocus = y => { 17 | this.scrollView.scrollTo({ y }); 18 | }; 19 | 20 | render() { 21 | const { children } = this.props; 22 | return ( 23 | { 25 | this.scrollView = r; 26 | }} 27 | > 28 | {children}  29 | 30 | ); 31 | } 32 | } 33 | ``` 34 | 35 | 然后在具体的输入组件中,我们需要传入父组件的`onFocus`函数: 36 | 37 | ```js 38 | // Child Component 39 | class FormTextInput extends Component { 40 | handleFocus = () => { 41 | if (!this.props.onFocus) return; 42 | this.props.onFocus(this.yPosition); 43 | }; 44 | 45 | handleLayout = ({ 46 | nativeEvent: { 47 | layout: { y } 48 | } 49 | }) => { 50 | this.yPosition = y; 51 | }; 52 | 53 | render() { 54 | return ( 55 | 56 | ); 57 | } 58 | } 59 | ``` 60 | 61 | 最后我们需要某个连接组件,将这两个控件关联起来: 62 | 63 | ```js 64 | // A developer using your weak non-contextual component 65 | class FormContainer extends Component { 66 | render() { 67 | return ( 68 |
{ 70 | this.form = r; 71 | }} 72 | > 73 | 74 | 75 | ); 76 | } 77 | } 78 | ``` 79 | 80 | 这种方式在一定程度上解决了强耦合的问题,不过在`Cards`组件中`CardTextInput`与`Card`还是需要显示的声明依赖关系。随着依赖复杂度的增加,我们不可避免的需要更多的构建弥合代码来进行依赖传递,也导致了大量的冗余的中间层组件。下面我们会使用`Context`来改造上述代码,如果希望在组件中使用`Context`,我们需要引入`contextTypes`、`getChildContext`、`childContextTypes`这三个属性。首先对于父组件,我们将`handleFocus`函数挂载到`Context`对象上: 81 | 82 | ```js 83 | // Parent Component 84 | class Formextends Component { 85 |   static childContextTypes = { 86 |   handleFocus: PropTypes.func, 87 |   } 88 | 89 | 90 |   getChildContext = () => ({ 91 |   handleFocus: this.handleFocus, 92 |   }) 93 | 94 | 95 |   handleFocus = (y) => { 96 |   this.scrollView.scrollTo({ y }); 97 |   } 98 | 99 | 100 |   render() { 101 |   const {children} = this.props; 102 |   return ( 103 |   {this.scrollView = r}}> 104 |   {children} 105 |   106 |   ); 107 |   } 108 | 109 | } 110 | ``` 111 | 112 | 然后在子组件中,我们显式声明依赖的父组件的`Context`结构: 113 | 114 | ```js 115 | // Child Component 116 | class FormTextInput extends Component { 117 | static contextTypes = { 118 | handleFocus: PropTypes.func 119 | }; 120 | 121 | handleFocus = () => { 122 | if (!this.context.handleFocus) return; 123 | this.context.handleFocus(this.yPosition); 124 | }; 125 | 126 | handleLayout = ({ 127 | nativeEvent: { 128 | layout: { y } 129 | } 130 | }) => { 131 | this.yPosition = y; 132 | }; 133 | 134 | render() { 135 | return ( 136 | 137 | ); 138 | } 139 | } 140 | ``` 141 | 142 | 最后在使用这两个组件的地方,我们不需要再以`ref`进行句柄传递,而是直接嵌套即可: 143 | 144 | ```js 145 | // A developer using your awesome it-just-works component 146 | class FormContainer extends Component { 147 | render() { 148 | return ( 149 |
150 | 151 | 152 | ); 153 | } 154 | } 155 | ``` 156 | 157 | 不过这里还是需要强调的是,`Context`的改变并不会触发`props`或者`state`的改变,因此也无法触发组件发生重渲染,并且大量使用`Context`的情况下也会导致依赖混乱,因此我们在真实的项目中还是需要慎用`Context`。 158 | 159 | # 无嵌套关系组件通信 160 | -------------------------------------------------------------------------------- /02~组件基础/06~组件库/Form/React Hook Form.md: -------------------------------------------------------------------------------- 1 | # React Hook Form 2 | 3 | # 与样式库协同 4 | 5 | # Antd 6 | 7 | 首先,我们定义组件构造函数: 8 | 9 | ```tsx 10 | import React from "react"; 11 | import { Input, Select, Switch } from "antd"; 12 | const { Option } = Select; 13 | 14 | export const inputField = (placeholder) => { 15 | return ; 16 | }; 17 | 18 | export const SelectField = (defaultValue, values) => { 19 | return ( 20 | 29 | ); 30 | }; 31 | 32 | export const SwitchField = () => { 33 | return ; 34 | }; 35 | ``` 36 | 37 | 然后将其利用 Controller 连接组件: 38 | 39 | ```ts 40 | import React from "react"; 41 | import { useForm, Controller } from "react-hook-form"; 42 | import { inputField, SelectField, SwitchField } from "./Inputs"; 43 | import { Button } from "antd"; 44 | 45 | function Login(props) { 46 | const { handleSubmit, control, errors, reset } = useForm(); 47 | const type = ["Student", "Developer", "other"]; 48 | 49 | const onSubmit = (data) => { 50 | console.log(data); 51 | setTimeout( 52 | () => 53 | reset({ 54 | FirstName: "", 55 | LastName: "", 56 | Email: "", 57 | }), 58 | 1000 59 | ); 60 | }; 61 | return ( 62 |
63 |
64 | 65 | 72 | {errors.FirstName && ( 73 | This field is required 74 | )} 75 |
76 |
77 | 78 | 85 | {errors.LastName && ( 86 | This field is required 87 | )} 88 |
89 |
90 | 91 | 101 | {errors.Email && ( 102 | Please add a valid email 103 | )} 104 |
105 |
106 | 107 | 116 |
117 |
118 | 119 | 125 |
126 | 129 |
130 | ); 131 | } 132 | 133 | export default Login; 134 | ``` 135 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/不可变操作.md: -------------------------------------------------------------------------------- 1 | # 不可变操作 2 | 3 | # JavaScript Immutablitiy 4 | 5 | React 开发中的一大痛点就是对于状态的处理与更新,譬如在我们需要创建编辑用户信息的表单时,往往我们会创建单一的响应处理函数来处理表单数据的变更,其形式可能如下: 6 | 7 | ```js 8 | updateState(event) { 9 |  const {name, value} = event.target; 10 |  let user = this.state.user; 11 |  user[name] = value; 12 |  return this.setState({user}); 13 | } 14 | ``` 15 | 16 | 不过这种方式算是典型的错误,其并不能实际地触发页面重渲染;这是因为变量 `user` 只是对于状态的引用,React 默认的更新机制采用了浅比较,因此并不会判断对象的属性值是否发生了变化。React 官方文档中建议我们将 `this.state` 当做不可变数据对待,我们不应该直接修改 `this.state` 中的某个状态的属性值,而是应该像纯函数那样构造出新的对象;本部分即是讨论几种 React 开发中常见的不可变数据结构的操作方法。 17 | 18 | ## 原生不可变性实现 19 | 20 | `Object.assign` 能够创建对象的拷贝,首个参数为需要拷贝的目标,我们往往会传入新创建的空对象;而后续的参数表示拷贝的其他来源。利用 `Object.assign` 复写的上述代码为: 21 | 22 | ```js 23 | updateState(event) { 24 |  const {name, value} = event.target; 25 |  let user = Object.assign({}, this.state.user); 26 |  user[name] = value; 27 |  return this.setState({user}); 28 | } 29 | ``` 30 | 31 | 通过 `Object.assign` 我们创建了原本 `user` 对象的数据拷贝,这样我们直接操作新对象的属性然后调用 `setState` 函数就能够正确地触发界面重渲染了。不过我们需要注意的是`Object.assign` 只是一层浅复制,在某些情况下有可能会造成数据的异常改变;另外 IE 中并不支持 `Object.assign`,如果我们的部署环境包括 IE 那么需要引入 [object-assign](https://www.npmjs.com/package/object-assign) 这样的垫片。除了 `Object.assign`,我们还可以使用对象的扩展操作符来创建新的对象: 32 | 33 | ```js 34 | updateState(event) { 35 |  const {name, value} = event.target; 36 |  let user = {...this.state.user, [name]: value}; 37 |  this.setState({user}); 38 | } 39 | ``` 40 | 41 | 扩展操作符能够自动将目标对象展开,将原属性复制到新对象中,其相较于 `Object.assign` 不需要额外的垫片(Babel 能够自动转化),并且代码也更为优雅。我们同样可以合并使用解构与扩展操作符: 42 | 43 | ``` 44 | updateState({target}) { 45 |  this.setState({user: {...this.state.user, [target.name]: target.value}}); 46 | } 47 | ``` 48 | 49 | ## immutability-helper 50 | 51 | immutability-helper 是早期 React 内置的 react-addons-update 的替代,它能够帮助开发者修个某个对象深层嵌套的属性并且返回新的对象。其基本引入方式为: 52 | 53 | ```js 54 | // import update from 'react-addons-update'; 55 | import update from "immutability-helper"; 56 | 57 | const state1 = ["x"]; 58 | const state2 = update(state1, { $push: ["y"] }); // ['x', 'y'] 59 | ``` 60 | 61 | 在上文中我们强调过 `Object.assign` 与扩展操作符都是一层浅复制,如果我们的数据结构嵌套层次较深时,我们就比较麻烦地去修改某个内部属性的值,譬如: 62 | 63 | ```js 64 | myData.x.y.z = 7; 65 | // or... 66 | myData.a.b.push(9); 67 | ``` 68 | 69 | 我们在上文中讨论过,直接修改某个对象的内部属性并不会影响到该对象的引用值;基本的做法应该是去创建 `myData` 的拷贝然后改变需要修改的部分: 70 | 71 | ``` 72 | const newData = deepCopy(myData); 73 | newData.x.y.z = 7; 74 | newData.a.b.push(9); 75 | ``` 76 | 77 | 不过深层拷贝的性能耗费往往较大,并且在某些包含嵌套的环境下并不现实;我们往往会选择仅拷贝需要更改的部分然后重用为更改的部分,不过这种方式在原生的 JavaScript 中往往会较为复杂: 78 | 79 | ```js 80 | const newData = extend(myData, { 81 | x: extend(myData.x, { 82 | y: extend(myData.x.y, { z: 7 }) 83 | }), 84 | a: extend(myData.a, { b: myData.a.b.concat(9) }) 85 | }); 86 | ``` 87 | 88 | 这种方式的性能损耗也较大,而 immutability-helper 正是提供了用于简化数据修改的语法糖,从而使得对象修改变得更为容易: 89 | 90 | ```js 91 | import update from "immutability-helper"; 92 | 93 | const newData = update(myData, { 94 | x: { y: { z: { $set: 7 } } }, 95 | a: { b: { $push: [9] } } 96 | }); 97 | ``` 98 | 99 | 该语法借鉴了 MongoDB 中查询语言的模式,以 `$` 为前缀的键被称为命令,而待修改的对象称为目标,这里我们讨论几种常见的用法: 100 | 101 | - 简单的添加数据 102 | 103 | ``` 104 | const initialArray = [1, 2, 3]; 105 | const newArray = update(initialArray, {$push: [4]}); // => [1, 2, 3, 4] 106 | ``` 107 | 108 | - 嵌套数组的切割 109 | 110 | ``` 111 | const collection = [1, 2, {a: [12, 17, 15]}]; 112 | const newCollection = update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}}); 113 | // => [1, 2, {a: [12, 13, 14, 15]}] 114 | ``` 115 | 116 | - 根据现有值进行更新 117 | 118 | ```js 119 | const obj = { a: 5, b: 3 }; 120 | const newObj = update(obj, { 121 | b: { 122 | $apply: function(x) { 123 | return x * 2; 124 | } 125 | } 126 | }); 127 | // => {a: 5, b: 6} 128 | // This is equivalent, but gets verbose for deeply nested collections: 129 | const newObj2 = update(obj, { b: { $set: obj.b * 2 } }); 130 | ``` 131 | 132 | - 对象合并 133 | 134 | ```js 135 | const obj = { a: 5, b: 3 }; 136 | const newObj = update(obj, { $merge: { b: 6, c: 7 } }); // => {a: 5, b: 6, c: 7} 137 | ``` 138 | 139 | ## ImmutableJS 140 | -------------------------------------------------------------------------------- /02~组件基础/05~Storybook/配置.md: -------------------------------------------------------------------------------- 1 | # 自定义配置 2 | 3 | Storybook 针对不同的开发场景提供了非常灵活的配置方案。 4 | 5 | # 自定义 Webpack 6 | 7 | Storybook 允许我们通过自定义 Webpack 配置文件来修改 Webpack 的打包流程。我们可以创建 `.storybook/webpack.config.js` 配置文件,然后进行 Webpack 调试,首先在文件中添加如下内容: 8 | 9 | ```js 10 | module.exports = async ({ config }) => 11 | console.dir(config.plugins, { depth: null }) || config; 12 | ``` 13 | 14 | 然后运行 `$ yarn storybook --debug-webpack`。当配置文件导出的是函数时,Storybook 会自动调用该函数,然后根据该函数的结果来修正 Webpack 的配置。譬如,我们要增加对 SCSS 的支持: 15 | 16 | ```js 17 | const path = require("path"); 18 | 19 | // Export a function. Accept the base config as the only param. 20 | module.exports = async ({ config, mode }) => { 21 | // `mode` has a value of 'DEVELOPMENT' or 'PRODUCTION' 22 | // You can change the configuration based on that. 23 | // 'PRODUCTION' is used when building the static version of storybook. 24 | 25 | // Make whatever fine-grained changes you need 26 | config.module.rules.push({ 27 | test: /\.scss$/, 28 | use: ["style-loader", "css-loader", "sass-loader"], 29 | include: path.resolve(__dirname, "../") 30 | }); 31 | 32 | // Return the altered config 33 | return config; 34 | }; 35 | ``` 36 | 37 | 值得说明的是,这里的 Webpack 自定义配置仅会影响到用户自身的组件渲染,而 Storybook 框架本身的渲染还是会根据自己的 Webpack 配置来。 38 | 39 | # 自定义 TypeScript 支持 40 | 41 | 笔者的 Web 项目主要是以 TypeScript 进行开发,首先需要添加 `awesome-typescript-loader`: 42 | 43 | ```sh 44 | $ yarn add -D typescript 45 | $ yarn add -D awesome-typescript-loader 46 | $ yarn add -D @types/storybook__react # typings 47 | $ yarn add -D @storybook/addon-info react-docgen-typescript-loader # optional but recommended 48 | $ yarn add -D jest "@types/jest" ts-jest #testing 49 | ``` 50 | 51 | 然后在自定义的 Webpack 配置中添加如下的配置: 52 | 53 | ```js 54 | module.exports = ({ config }) => { 55 | config.module.rules.push({ 56 | test: /\.(ts|tsx)$/, 57 | use: [ 58 | { 59 | loader: require.resolve("awesome-typescript-loader") 60 | }, 61 | // Optional 62 | { 63 | loader: require.resolve("react-docgen-typescript-loader") 64 | } 65 | ] 66 | }); 67 | config.resolve.extensions.push(".ts", ".tsx"); 68 | return config; 69 | }; 70 | ``` 71 | 72 | 参考的 tsconfig.json 配置如下: 73 | 74 | ```json 75 | { 76 | "compilerOptions": { 77 | "outDir": "build/lib", 78 | "module": "commonjs", 79 | "target": "es5", 80 | "lib": ["es5", "es6", "es7", "es2017", "dom"], 81 | "sourceMap": true, 82 | "allowJs": false, 83 | "jsx": "react", 84 | "moduleResolution": "node", 85 | "rootDirs": ["src", "stories"], 86 | "baseUrl": "src", 87 | "forceConsistentCasingInFileNames": true, 88 | "noImplicitReturns": true, 89 | "noImplicitThis": true, 90 | "noImplicitAny": true, 91 | "strictNullChecks": true, 92 | "suppressImplicitAnyIndexErrors": true, 93 | "noUnusedLocals": true, 94 | "declaration": true, 95 | "allowSyntheticDefaultImports": true, 96 | "experimentalDecorators": true, 97 | "emitDecoratorMetadata": true 98 | }, 99 | "include": ["src/**/*", "stories/**/*"], 100 | "exclude": ["node_modules", "build", "scripts"] 101 | } 102 | ``` 103 | 104 | 然后在 config.ts 文件中引入全部的 Stories: 105 | 106 | ```ts 107 | import { configure } from "@storybook/react"; 108 | // automatically import all files ending in *.stories.tsx 109 | const req = require.context("../stories", true, /\.stories\.tsx$/); 110 | 111 | function loadStories() { 112 | req.keys().forEach(req); 113 | } 114 | 115 | configure(loadStories, module); 116 | ``` 117 | 118 | 我们还可以利用 TSDocgen 扩展来自动地生成配套说明文档: 119 | 120 | ```ts 121 | import * as React from "react"; 122 | import { storiesOf } from "@storybook/react"; 123 | import { action } from "@storybook/addon-actions"; 124 | import TicTacToeCell from "./TicTacToeCell"; 125 | 126 | const stories = storiesOf("Components", module); 127 | 128 | stories.add( 129 | "TicTacToeCell", 130 | () => ( 131 | 136 | ), 137 | { info: { inline: true } } 138 | ); 139 | ``` 140 | -------------------------------------------------------------------------------- /02~组件基础/06~组件库/Antd/应用配置.md: -------------------------------------------------------------------------------- 1 | # create-react-app 2 | 3 | create-react-app 是业界最优秀的 React 应用开发工具之一,本文会尝试在 create-react-app 创建的工程中使用 antd 组件,并自定义 webpack 的配置以满足各类工程化需求。 4 | 5 | ```sh 6 | $ yarn create react-app antd-demo 7 | $ cd antd-demo 8 | $ yarn start 9 | ``` 10 | 11 | 工具会自动初始化一个脚手架并安装 React 项目的各种必要依赖,如果在过程中出现网络问题,请尝试配置代理或使用其他 npm registry。然后引入 antd: 12 | 13 | ```sh 14 | $ yarn add antd 15 | ``` 16 | 17 | 修改 src/App.js,引入 antd 的按钮组件。 18 | 19 | ```js 20 | import React, { Component } from "react"; 21 | import Button from "antd/es/button"; 22 | import "./App.css"; 23 | 24 | class App extends Component { 25 | render() { 26 | return ( 27 |
28 | 29 |
30 | ); 31 | } 32 | } 33 | 34 | export default App; 35 | ``` 36 | 37 | 修改 src/App.css,在文件顶部引入 antd/dist/antd.css。 38 | 39 | ```css 40 | @import '~antd/dist/antd.css'; 41 | 42 | .App { 43 | text-align: center; 44 | } 45 | 46 | ... 47 | ``` 48 | 49 | 然后我们可以使用 babel-plugin-import 来进行按需加载: 50 | 51 | ```sh 52 | $ yarn add babel-plugin-import 53 | ``` 54 | 55 | ```js 56 | + const { override, fixBabelImports } = require('customize-cra'); 57 | 58 | - module.exports = function override(config, env) { 59 | - // do stuff with the webpack config... 60 | - return config; 61 | - }; 62 | + module.exports = override( 63 | + fixBabelImports('import', { 64 | + libraryName: 'antd', 65 | + libraryDirectory: 'es', 66 | + style: 'css', 67 | + }), 68 | + ); 69 | ``` 70 | 71 | 然后移除前面在 src/App.css 里全量添加的 @import '~antd/dist/antd.css'; 样式代码,并且按下面的格式引入模块。 72 | 73 | ```js 74 | // src/App.js 75 | import React, { Component } from 'react'; 76 | - import Button from 'antd/es/button'; 77 | + import { Button } from 'antd'; 78 | import './App.css'; 79 | 80 | class App extends Component { 81 | render() { 82 | return ( 83 |
84 | 85 |
86 | ); 87 | } 88 | } 89 | 90 | export default App; 91 | ``` 92 | 93 | ## TypeScript 94 | 95 | 使用 create-react-app 一步步地创建一个 TypeScript 项目,并引入 antd。 96 | 97 | ```sh 98 | $ yarn create react-app antd-demo-ts --typescript 99 | $ npx create-react-app antd-demo-ts --typescript 100 | $ cd antd-demo-ts 101 | $ yarn start 102 | ``` 103 | 104 | ### ts-import-plugin 105 | 106 | ```json 107 | //tsconfig.json 108 | { 109 | ... 110 | "module": "ESNext", 111 | ... 112 | } 113 | ``` 114 | 115 | ```js 116 | // webpack.config.js 117 | const tsImportPluginFactory = require("ts-import-plugin"); 118 | 119 | module.exports = { 120 | // ... 121 | module: { 122 | rules: [ 123 | { 124 | test: /\.tsx?$/, 125 | loader: "awesome-typescript-loader", 126 | options: { 127 | getCustomTransformers: () => ({ 128 | before: [tsImportPluginFactory(/** options */)] 129 | }) 130 | }, 131 | exclude: /node_modules/ 132 | } 133 | ] 134 | } 135 | // ... 136 | }; 137 | ``` 138 | 139 | ts-import-plugin 会将引用转化为对单独文件的引用: 140 | 141 | ```ts 142 | // 原代码 143 | import { Alert, Card as C } from "antd"; 144 | 145 | // 转化为 146 | import Alert from "antd/lib/alert"; 147 | import "antd/lib/alert/style/index.less"; 148 | import { default as C } from "antd/lib/card"; 149 | import "antd/lib/card/style/index.less"; 150 | ``` 151 | 152 | # 自定义主题 153 | 154 | 按照 配置主题 的要求,自定义主题需要用到 less 变量覆盖功能。我们可以引入 customize-cra 中提供的 less 相关的函数 addLessLoader 来帮助加载 less 样式,同时修改 config-overrides.js 文件如下。 155 | 156 | ```js 157 | - const { override, fixBabelImports } = require('customize-cra'); 158 | + const { override, fixBabelImports, addLessLoader } = require('customize-cra'); 159 | 160 | module.exports = override( 161 | fixBabelImports('import', { 162 | libraryName: 'antd', 163 | libraryDirectory: 'es', 164 | - style: 'css', 165 | + style: true, 166 | }), 167 | + addLessLoader({ 168 | + javascriptEnabled: true, 169 | + modifyVars: { '@primary-color': '#1DA57A' }, 170 | + }), 171 | ); 172 | ``` 173 | 174 | 这里利用了 less-loader 的 modifyVars 来进行主题配置。 175 | -------------------------------------------------------------------------------- /03~状态管理/Redux/Dva/快速开始.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | Dva 脚手架提供了快速创建项目的命令: 4 | 5 | ```s 6 | $ dva new dva-quickstart 7 | ``` 8 | 9 | # 项目结构 10 | 11 | ```js 12 | import dva from "dva"; //引入依赖 13 | import "./index.css"; 14 | // 1. Initialize 15 | const app = dva(); //初始化 dva应用 16 | // 2. Plugins 17 | // app.use({}); //使用中间件 18 | // 3. Model 19 | // app.model(require('./models/example').default); // 加载model层 (后面详细解释model) 20 | // 4. Router 21 | app.router(require("./router").default); // 引入router 22 | // 5. Start 23 | app.start("#root"); // 挂载dva应用 24 | ``` 25 | 26 | router.js 中提供了对于路由的定义: 27 | 28 | ```js 29 | import React from "react"; 30 | import { Router, Route, Switch } from "dva/router"; // 引入 router,用的就是 react-router 31 | import IndexPage from "./routes/IndexPage"; // 引入路由绑定的高阶组件 32 | 33 | // 按照从上到下的顺序开始匹配url规则,匹配到了就是展示对应的组件view 34 | function RouterConfig({ history }) { 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | export default RouterConfig; 44 | ``` 45 | 46 | 路由页面 routes/IndexPage.js 中定义了页面结构: 47 | 48 | ```js 49 | import React from "react"; 50 | import { connect } from "dva"; 51 | import styles from "./IndexPage.css"; 52 | 53 | function IndexPage() { 54 | return ( 55 |
56 |

Yay! Welcome to dva!

57 |
58 | 68 |
69 | ); 70 | } 71 | 72 | IndexPage.propTypes = {}; 73 | // 这里 connect方法就是redux的connect,后面的IndexPage表示绑定的高阶组件 74 | // 在connect的第一个括号中,是可以拿到所有的model对象,这样就可以把对应的model对象绑定到我们的高阶组件上 75 | export default connect()(IndexPage); 76 | ``` 77 | 78 | # 数据模型 79 | 80 | ```js 81 | export default { 82 | namespace: "example", // 命名空间 作为 connect方法 中获取model对象state的 id 83 | state: {}, // 初始化state 84 | subscriptions: { 85 | // 订阅 86 | setup({ dispatch, history }) { 87 | // eslint-disable-line 88 | }, 89 | }, 90 | effects: { 91 | // 异步action的handler 92 | *fetch({ payload }, { call, put }) { 93 | // eslint-disable-line 94 | yield put({ type: "save" }); 95 | }, 96 | }, 97 | reducers: { 98 | //react-redux的reducers 用来接收action并且处理数据更新 99 | save(state, action) { 100 | return { ...state, ...action.payload }; 101 | }, 102 | }, 103 | }; 104 | ``` 105 | 106 | 更为复杂的模型定义如下: 107 | 108 | ```js 109 | export default { 110 | namespace: "list", // 这个namespace 是model的唯一识别id,在connect中需要使用这个绑定 111 | state: {}, 112 | subscriptions: { 113 | setup({ dispatch, history }) { 114 | // eslint-disable-line 115 | return history.listen(({ pathname }) => { 116 | if (pathname === "/") { 117 | dispatch({ 118 | type: "fetch", 119 | payload: {}, 120 | }); 121 | } 122 | }); 123 | }, 124 | }, 125 | effects: { 126 | *fetch({ payload }, { call, put }) { 127 | // eslint-disable-line 128 | // 这里假装 获取到了服务器的数据 129 | const fetchData = [0, 1, 2, 3]; 130 | yield put({ 131 | type: "save", 132 | list: fetchData, 133 | }); 134 | }, 135 | }, 136 | reducers: { 137 | // 保存 138 | save(state, action) { 139 | return { ...state, list: action.list }; 140 | }, 141 | // 新增 142 | add(state, action) { 143 | const [..._arr] = { ...state }.list; 144 | _arr.push(_arr.length); 145 | return { 146 | ...state, 147 | list: _arr, 148 | }; 149 | }, 150 | // 删除 151 | del(state, action) { 152 | return { 153 | ...state, 154 | list: state.list.filter((item, index) => { 155 | return index !== action.id; 156 | }), 157 | }; 158 | }, 159 | }, 160 | }; 161 | ``` 162 | -------------------------------------------------------------------------------- /04~工程实践/服务端渲染/服务端渲染性能浅析.md: -------------------------------------------------------------------------------- 1 | # 服务端渲染性能浅析 2 | 3 | 前几日笔者在[服务端渲染性能大乱斗:Vue, React, Preact, Rax, Marko](https://zhuanlan.zhihu.com/p/25003814)  一文中比较了当前流行的数个前端框架服务端渲染的性能表现,下图数值越高越好: 4 | 5 | 笔者看完这个数据对比之后不由好奇,缘何 React 服务端渲染的性能会如此之差;从设计理念的角度来看 React 本身专注于跨平台的界面库,其保证较好抽象层次的同时势必会付出一定的代价,并且 Facebook 在生产环境中并未大规模应用服务端渲染,也就未花费过多的精力来优化服务端渲染的性能。笔者也对比了下 React 与 Preact 有关服务端渲染的实现代码,确实高度的抽象需要额外的代码逻辑与对象创建,React 本身并没有冗余的部分,只是单纯地大量的毫秒级别额外对象操作的耗时的累加导致了最后性能表现的巨大差异。我们首先看下 Preact 的`renderToString`的函数实现,其紧耦合于 DOM 环境,以较低的抽象程度换取较少的代码实现: 6 | 7 | ```js 8 | /** The default export is an alias of `render()`. */ 9 | export default function renderToString(vnode, context, opts, inner, isSvgMode) { 10 | // 获取节点属性 11 | let { nodeName, attributes, children } = vnode || EMPTY, 12 | isComponent = false; 13 | context = context || {}; 14 | opts = opts || {}; 15 | 16 | let pretty = opts.pretty, 17 | indentChar = typeof pretty === "string" ? pretty : "\t"; 18 | 19 | if (vnode == null) { 20 | return ""; 21 | } // 字符串类型则直接返回 22 | 23 | if (!nodeName) { 24 | return encodeEntities(vnode); 25 | } // 处理组件 26 | 27 | if (typeof nodeName === "function") { 28 | isComponent = true; 29 | if (opts.shallow && (inner || opts.renderRootComponent === false)) { 30 | nodeName = getComponentName(nodeName); 31 | } else { 32 | //  ... 33 | 34 | if ( 35 | !nodeName.prototype || 36 | typeof nodeName.prototype.render !== "function" 37 | ) { 38 | // 处理无状态函数式组件 39 | //  ... 40 | } else { 41 | // 处理类组件 42 | //  ... 43 | } //递归处理下一层元素 44 | 45 | return renderToString( 46 | rendered, 47 | context, 48 | opts, 49 | opts.shallowHighOrder !== false 50 | ); 51 | } 52 | } // 将 JSX 渲染到 HTML 53 | 54 | let s = "", 55 | html; 56 | 57 | if (attributes) { 58 | let attrs = objectKeys(attributes); //处理所有元素属性 59 | // ... 60 | } // 处理多行属性 // ... 61 | 62 | if (html) { 63 | // 处理多行缩进 64 | // ... 65 | } else { 66 | // 递归处理子元素 67 | // ... 68 | } 69 | 70 | // ... 71 | 72 | return s; 73 | } 74 | ``` 75 | 76 | Preact 的实现还是比较简单明了的,我们继续来看下 React 中涉及到服务端渲染相关的代码,其主要涉及到 ReactDOMServer.js, ReactServerRendering.js, instantiateReactComponent.js, ReactCompositeComponent.js 以及 ReactReconciler.js 等几个文件,其中前两个文件算是专注于服务端渲染,而后三个文件则是用于定义 React 组件以及组件系统的组合与调和机制,其并不耦合于某个具体的平台,也是主要的以牺牲性能来换取较好地抽象层次的实现类。首先我们来从应用的角度考虑下两个可能影响服务端渲染性能的因素,一个是对于环境变量的设置。在 React 的源代码中我们可以发现很多如下的调试语句: 77 | 78 | ``` 79 | if (process.env.NODE_ENV !== 'production') { 80 |   ... 81 | 82 | } 83 | 84 | ``` 85 | 86 | 显而易见如果我们没有将环境变量设置为`production`,势必会在运行时调用更多的调试代码,拖慢整体性能。另一个有可能拖慢服务端渲染性能的因素是 React 在生成 HTML 后会对元素进行校验和计算并且附加到元素属性中: 87 | 88 | ``` 89 |
90 | 91 | ... 92 |
93 | ``` 94 | 95 | 上述代码中的`data-react-checksum`就是计算而来的校验和,该计算过程是会占用部分时间,不过影响甚微。笔者对于`renderToStringImpl`函数进行了断点性能分析,主要是利用`console.time`记录函数执行时间并且进行对比: 96 | 97 | ``` 98 | ... 99 | return transaction.perform(function () { 100 |   var componentInstance = instantiateReactComponent(element, true); 101 |   var reactDOMContainerInfo = ReactDOMContainerInfo(); 102 |   console.time('transaction'); 103 |   console.log('transaction 开始:' + Date.now()); 104 |   var markup = ReactReconciler.mountComponent(componentInstance, transaction, null, reactDOMContainerInfo, emptyObject, 0 /* parentDebugID */ 105 |   ); 106 |   console.log('transaction 结束:' + Date.now()); 107 |   console.timeEnd('transaction'); 108 |   ... 109 |   if (!makeStaticMarkup) { 110 |   console.time('markup'); 111 |   markup = ReactMarkupChecksum.addChecksumToMarkup(markup); 112 |   console.timeEnd('markup'); 113 | 114 | 115 |   } 116 | 117 |   return markup; 118 | ... 119 | // 运行结果为: 120 | // transaction: 12.643ms 121 | // markup: 0.249ms 122 | ``` 123 | 124 | 从运行结果上可以看出,计算校验和并未占用过多的时间比重,因此这也不会是拖慢服务端渲染性能的主因。实际上当我们调用`ReactDOMServer.renderToString`时,其会调用`ReactServerRendering.renderToStringImpl`这个内部实现,该函数的第二个参数`makeStaticMarkup`用来标识是否需要计算校验和。换言之,如果我们使用的是`ReactDOMServer.renderToStaticMarkup`,其会将`makeStaticMarkup`设置为`true`并且不计算校验和。完整的一次服务端渲染的对象与函数调用流程如下: 125 | 126 | 整个流程同样是递归解析组件树到 HTML 标记的过程,笔者同样是以断点计时的方式进行追踪,有趣的一个细节是从 Transaction 开始到首次调用 ReactReconciler 中`mountComponent`函数之间间隔 2ms,换言之,有大量的时间花费在了具体的解析之外,可能这种类型的抽象带来的额外消耗会是 React 服务端渲染性能较差的原因之一吧。 127 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/Hooks/基础元语/useEffect.md: -------------------------------------------------------------------------------- 1 | # useEffect 2 | 3 | useEffect 是一个 Effect Hook,常用于一些副作用的操作,在一定程度上可以充当 componentDidMount、componentDidUpdate、componentWillUnmount 这三个生命周期。 4 | 5 | ```js 6 | useEffect(() => { 7 | async function fetchData() { 8 | // You can await here 9 | const response = await MyAPI.getData(someId); 10 | // ... 11 | } 12 | fetchData(); 13 | }, [someId]); // Or [] if effect doesn't need props or state 14 | ``` 15 | 16 | useEffect 是一个神奇的函数,通过不同的组合搭配我们能够极大地精简原本类组件中的业务逻辑代码。useEffect 接收两个参数,分别是要执行的回调函数、依赖数组。 17 | 18 | - 如果依赖数组为空数组,那么回调函数会在第一次渲染结束后(componentDidMount)执行,返回的函数会在组件卸载时(componentWillUnmount)执行。 19 | - 如果不传依赖数组,那么回调函数会在每一次渲染结束后(componentDidMount 和 componentDidUpdate)执行。 20 | - 如果依赖数组不为空数组,那么回调函数会在依赖值每次更新渲染结束后(componentDidUpdate)执行,这个依赖值一般是 state 或者 props。 21 | 22 | ## 受控组件的状态变化 23 | 24 | 在编写 Input 这样的受控组件时,我们常常需要在 Props 中的 value 值变化之后,联动更新存储实际数据的内部 value 值: 25 | 26 | ```jsx 27 | const defaultProps = { 28 | formData: {}, 29 | }; 30 | 31 | export function VCForm({ formData = defaultProps.formData }) { 32 | // ... 33 | const [innerFormData, setInnerFormData] = React.useState(formData); 34 | // ... 35 | 36 | // 当外部 Props 状态变化后,更新数据 37 | React.useEffect(() => { 38 | if (formData) { 39 | setInnerFormData(formData); 40 | } 41 | }, [formData]); 42 | // ... 43 | 44 | return ( 45 |
{ 48 | setInnerFormData(newData); 49 | }} 50 | /> 51 | ); 52 | } 53 | ``` 54 | 55 | 这里需要注意的是,如果我们直接将默认值写在参数列表里,即 `formData = {}`;在外部参数未传入 formData,那么会发现每次组件更新都会触发 formData 被分配到新的默认值,也就导致了该组件的无限重复更新。因此我们需要仿造类组件中 defaultProps 的做法,将 defaultProps 以静态外部变量的方式存储并赋值。 56 | 57 | # useLayoutEffect 58 | 59 | useLayoutEffect 也是一个 Hook 方法,从名字上看和 useEffect 差不多,他俩用法也比较像。在 90% 的场景下我们都会用 useEffect,然而在某些场景下却不得不用 useLayoutEffect。useEffect 和 useLayoutEffect 的区别是: 60 | 61 | - useEffect 不会 block 浏览器渲染,而 useLayoutEffect 会。 62 | - useEffect 会在浏览器渲染结束后执行,useLayoutEffect 则是在 DOM 更新完成后,浏览器绘制之前执行。 63 | 64 | ```js 65 | const moveTo = (dom, delay, options) => { 66 | dom.style.transform = `translate(${options.x}px)`; 67 | dom.style.transition = `left ${delay}ms`; 68 | }; 69 | 70 | const Animate = () => { 71 | const ref = useRef(); 72 | useEffect(() => { 73 | moveTo(ref.current, 500, { x: 600 }); 74 | }, []); 75 | 76 | return
方块
; 77 | }; 78 | ``` 79 | 80 | 在 useEffect 里面会让这个方块往后移动 600px 距离,可以看到这个方块在移动过程中会闪一下。但如果换成了 useLayoutEffect 呢?会发现方块不会再闪动,而是直接出现在了 600px 的位置。原因是 useEffect 是在浏览器绘制之后执行的,所以方块一开始就在最左边,于是我们看到了方块移动的动画。然而 useLayoutEffect 是在绘制之前执行的,会阻塞页面的绘制,所以页面会在 useLayoutEffect 里面的代码执行结束后才去继续绘制,于是方块就直接出现在了右边。那么这里的代码是怎么实现的呢?以 preact 为例,useEffect 在 options.commit 阶段执行,而 useLayoutEffect 在 options.diffed 阶段执行。然而在实现 useEffect 的时候使用了 requestAnimationFrame,requestAnimationFrame 可以控制 useEffect 里面的函数在浏览器重绘结束,下次绘制之前执行。 81 | 82 | # useInterval 83 | 84 | ```ts 85 | function Counter() { 86 | const [count, setCount] = useState(0); 87 | 88 | useInterval(() => { 89 | setCount(count + 1); 90 | }, 1000); 91 | 92 | return

{count}

; 93 | } 94 | import { useEffect, useRef } from "react"; 95 | 96 | /* istanbul ignore next */ 97 | /** keep typescript happy */ 98 | const noop = () => {}; 99 | 100 | export function useInterval( 101 | callback: () => void, 102 | delay: number | null | false, 103 | immediate?: boolean 104 | ) { 105 | // Remember the latest callback: 106 | // 107 | // Without this, if you change the callback, when setInterval ticks again, it 108 | // will still call your old callback. 109 | // 110 | // If you add `callback` to useEffect's deps, it will work fine but the 111 | // interval will be reset. 112 | const savedCallback = useRef(noop); 113 | 114 | // Remember the latest callback. 115 | useEffect(() => { 116 | savedCallback.current = callback; 117 | }); 118 | 119 | // Execute callback if immediate is set. 120 | useEffect(() => { 121 | if (!immediate) return; 122 | if (delay === null || delay === false) return; 123 | savedCallback.current(); 124 | }, [immediate]); 125 | 126 | // Set up the interval. 127 | useEffect(() => { 128 | if (delay === null || delay === false) return undefined; 129 | const tick = () => savedCallback.current(); 130 | const id = setInterval(tick, delay); 131 | return () => clearInterval(id); 132 | }, [delay]); 133 | } 134 | ``` 135 | 136 | useInterval 还能够来暂停、终止定时器。 137 | 138 | # Links 139 | 140 | - https://medium.com/trabe/react-useeffect-hook-44d8aa7cccd0 141 | -------------------------------------------------------------------------------- /04~工程实践/03~设计模式/组件划分.md: -------------------------------------------------------------------------------- 1 | # 组件划分 2 | 3 | # 容器型组件和展示型组件 4 | 5 | 最简单的组件划分方式,就是将其二分,可以称为容器型组件和展示型组件,但是也有其他名字,比如臃肿型组件和简单型组件,智能型组件和傻瓜型组件,有状态组件和纯组件,封装型组件和元组件等等。它们不完全相同,但是在核心观点上是相似的。 6 | 7 | ![容器型组件与展示型组件对比](https://s2.ax1x.com/2019/10/05/uynQyV.png) 8 | 9 | 这种方式的优势在于: 10 | 11 | - 关注点分离。通过用这种方式开发组件,你可以更好的理解你的 app 和 UI。 12 | - 更好的复用性。你可以在不同的数据源中使用相同的展示型组件,也可以把它们放进不同容器型组件中更进一步的进行复用。 13 | - 展示型组件是你的 App 必不可少的"调色板",你可以把它们放在一个独立的页面中,让设计师随意拖拽它们的变量而不改变应用的逻辑。在这个页面上进行页面快照回归测试。 14 | - 这种方法逼你去把用于布局的组件抽出来,例如 Sidebar,Page,ContextMenu。然后通过子组件的方式引入而不是在各个容器型组件中复制粘贴已有的样式和布局。 15 | 16 | 为了与之前的概念做比较,这是一些相关但不同的二分法: 17 | 18 | - 有状态和无状态:有些组件使用 React 的 setState()方法,有些不用。容器型组件往往是有状态的而展示型组件往往是无状态的,这并不是一条铁律。展示型组件也可以是有状态的,容器型组件也可以是无状态的 19 | 20 | - 类和函数:从 React0.14 开始,组件既可以声明为类也可以声明为函数。函数式组件可以定义的更简单但是也缺少一些只能在类组件中使用的特写。有些限制可能未来会消除,但是在当下仍然是存在的。由于函数式组件更加易于理解,所以我建议你尽量的使用它。除非你需要 state,生命周期函数,或者性能优化,这些特性只有在类组件中才可以使用。 21 | 22 | - 纯和非纯:如果一个组件在输入相同 props 的情况下总是输出相同的结果,那我们称这个组件为 pure component。pure component 既可以声明为类组件也可以声明为函数式组件,即可以是有状态的也可以是无状态的。另一个重要的方面是,pure component 不依赖 props 和 state 的深层比对,所以可以在 shouldComponentUpdate 方法中进行浅比较优化性能,但是在未来可能有很多变化。 23 | 24 | ## 展示型组件 25 | 26 | - 关心数据的展示方式。 27 | - 内部可能包含展示型组件和容器型组件,并且通常存在其他 DOM 元素及其样式。 28 | - 允许通过 this.props.children 控制组件。 29 | - 不依赖 app 中的其它文件,像 Flux 的 actions 或 stores。 30 | - 不关心数据是如何加载和变化的。 31 | - 仅通过 props 接收数据和回调函数。 32 | - 几乎不用组件内的 state(如果用到的话,也仅仅是维护 UI 状态而不是数据状态)。 33 | - 除非需要用到 state,生命周期函数或性能优化,通常写成函数式组件。 34 | - 例如:Page,Sidebar,Story,UserInfo,List。 35 | 36 | ```js 37 | const Footer = props => { 38 | return ( 39 |
40 |
    41 |
  • Footer Information
  • 42 |
43 |
44 | ); 45 | }; 46 | ``` 47 | 48 | ## 容器型组件 49 | 50 | 其中容器组件往往会包含内部状态,其具备以下特点: 51 | 52 | - 关心数据的运作方式。 53 | - 内部可能包含展示型组件和容器型组件,但是通常没有任何用于自身的 DOM 元素,除了一些用于包裹元素的 div 标签,并且不存在样式。 54 | - 为展示型组件和容器型组件提供数据和操作数据的方法。 55 | - 调用 Flux actions 并以回调函数的方式给展示型组件提供 actions。 56 | - 通常是有状态的,并且作为数据源存在。 57 | - 通常由高阶函数生成例如 React Redux 的 connect(),Realy 的 createContainer,或者 Flux Utils 的 Container.create(),而不是手写的。 58 | - 例如:UserPage,FollowersSidebar,StoryContainer,FollowedUserList。 59 | 60 | ```jsx 61 | class SmartComponent01 extends Component { 62 | manageSomeData() { 63 | /* ... */ 64 | } 65 | makeSomeCalculations() { 66 | /* ... */ 67 | } 68 | handleSomeEvent = event => { 69 | /* ... */ 70 | }; 71 | 72 | render() { 73 | return ( 74 |
75 |   76 |    {" "} 77 |
78 | ); 79 | } 80 | } 81 | ``` 82 | 83 | ## 案例分析:TodoList 的拆分 84 | 85 | 在使用 React 中,你是否会出现过一个文件的代码很多,既存在应用数据的读取和处理,又存在数据的显示,而且每个组件还不能复用。首先我们来看一个容器组件和展示组件一起的例子吧。 86 | 87 | ```js 88 | class TodoList extends React.Component { 89 | constructor(props) { 90 | super(props); 91 | this.state = { 92 | todos: [] 93 | }; 94 | this.fetchData = this.fetchData.bind(this); 95 | } 96 | componentDidMount() { 97 | this.fetchData(); 98 | } 99 | fetchData() { 100 | fetch("/api/todos").then(data => { 101 | this.setState({ 102 | todos: data 103 | }); 104 | }); 105 | } 106 | render() { 107 | const { todos } = this.state; 108 | return ( 109 |
110 |
    111 | {todos.map((item, index) => { 112 | return
  • {item.name}
  • ; 113 | })} 114 |
115 |
116 | ); 117 | } 118 | } 119 | ``` 120 | 121 | 大家可以看到这个例子是没有办法复用的,因为数据的请求和数据的展示都在一个组件进行,要实现组件的复用,我们就需要将展示组件和容器组件分离出来。具体代码如下: 122 | 123 | ```js 124 | // 展示组件 125 | class TodoList extends React.Component { 126 | constructor(props) { 127 | super(props); 128 | } 129 | render() { 130 | const { todos } = this.props; 131 | return ( 132 |
133 |
    134 | {todos.map((item, index) => { 135 | return
  • {item.name}
  • ; 136 | })} 137 |
138 |
139 | ); 140 | } 141 | } 142 | // 容器组件 143 | class TodoListContainer extends React.Component { 144 | constructor(props) { 145 | super(props); 146 | this.state = { 147 | todos: [] 148 | }; 149 | this.fetchData = this.fetchData.bind(this); 150 | } 151 | componentDidMount() { 152 | this.fetchData(); 153 | } 154 | fetchData() { 155 | fetch("/api/todos").then(data => { 156 | this.setState({ 157 | todos: data 158 | }); 159 | }); 160 | } 161 | render() { 162 | return ( 163 |
164 | 165 |
166 | ); 167 | } 168 | } 169 | ``` 170 | -------------------------------------------------------------------------------- /04~工程实践/02~数据加载/React Query/快速开始.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | ```tsx 4 | import { 5 | useQuery, 6 | useMutation, 7 | useQueryClient, 8 | QueryClient, 9 | QueryClientProvider, 10 | } from "react-query"; 11 | import { getTodos, postTodo } from "../my-api"; 12 | 13 | // Create a client 14 | const queryClient = new QueryClient(); 15 | 16 | function App() { 17 | return ( 18 | // Provide the client to your App 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | function Todos() { 26 | // Access the client 27 | const queryClient = useQueryClient(); 28 | 29 | // Queries 30 | const query = useQuery("todos", getTodos); 31 | 32 | // Mutations 33 | const mutation = useMutation(postTodo, { 34 | onSuccess: () => { 35 | // Invalidate and refetch 36 | queryClient.invalidateQueries("todos"); 37 | }, 38 | }); 39 | 40 | return ( 41 |
42 |
    43 | {query.data.map((todo) => ( 44 |
  • {todo.title}
  • 45 | ))} 46 |
47 | 48 | 58 |
59 | ); 60 | } 61 | 62 | render(, document.getElementById("root")); 63 | ``` 64 | 65 | # Queries 66 | 67 | 查询是对一个异步数据源的声明性依赖,它与一个唯一的键相联系。查询可以与任何基于 Promise 的方法(包括 GET 和 POST 方法)一起使用,从服务器上获取数据。如果你的方法修改了服务器上的数据,我们建议使用 Mutations 来代替。要在你的组件或自定义钩子中订阅一个查询,至少要用以下方式调用 useQuery 钩: 68 | 69 | - 一个用于查询的唯一键 70 | - 一个函数,返回一个 Promise,该 Promise 返回数据,或抛出一个错误 71 | 72 | ```tsx 73 | import { useQuery } from "react-query"; 74 | 75 | function App() { 76 | const info = useQuery("todos", fetchTodoList); 77 | } 78 | ``` 79 | 80 | 你提供的唯一键在内部用于重新获取、缓存和在整个应用程序中共享你的查询。useQuery 返回的查询结果包含了所有关于查询的信息,你将需要这些信息来进行模板设计和对数据的其他使用。 81 | 82 | ```tsx 83 | const result = useQuery("todos", fetchTodoList); 84 | ``` 85 | 86 | `result`对象包含一些非常重要的状态,你需要注意这些状态才能有成效。一个查询在任何时候都只能处于以下状态之一。 87 | 88 | - `isLoading'或`status === 'loading'` - 查询没有数据,目前正在获取。 89 | - `isError`或`status === 'error'`- 查询遇到了一个错误 90 | - `isSuccess`或`status === 'success'` - 查询成功,数据可用。 91 | - `isIdle`或`status === 'idle'` - 查询目前处于禁用状态(稍后你会了解更多关于这个的信息 92 | 93 | 除了这些主要的状态,根据查询的状态,还有更多的信息可用。 94 | 95 | - `error` - 如果查询处于`isError`状态,错误可以通过`error`属性获得。 96 | - `data` - 如果查询处于`成功'状态,数据可通过`data`属性获得。 97 | - `isFetching` - 在任何状态下,如果查询在任何时候都在获取(包括后台重新获取)`isFetching`将是`true`。 98 | 99 | 对于大多数查询来说,通常只需检查 isLoading 状态,然后是 isError 状态,最后是假设数据可用并呈现成功状态。 100 | 101 | ```js 102 | function Todos() { 103 | const { isLoading, isError, data, error } = useQuery("todos", fetchTodoList); 104 | 105 | if (isLoading) { 106 | return Loading...; 107 | } 108 | 109 | if (isError) { 110 | return Error: {error.message}; 111 | } 112 | 113 | // We can assume by this point that `isSuccess === true` 114 | return ( 115 |
    116 | {data.map((todo) => ( 117 |
  • {todo.title}
  • 118 | ))} 119 |
120 | ); 121 | } 122 | ``` 123 | 124 | # 查询函数 125 | 126 | 查询函数实际上可以是任何返回 Promise 的函数。被返回的 Promise 应该是解决数据或抛出一个错误;下面所有的都是有效的查询函数配置。 127 | 128 | ```js 129 | useQuery(["todos"], fetchAllTodos); 130 | useQuery(["todos", todoId], () => fetchTodoById(todoId)); 131 | useQuery(["todos", todoId], async () => { 132 | const data = await fetchTodoById(todoId); 133 | return data; 134 | }); 135 | useQuery(["todos", todoId], ({ queryKey }) => fetchTodoById(queryKey[1])); 136 | ``` 137 | 138 | 对于 React Query 来说,要确定一个查询有错误,查询函数必须抛出。在查询函数中抛出的任何错误都将被持久化在查询的错误状态上。 139 | 140 | ```js 141 | const { error } = useQuery(["todos", todoId], async () => { 142 | if (somethingGoesWrong) { 143 | throw new Error("Oh no!"); 144 | } 145 | 146 | return data; 147 | }); 148 | ``` 149 | 150 | 虽然大多数工具,如 axios 或 graphql-request,都会对不成功的 HTTP 调用自动抛出错误,但有些工具,如 fetch,默认是不抛出错误的。如果是这种情况,你就需要自己抛出这些错误。这里有一个简单的方法,用流行的 fetch API 做到这一点。 151 | 152 | ```js 153 | useQuery(["todos", todoId], async () => { 154 | const response = await fetch("/todos/" + todoId); 155 | if (!response.ok) { 156 | throw new Error("Network response was not ok"); 157 | } 158 | return response.json(); 159 | }); 160 | ``` 161 | 162 | 查询键不仅仅是为了唯一地识别你正在获取的数据,而且还方便地传递到你的查询函数中,虽然并不总是必要的,但这使得在需要时可以提取你的查询函数。 163 | 164 | ```js 165 | function Todos({ status, page }) { 166 | const result = useQuery(["todos", { status, page }], fetchTodoList); 167 | } 168 | 169 | // Access the key, status and page variables in your query function! 170 | function fetchTodoList({ queryKey }) { 171 | const [_key, { status, page }] = queryKey; 172 | return new Promise(); 173 | } 174 | ``` 175 | -------------------------------------------------------------------------------- /03~状态管理/Redux/README.md: -------------------------------------------------------------------------------- 1 | # 在 React 中使用 Redux 2 | 3 | 在 [Redux 系列文章](https://ngte-web.gitbook.io/?q=redux)中我们详细介绍了 Redux 的设计与使用,React Redux 是官方提供的 Redux 与 React 的绑定库,用于将 Redux 中的 State 与 Action Creators 映射到 React 组件的 Props。本组件的设计思想可以查看[Presentational and Container Components](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.6bnhmpqtg),即将展示组件与容器组件分离,将展示组件尽可能地作为 Stateless 对待。在应用中,只有最顶层组件是对 Redux 可知(例如路由处理)这是很好的。所有它们的子组件都应该是“笨拙”的,并且是通过 props 获取数据。 4 | 5 | | | 容器组件 | 展示组件 | 6 | | ---------- | --------------------- | --------------------- | 7 | | 位置 | 最顶层,路由处理 | 中间和子组件 | 8 | | 使用 Redux | 是 | 否 | 9 | | 读取数据 | 从 Redux 获取 state | 从 props 获取数据 | 10 | | 修改数据 | 向 Redux 发起 actions | 从 props 调用回调函数 | 11 | 12 | # 组件数据流 13 | 14 | ![React Redux 数据流](http://p9.qhimg.com/d/inn/a8ab3ea4/react-redux.png) 15 | 16 | 我们用 react-redux 提供的 connect() 方法将“笨拙”的 Counter 转化成容器组件。connect() 允许你从 Redux store 中指定准确的 state 到你想要获取的组件中。这让你能获取到任何级别颗粒度的数据。首先来看下一个简单的 Counter 的示例: 17 | 18 | ```js 19 | export default class Counter extends Component { 20 | render() { 21 | return ; 22 | } 23 | } 24 | ``` 25 | 26 | ```js 27 | // 哪些 Redux 全局的 state 是我们组件想要通过 props 获取的? 28 | function mapStateToProps(state) { 29 | return { 30 | value: state.counter, 31 | }; 32 | } 33 | 34 | // 哪些 action 创建函数是我们想要通过 props 获取的? 35 | function mapDispatchToProps(dispatch) { 36 | return { 37 | onIncrement: () => dispatch(increment()), 38 | }; 39 | } 40 | 41 | /**或者也可以使用bindActionCreators 42 | //将Dispatch映射为Props 43 | ... 44 | import * as CounterActions from "../actions/counter"; 45 | ... 46 | function mapDispatchToProps(dispatch) { 47 | return bindActionCreators(CounterActions, dispatch) 48 | } 49 | **/ 50 | 51 | // 你可以传递一个对象,而不是定义一个 `mapDispatchToProps`: 52 | // export default connect(mapStateToProps, CounterActionCreators)(Counter); 53 | 54 | // 或者如果你想省略 `mapDispatchToProps`,你可以通过传递一个 `dispatch` 作为一个 props: 55 | // export default connect(mapStateToProps)(Counter); 56 | 57 | let App = connect(mapStateToProps, mapDispatchToProps)(Counter); 58 | const targetEl = document.getElementById("root"); 59 | const store = configureStore({ counter: 0 }); //初始化Store 60 | 61 | ReactDOM.render( 62 | 63 | 64 | , 65 | targetEl 66 | ); 67 | ``` 68 | 69 | 总结而言,各个部分的作用如下: 70 | 71 | ![React Redux 组件功能](https://s2.ax1x.com/2020/01/06/lyY2ut.md.png) 72 | 73 | # Provider & Store 74 | 75 | `` 使组件层级中的 connect() 方法都能够获得 Redux store。正常情况下,你的根组件应该嵌套在 `` 中才能使用 connect() 方法。如果你真的不想把根组件嵌套在 ``中,你可以把 store 作为 props 传递到每一个被 connet() 包装的组件,但是我们只推荐您在单元测试中对 store 进行伪造 (stub) 或者在非完全基于 React 的代码中才这样做。正常情况下,你应该使用 ``。 76 | 属性 77 | 78 | - store (Redux Store): 应用程序中唯一的 Redux store 对象 79 | - children (ReactElement) 组件层级的根组件。 80 | 81 | # connect:连接 React 组件与 Redux store。 82 | 83 | ```js 84 | connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]); 85 | ``` 86 | 87 | 连接操作不会改变原来的组件类,反而返回一个新的已与 Redux store 连接的组件类。 88 | 89 | ## mapStateToProps 90 | 91 | [mapStateToProps(state, [ownProps]): stateProps](Function): 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。如果你省略了这个参数,你的组件将不会监听 Redux store。如果指定了该回调函数中的第二个参数 ownProps,则该参数的值为传递到组件的 props,而且只要组件接收到新的 props,mapStateToProps 也会被调用。 92 | 93 | ## mapDispatchToProps 94 | 95 | [mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function): 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作 Redux action creator,而且这个对象会与 Redux store 绑定在一起,其中所定义的方法名将作为属性名,合并到组件的 props 中。如果传递的是一个函数,该函数将接收一个 dispatch 函数,然后由你来决定如何返回一个对象,这个对象通过 dispatch 函数与 action creator 以某种方式绑定在一起(提示:你也许会用到 Redux 的辅助函数 bindActionCreators())。如果你省略这个 mapDispatchToProps 参数,默认情况下,dispatch 会注入到你的组件 props 中。如果指定了该回调函数中第二个参数 ownProps,该参数的值为传递到组件的 props,而且只要组件接收到新 props,mapDispatchToProps 也会被调用。 96 | 97 | ## mergeProps 98 | 99 | [mergeProps(stateProps, dispatchProps, ownProps): props](Function): 如果指定了这个参数,mapStateToProps() 与 mapDispatchToProps() 的执行结果和组件自身的 props 将传入到这个回调函数中。该回调函数返回的对象将作为 props 传递到被包装的组件中。你也许可以用这个回调函数,根据组件的 props 来筛选部分的 state 数据,或者把 props 中的某个特定变量与 action creator 绑定在一起。如果你省略这个参数,默认情况下返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的结果。 100 | 101 | ## options 102 | 103 | [options](Object) 如果指定这个参数,可以定制 connector 的行为。 104 | 105 | - [pure = true](Boolean): 如果为 true,connector 将执行 shouldComponentUpdate 并且浅对比 mergeProps 的结果,避免不必要的更新,前提是当前组件是一个“纯”组件,它不依赖于任何的输入或 state 而只依赖于 props 和 Redux store 的 state。默认值为 true。 106 | - [withRef = false](Boolean): 如果为 true,connector 会保存一个对被包装组件实例的引用,该引用通过 getWrappedInstance() 方法获得。默认值为 false 107 | 108 | # Links 109 | 110 | - https://mp.weixin.qq.com/s/axauH4xpq-ZV3FFHI9XWLg 动手实现一个 react-redux 111 | -------------------------------------------------------------------------------- /03~状态管理/XState/有限状态机.md: -------------------------------------------------------------------------------- 1 | # 有限状态机 2 | 3 | 有限状态机 (Finite State Machine, FSM) 是一种数学模型用来描述系统的行为,这个系统在任何时间点上都只会存在于一个状态。举例来说,红绿灯就有 红灯、绿灯、黄灯 三种状态,在任何时间点上一定是这三种状态的其中一种,不可能在一个时间点上存在两种或两种以上的状态。 4 | 5 | 一个正式的有限状态机包含五个部分: 6 | 7 | - 有限数量的状态 (state) 8 | - 有限数量的事件 (event) 9 | - 一个初始状态 (initial state) 10 | - 一个转换函式 (transition function),传入当前的状态及事件时会返回下一个状态 11 | - 具有 0 至 n 个最终状态 (final state) 12 | 13 | 需要强调的是这裡的 状态 (State) 指的是系统定性的 mode 或 status,并不是指系统内所有的资料。举例来说,水有 4 种状态 (State)-固态、液态、气态 以及等离子态,这就属于状态,但水的温度是可变的定量且无限的可能就不属于状态! 14 | 15 | # 建立第一个 Machine 16 | 17 | XState 的 Machine 其实就是一个 State Machine (精确地说是 Statechart),所以我们在建立一个 Machine 要先整理我们的,程序有哪些状态,哪些事件,以及初始状态。 18 | 19 | ```js 20 | import { Machine } from "xstate"; 21 | 22 | const lightMachine = Machine({ 23 | states: { 24 | red: {}, 25 | green: {}, 26 | yellow: {}, 27 | }, 28 | }); 29 | ``` 30 | 31 | 首先我们需要订定 Machine 会有哪些状态,传给 Machine 一个 object 内部必须有 states 这个属性,而 states object 的每个 key 就是这个 Machine 拥有的状态。所以这段,程序码代表这个 Machine 拥有 red, green, yellow 三种状态。 32 | 33 | ```js 34 | import { Machine } from "xstate"; 35 | 36 | const lightMachine = Machine({ 37 | states: { 38 | red: {}, 39 | green: {}, 40 | yellow: {}, 41 | }, 42 | }); 43 | ``` 44 | 45 | 接下来我们要定义初始状态,假如说我们希望一开始是红灯,那就给 initial 如下: 46 | 47 | ```js 48 | import { Machine } from "xstate"; 49 | 50 | const lightMachine = Machine({ 51 | initial: "red", 52 | states: { 53 | red: {}, 54 | green: {}, 55 | yellow: {}, 56 | }, 57 | }); 58 | ``` 59 | 60 | initial 给 'red' 这样我们的 lightMachine 的初始状态就会是 red。接下来我们要定义每个状态下会有什麽事件,遇到这些事件时,会转换成什麽状态。这裡我们订定三个状态下都会有 CLICK 事件,并且状态的转换是 red -> green -> yellow -> red ... 那我们的,程序码就会像下这面这样: 61 | 62 | ```js 63 | import { Machine } from "xstate"; 64 | 65 | const lightMachine = Machine({ 66 | initial: "red", 67 | states: { 68 | red: { 69 | on: { 70 | CLICK: "green", 71 | }, 72 | }, 73 | green: { 74 | on: { 75 | CLICK: "yellow", 76 | }, 77 | }, 78 | yellow: { 79 | on: { 80 | CLICK: "red", 81 | }, 82 | }, 83 | }, 84 | }); 85 | ``` 86 | 87 | 我们在每个状态下加入 on 属性,on 的 key 代表事件名称,value 则代表转移的下一个状态。这时候我们就可以拿 lightMachine 来使用了!透过 .transition(state, event) 这个方法来取得下一个状态: 88 | 89 | ```js 90 | import { Machine } from "xstate"; 91 | 92 | const lightMachine = Machine({ 93 | //... 94 | }); 95 | 96 | const state0 = lightMachine.initialState; 97 | console.log(state0); 98 | const state1 = lightMachine.transition(state0, "CLICK"); 99 | console.log(state1); 100 | const state2 = lightMachine.transition(state1, "CLICK"); 101 | console.log(state2); 102 | const state3 = lightMachine.transition(state2, "CLICK"); 103 | console.log(state3); 104 | ``` 105 | 106 | 这个回传的 state object 有两个常用的方法及属性分别是: 107 | 108 | - value 109 | - matches(parentStateValue) 110 | - nextEvents 111 | 112 | value 可以拿到当前的状态,matches 则可以用来判断现在是否在某个状态,比如说: 113 | 114 | ```js 115 | import { Machine } from "xstate"; 116 | 117 | const lightMachine = Machine({ 118 | //... 119 | }); 120 | 121 | const state0 = lightMachine.initialState; 122 | console.log(state0.value); // 'red' 123 | const state1 = lightMachine.transition(state0, "CLICK"); 124 | console.log(state1.value); // 'green' 125 | 126 | state0.matches("red"); // true 127 | state0.matches("yellow"); // false 128 | state0.matches("green"); // false 129 | ``` 130 | 131 | nextEvents 则可以拿到该 state 有哪些 events 可以使用: 132 | 133 | ```js 134 | import { Machine } from "xstate"; 135 | 136 | const lightMachine = Machine({ 137 | //... 138 | }); 139 | 140 | const state0 = lightMachine.initialState; 141 | console.log(state0.nextEvents); // 'CLICK' 142 | ``` 143 | 144 | 这样一来我们就完成了一个简单的 Machine,但我们的 lightMachine 每次都要传入当前的 state 跟 event 才能做状态转换,这是为了让 transition 保持是一个 Pure Function,它不会改变 lightMachine 物件的状态,方便我们做单元测试。但我们通常不想要自己储存及管理状态,所以 XState 提供了 Interpret! 145 | 146 | # Interpret 147 | 148 | XState 提供了一个叫 interpret 的 function 可以把一个 machine 实例转换成一个具有状态的 service,如下: 149 | 150 | ```js 151 | import { Machine, interpret } from "xstate"; 152 | 153 | const lightMachine = Machine({ 154 | //... 155 | }); 156 | 157 | const service = interpret(lightMachine); 158 | 159 | // 启动 service 160 | service.start(); 161 | 162 | // Send events 163 | service.send("CLICK"); 164 | 165 | // 停止 service 当你不在使用它 166 | service.stop(); 167 | ``` 168 | 169 | interpret 得到的 service 具有自己的状态,当 start() 后,这个 service 就会到初始状态,同时可以对他传送(send)事件,同时也可以透过 service.state 拿到当前的状态,如下 170 | 171 | ```js 172 | import { Machine, interpret } from "xstate"; 173 | 174 | const lightMachine = Machine({ 175 | //... 176 | }); 177 | 178 | const service = interpret(lightMachine); 179 | 180 | // 启动 service 181 | service.start(); 182 | 183 | console.log(service.state.value); // 'red' 184 | service.send("CLICK"); // Send events 185 | console.log(service.state.value); // 'green' 186 | 187 | // 停止 service 当你不在使用它 188 | service.stop(); 189 | ``` 190 | 191 | 这样一来我们就可以很简单的透过 service 来管理及保存当前的状态! 192 | -------------------------------------------------------------------------------- /03~状态管理/Zustand/02.状态分形与 Slice.md: -------------------------------------------------------------------------------- 1 | # 状态分形与 Slice 2 | 3 | # Slice 模式 4 | 5 | ```ts 6 | import { create, StateCreator } from "zustand"; 7 | 8 | interface BearSlice { 9 | bears: number; 10 | addBear: () => void; 11 | eatFish: () => void; 12 | } 13 | const createBearSlice: StateCreator< 14 | BearSlice & FishSlice, 15 | [], 16 | [], 17 | BearSlice 18 | > = (set) => ({ 19 | bears: 0, 20 | addBear: () => set((state) => ({ bears: state.bears + 1 })), 21 | eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), 22 | }); 23 | 24 | interface FishSlice { 25 | fishes: number; 26 | addFish: () => void; 27 | } 28 | const createFishSlice: StateCreator< 29 | BearSlice & FishSlice, 30 | [], 31 | [], 32 | FishSlice 33 | > = (set) => ({ 34 | fishes: 0, 35 | addFish: () => set((state) => ({ fishes: state.fishes + 1 })), 36 | }); 37 | 38 | const useBoundStore = create()((...a) => ({ 39 | ...createBearSlice(...a), 40 | ...createFishSlice(...a), 41 | })); 42 | ``` 43 | 44 | # 使用 Props 初始状态 45 | 46 | 在需要依赖性注入的情况下,例如当一个 store 应该用来自组件的 props 进行初始化时,推荐的方法是使用一个带有 React.context 的 vanilla store。 47 | 48 | ```ts 49 | import { createStore } from "zustand"; 50 | 51 | interface BearProps { 52 | bears: number; 53 | } 54 | 55 | interface BearState extends BearProps { 56 | addBear: () => void; 57 | } 58 | 59 | type BearStore = ReturnType; 60 | 61 | const createBearStore = (initProps?: Partial) => { 62 | const DEFAULT_PROPS: BearProps = { 63 | bears: 0, 64 | }; 65 | return createStore()((set) => ({ 66 | ...DEFAULT_PROPS, 67 | ...initProps, 68 | addBear: () => set((state) => ({ bears: ++state.bears })), 69 | })); 70 | }; 71 | ``` 72 | 73 | ## [Creating a context with `React.createContext`](https://docs.pmnd.rs/zustand/guides/initialize-state-with-props#creating-a-context-with-react.createcontext) 74 | 75 | ```ts 76 | import { createContext } from "react"; 77 | 78 | export const BearContext = createContext(null); 79 | ``` 80 | 81 | ## [Basic component usage](https://docs.pmnd.rs/zustand/guides/initialize-state-with-props#basic-component-usage) 82 | 83 | ```tsx 84 | // Provider implementation 85 | import { useRef } from "react"; 86 | 87 | function App() { 88 | const store = useRef(createBearStore()).current; 89 | return ( 90 | 91 | 92 | 93 | ); 94 | } 95 | // Consumer component 96 | import { useContext } from "react"; 97 | import { useStore } from "zustand"; 98 | 99 | function BasicConsumer() { 100 | const store = useContext(BearContext); 101 | if (!store) throw new Error("Missing BearContext.Provider in the tree"); 102 | const bears = useStore(store, (s) => s.bears); 103 | const addBear = useStore(store, (s) => s.addBear); 104 | return ( 105 | <> 106 |
{bears} Bears.
107 | 108 | 109 | ); 110 | } 111 | ``` 112 | 113 | ## Common patterns 114 | 115 | ### Wrapping the context provider 116 | 117 | ```tsx 118 | // Provider wrapper 119 | import { useRef } from "react"; 120 | 121 | type BearProviderProps = React.PropsWithChildren; 122 | 123 | function BearProvider({ children, ...props }: BearProviderProps) { 124 | const storeRef = useRef(); 125 | if (!storeRef.current) { 126 | storeRef.current = createBearStore(props); 127 | } 128 | 129 | return ( 130 | 131 | {children} 132 | 133 | ); 134 | } 135 | ``` 136 | 137 | ### [Extracting context logic into a custom hook](https://docs.pmnd.rs/zustand/guides/initialize-state-with-props#extracting-context-logic-into-a-custom-hook) 138 | 139 | ```tsx 140 | // Mimic the hook returned by `create` 141 | import { useContext } from "react"; 142 | import { useStore } from "zustand"; 143 | 144 | function useBearContext( 145 | selector: (state: BearState) => T, 146 | equalityFn?: (left: T, right: T) => boolean 147 | ): T { 148 | const store = useContext(BearContext); 149 | if (!store) throw new Error("Missing BearContext.Provider in the tree"); 150 | return useStore(store, selector, equalityFn); 151 | } 152 | 153 | // Consumer usage of the custom hook 154 | function CommonConsumer() { 155 | const bears = useBearContext((s) => s.bears); 156 | const addBear = useBearContext((s) => s.addBear); 157 | return ( 158 | <> 159 |
{bears} Bears.
160 | 161 | 162 | ); 163 | } 164 | ``` 165 | 166 | ### [Complete example](https://docs.pmnd.rs/zustand/guides/initialize-state-with-props#complete-example) 167 | 168 | ```tsx 169 | // Provider wrapper & custom hook consumer 170 | function App2() { 171 | return ( 172 | 173 | 174 | 175 | ); 176 | } 177 | ``` 178 | -------------------------------------------------------------------------------- /03~状态管理/Zustand/99~参考资料/2022-精读 zustand 源码.md: -------------------------------------------------------------------------------- 1 | > [原文地址](https://zhuanlan.zhihu.com/p/461152248) 2 | 3 | # 精读 zustand 源码 4 | 5 | # 手写一个 toy 级别的 zustand 6 | 7 | 上面我们分析了 zustand 执行的过程及状态管理的流程,下面我们就尝试着手写 toyzustand,这块我们分成两块,一个是创建 store 部分,一个是创建 useStore hooks 的部分,具体如下: 8 | 9 | ```js 10 | function createStore(createState) { 11 | let state; 12 | let listeners = new Set(); 13 | // 获取store内容 14 | const getState = () => state; 15 | // 更新store内容 16 | const setState = (partial, replace) => { 17 | const nextState = typeof partial === "function" ? partial(state) : partial; 18 | 19 | if (nextState !== state) { 20 | const prevState = state; 21 | state = replace ? nextState : Object.assign({}, state, nextState); 22 | listeners.forEach((listener) => listener(state, prevState)); 23 | } 24 | }; 25 | // 添加订阅信息 26 | const subscribe = (listener) => { 27 | listeners.add(listener); 28 | // 清除订阅信息 29 | return () => { 30 | listeners.delete(listener); 31 | }; 32 | }; 33 | // 清除所有的listener 34 | const destroy = () => listeners.clear(); 35 | 36 | const api = { getState, setState, destroy, subscribe }; 37 | // 创建初始的state 38 | state = createState(setState, getState, api); 39 | 40 | return api; 41 | } 42 | 43 | export default createStore; 44 | ``` 45 | 46 | 生成 hooks 方法: 47 | 48 | ```js 49 | import { useLayoutEffect } from "react"; 50 | import { useReducer, useRef } from "react"; 51 | import createStore from "./createStore"; 52 | 53 | function create(createState) { 54 | // 根据createStore 结合createState 创建一个store 55 | const api = createStore(createState); 56 | 57 | /** 58 | * @description 创建 hooks 59 | * @param {Function} selector 可选的,返回store的内容,默认api.getState 60 | * @param {Function} enqulityFn 可选,默认用Object.is 判断 61 | * @returns 62 | */ 63 | const useStore = (selector = api.getState, enqulityFn = Object.is) => { 64 | // 生辰一个forceUpdate函数 65 | const [, forceUpdate] = useReducer((c) => c + 1, 0); 66 | 67 | const state = api.getState(); 68 | const stateRef = useRef(state); 69 | // 存储方法 70 | const selectorRef = useRef(selector); 71 | const enqulityFnRef = useRef(enqulityFn); 72 | 73 | // 当前current状态存储 74 | let currentStateRef = useRef(); 75 | if (currentStateRef.current === undefined) { 76 | currentStateRef.current = selector(state); 77 | } 78 | 79 | /** 80 | * 当前用户所需要的状态切片(这块需要注意,zustand用户可以根据selector获取部分store内容值) 81 | * 所以我们判断是否需要更新,对比的是切片内容,而非整个store 82 | */ 83 | let newStateSlice; 84 | // 更新标志 85 | let hasNewStateSlice = false; 86 | 87 | if ( 88 | stateRef.current !== state || 89 | selector !== selectorRef.current || 90 | enqulityFn !== enqulityFnRef.current 91 | ) { 92 | newStateSlice = selector(state); 93 | hasNewStateSlice = !enqulityFn(newStateSlice, currentStateRef.current); 94 | } 95 | 96 | // 初始化数据 97 | useLayoutEffect(() => { 98 | if (hasNewStateSlice) { 99 | currentStateRef.current = newStateSlice; 100 | } 101 | stateRef.current = state; 102 | selectorRef.current = selector; 103 | enqulityFnRef.current = enqulityFn; 104 | }); 105 | 106 | // 添加state变化订阅事件 107 | useLayoutEffect(() => { 108 | const listener = () => { 109 | // 获取当前最新的state状态值 110 | const nextState = api.getState(); 111 | // 拿到当前用户所需的store切片 112 | const nextStateSlice = selectorRef.current(nextState); 113 | // 比较当前用户current切片 与 最新store切片是否是一样的,如果不一样,就更新到最新的切片 114 | if (!enqulityFnRef.current(nextStateSlice, currentStateRef.current)) { 115 | stateRef.current = nextState; 116 | currentStateRef.current = nextStateSlice; 117 | forceUpdate(); 118 | } 119 | }; 120 | const unSubscribe = api.subscribe(listener); 121 | // 当组件销毁,我们需要取消订阅 122 | return unSubscribe; 123 | }, []); 124 | 125 | // 返回用户所需切片 126 | const sliceToReturn = hasNewStateSlice 127 | ? newStateSlice 128 | : currentStateRef.current; 129 | 130 | return sliceToReturn; 131 | }; 132 | // 将修改store的方法{getState, setState, destroy, subscribe}暴露出去,这样用户可以脱离react组件去使用状态管理 133 | // example: useStore.getState() .... 134 | Object.assign(useStore, api); 135 | 136 | return useStore; 137 | } 138 | 139 | export default create; 140 | ``` 141 | 142 | 项目中使用方法: 143 | 144 | ```js 145 | // 创建store 146 | import create from "../create"; 147 | 148 | export const useCounterStore = create((set) => ({ 149 | count: 0, 150 | increament: () => set((state) => ({ count: state.count + 1 })), 151 | })); 152 | 153 | // 项目中使用方式 154 | import React from "react"; 155 | import { useCounterStore } from "./store"; 156 | 157 | const Other = () => { 158 | const counter = useCounterStore(); 159 | return ( 160 |
161 |

Other

162 |
163 |
{counter.count}
164 |
165 |
166 | ); 167 | }; 168 | 169 | export default Other; 170 | ``` 171 | -------------------------------------------------------------------------------- /02~组件基础/04~React Router/配置与匹配.md: -------------------------------------------------------------------------------- 1 | # 路由配置与匹配 2 | 3 | # 路由配置 4 | 5 | 首先我们需要从 react-router-dom 中导入相应组件: 6 | 7 | ```js 8 | import React from "react"; 9 | import { BrowserRouter as Router, Route, Link } from "react-router-dom"; 10 | ``` 11 | 12 | 然后定义`Home`,`About`这两个组件: 13 | 14 | ```js 15 | const Home = () => ( 16 |
17 |

Home

  18 |
19 | ); 20 | 21 | const About = () => ( 22 |
23 |

About

  24 |
25 | ); 26 | ``` 27 | 28 | Route 组件会在 URL 满足匹配规则时渲染传入的组件,其支持多种匹配模式与多种组件渲染模式,基本使用如下: 29 | 30 | ```jsx 31 | import { BrowserRouter as Router, Route } from "react-router-dom"; 32 | 33 | 34 |
35 |   36 |   37 |
38 |
; 39 | ``` 40 | 41 | React Router 定义了 3 种不同的 Route 渲染语法: 42 | 43 | - ``: 传入某个组件类 44 | - ``: 传入某个渲染函数 45 | - ``: 传入子元素 46 | 47 | * 组件类 48 | 传入某个当路径匹配时进行渲染的 React 组件,该组件会被传入`context.router` 中的所有属性: 49 | 50 | ```js 51 | ; 52 | 53 | const User = ({ match }) => { 54 | return

Hello {match.username}!

; 55 | }; 56 | ``` 57 | 58 | - 渲染函数 59 | 60 | 我们也可以传入某个路径匹配时才会调用的回掉函数,React Router 也会自动将 `context.router` 中的属性以参数传入到该渲染函数中,这个会非常方便于行内渲染与包裹,譬如我们可以编写如下行内渲染类: 61 | 62 | ``` 63 |
Home
}/> 64 | ``` 65 | 66 | 同时我们也可以在该回调函数中对组件进行适当的封装,譬如添加渐入渐出的动画效果: 67 | 68 | ```jsx 69 | const FadingRoute = ({ component: Component, ...rest }) => ( 70 |   ( 71 |   72 |   73 |   74 |   )}/> 75 | ) 76 | 77 | 78 | ``` 79 | 80 | 如果我们希望在设置 Route 渲染对象的同时传入额外的 Props 属性,那么也可以利用 `render` 渲染函数: 81 | 82 | ``` 83 | const App = () => { 84 |   const color = 'red' 85 |   return ( 86 |   ( 87 |   88 |   )} /> 89 |   ) 90 | } 91 | ``` 92 | 93 | 注意,`` 的优先级高于``,因此要避免在``中同时使用这两种方式。 94 | 95 | - 子元素 96 | 97 | 有时我们希望无论路径是否匹配都能执行某些渲染或者逻辑操作,此时我们可以使用`children`属性,其工作机制类似`render`函数不过无论路径是否匹配其都会执行。该函数会被传入某个包含`match`与`history`属性的对象,如果不匹配时`match`对象会被设置为`null`;我们可以通过该对象来了解路由是否匹配,从而动态地调整界面展示。譬如我们需要在路由匹配成功时添加`active`类名: 98 | 99 | ```js 100 |
    101 | 102 | 103 |
; 104 | 105 | const ListItemLink = ({ to, ...rest }) => ( 106 | ( 109 |
  • 110 |      {' '} 111 |
  • 112 | )} 113 | /> 114 | ); 115 | ``` 116 | 117 | 我们也可以添加统一的动画控制,下述代码中的`Animate`无论路由匹配是否成功都会被执行,因此我们可以用 React 生命周期中的回调函数来控制子元素的淡入淡出动画: 118 | 119 | ``` 120 | ( 121 |   122 |   {match && } 123 |   124 | )}/> 125 | ``` 126 | 127 | 注意,``在三者中的优先级最低。 128 | 129 | ## 模糊匹配 130 | 131 | # 动态路由 132 | 133 | # 路由权限控制 134 | 135 | ```tsx 136 | const PrivateRoute = ({ component, isAuthenticated, ...rest }: any) => { 137 | const routeComponent = (props: any) => 138 | isAuthenticated ? ( 139 | React.createElement(component, props) 140 | ) : ( 141 | 142 | ); 143 | return ; 144 | }; 145 | ``` 146 | 147 | 该组件的用法如下: 148 | 149 | ```tsx 150 | 155 | ``` 156 | 157 | 上述函数式组件的用法缺乏了类型约束,我们可以扩展上述简单的组件用法: 158 | 159 | ```tsx 160 | import * as React from "react"; 161 | import { Redirect, Route, RouteProps } from "react-router"; 162 | 163 | export interface ProtectedRouteProps extends RouteProps { 164 | isAuthenticated: boolean; 165 | authenticationPath: string; 166 | } 167 | 168 | export class ProtectedRoute extends Route { 169 | public render() { 170 | let redirectPath: string = ""; 171 | if (!this.props.isAuthenticated) { 172 | redirectPath = this.props.authenticationPath; 173 | } 174 | 175 | if (redirectPath) { 176 | const renderComponent = () => ( 177 | 178 | ); 179 | return ( 180 | 181 | ); 182 | } else { 183 | return ; 184 | } 185 | } 186 | } 187 | ``` 188 | 189 | 然后其用法如下: 190 | 191 | ```tsx 192 | const defaultProtectedRouteProps: ProtectedRouteProps = { 193 | isAuthenticated: this.props.state.session.isAuthenticated, 194 | authenticationPath: "/login" 195 | }; 196 | 197 | ; 203 | ``` 204 | -------------------------------------------------------------------------------- /04~工程实践/组件测试/Enzyme.md: -------------------------------------------------------------------------------- 1 | # Enzyme 2 | 3 | Enzyme 是由 Airbnb 开源的一个 React 的 JavaScript 测试工具,允许我们在 DOM 环境中测试 React 组件。使 React 组件的输出更加容易 extrapolate。Enzyme 的 API 和 jQuery 操作 DOM 一样灵活易用,因为它使用的是 cheerio 库来解析虚拟 DOM,而 cheerio 的目标则是做服务器端的 jQuery。Enzyme 兼容大多数断言库和测试框架,如 chai、mocha、jasmine 等。 4 | 5 | 首先需要在项目中安装 Enzyme: 6 | 7 | ``` 8 | $ npm i enzyme @types/enzyme enzyme-to-json enzyme-adapter-react-16 -D 9 | ``` 10 | 11 | 然后在 jest.config.js 文件中添加 snapshotSerializers 与 setupTestFrameworkScriptFile 配置: 12 | 13 | ```js 14 | module.exports = { 15 | // OTHER PORTIONS AS MENTIONED BEFORE 16 | 17 | // Setup Enzyme 18 | snapshotSerializers: ["enzyme-to-json/serializer"], 19 | setupTestFrameworkScriptFile: "/src/setupEnzyme.ts" 20 | }; 21 | ``` 22 | 23 | 然后创建初始化文件: 24 | 25 | ```ts 26 | // src/setupEnzyme.ts 27 | import { configure } from "enzyme"; 28 | import * as EnzymeAdapter from "enzyme-adapter-react-16"; 29 | configure({ adapter: new EnzymeAdapter() }); 30 | ``` 31 | 32 | 简单的 React 组件如下: 33 | 34 | ```tsx 35 | import * as React from "react"; 36 | 37 | export class CheckboxWithLabel extends React.Component< 38 | { 39 | labelOn: string; 40 | labelOff: string; 41 | }, 42 | { 43 | isChecked: boolean; 44 | } 45 | > { 46 | constructor(props) { 47 | super(props); 48 | this.state = { isChecked: false }; 49 | } 50 | 51 | onChange = () => { 52 | this.setState({ isChecked: !this.state.isChecked }); 53 | }; 54 | 55 | render() { 56 | return ( 57 | 65 | ); 66 | } 67 | } 68 | ``` 69 | 70 | 其对应的测试文件如下: 71 | 72 | ```tsx 73 | import * as React from "react"; 74 | import { shallow } from "enzyme"; 75 | 76 | import { CheckboxWithLabel } from "./checkboxWithLabel"; 77 | 78 | test("CheckboxWithLabel changes the text after click", () => { 79 | const checkbox = shallow(); 80 | 81 | // Interaction demo 82 | expect(checkbox.text()).toEqual("Off"); 83 | checkbox.find("input").simulate("change"); 84 | expect(checkbox.text()).toEqual("On"); 85 | 86 | // Snapshot demo 87 | expect(checkbox).toMatchSnapshot(); 88 | }); 89 | ``` 90 | 91 | # 测试组件的渲染 92 | 93 | 对于大部分没有交互的组件,下面的测试用例已经足够: 94 | 95 | ```jsx 96 | test("render a label", () => { 97 | const wrapper = shallow(); 98 | expect(wrapper).toMatchSnapshot(); 99 | }); 100 | 101 | test("render a small label", () => { 102 | const wrapper = shallow(); 103 | expect(wrapper).toMatchSnapshot(); 104 | }); 105 | 106 | test("render a grayish label", () => { 107 | const wrapper = shallow(); 108 | expect(wrapper).toMatchSnapshot(); 109 | }); 110 | ``` 111 | 112 | # Props 测试 113 | 114 | 有的时候如果你想测试的更精确和看到真实的值。那样的话需要在 Enzyme API 中使用 Jest 的 断言。 115 | 116 | ```js 117 | test("render a document title", () => { 118 | const wrapper = shallow(); 119 | expect(wrapper.prop("title")).toEqual("Events"); 120 | }); 121 | 122 | test("render a document title and a parent title", () => { 123 | const wrapper = shallow( 124 | 125 | ); 126 | expect(wrapper.prop("title")).toEqual("Events — Event Radar"); 127 | }); 128 | ``` 129 | 130 | 有的时候你不能用快照。比如组件里面有随机 ID 像下面的代码: 131 | 132 | ```js 133 | test("render a popover with a random ID", () => { 134 | const wrapper = shallow(Hello Jest!); 135 | expect(wrapper.prop("id")).toMatch(/Popover\d+/); 136 | }); 137 | ``` 138 | 139 | # 事件测试 140 | 141 | 我们可以模拟类似 `click` 或者 `change` 这样的事件然后把组件和快照做比较: 142 | 143 | ```js 144 | test("render Markdown in preview mode", () => { 145 | const wrapper = shallow(); 146 | expect(wrapper).toMatchSnapshot(); 147 | wrapper.find('[name="toggle-preview"]').simulate("click"); 148 | expect(wrapper).toMatchSnapshot(); 149 | }); 150 | ``` 151 | 152 | 有的时候你想要测试一个子组件中一个元素是怎样影响组件的。你需要使用 Enzyme 的 mount 方法来渲染一个真实的 DOM。 153 | 154 | ```js 155 | test("open a code editor", () => { 156 | const wrapper = mount(); 157 | expect(wrapper.find(".ReactCodeMirror")).toHaveLength(0); 158 | wrapper.find("button").simulate("click"); 159 | expect(wrapper.find(".ReactCodeMirror")).toHaveLength(1); 160 | }); 161 | ``` 162 | 163 | # 测试事件处理 164 | 165 | 类似于在事件测试中,由使用快照测试组件的输出呈现替换为使用 Jest 的 mock 函数来测试事件处理程序本身: 166 | 167 | ```js 168 | test("pass a selected value to the onChange handler", () => { 169 | const value = "2"; 170 | const onChange = jest.fn(); 171 | const wrapper = shallow(  111 |

    112 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 113 | eiusmod tempor incididunt {this.state.str} ut labore et dolore magna 114 | aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco 115 | laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor 116 | in reprehenderit in voluptate velit esse cillum dolore{" "} 117 | {this.state.str} eu fugiat nulla pariatur. Excepteur sint occaecat{" "} 118 | {this.state.str} cupidatat non proident, sunt in culpa qui officia 119 | deserunt mollit anim id est laborum. 120 |

    121 |
    122 | ); 123 | } 124 | } 125 | 126 | // after 127 | class Ipsum extends React.Component { 128 | listeners = []; 129 | listen = fn => { 130 | this.listeners.push(fn); 131 | }; 132 | unlisten = fn => { 133 | this.listeners = this.listeners.filter(x => x !== fn); 134 | }; 135 | onChange = e => { 136 | this.listeners.forEach(x => x(e)); 137 | }; 138 | render() { 139 | return ( 140 |
    141 |   142 |

    143 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 144 | eiusmod tempor incididunt ut labore et 145 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud 146 | exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 147 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum 148 | dolore eu fugiat nulla pariatur. 149 | Excepteur sint occaecat 150 | cupidatat non proident, sunt in culpa qui 151 | officia deserunt mollit anim id est laborum. 152 |

    153 |
    154 | ); 155 | } 156 | } 157 | 158 | // The secret sauce here is that components can now return strings 159 | class Chunk extends React.Component { 160 | state = { 161 | str: this.props.initial || "" 162 | }; 163 | componentDidMount() { 164 | this.props.listen(e => this.setState({ str: e.target.value })); 165 | } 166 | render() { 167 | return this.state.str; 168 | } 169 | } 170 | // render(, window.root) 171 | render(, window.root); 172 | ``` 173 | -------------------------------------------------------------------------------- /02~组件基础/04~React Router/控制与切换.md: -------------------------------------------------------------------------------- 1 | # 路由控制与切换 2 | 3 | # 编码控制 4 | 5 | ## 使用 router 对象 6 | 7 | 在基础示例中我们讨论过,传入 `Route` 配置中的组件会被进行包裹传入`go`、`push`等可用于手动路由控制的 Props,而对于非路由配置中的组件,React Router 同样提供了便捷的`withRouter`,允许以高阶函数的方式将`router`对象属性传入组件中。`withRouter`会在每次路由跳转时重新渲染被包裹的组件。这里我们编写一个简单的示例,即在组件树的某个组件中显示当前的位置信息。 8 | 9 | ```js 10 | // 简单的用于显示当前路径信息的组件 11 | class ShowTheLocation extends React.Component { 12 | static propTypes = { 13 | location: PropTypes.object.isRequired 14 | }; 15 | 16 | render() { 17 | return
    You are now at {this.props.location.pathname}
    ; 18 | } 19 | } 20 | 21 | // 类似于 Redux 中的 connected,将组件链接到 router,并且将 router 的属性以 Props 传入到组件中 22 | const ShowTheLocationWithRouter = withRouter(ShowTheLocation); 23 | ``` 24 | 25 | 每个``都会将一个 router 对象绑定到 React Context 中,并且方便``与子级别的``, ``, `` 进行通信。虽然这个接口比较方便,不过鉴于其主要是用于内部通信,并且 Context 本身也是实验性的接口,有可能会在 React 未来版本中发生变化,因此我们建议尽量谨慎使用该接口。`context.router` 对象的属性为: 26 | 27 | ```js 28 | context.router = { 29 | ...history, 30 | match 31 | }; 32 | ``` 33 | 34 | ## 自定义 history 对象 35 | 36 | 当我们使用 `BrowserRouter`、`HashRouter`、`MemoryRouter` 以及 `NativeRouter` 作为根路由时,它们会自动地创建 `history` 对象,该对象能够被自动注入到 React 组件中;不过很多时候我们也需要在组件外,譬如状态管理代码中进行些路由控制。此时如果我们需要在组件树之外使用 `history` 对象,就需要手动创建 `history` 对象,然后在声明根 Router 对象时引用该对象: 37 | 38 | ```js 39 | // history.js 40 | import { createBrowserHistory } from 'history' 41 | export default createBrowserHistory() 42 | 43 | 44 | // index.js 45 | import { Router } from 'react-router-dom'; 46 | import history from './history' 47 | 48 | 49 | ReactDOM.render(( 50 |   51 |   52 |   53 | ), document.getElementById('root')) 54 | 55 | 56 | // nav.js 57 | import history from './history' 58 | 59 | 60 | export default function nav(loc) { 61 |   history.push(loc); 62 | } 63 | ``` 64 | 65 | 我们在上文中介绍过 `history` 中`browser history`、`hash history`、`memory history`的区别,而`history`对象还包含以下属性与方法: 66 | 67 | - `length` - (number) 历史记录的数目 68 | - `action` - (string) 当前动作 (`PUSH`, `REPLACE`, 或者 `POP`) 69 | - `location` - (object) 当前地址路径,可能包含以下属性 70 | - `pathname` - (string) URL 的路径 71 | - `search` - (string) URL 中查询字符串 72 | - `hash` - (string)URL hash fragment 73 | - `state` - (string) `push(path, state)` 函数传入的`state`参数,仅可用于 browser history 或者 memory history 74 | - `push(path, [state])` - (function) 向当前 history stack 压入新记录 75 | - `replace(path, [state])` - (function) 替换当前 history stack 顶部记录 76 | - `go(n)` - (function) 将 history stack 指针移动`n`步 77 | - `goBack()` - (function) 等价于 `go(-1)` 78 | - `goForward()` - (function) 等价于 `go(1)` 79 | - `block(prompt)` - (function) 避免跳转 80 | 81 | ## 参数传递 82 | 83 | # 转场控制 84 | 85 | ## 权限控制 86 | 87 | ```js 88 | /** 89 | * Returns the pages available when the authentication is complete 90 | * @private 91 | * @returns {XML} 92 | * @constructor 93 | */ 94 | function PrivateRoutes() { 95 | return ( 96 |
    97 | {/* this div here is worthless */} 98 | 99 | 100 |   101 |
    102 | ); 103 | } 104 | 105 | /** 106 | * Returns all the pages that are public and do not require any authentication 107 | * @private 108 | * @returns {XML} 109 | * @constructor 110 | */ 111 | function PublicRoutes() { 112 | return ( 113 |
    114 | {/* this div here is worthless */} 115 | 116 | 117 |   118 |
    119 | ); 120 | } 121 | 122 | /** 123 | * All the application routes 124 | * @returns {XML} 125 | */ 126 | function Routes({ isAuthenticated }) { 127 | return ( 128 | 129 | 130 |
    131 | {/* this div here is worthless */} 132 | 133 |  {" "} 134 | {do { 135 | if (isAuthenticated) { 136 | ; 137 | } else { 138 | } 141 | />; 142 | } 143 | }} 144 |   145 |
    146 |
    147 |
    148 | ); 149 | } 150 | ``` 151 | 152 | ```js 153 | class App extends Component { 154 | //snip our user stuff from earlier 155 | 156 | render() { 157 | return ( 158 | 159 | 160 | 161 | 162 |   163 | 164 | ); 165 | } 166 | } 167 | 168 | const MatchWhenAuthorized = ({ component: Component, ...rest }) => ( 169 | 172 | isAuthenticated() ? ( 173 | 174 | ) : ( 175 | 181 | ) 182 | } 183 | /> 184 | ); 185 | ``` 186 | 187 | ## 转场提示 188 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/Hooks/基础元语/useCallback & useMemo.md: -------------------------------------------------------------------------------- 1 | # useCallback 与 useMemo 2 | 3 | ```js 4 | function Button({ onClick, color, children }) { 5 | const textColor = slowlyCalculateTextColor(this.props.color); 6 | return ( 7 | 13 | ); 14 | } 15 | export default React.memo(Button); // ✅ Uses shallow comparison 16 | ``` 17 | 18 | 不过在实际场景下,很多的 Callback 还是会被重新生成,我们还是需要在子组件中进行精细地 shouldComponentUpdate 控制。 19 | 20 | # 闭包冻结 21 | 22 | 在 useCallback 的使用中,有时候我们会碰到所谓的闭包冻结的现象,即如下所示: 23 | 24 | ```js 25 | let Test = () => { 26 | /** Search base infos */ 27 | const [searchID, setSearchID] = useState(0); 28 | 29 | /** Search info action */ 30 | const onSearchInfos = useCallback(() => { 31 | let fetchUrl = "/api/getSearchInfos"; 32 | let fetchParams = { searchID }; 33 | fetch(fetchUrl, { 34 | method: "POST", 35 | body: JSON.stringify(fetchParams), 36 | }) 37 | .then((res) => res.json()) 38 | .then((res) => { 39 | console.log(res); 40 | }); 41 | }, []); 42 | 43 | return ( 44 | <> 45 | 52 | 59 | 60 | ); 61 | }; 62 | ``` 63 | 64 | 点击 button1 按钮,searchID 的值加 1,点击 button2 发送一个请求。然而问题是,当我们点击了四次 button1,把 searchID 的值更改到了 4,然后点击 button2,会发现,发送出去的请求,searhID 的值是 0。 65 | 66 | ## 原因分析 67 | 68 | 我们可以使用如下代码来理解: 69 | 70 | ```js 71 | storage = { a: 1 }; 72 | 73 | (() => { 74 | let b = storage.a; 75 | 76 | storage.f = () => { 77 | console.log(b); 78 | }; 79 | })(); 80 | 81 | storage.a = 2; 82 | 83 | storage.f(); 84 | ``` 85 | 86 | 参考 Hooks 的实现原理,我们可以模拟写出如下的代码: 87 | 88 | ```js 89 | // React has some component-local storage that it tracks behind the scenes. 90 | // useState and useCallback both hook into this. 91 | // 92 | // Imagine there's a 'storage' variable for every instance of your 93 | // component. 94 | const storage = {}; 95 | 96 | function useState(init) { 97 | if (storage.data === undefined) { 98 | storage.data = init; 99 | } 100 | 101 | return [storage.data, (value) => (storage.data = value)]; 102 | } 103 | 104 | function useCallback(fn) { 105 | // The real version would check dependencies here, but since our callback 106 | // should only update on the first render, this will suffice. 107 | if (storage.callback === undefined) { 108 | storage.callback = fn; 109 | } 110 | 111 | return storage.callback; 112 | } 113 | 114 | function MyComponent() { 115 | const [data, setData] = useState(0); 116 | const callback = useCallback(() => data); 117 | 118 | // Rather than outputting DOM, we'll just log. 119 | console.log("data:", data); 120 | console.log("callback:", callback()); 121 | 122 | return { 123 | increase: () => setData(data + 1), 124 | }; 125 | } 126 | 127 | let instance = MyComponent(); // Let's 'render' our component... 128 | 129 | instance.increase(); // This would trigger a re-render, so we call our component again... 130 | instance = MyComponent(); 131 | 132 | instance.increase(); // and again... 133 | instance = MyComponent(); 134 | ``` 135 | 136 | ## 解决方案 137 | 138 | ### 添加依赖 139 | 140 | ```js 141 | const onSearchInfos = useCallback(() => { 142 | // ... 143 | }, [searchID]); 144 | ``` 145 | 146 | ### 使用 Ref 147 | 148 | ```js 149 | interface IRef { 150 | current: any; 151 | } 152 | 153 | let Test = () => { 154 | /** Search base infos */ 155 | const [searchID, setSearchID] = useState(0); 156 | 157 | /** 解决闭包问题 */ 158 | const fetchRef: IRef = useRef(); // hooks为我们提供的一个通用容器,里面有一个current属性 159 | fetchRef.current = { 160 | // 为current这个属性添加一个searchID,每当searchID状态变更的时候,Test都会进行重新渲染,从而current能拿到最新的值 161 | searchID, 162 | }; 163 | 164 | /** Search info action */ 165 | const onSearchInfos = useCallback(() => { 166 | let fetchUrl = "/api/getSearchInfos"; 167 | let fetchParams = { ...fetchRef.current }; // 解构参数,这里拿到的是外层fetchRef的引用 168 | fetch(fetchUrl, { 169 | method: "POST", 170 | body: JSON.stringify(fetchParams), 171 | }) 172 | .then((res) => res.json()) 173 | .then((res) => { 174 | console.log(res); 175 | }); 176 | }, []); 177 | 178 | return ( 179 | <> 180 | 187 | 194 | 195 | ); 196 | }; 197 | ``` 198 | 199 | # useMemo 200 | 201 | useMemo 的用法类似 useEffect,常常用于缓存一些复杂计算的结果。useMemo 接收一个函数和依赖数组,当数组中依赖项变化的时候,这个函数就会执行,返回新的值。 202 | 203 | ```js 204 | const sum = useMemo(() => { 205 | // 一系列计算 206 | }, [count]); 207 | ``` 208 | 209 | # Links 210 | 211 | - https://blog.csdn.net/sinat_17775997/article/details/94453167 212 | - https://nikgrozev.com/2019/04/07/reacts-usecallback-and-usememo-hooks-by-example/ 213 | -------------------------------------------------------------------------------- /03~状态管理/XState/React.md: -------------------------------------------------------------------------------- 1 | # 在 React 中使用 2 | 3 | ## Hooks 4 | 5 | 这裡我们使用 React 当作 UI Library 来实作,需求是画面上会有一个 Button 以及一个圆点,点击 Button 以后圆点的颜色会改变,颜色改变顺序为 红 → 绿 → 黄 → 红... 不断接续。 6 | 7 | ```js 8 | const LIGHT_STATES = { 9 | RED: "RED", 10 | GREEN: "green", 11 | YELLOW: "yellow", 12 | }; 13 | 14 | const LIGHT_EVENTS = { 15 | CLICK: "CLICK", 16 | }; 17 | 18 | export const lightMachine = Machine({ 19 | initial: LIGHT_STATES.RED, 20 | states: { 21 | [LIGHT_STATES.RED]: { 22 | on: { 23 | [LIGHT_EVENTS.CLICK]: LIGHT_STATES.GREEN, 24 | }, 25 | }, 26 | [LIGHT_STATES.GREEN]: { 27 | on: { 28 | [LIGHT_EVENTS.CLICK]: LIGHT_STATES.YELLOW, 29 | }, 30 | }, 31 | [LIGHT_STATES.YELLOW]: { 32 | on: { 33 | [LIGHT_EVENTS.CLICK]: LIGHT_STATES.RED, 34 | }, 35 | }, 36 | }, 37 | }); 38 | ``` 39 | 40 | 然后在组件中定义状态: 41 | 42 | ```js 43 | import React from 'react'; 44 | import { useMachine } from '@xstate/react'; 45 | import { lightMachine } from './lightMachine'; 46 | 47 | function App() { 48 | const [state, send] = useMachine(lightMachine); 49 | return ( 50 | //... 51 | ); 52 | } 53 | ``` 54 | 55 | React 的部分我们使用了 XState 官方提供的 @xstate/react Library,这裡用到的 useMachine 其实就是用了前面提到的 interpret 它已经帮我们产生好 service 并会回传 [state, send, service] 。 56 | 57 | ```js 58 | import React from "react"; 59 | import { useMachine } from "@xstate/react"; 60 | import { lightMachine } from "./lightMachine"; 61 | 62 | function App() { 63 | const [state, send] = useMachine(lightMachine); 64 | return ( 65 |
    66 | {state.matches(LIGHT_STATES.RED) && } 67 | {state.matches(LIGHT_STATES.GREEN) && } 68 | {state.matches(LIGHT_STATES.YELLOW) && } 69 | 76 |
    77 | ); 78 | } 79 | ``` 80 | 81 | 最后 return 时只要透过 state.matches 决定要显示哪个状态的画面,并且在 button onClick 时传送 LIGHT_EVENTS.CLICK 事件就可以啦。 82 | 83 | ## 在类组件中使用 84 | 85 | 当然,我们也可以在类组件中使用,定义如下的状态机: 86 | 87 | ```js 88 | import { Machine } from "xstate"; 89 | 90 | // This machine is completely decoupled from React 91 | export const toggleMachine = Machine({ 92 | id: "toggle", 93 | initial: "inactive", 94 | states: { 95 | inactive: { 96 | on: { TOGGLE: "active" }, 97 | }, 98 | active: { 99 | on: { TOGGLE: "inactive" }, 100 | }, 101 | }, 102 | }); 103 | ``` 104 | 105 | 对状态机进行解释,并将其服务实例放在组件实例上。对于本地状态,this.state.current 将持有当前的状态机状态。你可以使用除.current 以外的属性名。当组件被挂载时,服务将通过 this.service.start() 启动。当组件将卸载时,服务通过 this.service.stop() 停止。事件通过 this.service.send(event) 发送给服务。 106 | 107 | ```js 108 | import React from "react"; 109 | import { Machine, interpret } from "xstate"; 110 | import { toggleMachine } from "../path/to/toggleMachine"; 111 | 112 | class Toggle extends React.Component { 113 | state = { 114 | current: toggleMachine.initialState, 115 | }; 116 | 117 | service = interpret(toggleMachine).onTransition((current) => 118 | this.setState({ current }) 119 | ); 120 | 121 | componentDidMount() { 122 | this.service.start(); 123 | } 124 | 125 | componentWillUnmount() { 126 | this.service.stop(); 127 | } 128 | 129 | render() { 130 | const { current } = this.state; 131 | const { send } = this.service; 132 | 133 | return ( 134 | 137 | ); 138 | } 139 | } 140 | ``` 141 | 142 | # 更复杂的搜索的例子 143 | 144 | > https://medium.com/weekly-webtips/intro-to-xstate-a-true-state-management-system-library-for-react-d8c0051c71e4 145 | 146 | ```js 147 | import { Machine, assign } from "xstate"; 148 | import { search } from "../../services/github"; 149 | 150 | const statechart = { 151 | id: "search", 152 | context: { 153 | result: [], 154 | }, 155 | initial: "idle", 156 | on: { 157 | SEARCH: [ 158 | { 159 | target: "searching", 160 | cond: { 161 | type: "search query has more than one character", 162 | }, 163 | }, 164 | { 165 | target: "idle", 166 | actions: ["resetSearchResults"], 167 | }, 168 | ], 169 | }, 170 | states: { 171 | idle: {}, 172 | searching: { 173 | invoke: { 174 | src: "searchService", 175 | onDone: { 176 | target: "loaded", 177 | actions: ["storeResult"], 178 | }, 179 | onError: { 180 | target: "failure", 181 | }, 182 | }, 183 | }, 184 | loaded: {}, 185 | failure: {}, 186 | }, 187 | }; 188 | 189 | const machineConfig = { 190 | services: { 191 | searchService: (_, event) => { 192 | return search(event.entity, { 193 | q: event.q, 194 | }); 195 | }, 196 | }, 197 | actions: { 198 | storeResult: assign({ 199 | result: (_, event) => { 200 | return event.data.items; 201 | }, 202 | }), 203 | resetSearchResults: assign({ 204 | result: () => { 205 | return []; 206 | }, 207 | }), 208 | }, 209 | guards: { 210 | "search query has more than one character": (_, event) => { 211 | return event.q.length >= 2; 212 | }, 213 | }, 214 | }; 215 | 216 | export const searchMachine = Machine(statechart, machineConfig); 217 | ``` 218 | -------------------------------------------------------------------------------- /02~组件基础/02~组件数据流/Hooks/基础元语/useSubscription.md: -------------------------------------------------------------------------------- 1 | # useSubscription 2 | 3 | ```ts 4 | import React, { useMemo } from "react"; 5 | import useSubscription from "./useSubscription"; 6 | 7 | // In this example, "source" is an event dispatcher (e.g. an HTMLInputElement) 8 | // but it could be anything that emits an event and has a readable current value. 9 | function Example({ source }) { 10 | // In order to avoid removing and re-adding subscriptions each time this hook is called, 11 | // the parameters passed to this hook should be memoized. 12 | const subscription = useMemo( 13 | () => ({ 14 | getCurrentValue: () => source.value, 15 | subscribe: (callback) => { 16 | source.addEventListener("change", callback); 17 | return () => source.removeEventListener("change", callback); 18 | }, 19 | }), 20 | 21 | // Re-subscribe any time our "source" changes 22 | // (e.g. we get a new HTMLInputElement target) 23 | [source] 24 | ); 25 | 26 | const value = useSubscription(subscription); 27 | 28 | // Your rendered output here ... 29 | } 30 | ``` 31 | 32 | ```ts 33 | import {useEffect, useState} from 'react'; 34 | 35 | // Hook used for safely managing subscriptions in concurrent mode. 36 | // 37 | // In order to avoid removing and re-adding subscriptions each time this hook is called, 38 | // the parameters passed to this hook should be memoized in some way– 39 | // either by wrapping the entire params object with useMemo() 40 | // or by wrapping the individual callbacks with useCallback(). 41 | export function useSubscription({ 42 | // (Synchronously) returns the current value of our subscription. 43 | getCurrentValue, 44 | 45 | // This function is passed an event handler to attach to the subscription. 46 | // It should return an unsubscribe function that removes the handler. 47 | subscribe, 48 | }: {| 49 | getCurrentValue: () => Value, 50 | subscribe: (callback: Function) => () => void, 51 | |}): Value { 52 | // Read the current value from our subscription. 53 | // When this value changes, we'll schedule an update with React. 54 | // It's important to also store the hook params so that we can check for staleness. 55 | // (See the comment in checkForUpdates() below for more info.) 56 | const [state, setState] = useState(() => ({ 57 | getCurrentValue, 58 | subscribe, 59 | value: getCurrentValue(), 60 | })); 61 | 62 | let valueToReturn = state.value; 63 | 64 | // If parameters have changed since our last render, schedule an update with its current value. 65 | if ( 66 | state.getCurrentValue !== getCurrentValue || 67 | state.subscribe !== subscribe 68 | ) { 69 | // If the subscription has been updated, we'll schedule another update with React. 70 | // React will process this update immediately, so the old subscription value won't be committed. 71 | // It is still nice to avoid returning a mismatched value though, so let's override the return value. 72 | valueToReturn = getCurrentValue(); 73 | 74 | setState({ 75 | getCurrentValue, 76 | subscribe, 77 | value: valueToReturn, 78 | }); 79 | } 80 | 81 | // It is important not to subscribe while rendering because this can lead to memory leaks. 82 | // (Learn more at reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects) 83 | // Instead, we wait until the commit phase to attach our handler. 84 | // 85 | // We intentionally use a passive effect (useEffect) rather than a synchronous one (useLayoutEffect) 86 | // so that we don't stretch the commit phase. 87 | // This also has an added benefit when multiple components are subscribed to the same source: 88 | // It allows each of the event handlers to safely schedule work without potentially removing an another handler. 89 | // (Learn more at https://codesandbox.io/s/k0yvr5970o) 90 | useEffect( 91 | () => { 92 | let didUnsubscribe = false; 93 | 94 | const checkForUpdates = () => { 95 | // It's possible that this callback will be invoked even after being unsubscribed, 96 | // if it's removed as a result of a subscription event/update. 97 | // In this case, React will log a DEV warning about an update from an unmounted component. 98 | // We can avoid triggering that warning with this check. 99 | if (didUnsubscribe) { 100 | return; 101 | } 102 | 103 | setState(prevState => { 104 | // Ignore values from stale sources! 105 | // Since we subscribe an unsubscribe in a passive effect, 106 | // it's possible that this callback will be invoked for a stale (previous) subscription. 107 | // This check avoids scheduling an update for that stale subscription. 108 | if ( 109 | prevState.getCurrentValue !== getCurrentValue || 110 | prevState.subscribe !== subscribe 111 | ) { 112 | return prevState; 113 | } 114 | 115 | // Some subscriptions will auto-invoke the handler, even if the value hasn't changed. 116 | // If the value hasn't changed, no update is needed. 117 | // Return state as-is so React can bail out and avoid an unnecessary render. 118 | const value = getCurrentValue(); 119 | if (prevState.value === value) { 120 | return prevState; 121 | } 122 | 123 | return {...prevState, value}; 124 | }); 125 | }; 126 | const unsubscribe = subscribe(checkForUpdates); 127 | 128 | // Because we're subscribing in a passive effect, 129 | // it's possible that an update has occurred between render and our effect handler. 130 | // Check for this and schedule an update if work has occurred. 131 | checkForUpdates(); 132 | 133 | return () => { 134 | didUnsubscribe = true; 135 | unsubscribe(); 136 | }; 137 | }, 138 | [getCurrentValue, subscribe], 139 | ); 140 | 141 | // Return the current value for our caller to use while rendering. 142 | return valueToReturn; 143 | } 144 | ``` 145 | -------------------------------------------------------------------------------- /04~工程实践/02~数据加载/数据请求.md: -------------------------------------------------------------------------------- 1 | # 数据请求 2 | 3 | # 数据请求库的特性 4 | 5 | 假设有个最简单的需求,MovieList 页面需要从服务端获取数据并展示。这里先抛出几个问题: 6 | 7 | - 请求的方法写在哪里?请求来的数据放在哪里? 8 | - 如果请求还未完成,组件已被 Unmount,有没有做特殊处理? 9 | - 请求的参数来自哪里,参数变化会触发重新请求吗?调用写在哪里? 10 | - 请求对应的几个状态: loading,refreshing,error 在组件上处理了么? 11 | - 如果请求需要轮询,怎么做?有没有处理页面 visibilitychange 的情况? 12 | - 如果需要在参数变化后重新请求,如果参数频繁更新,会出现竞态(旧的请求因为慢,晚于后发的请求 resolve)的问题吗? 13 | 14 | 这里我们以 umijs 的 useRequest 为例,讨论常用的数据请求库应该包含哪些特性。 15 | 16 | ## 基础网络请求 17 | 18 | ```ts 19 | function getUsername() { 20 | return Promise.resolve("jack"); 21 | } 22 | 23 | export default () => { 24 | const { data, error, loading } = useRequest(getUsername); 25 | 26 | if (error) return
    failed to load
    ; 27 | if (loading) return
    loading...
    ; 28 | return
    Username: {data}
    ; 29 | }; 30 | ``` 31 | 32 | 这是一个最简单的网络请求示例。在这个例子中 useRequest 接收了一个 Promise 函数。在组件初始化时,会自动触发 getUsername 执行,并自动管理 data、loading、error 等数据,我们只需要根据状态来写相应的 UI 实现即可。 33 | 34 | ## 手动请求 35 | 36 | 对于“写”请求,我们一般需要手动触发,比如添加用户,编辑信息,删除用户等等。useRequest 只需要配置 manual = true,即可阻止初始化执行。只有触发 run 时才会开始执行。 37 | 38 | ```ts 39 | export default () => { 40 | const { run, loading } = useRequest(changeUsername, { manual: true }); 41 | 42 | return ( 43 | 46 | ); 47 | }; 48 | ``` 49 | 50 | ## 轮询 51 | 52 | 对于需要保持新鲜度的数据,我们通常需要不断发起网络请求以更新数据。useRequest 只要配置 poilingInterval 即可自动定时发起网络请求。 53 | 54 | ```ts 55 | export default () => { 56 | const { data } = useRequest(getUsername, { pollingInterval: 1000 }); 57 | return
    Username: {data}
    ; 58 | }; 59 | ``` 60 | 61 | 同时通过设置 pollingWhenHidden,我们可以智能的实现在屏幕隐藏时,暂停轮询。等屏幕恢复可见时,继续请求,以节省资源。当然你也可以通过 run/cancel 来手动控制定时器的开启和关闭。 62 | 63 | ## 并行请求 64 | 65 | 同一个接口,我们需要维护多个请求状态。示例中的并行请求有几个特点: 66 | 67 | - 删除 n 个不同的用户,则需要维护 n 个请求状态。 68 | - 多次删除同一个用户,则只需要维护最后一个请求。 69 | 70 | ![并行请求](https://s1.ax1x.com/2020/03/23/8Tgc4A.md.png) 71 | 72 | useRequest 通过设置 fetchKey,即可对请求进行分类。相同分类的请求,只会维护一份状态。不同分类的请求,则会维护多份状态。在下面的代码中,我们通过 userId 将请求进行分类,同时我们可以通过 fetches[userId] 拿到当前分类的请求状态! 73 | 74 | ```ts 75 | export default () => { 76 | const { run, fetches } = useRequest(deleteUser, { 77 | manual: true, 78 | fetchKey: (id) => id, // 不同的 ID,分类不同 79 | }); 80 | return ( 81 |
    82 | 90 | 98 | 106 |
    107 | ); 108 | }; 109 | ``` 110 | 111 | ## 防抖 & 节流 112 | 113 | 通常在边输入边搜索的场景中,我们会用到防抖功能,以节省不必要的网络请求。通过 useRequest,只需要配置一个 debounceInterval,就可以非常简单的实现对网络请求的节流操作。 114 | 115 | ```ts 116 | export default () => { 117 | const { data, loading, run, cancel } = useRequest(getEmail, { 118 | debounceInterval: 500, 119 | manual: true, 120 | }); 121 | return ( 122 |
    123 | 131 |
    132 | ); 133 | }; 134 | ``` 135 | 136 | ## 缓存 & SWR & 预加载 137 | 138 | SWR 是 stale-while-revalidate 的简称,最主要的能力是:我们在发起网络请求时,会优先返回之前缓存的数据,然后在背后发起新的网络请求,最终用新的请求结果重新触发组件渲染。swr 特性在特定场景,对用户非常友好。在 SWR 场景下,我们会对接口数据进行缓存,当下次请求该接口时,我们会先返回缓存的数据,同时,在背后发起新的网络请求,待新数据拿到后,重新触发渲染。 139 | 140 | 对于一些数据不是经常变化的接口,使用 SWR 后,可以极大提高用户使用体验。比如下面的图片例子,当我们第二次访问该文章时,直接返回了缓存的数据,没有任何的等待时间。同时,我们可以看到“最新访问时间”在 2 秒后更新了,这意味着新的请求数据返回了。 141 | 142 | ```ts 143 | const { data, loading } = useRequest(getArticle, { 144 | cacheKey: "articleKey", 145 | }); 146 | ``` 147 | 148 | 同时需要注意,同一个 cacheyKey 的数据是全局共享的。通过这个特性,我们可以实现“预加载”功能。比如鼠标 hover 到文章标题时,我们即发送读取文章详情的请求,这样等用户真正点进文章时,数据早已经缓存好了。 149 | 150 | ## 屏幕聚焦重新请求 151 | 152 | 通过配置 refreshOnWindowFocus,我们可以实现,在屏幕重新聚焦或可见时,重新发起网络请求。这个特性有什么用呢?它可以保证多个 tab 间数据的同步性。也可以解决长间隔之后重新打开网站的数据新鲜度问题。 153 | 154 | ## 集成请求库 155 | 156 | 考虑到使用便捷性,useRequest 集成了 umi-request。如果第一个参数不是 Promise,我们会通过 umi-request 来发起网络请求。当然如果你想用 axios,也是可以的,通过 requstMethod 即可定制你自己的请求方法。 157 | 158 | ```ts 159 | // 用法 1 160 | const { data, error, loading } = useRequest("/api/userInfo"); 161 | // 用法 2 162 | const { data, error, loading } = useRequest({ 163 | url: "/api/changeUsername", 164 | method: "post", 165 | }); 166 | // 用法 3 167 | const { data, error, loading, run } = useRequest( 168 | (userId) => `/api/userInfo/${userId}` 169 | ); 170 | // 用法 4 171 | const { loading, run } = useRequest((username) => ({ 172 | url: "/api/changeUsername", 173 | method: "post", 174 | data: { username }, 175 | })); 176 | ``` 177 | 178 | ## 分页 179 | 180 | 中台应用中最多的就是表格和表单了。对于一个表格,我们要处理非常多的请求逻辑,包括不限于: 181 | 182 | - page、pageSize、total 管理 183 | - 筛选条件变化,重置分页,重新发起网络请求 184 | 185 | useRequest 通过配置 paginated = true,即可进入分页模式,自动帮你处理表格常见逻辑,同时我们对 antd Table 做了特殊支持,只用简单几行代码,就可以实现下面图中这样复杂的逻辑,提效百倍。 186 | 187 | ![](https://s1.ax1x.com/2020/03/23/8Tg7Nj.md.png) 188 | 189 | ```ts 190 | export default () => { 191 | const [gender, setGender] = useState("male"); 192 | const { tableProps } = useRequest( 193 | (params) => { 194 | return getTableData({ ...params, gender }); 195 | }, 196 | { 197 | paginated: true, 198 | refreshDeps: [gender], 199 | } 200 | ); 201 | const columns = []; 202 | return ; 203 | }; 204 | ``` 205 | 206 | ## 加载更多 207 | 208 | 加载更多的场景也是日常开发中常见的需求。在加载场景中,我们一般需要处理: 209 | 210 | - 分页 offset、pageSize 等管理 211 | - 首次加载,加载更多状态管理 212 | - 上拉自动加载更多 213 | - 组件第二次加载时,希望能记录之前的数据,并滚动到之前的位置 214 | 215 | useRequest 通过设置 loadMore = true,即可进入加载更多模式,配合其它参数,可以帮你处理上面所有的逻辑。 216 | 217 | ```ts 218 | const { data, loading, loadMore, loadingMore } = useRequest( 219 | (d) => getLoadMoreList(d?.nextId, 3), 220 | { 221 | loadMore: true, 222 | cacheKey: "loadMoreDemoCacheId", 223 | fetchKey: (d) => `${d?.nextId}-`, 224 | } 225 | ); 226 | ``` 227 | --------------------------------------------------------------------------------