简介
我们继续上一节的内容,开始分析 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
对象中的 theme
与toggleTheme
,我们需要这样做:
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;
这里我们做了几步工作:
- 定义了一个带主题的高阶组件
withTheme
函数。 - 定义了一个转发 ref 函数组件
RefComponent
。 - 定义了一个带主题的组件
ThemeComponent
。 - 利用
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
就可以访问了。
可以看到,效果跟我们之前的一样。
高阶组件的约定
-
不要改变原始组件,高级组件应该是一个纯函数。
... /** * 带主题的高阶组件 * @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
组件做任何额外的操作。 -
将不相关的 props 传递给被包裹的组件。
<WrappedComponent ref={ref} {...this.props} {...getThemeData(appContext)} />
可以看到,我们保留了
WrappedComponent
组件自身的props
,只是额外通过getThemeData
方法添加了一些参数。 -
最大化可组合性。
我们定义的
withTheme
是一个返回高阶组件的函数,可以接受getThemeData
参数,提供给使用者自定义props
里面的内容。 -
约定:包装显示名称以便轻松调试。
我们直接定义了一个叫
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 代码作用域内。
例如,在如下代码中,虽然 React
和 CustomButton
并没有被直接使用,但还是需要导入:
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
语法创建节点的时候使用 React
的 API
。
我们重新运行一下项目:
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");-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 引入或自定义的组件,如 <Foo /> 会编译为 React.createElement(Foo)
。
建议使用大写字母开头命名自定义组件。如果你确实需要一个以小写字母开头的组件,则在 JSX 中使用它之前,必须将它赋值给一个大写字母开头的变量。
总结
这一节我们介绍了 Refs 转发、Fragments、高级组件、深入 JSX等知识点,可能有些小伙伴要说了:“我项目从头到尾就没用到什么高级组件、Refs 转发”,是的!也并不是所有场景都需要用到这些高级技巧,比如你把你所有的组件逻辑都抽离到了 “高阶组件” 中,这样高阶组件就会变得十分臃肿,甚至还会造成性能问题,这样就有点得不偿失了,具体还得跟自己项目实际情况来使用,就像我们 Demo
中的换肤功能、我们使用了高阶组件来处理,可以帮我们省了很多隐藏的工作,使每个组件逻辑变得更清晰,你只需简单的调用一个方法,就可以具备有换肤功能了,小伙伴不用慌张,当你在项目中不断总结跟磨练,最后具备一定经验的时候,你自然而然就懂了。
好啦,这节到这就结束啦。
Demo 项目全部代码:https://gitee.com/vv_bug/react-demo-day5/tree/dev/
/* 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
觉得写得不错的可以点点关注,帮忙转发跟点赞。