快来跟我一起学 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 (
        <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
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(<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
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 (
    <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
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 <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
1-5.png

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容