快来跟我一起学 React(Day6)

Posted vv_小虫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快来跟我一起学 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

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

代码分割

因为我们这一节分析的主要是 React 的 “高级指引” 部分内容,所以我们先在 src 目录下创建一个 advanced-guides 目录,用来存放 “高级指引” 的内容:

mkdir ./src/advanced-guides

然后在 src/advanced-guides 目录下创建一个 index.tsx 文件:

/**
 * 核心概念列表
 */
import CodeSplit from "./code-split";

function AdvancedGuides() 
    return (
        <div>
            /* 代码分割 */
            <CodeSplit/>
        </div>
    );
;
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 = (
    <div className="root">
        /* 核心概念 */
        <MainConcepts/>
        /* 高级指引 */
        <AdvancedGuides/>
    </div>
);
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 (
        <React.Fragment>
            /* 渲染异步组件 */
            <React.Suspense fallback=<div>Loading...</div>>
                <LazyComponent/>
            </React.Suspense>
        </React.Fragment>
    );

export default CodeSplit;

可以看到,我们用 React.lazy 方法定义了一个异步组件,然后在 React.Suspense 组件中渲染了这个异步组件(注意:React.lazy 返回的组件必须配合 Suspense 组件使用,而且 Suspense 组件必须提供 fallback 属性,Suspense 组件我们后面再详细解析)。

然后在 src/advanced-guides/code-split 目录下创建一个 lazy.com.tsx 文件:

function LazyComponent()
    return (
        <div>我是一个异步组件</div>
    );

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(<div>Loading...</div>);
  useEffect(() => 
    LazyComponent.then((module: any) => 
      setCom((React.createElement(module.default, , [])) as any);
    );
  , []);
  return (
    <React.Fragment>
      /* 渲染异步组件 */ 
       Com 
    </React.Fragment>
  );


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 = (
    <div className="root">
        /* 核心概念 */
        <MainConcepts/>
        /* 高级指引 */
        <AdvancedGuides/>
    </div>
);
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(
  <App/>,
  document.getElementById("root")
);

src/app.tsx 文件内容:

import MainConcepts from "./main-concepts";
import AdvancedGuides from "./advanced-guides";
import React from "react";

function App()
  return (
    <div className="root">
      /* 核心概念 */
      <MainConcepts/>
      /* 高级指引 */
      <AdvancedGuides/>
    </div>
  )

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<AppContextType>(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<AppContextType>(
    theme: Themes.Light,
    toggleTheme
  );
  return (
    <AppContext.Provider value= appContext >
      <div className= ["theme-light", "theme-dark"][appContext.theme] >
        /* 核心概念 */ 
        <MainConcepts/>
        /* 高级指引 */ 
        <AdvancedGuides/>
      </div>
    </AppContext.Provider>
  );


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 (
    <React.Fragment>
      /* 类组件方式 */ 
      <ContextCom/>
    </React.Fragment>
  );


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 (
      <div>
        <button onClick= this.context.toggleTheme >点我切换主题</button>
      </div>
    );
  


// 定义 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 (
    <div>
      /* 代码分割 */ 
      <CodeSplit/>
      /* Context */ 
      <Context/>
    </div>
  );
;
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 (
    <div>
      <AppContext.Consumer>
         (toggleTheme) => <button onClick= toggleTheme >点我切换主题</button> 
      </AppContext.Consumer>
    </div>
  );


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 (
    <React.Fragment>
      /* 类组件方式 */ 
      <ContextCom/>
      /* 函数组件方式 */ 
      <ContextFunc/>
    </React.Fragment>
  );


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 (
    <div>
      /* 代码分割 */ 
      <CodeSplit/>
      /* Context */ 
      <Context/>
      /* 报错的组件 */ 
      <ErrorCom/>
    </div>
  );
;
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 <h1>Something went wrong.</h1>;
    

    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 (
    <ErrorBoundaries>
      <div>
        /* 代码分割 */ 
        <CodeSplit/>
        /* Context */ 
        <Context/>
        /* 报错的组件 */ 
        <ErrorCom/>
      </div>
    </ErrorBoundaries>
  );
;
export default AdvancedGuides;

我们重新运行项目看结果:

npm start

可以看到,src/advanced-guides/error-boundaries.tsx 组件中成功捕捉到了错误,应用也没有全部挂掉,只是 src/advanced-guides/index.tsx 组件中的内容:

 <ErrorBoundaries>
      <div>
        /* 代码分割 */ 
        <CodeSplit/>
        /* Context */ 
        <Context/>
        /* 报错的组件 */ 
        <ErrorCom/>
      </div>
    </ErrorBoundaries>

由于错误的原因,直接替换成了:

if (this.state.hasError) 
   // 你可以自定义降级后的 UI 并渲染
   return <h1>Something went wrong.</h1>;

边界处理组件在错误的捕获与收集上很有用处,可以结合一些错误收集框架做线上错误统计,快速分析出一些 bug 问题原因。

总结

我们通过 Demo 演示了什么是异步组件、Context 对象、错误边界组件,有些小伙伴要说了 ”我们何不把所有的组件都做成异步组件?所有的全局数据共享都用 Context?给所有的模块都加上错误边界组件?“,小伙伴一定要结合具体项目场景来使用这些高级特性,比如你项目本来就不大,你还把所有的组件都做成异步组件,这样做不但没有加快应用渲染速度,反而会引起服务器压力过大,然后把所有的全局状态共享都用 Context 处理,这样做虽然可以达到效果,但是当 Context 对象中逻辑过于庞大,这样做反而不利于全局状态的管理,而且管理不好还会造成状态更新频繁而引起性能问题,最后你会得不偿失的。

好啦,这节到这就结束啦。

Demo 项目代码下载:https://gitee.com/vv_bug/react-demo-day5/tree/dev

欢迎志同道合的小伙伴一起交流,一起学习。

觉得写得不错的可以点点关注,帮忙转发跟点赞。

以上是关于快来跟我一起学 React(Day6)的主要内容,如果未能解决你的问题,请参考以下文章

快来跟我一起学 React(Day8)

快来跟我一起学 React(Day4)

快来跟我一起学 React(Day4)

快来跟我一起学 React(Day2)

快来跟我一起学 React(Day7)

快来跟我一起学 React(Day7)