快来跟我一起学 React(Day7)

Posted vv_小虫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快来跟我一起学 React(Day7)相关的知识,希望对你有一定的参考价值。

简介

我们继续上一节的内容,开始分析 React 官网:https://reactjs.org/docs/accessibility.html 的 “高级指引” 部分,这一部分会涉及到 Refs 转发、Fragments、高阶组件等概念的分析,比前面章节的难度还是略微大一些的,所以一定要跟上节奏哦,我们一起出发吧!

知识点

  • Refs 转发
  • Fragments
  • 高阶组件
  • 深入 JSX

准备

我们直接用上一节中的 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

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

https://gitee.com/vv_bug/react-demo-day5/tree/dev)

Refs 转发

Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。但其对某些组件,尤其是可重用的组件库是很有用的。

解释起来有点抽象,我们还是用 Demo 来演示一下。

转发 refs 到 DOM 组件

因为上一节测试 “错误边界” 组件的时候抛了一个错误, src/advanced-guides 目录下的模块都变成了 “Something went wrong”,所以我们先修改一下 src/advanced-guides/error.tsx 组件,让它不要再报错了:

function ErrorCom(): null
  return null;
  // throw new Error("报错啦!");

export default ErrorCom;

我们在 src/advanced-guides 目录下创建一个 forward-ref 目录:

mkdir ./src/advanced-guides/forward-ref

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

import React from "react";
import CusInput from "./cus-input";

function ForwardRef() 
  let cusInputRef: any;
  const handleInputRef = (ref: any) => 
    cusInputRef = ref;
  ;
	/*
		让 input 元素聚焦
	*/
  function focusInput() 
    cusInputRef && cusInputRef.focus();
  

  return (
    <React.Fragment>
      /* 自定义 input 组件  */ 
      <CusInput ref= handleInputRef />
      <button onClick= focusInput >聚焦 input</button>
    </React.Fragment>
  );


export default ForwardRef;

可以看到,我们自定义了一个 CusInput,然后获取了 CusInput 元素的引用 ref,最后通过 ref 让自定义的 CusInput 元素自动获取焦点。

ok,然后我们在 src/advanced-guides/forward-ref 目录下创建一个 cus-input.tsx 组件:

import React from "react";
function CusInput(props:any, ref: any) 
  return (
    <div>
      <input ref=ref/>
    </div>
  );

export default React.forwardRef(CusInput);

可以看到,我们定义了一个函数式组件 CusInput,并且通过 React.forwardRef 方法把 CusInput 组件的 ref 指向了其子元素 input

最后我们 src/advanced-guides/index.tsx 组件中引入 src/advanced-guides/forward-ref/index.tsx 组件:

/**
 * 核心概念列表
 */
import CodeSplit from "./code-split";
import Context from "./context";
import ErrorBoundaries from "./error-boundaries";
import ErrorCom from "./error";
import ForwardRef from "./forward-ref";

function AdvancedGuides() 
  return (
    <ErrorBoundaries>
      <div>
        /* 代码分割 */ 
        <CodeSplit/>
        /* Context */ 
        <Context/>
        /* 报错的组件 */ 
        <ErrorCom/>
        /* Refs 转发 */ 
        <ForwardRef/>
      </div>
    </ErrorBoundaries>
  );
;
export default AdvancedGuides;

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

npm start

可以看到,当我们点击 “聚焦 input” 按钮的时候,input 元素自动被聚焦了。

接下来我们用类组件的形式来实现一下 src/advanced-guides/forward-ref/cus-input.tsx 组件。

我们在 src/advanced-guides/forward-ref 目录下创建一个 cus-input.com.tsx 文件:

import React from "react";
import PropTypes from "prop-types";

type Prop = 
  handleRef: (ref: any) => void
;

class CusInputCom extends React.Component<Prop> 
  static propTypes = 
    handleRef: PropTypes.func
  

  render() 
    return (
      <div>
        <input ref= this.props.handleRef />
      </div>
    );
  


export default React.forwardRef((props, ref: any) => 
  return <CusInputCom  ...props  handleRef= ref />;
);

然后在 src/advanced-guides/forward-ref/index.tsx 组件中引入 src/advanced-guides/forward-ref/cus-input.com.tsx 组件:

import React from "react";
import CusInput from "./cus-input";
import CusInputCom from "./cus-input.com";

