简介
我们继续上一节的内容,开始分析 React 官网:https://reactjs.org/docs/accessibility.html 的 “高级指引” 部分,这一部分会涉及到异步组件、全局上下文对象、错误边界组件等概念的分析,比前面章节的难度还是略微大一些的,所以一定要跟上节奏哦,我们一起出发吧!
知识点
代码分割
异步组件
全局上下文对象 Context
错误边界组件
准备
我们直接用上一节中的 react-demo-day5
项目来作为我们的 Demo
项目,还没有创建的小伙伴可以直接执行以下命令 clone
一份代码:
git clone -b dev https://gitee.com/vv_bug/react-demo-day5.git
接着进入到项目根目录 react-demo-day5
,并执行以下命令来安装依赖与启动项目:
npm install --registry https://registry.npm.taobao.org && npm start
等项目打包编译成功,浏览器会自动打开项目入口,看到上面截图的效果的时候,我们的准备工作就完成了。
代码分割
因为我们这一节分析的主要是 React
的 “高级指引” 部分内容,所以我们先在 src
目录下创建一个 advanced-guides
目录,用来存放 “高级指引” 的内容:
mkdir ./src/advanced-guides
然后在 src/advanced-guides
目录下创建一个 index.tsx
文件:
/**
* 核心概念列表
*/
import CodeSplit from "./code-split";
function AdvancedGuides() {
return (
{/* 代码分割 */}
);
};
export default AdvancedGuides;
接着在 src/main.tsx
文件中引入 AdvancedGuides
组件:
import React from "react";
import ReactDOM from "react-dom";
import "./main.scss";
import MainConcepts from "./main-concepts";
import AdvancedGuides from "./advanced-guides";
// App 组件
const App = (
{/* 核心概念 */}
{/* 高级指引 */}
);
ReactDOM.render(
App,
document.getElementById("root")
);
ok,我们 “高级指引” 部分的内容就可以在 AdvancedGuides
组件中做测试了。
我们首先在 src/advanced-guides
目录下创建一个 code-split
目录,准备做 “代码分割” 的测试:
mkdir ./src/advanced-guides/code-split
接着在 src/advanced-guides/code-split
目录下创建一个 index.tsx
文件:
import React from "react";
// 定义一个异步组件
const LazyComponent = React.lazy(()=>import("./lazy.com"));
function CodeSplit(){
return (
{/* 渲染异步组件 */}
Loading...
}>
);
}
export default CodeSplit;
可以看到,我们用 React.lazy
方法定义了一个异步组件,然后在 React.Suspense
组件中渲染了这个异步组件(注意:React.lazy 返回的组件必须配合 Suspense 组件使用,而且 Suspense 组件必须提供 fallback 属性,Suspense 组件我们后面再详细解析 )。
然后在 src/advanced-guides/code-split
目录下创建一个 lazy.com.tsx
文件:
function LazyComponent(){
return (
我是一个异步组件
);
}
export default LazyComponent;
可以看到,我们定义了一个简单的 “异步组件”。
我们重新运行项目看结果:
npm start
可以看到,我们的 lazy.com.tsx
组件被单独分割到了一个 js
文件中,当这个 js
文件加载并执行完毕后,页面显示了这个异步组件的内容。
其实我们还可以利用 State
单独使用异步组件。
我们修改一下 src/advanced-guides/code-split/index.tsx
组件:
import React, {useState, useEffect} from "react";
// 定义一个异步组件
const LazyComponent = import("./lazy.com");
function CodeSplit() {
let [Com, setCom] = useState(Loading...
);
useEffect(() => {
LazyComponent.then((module: any) => {
setCom((React.createElement(module.default, {}, [])) as any);
});
}, []);
return (
{/* 渲染异步组件 */ }
{ Com }
);
}
export default CodeSplit;
可以看到,我们利用 useEffect
定义了一个 Hook
,然后通过 LazyComponent.then
获取到了异步组件 lazy.com.tsx
,最后利用 State
把组件渲染到了页面,效果跟前面一样,我就不演示了,小伙伴自己跑一下代码看效果哦。
所以我们大胆猜测一下,Suspense
组件的是不是也是这样实现的呢?这个答案就留到我们后面源码解析部分再去解析了。
Context
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
解释起来可能有点抽象,我们还是利用 Demo
来演示一下。
比如我们的应用需要添加一个换主题的功能,能够切换 Dark
跟 Light
主题。
我们首先在 src
目录下创建一个主题样式文件 themes.scss
:
touch ./src/themes.scss
接着我们在 src/themes.scss
中定义两种主题 Dark
跟 Light
:
/* Light 主题 */
.theme-light {
color: black;
background-color: white;
}
/* Dark 主题 */
.theme-dark {
color: white;
background-color: darkgray;
}
可以看到,我们简单的定义了两个样式 theme-light
跟 theme-dark
。
接着我们在 src/main.tsx
入口文件中引入这个主题样式文件 themes.scss
:
import React from "react";
import ReactDOM from "react-dom";
import "./main.scss";
// 引入主题样式
import "./themes.scss";
import MainConcepts from "./main-concepts";
import AdvancedGuides from "./advanced-guides";
// App 组件
const App = (
{/* 核心概念 */}
{/* 高级指引 */}
);
ReactDOM.render(
App,
document.getElementById("root")
);
然后我们对 src/main.tsx
入口进行一下改造,把 App
组件单独提出到一个文件中去。
首先在 src
目录下创建一个 app.tsx
文件作为 App
组件:
touch ./src/app.tsx
然后将 src/main.tsx
中的 App
组件抽离到 src/app.tsx
,抽离后的 src/main.tsx
文件:
import React from "react";
import ReactDOM from "react-dom";
import "./main.scss";
// 引入主题样式
import "./themes.scss";
// App 组件
import App from "./app";
ReactDOM.render(
,
document.getElementById("root")
);
src/app.tsx
文件内容:
import MainConcepts from "./main-concepts";
import AdvancedGuides from "./advanced-guides";
import React from "react";
function App(){
return (
{/* 核心概念 */}
{/* 高级指引 */}
)
}
export default App;
React.createContext
创建一个 Context 对象。
在 src
目录下创建一个 app-context.tsx
文件:
// 定义主题枚举类型
import React from "react";
export enum Themes {Light, Dark};
// 定义 AppContext 类型
export type AppContextType = {
theme: Themes,
toggleTheme: () => void
};
// AppContext 的默认值
export const defaultAppContext = {
theme: Themes.Light,
toggleTheme: () => {
}
};
// 创建一个 AppContext 对象
export const AppContext = React.createContext(defaultAppContext);
可以看到,我们创建并导出了一个 AppContext
对象。
Context.Provider
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。Provider 接收一个 value
属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
当 Provider 的 value
值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate
函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
我们利用 Context.Provider
组件把 AppContext
对象共享给所有的组件,修改一下 src/app.tsx
:
import MainConcepts from "./main-concepts";
import AdvancedGuides from "./advanced-guides";
import React, {useState} from "react";
import {AppContext, Themes, AppContextType} from "./app-context";
function App() {
function toggleTheme() {
setAppContext((preAppContext) => {
return {
theme: Themes.Light === preAppContext.theme ? Themes.Dark : Themes.Light,
toggleTheme
};
});
}
let [appContext, setAppContext] = useState({
theme: Themes.Light,
toggleTheme
});
return (
{/* 核心概念 */ }
{/* 高级指引 */ }
);
}
export default App;
可以看到,我们用 AppContext.Provider
组件把我们的 AppContext
对象中的 value
属性共享给了所有组件,并且用 useState
创建了一个 State
去管理这个 value
的状态。
那么我们的子组件中怎么才能拿到 AppContext
对象共享的 value
值呢?
Class.contextType
我们可以利用类组件中的 contextType
声明来获取到 AppContext
对象。
我们在 src/advanced-guides
目录下创建一个 context
目录:
mkdir ./src/advanced-guides/context
接着在 src/advanced-guides/context
目录下创建一个 index.tsx
文件:
import React from "react";
import ContextCom from "./context.com";
function Context() {
return (
{/* 类组件方式 */ }
);
}
export default Context;
然后在 src/advanced-guides/context
目录下创建一个 context.com.tsx
组件:
import React from "react";
import {AppContext} from "../../app-context";
class ContextCom extends React.Component {
render() {
return (
点我切换主题
);
}
}
// 定义 ContextCom 组件的 contextType 类型
ContextCom.contextType = AppContext;
export default ContextCom;
最后在 src/advanced-guides/index.tsx
文件中引入 src/advanced-guides/context/index.tsx
组件:
/**
* 核心概念列表
*/
import CodeSplit from "./code-split";
import Context from "./context";
function AdvancedGuides() {
return (
{/* 代码分割 */ }
{/* Context */ }
);
};
export default AdvancedGuides;
重新运行项目看结果:
npm start
可以看到,我们成功的利用 Context
实现了 “换主题” 的效果。
Context.Consumer
此组件可以让你在 函数式组件 中可以订阅 context。
接下来我们用函数式组件来实现一下 src/advanced-guides/context/context.com.tsx
组件。
首先在 src/advanced-guides/context
目录下创建一个 context.func.tsx
组件:
import {AppContext} from "../../app-context";
import React from "react";
function ContextFunc() {
return (
{ ({toggleTheme}) => 点我切换主题 }
);
}
export default ContextFunc;
然后在 src/advanced-guides/context/index.tsx
组件中引入 context.func.tsx
组件:
import React from "react";
import ContextCom from "./context.com";
import ContextFunc from "./context.func";
function Context() {
return (
{/* 类组件方式 */ }
{/* 函数组件方式 */ }
);
}
export default Context;
效果跟前面一样,我就不演示了,小伙伴自己跑跑项目看效果哦。
其实在 React
中,像这种全局共享数据方案有很多,像 Redux
、Mobox
等第三方状态管理库,我们后面讲 React
全家桶的时候会详细介绍,当然,一些简单的全局数据共享,我们直接用 Context
方案就可以了,没必要引入那些重量级的全局状态管理框架了。
错误边界
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI ,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
注意
错误边界无法 捕获以下场景中产生的错误:
事件处理
异步代码(例如 setTimeout
或 requestAnimationFrame
回调函数)
服务端渲染
它自身抛出来的错误(并非它的子组件)
我们还是来演示一下效果吧。
首先在 src/advanced-guides
目录下创建一个 error.tsx
组件:
touch ./src/advanced-guides/error.tsx
src/advanced-guides/error.tsx
:
function ErrorCom(): null{
throw new Error("报错啦!");
}
export default ErrorCom;
可以看到,我们创建了一个函数式组件 ErrorCom
,然后直接通过 throw
抛出了一个 Error
。
我们在 src/advanced-guides/index.tsx
文件中引入 error.tsx
组件:
/**
* 核心概念列表
*/
import CodeSplit from "./code-split";
import Context from "./context";
import ErrorCom from "./error";
function AdvancedGuides() {
return (
{/* 代码分割 */ }
{/* Context */ }
{/* 报错的组件 */ }
);
};
export default AdvancedGuides;
然后我们重新运行项目看结果:
npm start
可以看到,直接报错了,整个页面都挂了。
但是在我们正常的项目开发中,我们并不希望因为某一个组件出错整个应用都挂掉的情况。
接下来我们就用 "错误边界" 组件来处理一下这种情况。
我们在 src/advanced-guides
目录下创建一个 error-boundaries.tsx
组件:
import React from "react";
class ErrorBoundaries extends React.Component {
state = {
hasError: false
};
static getDerivedStateFromError() {
// 更新 state 使下一次渲染能够显示降级后的 UI
return {hasError: true};
}
componentDidCatch(error: any, errorInfo: any) {
// eslint-disable-next-line no-console
console.log("error", error);
// eslint-disable-next-line no-console
console.log("errorInfo", errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return Something went wrong. ;
}
return this.props.children;
}
}
export default ErrorBoundaries;
可以看到,ErrorBoundaries
组件中声明了一个静态的方法 getDerivedStateFromError
跟一个 componentDidCatch
方法。
static getDerivedStateFromError
此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state。
componentDidCatch
此生命周期在后代组件抛出错误后被调用。 它接收两个参数:
error
—— 抛出的错误。
info
—— 带有 componentStack
key 的对象。
接着我们在 src/advanced-guides/index.tsx
组件中引用 ErrorBoundaries
组件:
/**
* 核心概念列表
*/
import CodeSplit from "./code-split";
import Context from "./context";
import ErrorBoundaries from "./error-boundaries";
import ErrorCom from "./error";
function AdvancedGuides() {
return (
{/* 代码分割 */ }
{/* Context */ }
{/* 报错的组件 */ }
);
};
export default AdvancedGuides;
我们重新运行项目看结果:
npm start
可以看到,src/advanced-guides/error-boundaries.tsx
组件中成功捕捉到了错误,应用也没有全部挂掉,只是 src/advanced-guides/index.tsx
组件中的内容:
{/* 代码分割 */ }
{/* Context */ }
{/* 报错的组件 */ }
由于错误的原因,直接替换成了:
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return Something went wrong. ;
}
边界处理组件在错误的捕获与收集上很有用处,可以结合一些错误收集框架做线上错误统计,快速分析出一些 bug 问题原因。
总结
我们通过 Demo
演示了什么是异步组件、Context 对象、错误边界组件,有些小伙伴要说了 ”我们何不把所有的组件都做成异步组件?所有的全局数据共享都用 Context?给所有的模块都加上错误边界组件?“,小伙伴一定要结合具体项目场景来使用这些高级特性,比如你项目本来就不大,你还把所有的组件都做成异步组件,这样做不但没有加快应用渲染速度,反而会引起服务器压力过大,然后把所有的全局状态共享都用 Context
处理,这样做虽然可以达到效果,但是当 Context
对象中逻辑过于庞大,这样做反而不利于全局状态的管理,而且管理不好还会造成状态更新频繁而引起性能问题,最后你会得不偿失的。
好啦,这节到这就结束啦。
Demo 项目代码下载:https://gitee.com/vv_bug/react-demo-day5/tree/dev
欢迎志同道合的小伙伴一起交流,一起学习。 觉得写得不错的可以点点关注,帮忙转发跟点赞。