快来跟我一起学 React(Day6)

简介

我们继续上一节的内容,开始分析 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
1-1.png

等项目打包编译成功,浏览器会自动打开项目入口,看到上面截图的效果的时候,我们的准备工作就完成了。

代码分割

因为我们这一节分析的主要是 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
1-2.png

可以看到,我们的 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 来演示一下。

比如我们的应用需要添加一个换主题的功能,能够切换 DarkLight 主题。

我们首先在 src 目录下创建一个主题样式文件 themes.scss

touch ./src/themes.scss

接着我们在 src/themes.scss 中定义两种主题 DarkLight

/* Light 主题 */
.theme-light {
  color: black;
  background-color: white;
}

/* Dark 主题 */
.theme-dark {
  color: white;
  background-color: darkgray;
}

可以看到,我们简单的定义了两个样式 theme-lighttheme-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
1-3.gif

可以看到,我们成功的利用 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 中,像这种全局共享数据方案有很多,像 ReduxMobox 等第三方状态管理库,我们后面讲 React 全家桶的时候会详细介绍,当然,一些简单的全局数据共享,我们直接用 Context 方案就可以了,没必要引入那些重量级的全局状态管理框架了。

错误边界

错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

注意

错误边界无法捕获以下场景中产生的错误:

  • 事件处理
  • 异步代码(例如 setTimeoutrequestAnimationFrame 回调函数)
  • 服务端渲染
  • 它自身抛出来的错误(并非它的子组件)

我们还是来演示一下效果吧。

首先在 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
1-4.png

可以看到,直接报错了,整个页面都挂了。

但是在我们正常的项目开发中,我们并不希望因为某一个组件出错整个应用都挂掉的情况。

接下来我们就用 "错误边界" 组件来处理一下这种情况。

我们在 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

此生命周期在后代组件抛出错误后被调用。 它接收两个参数:

  1. error —— 抛出的错误。
  2. 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
1-5.png

可以看到,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

欢迎志同道合的小伙伴一起交流,一起学习。
觉得写得不错的可以点点关注,帮忙转发跟点赞。

你可能感兴趣的:(快来跟我一起学 React(Day6))