function ForwardRef() 
  let cusInputRef: any;
  let cusInputRef2: any;
  const handleInputRef = (ref: any) => 
    cusInputRef = ref;
  ;
  const handleInputRef2 = (ref: any) => 
    cusInputRef2 = ref;
  ;

  function focusInput() 
    cusInputRef && cusInputRef.focus();
  
  function focusInput2() 
    cusInputRef2 && cusInputRef2.focus();
  

  return (
    <React.Fragment>
      /* 自定义 input 组件  */ 
      <CusInput ref= handleInputRef />
      /* 自定义 input 组件  */ 
      <CusInputCom ref= handleInputRef2 />
      <button onClick= focusInput >聚焦 input</button>
      <button onClick= focusInput2 >聚焦 input2</button>
    </React.Fragment>
  );


export default ForwardRef;

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

npm start

可以看到,我们分别用 “函数组件”、“类组件” 实现了 CusInput 组件。

React.createRef

我们上面用的都是使用了一个方法去接受组件的 ref 属性:

let cusInputRef: any;
const handleInputRef = (ref: any) => 
  cusInputRef = ref;
; 
/* 自定义 input 组件  */ 
 <CusInput ref= handleInputRef />

其实接受一个组件的 ref 属性,除了利用函数外,我们还可以利用 React 提供的 createRef 方法。

我们来改造一下 src/advanced-guides/forward-ref/index.tsx 组件:

import React from "react";
import CusInput from "./cus-input";
import CusInputCom from "./cus-input.com";

function ForwardRef() 
  // let cusInputRef: any;
  // let cusInputRef2: any;
  let cusInputRef = React.createRef<htmlInputElement>();
  let cusInputRef2 = React.createRef<HTMLInputElement>();
  function focusInput() 
    cusInputRef?.current?.focus();
  
  function focusInput2() 
    cusInputRef2?.current?.focus();
  

  return (
    <React.Fragment>
      /* 自定义 input 组件  */ 
      <CusInput ref= cusInputRef />
      /* 自定义 input 组件  */ 
      <CusInputCom ref= cusInputRef2 />
      <button onClick= focusInput >聚焦 input</button>
      <button onClick= focusInput2 >聚焦 input2</button>
    </React.Fragment>
  );


export default ForwardRef;

我们还需要简单的修改一下 src/advanced-guides/forward-ref/cus-input.com.tsx 组件:

import React from "react";
import PropTypes from "prop-types";

type Prop = 
  handleRef: React.RefObject<HTMLInputElement>
;

class CusInputCom extends React.Component<Prop> 
  static propTypes = 
    handleRef: PropTypes.object
  
  render() 
    return (
      <div>
        <input ref= this.props.handleRef />
      </div>
    );
  


export default React.forwardRef((props, ref: any) => 
  return <CusInputCom  ...props  handleRef= ref />;
);

可以看到,我们把之前的函数接受 ref 全改成了 React.createRef<HTMLInputElement>() 方式:

let cusInputRef = React.createRef<HTMLInputElement>();
  let cusInputRef2 = React.createRef<HTMLInputElement>();

React 会自动把 ref 挂载到传入对象的 current 属性中:

 function focusInput() 
    cusInputRef?.current?.focus();
  
  function focusInput2() 
    cusInputRef2?.current?.focus();
  

效果跟上面一样,我就不演示了。

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

Fragments

React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。

其实我们在 Demo 中已经使用过 Fragment 组件了,比如我们的 src/advanced-guides/forward-ref/index.tsx 组件:

return (
    <React.Fragment>
      /* 自定义 input 组件  */ 
      <CusInput ref= cusInputRef />
      /* 自定义 input 组件  */ 
      <CusInputCom ref= cusInputRef2 />
      <button onClick= focusInput >聚焦 input</button>
      <button onClick= focusInput2 >聚焦 input2</button>
    </React.Fragment>
  );

我们可以试着把 src/advanced-guides/forward-ref/index.tsx 组件中的 React.Fragment 组件去掉:

可以看到,IDE 就直接报错了,说 “JSX 语法必须包含一个父元素”。

有童鞋要说了,“我们可以直接定一个 div 元素或者其它元素呀”,是的!你可以这样做,但是当我们定义的是 div 元素的时候,最后是会被渲染到 DOM 中的,而我们想要的是不需要渲染到 DOM 中,所以我们就可以使用 Fragment 组件。

短语法

你可以使用一种新的,且更简短的语法来声明 Fragments。它看起来像空标签:

