翻译: 疯狂的技术宅
正如 我们的 React 教程的第一部分 https://mp.weixin.qq.com/s/OuCIliiqgFggI6ta508qXw 中所指出的, 开始使用 React 相对容易. 首先使用 Create React App(CRA)初始化一个新项目, 然后开始开发. 不过遗憾的是, 随着时间的推移, 代码可能会变得难以维护, 特别是在你不熟悉 React 的情况下. 组件有可能会变大, 或者你可能最终得到一堆不是组件的组件, 最终你可能会到处编写重复的代码.
这时候你就应该试着开始真正的 React 之旅了 -- Think in React.
每当开发一个新的程序时, 你需要为其做好在以后转换为 React 应用的新设计, 首先试着确定设计草图中的组件, 如何分离它们以使其更易于管理, 以及哪些元素是重复的(或他们的行为). 尽量避免添加可能 "将来有用" 的代码 -- 虽然这很诱人, 但可能未来永远也不会到来, 你将留下一堆具有大量可配置选项的多余通用功能 / 组件.
此外, 如果一个组件大于 2 到 3 个窗口的高度, 也许值得分离(如果可能的话) -- 以后更容易阅读.
React 中的受控组件与非受控组件
在大多数应用中, 需要输入和与用户进行某种形式的交互, 允许他们输入内容, 上传文件, 选择字段等. React 用两种不同的方式处理用户交互 -- 受控和非受控组件.
顾名思义, 受控组件的值由 React 控制, 能为与用户交互的元素提供值, 而不受控制的元素不获取值属性. 多亏了这一点, 我们才能把 React 状态作为单一的事实来源, 因此我们在屏幕上看到的与当前拥有的状态是一致的. 开发人员需要传递一个函数, 该函数用来响应用户与表单的交互, 这将会改变它的状态.
- class ControlledInput extends React.Component {
- state = {
- value: ""
- };
- onChange = (e) => this.setState({ value: e.target.value });
- render() {
- return (
- <input value={this.state.value} onChange={this.onChange}/>
- );
- }
- }
在 React 的非受控组件中, 我们不关心值的变化情况, 如果想要知道其确切的值, 只需通过 ref 访问它.
- class UncontrolledInput extends React.Component {
- input = React.createRef();
- getValue = () => {
- console.log(this.input.current.value);
- };
- render() {
- return (
- <input ref={this.input}/>
- );
- }
- }
那么应该怎么选择呢? 在大数情况下用受控组件是可行的, 不过也有一些例外. 例如使用非受控制组件的一种情况是 file 类型输入, 因为它的值是只读的, 不能在编码中去设置(需要用户交互). 另外我发现受控组件更容易理解和于使用. 对受控组件的验证是基于重新渲染的, 状态可以更改, 并且可以很轻松的显示输入中存在的问题(例如格式错误或者输入为空).
Refs
在前面我们提到过 refs, 这是一个特殊功能, 可以在类组件中使用, 直到 16.8 中出现了 hooks.
refs 可以通过引用让开发人员访问 React 组件或 DOM 元素(取决于我们附加 ref 的类型). 最好仅在必须的场景中使用它们, 因为它们会使代码难以阅读, 并打破从上到下的数据流. 然而, 有些情况下它们是必要的, 特别是在 DOM 元素上(例如: 用编码方式改变焦点). 附加到 React 组件元素时, 你可以自由使用所引用的组件中的方法. 不过还是应该避免这种做法, 因为有更好的方法来处理它(例如, 提升状态并将功能移动到父组件).
refs 还可以做到:
使用字符串字面量(历史遗留的, 应该避免),
使用在 ref 属性中设置的回调函数,
通过创建 ref 作为 React.createRef() , 并将其绑定到类属性, 并通过它去访问(请注意, 在 componentDidMount 生命周期中将提供引用).
没有传递引用的一种情况是当在组件上使用高阶组件时 -- 原因是可以理解的, 因为 ref 不是 prop(类似于 key)所以它没有被传递下来, 并且它将引用 HOC 而不是被它包裹的组件. 在这种情况下, 我们可以使用 React.forwardRef, 它把 props 和 ref 作为参数, 然后可以将其分配给 prop 并传递给我们想要访问的组件.
- function withNewReference(Component) {
- class Hoc extends React.Component {
- render() {
- const {forwardedRef, ...props} = this.props;
- return <Component ref={forwardedRef} {...props}/>;
- }
- }
- return React.forwardRef((props, ref) => {
- return <Hoc {...props} forwardedRef={ref} />;
- });
- }
错误边界
事情越复杂, 出现问题的概率就越高. 这就是为什么 React 中会有错误边界. 那他们是怎么工作的呢?
如果出现问题并且没有错误边界作为其父级, 则会导致整个 React 应用失败. 不显示信息比误导用户并显示错误信息要好, 但这并不意味着你应该放任整个应用崩溃并显示白屏. 通过错误边界, 可以得到更多的灵活性. 你可以在整个应用程序中使用并显示一个错误消息, 或者在某些小部件中使用它但是不显示, 或者显示少量信息来代替这些小部件.
请记住, 它仅涉及声明性代码的问题, 而不是你为了处理某些事件或者调用而编写的命令式代码. 对于这些情况, 你仍应使用常规的 try/catch 方法.
在错误边界也可以将信息发送到你使用的 Error Logger (在 componentDidCatch 生命周期方法中).
- class ErrorBoundary extends React.Component {
- state = { hasError: false };
- static getDerivedStateFromError(error) {
- return { hasError: true };
- }
- componentDidCatch(error, info) {
- logToErrorLogger(error, info);
- }
- render() {
- if (this.state.hasError) {
- return <div>Help, something went wrong.</div>;
- }
- return this.props.children;
- }
- }
高阶组件
** 高阶组件(HOC)** 经常在 React 中被提及, 这是一种非常流行的模式, 你可能会用到它(或者已经在用了). 如果你熟悉 HOC, 可能已经在很多库中看到过 withNavigation,connect,withRouter.
HOC 只是一种把组件作为参数的函数, 并且与没有 HOC 包装器的组件相比, 能够返回具有扩展功能的新组件. 多亏了这一点, 你可以实现一些易于扩展的功能, 以此增强自己的组件(例如: 访问导航). HOC 也有一些其它形式的调用方式, 这取决于我们当前拥有什么, 唯一的参数必须要传入一个组件, 但它也可以接受额外的参数 -- 一些选项, 或者像在 connect 中一样, 首先使用 configurations 调用一个函数, 该函数稍后返回一个带参组件, 并返回 HOC .
以下是一些你应该做的和要避免做的事情:
为包装器 HOC 函数添加显示名称(这样你就能知道它到底是干什么用的, 实际上是通过更改 HOC 组件显示名称来做到).
不要在渲染方法中使用 HOC -- 你应该在其中使用增强组件, 而不是在那里创建新的 HOC 组件, 因为它一直在重新装载并丢失其当前状态.
静态方法不会被自动复制, 所以如果你想在新创建的 HOC 中使用一些静态方法, 需要自己去复制它们.
涉及到的 Refs 不会被传递, 所以使用前面提到的 React.forwardRef 来解决这些问题.
- export function importantHoc() {
- return (Component) => class extends React.Component {
- importantFunction = () => {
- console.log("Very Important Function");
- };
- render() {
- return (
- <Component
- {...this.props}
- importantFunction={this.importantFunction}
- />
- );
- }
- };
- }
样式
样式不一定与 React 本身有关, 但出于各种原因还是值得一提的.
首先, 常规 CSS / 内联样式在这里能够正常应用, 你只需在 className 属性中添加 CSS 中的类名, 它就能正常工作. 内联样式与常规 html 样式略有不同. 样式属性也是使用驼峰命名法, 因此 border-radius 会变成 borderRadius .
React 似乎推广了一些不仅在 React 中变得普遍的解决方案, 例如最近集成在 CRA 中的 CSS 模块, 你可以在其中简单地导入 name.modules.CSS 并用其属性来调整组件的样式 (某些 IDE(例如 webStorm) 也具有自动完成功能, 能告诉你可用的名称.
在 React 中另一个流行的解决方案是 CSS-in-JS(例如, https://emotion.sh/docs/introduction 库). 再说一点, CSS 模块和 emotion(或者一般来说是 CSS-in-JS)对 React 没有限制.
React 中的 Hooks
自重写以来,**Hooks ** 很可能是 React 最受热切期待的补充. 这个产品是否能不负众望? 从我的角度来看, 是的, 因为它确实是一个很棒的功能. 它们本质上是带来了新的体验, 例如:
允许删除许多 class 组件, 这些组件我们仅仅是使用而不归我们拥有, 例如本地状态或 ref, 所以组件的代码看上去更容易阅读.
可以让你用更少的代码来获得相同的效果.
使函数更容易理解和测试, 例如: 用 .
也可以携带参数, 一个 hook 返回的结果可以很容易地被另一个 hook 使用(例如, useEffect 中的 setState 被 useState 使用).
比类更好地缩小方式, 这对于 minifiers 来说往往更成问题.
可能会删除 HOC 并在你的应用中渲染 props , 尽管 hook 被设计用于解决其他问题, 但仍会引入新问题.
能够被熟练的 React 开发人员定制
默认的 React hook 很少. 其中三个基本的 hook 是 useState,useEffect 和 useContext. 还有一些其它的, 例如 useRef 和 useMemo, 不过现在我们把重点放在基础知识上.
先看一下 useState, 让我们用它来创建一个简单的计数器的. 它是如何工作的? 基本上整个结构非常简单:
- export function Counter() {
- const [counter, setCounter] = React.useState(0);
- return (
- <div>
- {counter}
- <button onClick={() => setCounter(counter + 1)}>+</button>
- </div>
- );
- };
它用 initialState (值)调用, 并返回一个带有两个元素的数组. 由于数组解构分配, 我们可以立即将变量分配给这些元素. 第一个是更新后的最后一个状态, 而另一个是我们将用于更新值的函数. 看起来相当容易, 不是吗?
此外, 由于这些组件曾经被称为无状态功能组件, 现在这种名称不再适用, 因为它们可以具有如上所示的状态. 所以叫类组件和函数组件似乎更符合它们的实际操作, 至少从 16.8.0 开始.
更新函数 (在我们的例子中是 setCounter) 也可以用作一个函数, 它将以前的值作为参数, 格式如下:
- <button onClick={()=>
- setCounter(prevCounter => prevCounter + 1)}>+
- </button>
- <button onClick={()=>
- setCounter(prevCounter => prevCounter - 1)}>-
- </button>
与执行浅合并的 this.setState 类组件不同, 设置函数 (在我们的例子中为 setCounter ) 会覆盖整个状态.
另外, initialState 也可以是一个函数, 而不仅仅是一个普通的值. 这有其自身的好处, 因为该函数将会只在组件的初始渲染期间运行, 之后将不再被调用.
const [counter, setCounter] = useState(() => calculateComplexInitialValue());
最后, 如果我们要使用 setCounter 与在当前状态 (counter) 的同一时刻完全相同的值, 那么组件 将不会 重新渲染.
另一方面, useEffect 为我们的功能组件添加副作用, 无论是订阅, API 调用, 计时器, 还是任何我们认为有用的东西. 我们传给 useEffect 的任何函数都将在 render 之后运行, 并且是在每次渲染之后执行, 除非我们添加一个限制, 把应该重新运行时需要更改的属性作为函数的第二个参数. 如果我们只想在 mount 上运行它并在 unmount 上清理, 那么只需要在其中传递一个空数组.
- const fetchApi = async () => {
- const value = await fetch("https://jsonplaceholder.typicode.com/todos/1");
- console.log(await value.JSON());
- };
- export function Counter() {
- const [counter, setCounter] = useState(0);
- useEffect(() => {
- fetchApi();
- }, []);
- return (
- <div>
- {counter}
- <button onClick={() => setCounter(prevCounter => prevCounter + 1)}>+</button>
- <button onClick={() => setCounter(prevCounter => prevCounter - 1)}>-</button>
- </div>
- );
- };
由于把空数组作为第二个参数, 所以上面的代码只运行一次. 在这种情况下它类似于 componentDidMount, 但稍后会触发它. 如果你想在浏览器处理之前调用一个类似的 hook, 可以用 useLayoutEffect, 但这些更新将会被同步应用, 这一点与 useEffect 不同.
useContext 似乎是最容易理解的, 因为我们提供了想要访问的上下文(由 createContext 函数返回的对象提供), 而它为我们提供了该上下文的值.
const context = useContext(Context);
最后, 要编写自己的 hook, 你可以像这样写:
- function useWindowWidth() {
- let [windowWidth, setWindowWidth] = useState(Windows.innerWidth);
- function handleResize() {
- setWindowWidth(Windows.innerWidth);
- }
- useEffect(() => {
- Windows.addEventListener('resize', handleResize);
- return () => Windows.removeEventListener('resize', handleResize);
- }, []);
- return windowWidth;
- }
基本上, 我们使用常规的 useState hook, 我们将其指定为窗口宽度的初始值, 然后在 useEffect 中添加一个监听器, 它将在窗口调整大小时触发 handleResize. 在组件被卸载后会我们会及时知道(查看 useEffect 中的返回值). 是不是很简单?
注意: use 在 hook 中很重要. 之所以使用它, 是因为它允许 React 检查你是否做了不好的事情, 例如从常规 JS 函数调用 hook.
类型检查
在支持 Flow 和 TypeScript 之前, React 有自己的属性检查机制.
PropTypes 检查 React 组件接收的属性 (props) 是否与我们的内容一致. 如果一致(例如: 应该是对象而不是数组), 将会在控制台中收到警告. 请务必注意: PropTypes 仅在开发模式下进行检查, 因为它们会影响性能并在控制台中显示上述警告.
从 React 15.5 开始, PropTypes 被放到了不同的包里, 需要单独安装. 它在名为 propTypes(surprise)的静态属性中对属性进行声明, 可以把它与 defaultProps 结合使用, 如果属性未定义就会使用它们(undefined 是唯一的情况). DefaultProps 与 PropTypes 无关, 不过它们可以解决由于 PropTypes 而可能出现的一些警告.
另外两个选择是 Flow 和 TypeScript, 它们现在更受欢迎(特别是 TypeScript ).
TypeScript 是 Microsoft 开发的 JavaScript 的类型超集, 它可以在程序运行之前检查错误, 并为开发工作提供卓越的自动完成功能. 它还极大地改善了重构过程. 由于受到 Microsoft 的支持, 它有丰富的类型语言特征, 也是一个相当安全的选择.
Flow 与 TypeScript 不同, 它不是一种语言, 而是 JavaScript 的静态类型检查器, 因此它更像是 JavaScript 中的工具而并非语言. Flow 背后的整个思路与 TypeScript 完全相似. 它允许你添加类型, 以便在运行代码之前杜绝可能出现的错误. 就像 TypeScript 一样, CRA(创建 React App)从一开始就支持 Flow.
我发现 TypeScript 更快(几乎是即时的), 特别是在自动完成中, Flow 似乎有点慢. 值得注意的是, 我自己用的 WebStorm 等 IDE 使用 CLI 与 Flow 集成. 但是在文件中集成可选用法似乎更容易, 只需要在文件开头添加 // @flow 就可进行类型检查. 另外据我所知, 似乎 TypeScript 最终赢得了与 Flow 的战斗 -- 它现在更受欢迎, 并且一些最流行的库正在从 Flow 转向 TypeScript.
官方文档中还提到了更多的选择, 例如 Reason(由 Facebook 开发并在 React 社区中获得普及),Kotlin(由 JetBrains 开发的语言)等等.
显然, 对于前端开发人员来说, 最简单的方法是使用 Flow 和 TypeScript, 而不是切换到 Kotlin 或 F#. 但是, 对于正在转型到前端的后端开发人员来说, 这可能更容易入手.
生产模式和 React 性能
对于生产模式, 你需要做的最基本和明显的改变是: 把 DefinePlugin 切换到 "production", 并在 Webpack 的情况下添加 UglifyJsPlugin. 在使用 CRA 的情况下, 它就像使用 NPM run build(将运行 react-scripts build)一样简单. 请注意, Webpack 和 CRA 不是唯一的选项, 因为你可以使用其他构建工具, 如 Brunch. 这通常包含在官方文档中, 无论是官方的 React 文档还是特定工具的文档. 要确保模式设置正确, 你可以使用 React Developer Tools, 它会告诉你正在用的那种构建 (生产与开发) 模式应该怎么配置. 上述步骤会使你的应用在没有来自 React 的检查和警告的情况下运行, 并且 bundle 本身也将被最小化.
你还可以为 React 应用做更多的事. 你如何处理构建的 JS 文件? 如果尺寸相对较小, 你可以从 "bundle.js" 开始, 或者做一些类似 "vendor + bundle" 或者 "vendor + 最小化需要部件 + 在需要时导入东西" 之类的处理. 当你是处理一个非常大的应用时, 不需要在一开始就导入所有内容. 请注意, 在主 bundle 中去 bundling 一些不会被使用的 JavaScript 代码只会增加 bundle 包的大小, 并会使应用在启动时的加载速度变慢.
如果你计划冻结库的版本, 并认为它们可能长时间内不会被更改, 那么 Vendor bundles 可能很有用. 此外, 更大的文件更适合用 gzipping, 因此从拆分获得的好处有时可能不值得. 这取决于文件大小, 有时你需要自己去尝试.
代码拆分
代码拆分的方式比这里给出的建议多得多, 但让我们关注 CRA 和 React 本身可用的内容. 基本上, 为了将代码分成不同的块, 可以使用 import(), 这可以用 Webpack 支持( import 本身是第 3 阶段的提案, 所以它还不是语言标准的一部分). 每当 Webpack 看到 import 时, 它就会知道需要在这个阶段开始拆分代码, 并且不能将它包含在主包中(它在 import 中的代码).
现在我们可以将它与 React.lazy() 连接起来, 它需要 import() 一个文件路径, 其中包含需要在那个地方渲染的组件. 接下来, 我们可以用 React.suspense(), 它会在该位置显示不同的组件, 一直到导入的组件全部加载完毕. 有人可能会想, 如果我要导入单个组件, 是不是就不需要它了呢?
实际上并非如此, 因为 React.lazy() 将显示我们 import() 的组件, 但 import() 可能会获取比单个组件更大的块. 例如这个组件可能包含其他库, 或更多代码, 所以不只是需要一个文件 -- 它可能是绑在一起的多个文件. 最后, 我们可以将所有这些包装在 ErrorBoundary 中 *(你可以在本文关于错误边界的那部分中找到代码)* 如果某些内容因我们想要导入的组件而失败(例如出现网络错误), 这将作为备用方案.
- import ErrorBoundary from './ErrorBoundary';
- const ComponentOne = React.lazy(() => import('./ComponentOne'));
- function MyComponent() {
- return (
- <ErrorBoundary>
- <React.Suspense fallback={<div>Loading...</div>}>
- <ComponentOne/>
- </React.Suspense>
- </ErrorBoundary>
- );
- }
这是一个简单的例子, 但显然你可以做得更多. 你可以使用 import 和 React.lazy 进行动态路由划分(例如: 管理员与常规用户). 请注意, React.lazy 仅支持默认导出, 并且不支持服务器端呈现.
React 代码性能
关于性能, 如果你的 React 应用运行缓慢, 有两种工具可以帮助你找出问题.
第一个是 Chrome Performance Tab, 它会告诉你每个组件会发生什么(例如, mount,update ). 有了它你应该能够确定哪个组件可能会出现性能问题, 然后进行优化.
另一种选择是 DevTools Profiler , 它在 React 16.5+ 中可用, 并与 shouldComponentUpdate 配合(或 PureComponent, 在本教程的第一部分 https://mp.weixin.qq.com/s/OuCIliiqgFggI6ta508qXw 中解释), 我们可以提高一些关键组件的性能.
显然, 对网络进行基本优化是最佳的, 例如对一些事件进行去抖动 (例如, 滚动), 对动画保持谨慎(使用变换而不是通过改变高度并实现动画) 等等. 这些问题很容易被忽略, 特别是如果你刚刚掌握了 React.
2019 年及以后的 React 现状
如果要讨论 React 的未来, 我个人不会太在意. 从我的角度来看, React 在 2019 年及以后的地位很难被撼动.
React 拥有如此强大的地位, 在一个大社区的支持下很难被废弃. React 社区非常棒, 它总是产生新的创意, 核心团队一直在不断努力改进 React, 并添加新功能和修复旧问题. React 也得到了一家大公司的支持, 但许可证已经不是问题 -- 它现在使用 MIT license.
是的, 有一些事情有望改变或改进; 例如, 使 React 稍微小一些 (提到的一个措施是删除合成事件) 或将 className 重命名为 class. 当然, 即使这些看似微小的变化也可能导致诸如影响浏览器兼容性等问题. 就个人而言, 我也想知道当 WebComponent 获得更多人气时会发生什么, 因为它可能会增加一些 React 经常用到的东西. 我不相信他们会成为一个彻头彻尾的替代者, 但我相信他们可以很好地相互补充.
至于短期, hook 刚刚被加入到 React. 这可能是自 React 重写以来发生的最大变化, 因为它们将带来更多可能性并增强更多功能组件(现在他们真的被大肆宣传).
最后, 正如我最近所做的那样, 有 React Native. 对我来说, 这是一项伟大的技术, 在过去的几年中发生了很大的变化. React Native 正在重写它的核心, 这应该以与 React 重写类似的方式完成(它全部是内部的, 几乎没有任何东西应该为开发人员改变). 异步渲染成为本机和 JavaScript 之间更快更轻量级的桥梁. 当然还有更多改变.
在 React 生态中有很多值得期待的东西, 但 hook(以及 React Native, 如果有人喜欢手机应用的话)的更新可能将会是我们在 2019 年所能看到的最重要的变化.
来源: https://juejin.im/post/5c8b63d3e51d457d003d9ce0