美文网首页
快来跟我一起学 React(Day6)

快来跟我一起学 React(Day6)

作者: vv_小虫虫 | 来源:发表于2021-03-25 21:19 被阅读0次

    简介

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

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

    相关文章

      网友评论

          本文标题:快来跟我一起学 React(Day6)

          本文链接:https://www.haomeiwen.com/subject/fuorhltx.html