return (
    <>
      /* 自定义 input 组件  */ 
      <CusInput ref= cusInputRef />
      /* 自定义 input 组件  */ 
      <CusInputCom ref= cusInputRef2 />
      <button onClick= focusInput >聚焦 input</button>
      <button onClick= focusInput2 >聚焦 input2</button>
    </>
  );

带 key 的 Fragments

使用显式 React.Fragment 语法声明的片段可能具有 key。一个使用场景是将一个集合映射到一个 Fragments 数组 - 举个例子,创建一个描述列表:

function Glossary(props) 
  return (
    <dl>
      props.items.map(item => (
        // 没有`key`,React 会发出一个关键警告
        <React.Fragment key=item.id>
          <dt>item.term</dt>
          <dd>item.description</dd>
        </React.Fragment>
      ))
    </dl>
  );

key 是唯一可以传递给 Fragment 组件的属性。

高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

具体而言,高阶组件是参数为组件,返回值为新组件的函数。

使用 HOC 解决横切关注点问题

组件是 React 中代码复用的基本单元。但你会发现某些模式并不适合传统组件。

例如我们之前的切换主题组件 src/advanced-guides/context/context.func.tsx ,如果我们需要获取到 Context 对象中的 themetoggleTheme,我们需要这样做:

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;

我们需要利用 AppContext.Consumer 方式,或者类组件中的静态 contextType 属性方式来获取到 Context 对象中的数据。

小伙伴有没有想过,我们的自定义组件需要关心 Context 对象怎么获取吗?能不能有一种方式直接把 Context 中的数据直接通过 props 传递给我呢?我每次写一个组件为了去获取 Context 还得去写这么多代码。

ok,“高阶组件” 来了!

我们首先在 src/advanced-guides 目录下创建一个 hoc 目录:

mkdir ./src/advanced-guides/hoc

然后在 src/advanced-guides/hoc 目录下创建一个高级组件 with-theme.tsx 组件:

touch ./src/advanced-guides/hoc/with-theme.tsx

然后将以下内容写入到 src/advanced-guides/hoc/with-theme.tsx 组件:

import React from "react";
import AppContext, AppContextType, Themes from "../../app-context";

export type ThemeType = 
    theme: Themes,
    toggleTheme: () => void
;
export type getThemeDataType = 
    (appContext: AppContextType): ThemeType;
;

/**
 * 带主题的高阶组件
 * @param getThemeData
 */
function withTheme(getThemeData: getThemeDataType) 
    return function (WrappedComponent: typeof React.Component | React.FunctionComponent) 
        // 转发 ref 函数组件
        const RefComponent = (props: any, ref: any) => 
            class ThemeComponent extends React.Component 
                render() 
                    return (
                        <AppContext.Consumer>
                            (appContext) => (
                                <WrappedComponent
                                    ref=ref
                                    ...this.props
                                    ...getThemeData(appContext)
                                />
                            )
                        </AppContext.Consumer>
                    );
                ;
            

            return (
                <ThemeComponent
                    ...props
                />
            );
        ;
        // 转发 ref
        return React.forwardRef(RefComponent);
    ;


export default withTheme;

这里我们做了几步工作:

  1. 定义了一个带主题的高阶组件 withTheme 函数。
  2. 定义了一个转发 ref 函数组件 RefComponent
  3. 定义了一个带主题的组件 ThemeComponent
  4. 利用 AppContext.Consumer 获取到了 AppContext 对象中的数据。

这已经算是一个比较复杂的高阶组件了,因为里面还包含了高阶组件的 ref 转发等功能(算是对前面 forwad-ref 内容的复习了,不熟悉的童鞋记得去看一下前面的文章哦 )。

接着我们修改一下 src/advanced-guides/context/context.com.tsx 组件:

import React from "react";
import withTheme,ThemeType from "../hoc/with-theme";
type Prop =ThemeType & 
;
class ContextCom extends React.Component<Prop> 
  render() 
    return (
      <div>
        <button onClick= this.props.toggleTheme >点我切换主题</button>
      </div>
    );
  

// 构造一个带主题功能的组件
export default withTheme((appContext) => appContext)(ContextCom);

同样修改一下 src/advanced-guides/context/context.func.tsx 组件:

import React from "react";
import withTheme from "../hoc/with-theme";

function ContextFunc(props: any) 
    return (
        <div>
            <button onClick=props.toggleTheme>点我切换主题</button>
        </div>
    );

// 构造一个带主题功能的组件
export default withTheme((appContext) => appContext)(ContextFunc);

