react --(5)高级用法(一)之代码分割、context、错误边界、HOC、JSX、falsy值
2019-11-23:
学习内容:
flasy值:
一、代码分割:
打包:大多数 React 应用都会使用 Webpack 或 Browserify 这类的构建工具来打包文件。打包是一个将文件引入并合并到一个单独文件的过程,最终形成一个 “bundle”。接着在页面上引入该 bundle,整个应用即可一次性加载。
随着你的应用增长,你的代码包也将随之增长。尤其是在整合了体积巨大的第三方库的情况下。你需要关注你代码包中所包含的代码,以避免因体积过大而导致加载时间过长。
分割:代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
进行代码分割能够帮助你“懒加载”当前用户所需要的内容,能够显著地提高你的应用性能。尽管并没有减少应用整体的代码体积,但你可以避免加载用户永远不需要的代码,并在初始加载的时候减少所需加载的代码量。
(1)webpack 的动态import:
https://webpack.docschina.org/guides/code-splitting/
(2)React.lazy:
const OtherComponent = React.lazy(() => import('./OtherComponent')); function MyComponent() { return ( <div> <OtherComponent /> </div> ); }
像渲染常规组件一样处理动态引入(的组件)。
这个代码将会在渲染组件时,自动导入包含 OtherComponent
组件的包。React.lazy
接受一个函数,这个函数需要动态调用 import()
。它必须返回一个 Promise
,该 Promise 需要 resolve 一个 defalut
export 的 React 组件。
(3)suspense:
如果在 MyComponent
渲染完成后,包含 OtherComponent
的模块还没有被加载完成,我们可以使用加载指示器为此组件做优雅降级。这里我们使用 Suspense
组件来解决:
const OtherComponent = React.lazy(() => import('./OtherComponent')); function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense> </div> ); }
fallback
属性接受任何在组件加载过程中你想展示的 React 元素。你可以将 Suspense
组件置于懒加载组件之上的任何位置。你甚至可以用一个 Suspense
组件包裹多个懒加载组件。
(4)异常捕获边界(Error):(《错误边界》)
如果模块加载失败(如网络问题),它会触发一个错误。你可以通过异常捕获边界(Error boundaries)技术来处理这些情况,以显示良好的用户体验并管理恢复事宜。
import MyErrorBoundary from './MyErrorBoundary'; const OtherComponent = React.lazy(() => import('./OtherComponent')); const AnotherComponent = React.lazy(() => import('./AnotherComponent')); const MyComponent = () => ( <div> <MyErrorBoundary> <Suspense fallback={<div>Loading...</div>}> <section> <OtherComponent /> </section> </Suspense> </MyErrorBoundary> </div> );
(5)如何选择代码分割的依据:
需要确保选择的位置能够均匀地分割代码包而不会影响用户体验。
从路由开始。
大多数网络用户习惯于页面之间能有个加载切换过程。你也可以选择重新渲染整个页面,这样您的用户就不必在渲染的同时再和页面上的其他元素进行交互。
使用React Rounter第三方库来配置给予路由的代码分割:
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import React, { Suspense, lazy } from 'react'; const Home = lazy(() => import('./routes/Home')); const About = lazy(() => import('./routes/About')); const App = () => ( <Router> <Suspense fallback={<div>Loading...</div>}> <Switch> <Route exact path="/" component={Home}/> <Route path="/about" component={About}/> </Switch> </Suspense> </Router> );
二、Context:
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
典型的react应用中:数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。
Context 设计目的:为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
Context 能让你将这些数据向组件树下所有的组件进行“广播”,所有的组件都能访问到这些数据,也能访问到后续的数据更新。使用 context 的通用的场景包括管理当前的 locale,theme,或者一些缓存数据。
(1)何时使用context:
Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
因为必须将这个值层层传递所有组件。
(2)使用Context 前的考虑:
Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。如果你只是想避免层层传递一些属性,组件“组合继承”有时候是一个比 context 更好的解决方案。
🌰:一种无需 context 的解决方案是将 Avatar
组件自身传递下去(“组合继承”中的包含关系),因而中间组件无需知道 user
或者 avatarSize
等 props:
这种变化下,只有最顶部的 Page 组件需要知道 Link
和 Avatar
组件是如何使用 user
和 avatarSize
的。
这种对组件的控制反转减少了在你的应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的。
(3)Context API:
i、React.createContext(默认值):
创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider
中读取到当前的 context 值。
ii、Context.Provider:
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。
Provider 接收一个 value
属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
当 Provider 的 value
值发生变化时,它内部的所有消费组件都会重新渲染。
iii、Class.contextType:
挂载在 class 上的 contextType
属性会被重赋值为一个由 React.createContext()
创建的 Context 对象。这能让你使用 this.context
来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
iiii、<Context.Consumer>:
这里,React 组件也可以订阅到 context 变更。这能让你在函数式组件中完成订阅 context。
(4)注意事项:
因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。
举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 value
属性总是被赋值为新的对象:
三、错误边界:
错误边界是为了追踪非React组件代码(尤其是JS代码)引起的错误。
(1)生命周期方法:(作用类似try...catch...)
如果一个 class 组件中定义了 static getDerivedStateFromError()
或 componentDidCatch()
这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError()
渲染备用 UI ,使用 componentDidCatch()
打印错误信息。
🌰:
⚠️注意:错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误(自身内跟普通代码一样用try...catch...)。如果一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界,这也类似于 JavaScript 中 catch {} 的工作机制。
假如未捕获错误:自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。把一个错误的 UI 留在那比完全移除它要更糟糕。
(2)组件栈追踪:
在开发环境(生产模式必须禁用)下,React 16 会把渲染期间发生的所有错误打印到控制台,即使该应用意外的将这些错误掩盖。除了错误信息和 JavaScript 栈外,React 16 还提供了组件栈追踪:查看文件名和行号,这一功能在 Create React App 项目中默认开启,不然手动将该插件添加到你的 Babel 配置中
四、高阶组件(HOC):装饰器、compose
高阶组件是参数为组件,返回值为新组件的函数。HOC 在 React 的第三方库中很常见,例如 Redux 的 connect
和 Relay 的 createFragmentContainer
。
注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。
被包装组件接收来自容器组件的所有 prop,同时也接收一个新的用于 render 的 data
prop。HOC 不需要关心数据的使用方式或原因,而被包装组件也不需要关心数据是怎么来的。
i、约定:将不相关的 props 传递给被包裹的组件
HOC 为组件添加特性。自身不应该大幅改变约定。HOC 返回的组件与原组件应保持类似的接口。HOC 应该透传与自身无关的 props。
ii、约定:最大化可组合性:
HOC 通常可以接收多个参数。
实际是:
connect
是一个返回高阶组件的高阶函数!
这种形式可能看起来令人困惑或不必要,但它有一个有用的属性。 像 connect
函数返回的单参数 HOC 具有签名 Component => Component
。 输出类型与输入类型相同的函数很容易组合在一起。
同样的属性也允许 connect
和其他 HOC 承担装饰器的角色,装饰器是一个实验性的 JavaScript 提案。)
许多第三方库都提供了 compose
工具函数,包括 lodash (比如 lodash.flowRight
), Redux 和 Ramda。
iii、约定:包装显示名称以便轻松调试:
HOC 创建的容器组件会与任何其他组件一样,会显示在 React Developer Tools 中。为了方便调试,请选择一个显示名称,以表明它是 HOC 的产物。
最常见的方式是用 HOC 包住被包装组件的显示名称。比如高阶组件名为 withSubscription
,并且被包装组件的显示名称为 CommentList
,显示名称应该为 WithSubscription(CommentList)
iiii、⚠️注意事项:
a)不要在render 方法中使用HOC:
React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render
返回的组件与前一个渲染中的组件相同(===
),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。
通常,你不需要考虑这点。但对 HOC 来说这一点很重要,因为这代表着你不应在组件的 render 方法中对一个组件应用 HOC。这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
在极少数情况下,你需要动态调用 HOC。你可以在组件的生命周期方法或其构造函数中进行调用。
b)Refs 不会被传递:
ref
实际上并不是一个 prop - 就像 key
一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。
五、深入JSX:
JSX 仅仅只是 React.createElement(component, props, ...children)
函数的语法糖
大写字母开头的 JSX 标签意味着它们是 React 组件。
由于 JSX 会编译为 React.createElement
调用形式,所以 React
库也必须包含在 JSX 代码作用域内:所以使用JSX 需要import React from 'react'
可以用点语法来引用一个React 组件里的组件,例如:
1、JSX 的Props:
<MyComponent foo={...props}>
{ }中可以传递很多东西作为props传给组件,例如js表达式(for,if else 不是js表达式)、字符串字面量、用 ... 展开props属性传递给组件。
2、JSX 的子元素:
包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children
传递给外层组件。
(1)字符串字面量:效果类似<div>Hello World</div>(等价于: <div>{ 'Hello World' }</div> ) JSX 会移除行首尾的空格以及空行。与标签相邻的空行均会被删除,文本字符串之间的新行会被压缩为一个空格。
(2)其他的JSX:组合成嵌套。
(3)函数:这个函数作为 props.children ,要注意,
props.children
和其他 prop 一样,它可以传递任意类型的数据,而不仅仅是 React 已知的可渲染类型,只要这个React组件正确调用。例如:
(4)布尔类型、Null 以及 Undefined 将会忽略:传了等于没有传。但是有助于依据特定条件来渲染其他的 React 元素,如:
&&
之前的表达式总是布尔值
但是0是会被传递的,即使0是一个falsy值,如需要用0作bool条件,必须加上&&。