可以看到,是不是变得很简答了呢?我们不需要再考虑 “AppContext 该怎么获取”了,我们只需要利用 withTheme 高阶函数,它就会自动的把 AppContext 中跟主题相关的数据给到这个组件,我们直接在组件中通过 Props 就可以访问了。

可以看到,效果跟我们之前的一样。

高阶组件的约定

  1. 不要改变原始组件,高级组件应该是一个纯函数。

    ...
    /**
     * 带主题的高阶组件
     * @param getThemeData
     */
    function withTheme(getThemeData: getThemeDataType) 
        return function (WrappedComponent: typeof React.Component | React.FunctionComponent) 
            // 转发 ref 函数组件
            const RefComponent = (props: any, ref: any) => 
                class ThemeComponent extends React.Component 
                    render() 
                        return (
                            <AppContext.Consumer>
                                (appContext) => (
                                    <WrappedComponent
                                        ref=ref
                                        ...this.props
                                        ...getThemeData(appContext)
                                    />
                                )
                            </AppContext.Consumer>
                        );
                    ;
                
    
                return (
                    <ThemeComponent
                        ...props
                    />
                );
            ;
            // 转发 ref
            return React.forwardRef(RefComponent);
        ;
    
    
    export default withTheme;
    

    可以看到,我们并没有对传入的 WrappedComponent 组件做任何额外的操作。

  2. 将不相关的 props 传递给被包裹的组件。

     <WrappedComponent
       ref=ref
       ...this.props
       ...getThemeData(appContext)
       />
    

    可以看到,我们保留了 WrappedComponent 组件自身的 props,只是额外通过 getThemeData 方法添加了一些参数。

  3. 最大化可组合性。

    我们定义的 withTheme 是一个返回高阶组件的函数,可以接受 getThemeData 参数,提供给使用者自定义 props 里面的内容。

  4. 约定:包装显示名称以便轻松调试。

    我们直接定义了一个叫 ThemeComponent 的组件,方便调试。

深入 JSX

实际上,JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖。如下 JSX 代码:

<MyButton color="blue" shadowSize=2>
  Click Me
</MyButton>

会编译为:

React.createElement(
  MyButton,
  color: 'blue', shadowSize: 2,
  'Click Me'
)

如果没有子节点,你还可以使用自闭合的标签形式,如:

<div className="sidebar" />

会编译为:

React.createElement(
  'div',
  className: 'sidebar'
)

指定 React 元素类型

JSX 标签的第一部分指定了 React 元素的类型。

大写字母开头的 JSX 标签意味着它们是 React 组件。这些标签会被编译为对命名变量的直接引用,所以,当你使用 JSX 表达式时,Foo 必须包含在作用域内。

React 必须在作用域内

由于 JSX 会编译为 React.createElement 调用形式,所以 React 库也必须包含在 JSX 代码作用域内。

例如,在如下代码中,虽然 ReactCustomButton 并没有被直接使用,但还是需要导入:

import React from 'react';
import CustomButton from './CustomButton';
function WarningButton() 
 // return React.createElement(CustomButton, color: 'red', null);
  return <CustomButton color="red" />;

如果你不使用 javascript 打包工具而是直接通过 <script> 标签加载 React,则必须将 React 挂载到全局变量中。

但是在我们的 react-demo-day5 项目中,我们在使用 JSX 的时候可以不用引入 React,比如我们的 src/main-concepts/components-and-props/welcome.func.tsx 组件:

import PropTypes from "prop-types";
type Prop = 
    readonly name: string, // 姓名
;
function Welcome(props: Prop) 
    return <h1>我是函数式组件,Hello, props.name</h1>;

Welcome.propTypes=
    name: PropTypes.string
;
Welcome.defaultProps = 
    name: "小虫"
;
export default Welcome;

可以看到,我们用了 JSX 语法,但是并没有引入 React,为什么还能正常运行呢?

因为在 React17+ 版本后,React 已经把 JSX 生成 React 元素节点的 API 全部提取到了 react-jsx-xxx.js 文件中去了,那有小伙伴疑问了: “我们也并没有引入 react-jsx-xxx.js 文件呀”。

其实在我们项目中,我们使用了 React 官方提供的 babel-loader 插件集合,我们可以找到 babel.config.js 文件:

module.exports = 
    presets: [
        [
            "babel-preset-react-app", // 添加 react-app 插件集合
            
                runtime: require.resolve("react/jsx-runtime") ? "automatic" : "classic"
            
        ],
    ]
;

我们做了判断,当项目中有 react/jsx-runtime 模块的时候,就使用 react/jsx-runtime,没有就使用之前 React 中的 API 方式创建 JSX 元素节点。

我们可以试一下,修改一下 react-demo-day5/babel.config.js 文件:

module.exports = 
    presets: [
        [
            "babel-preset-react-app", // 添加 react-app 插件集合
            
                runtime: "classic"
            
        ],
    ]
;

可以看到,我们给 runtime 传递了一个 classic 参数,告诉 babel,在解析 JSX 语法创建节点的时候使用 ReactAPI

我们重新运行一下项目:

npm start

可以看到,项目运行的时候直接报错了,说找不到 React 变量。

我们还是把 babel.config.js 改回来吧:

module.exports = 
    presets: [
        [
            "babel-preset-react-app", // 添加 react-app 插件集合
            
                runtime: require.resolve("react/jsx-runtime") ? "automatic" : "classic"
            
        ],
    ]
;

我们再次重新运行项目:

npm start

然后我们看一下经过 babel-preset-react-app 插件处理过后的 src/main-concepts/components-and-props/welcome.func.tsx 组件:

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, 
/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ );
/* harmony import */ var prop_types__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! prop-types */ "./node_modules/prop-types/index.js");
/* harmony import */ var prop_types__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(prop_types__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react/jsx-dev-runtime */ "./node_modules/react/jsx-dev-runtime.js");
var _jsxFileName = "/xxx/react-demo-day5/src/main-concepts/components-and-props/welcome.func.tsx";



function Welcome(props) 
  return /*#__PURE__*/(0,react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_1__.jsxDEV)("h1", 
    children: ["\\u6211\\u662F\\u51FD\\u6570\\u5F0F\\u7EC4\\u4EF6\\uFF0CHello, ", props.name]
  , void 0, true, 
    fileName: _jsxFileName,
    lineNumber: 6,
    columnNumber: 12
  , this);


Welcome.propTypes = 
  name: (prop_types__WEBPACK_IMPORTED_MODULE_0___default().string)
;
Welcome.defaultProps = 
  name: "小虫"
;
const __WEBPACK_DEFAULT_EXPORT__ = (Welcome);

可以看到, babel-preset-react-app 插件会自动的引入 react/jsx-dev-runtime.js 模块,并且利用 react/jsx-dev-runtime.js 模块的 jsxDEV 方法创建一个 React 元素。

在 JSX 类型中使用点语法

在 JSX 中,你也可以使用点语法来引用一个 React 组件。当你在一个模块中导出许多 React 组件时,这会非常方便。例如,如果 MyComponents.DatePicker 是一个组件,你可以在 JSX 中直接使用:

import React from 'react';

const MyComponents = 
  DatePicker: function DatePicker(props) 
    return <div>Imagine a props.color datepicker here.</div>;
  


function BlueDatePicker() 
  return <MyComponents.DatePicker color="blue" />;

用户定义的组件必须以大写字母开头

以小写字母开头的元素代表一个 HTML 内置组件,比如 或者 会生成相应的字符串 'div' 或者 'span' 传递给 React.createElement(作为参数)。大写字母开头的元素则对应着在 JavaScript 引入或自定义的组件,如 会编译为 React.createElement(Foo)

建议使用大写字母开头命名自定义组件。如果你确实需要一个以小写字母开头的组件,则在 JSX 中使用它之前,必须将它赋值给一个大写字母开头的变量。

总结

这一节我们介绍了 Refs 转发、Fragments、高级组件、深入 JSX等知识点,可能有些小伙伴要说了:“我项目从头到尾就没用到什么高级组件、Refs 转发”,是的!也并不是所有场景都需要用到这些高级技巧,比如你把你所有的组件逻辑都抽离到了 “高阶组件” 中,这样高阶组件就会变得十分臃肿,甚至还会造成性能问题,这样就有点得不偿失了,具体还得跟自己项目实际情况来使用,就像我们 Demo 中的换肤功能、我们使用了高阶组件来处理,可以帮我们省了很多隐藏的工作,使每个组件逻辑变得更清晰,你只需简单的调用一个方法,就可以具备有换肤功能了,小伙伴不用慌张,当你在项目中不断总结跟磨练,最后具备一定经验的时候,你自然而然就懂了。

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

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

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

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

快来跟我一起学 React(Day8)

快来跟我一起学 React(Day4)

快来跟我一起学 React(Day4)

快来跟我一起学 React(Day2)

快来跟我一起学 React(Day6)

快来跟我一起学 React(Day6)