在第二篇文章 《新型前端开发方式》 中有说到 React 有很爽的一点就是给我们一种创造 HTML 标签的能力,那么今天这篇文章就详细讲解下 React 是如何提供这种能力的,作为前端开发者如何来运用这种能力。

在第三篇文章 《JavaScript代码里写HTML一样可以很优雅》 中介绍了 JavaScript 的扩展语法 JSX,相信大家已经知道了,所谓的创造新的 HTML 的能力,其实就是以极其类似 HTML 的 JSX 语法来使用基于 React 编写的视图层组件。所以说,要完成今天的任务,我们只需要搞清楚一个问题即可:如何基于 React 编写视图层组件。

内容摘要

  • 定义组件两种方式:类继承组件、函数式组件。
  • 类继承组件有更丰富的特性,函数式组件书写更简洁,执行效率更高。
  • 组件名称首字母要大写。
  • 属性是一个组件的外部输入。
  • 属性值可以通过 {} 设置任意的 JS 表达式。
  • 属性是只读的。
  • 属性可以设置默认值。
  • 属性可以设置类型,开发阶段 React 会对属性进行类型检查。
  • 为组件所有属性设置类型检查是个好习惯,有助于协作开发。

通过内容摘要可以让你快速了解本文内容是否对你有用,从而决定是否继续阅读,节省你的时间也是一件很有意义的事情。

定义组件的几种姿势

下面介绍一下在 React 中定义组件的几种方式。

1. 类继承

有过 Java 等面向对象开发经验的同学一定很容易接受这种方式。ES6 为 JavaScript 增加了类和类继承的特性。子类会继承父类的“基因”(成员方法、属性),如果父类是一个组件,那子类自然而然也是一个组件。

React 提供了 ComponentPureComponent 两个父类,他们之间有一点点区别,我们在之后的文章中会详细介绍,现在你可以将他们同等看待,暂且无须理会。

通过继承自 React 提供的组件基类,我们可以这样来创建一个组件:

import React, {Component} from 'react';

class HelloMessage extends Component {
render() {
return <div>Hello world.</div>;
}
}

通过类继承的方式创建一个组件,就是这么简单,只要继承 Component 基类并实现 render 方法即可。然后就可以把 HelloMessage 当成一个新的“HTML 标签”来用了,如下你可以把它渲染到页面上:

ReactDOM.render(<HelloMessage />, document.querySelector('#root'));

你也可以用它来装配其它组件,如:

import React, {Component} from 'react';

class HelloMessageList extends React.Component {
render() {
return (
<div>
<HelloMessage />
<HelloMessage />
<HelloMessage />
</div>
)
}
}

当然,例子没有任何实际意义,只是为了演示组件的定义及其用法。

演示代码:https://codepen.io/Sarike/pen...

2. 函数式组件

顾名思义,函数式组件,就是以函数的形式来定义一个组件,如下所示:

import React from 'react';

function HelloMessage() {
return <div>Hello world.</div>;
} // 或者: const HelloMessage = () => <div>Hello world.</div>;

实际上就是只实现了类继承方式中的 render 方法。

示例代码:https://codepen.io/Sarike/pen...

类继承 vs 函数式组件

这两种定义组件的方式,在实际的开发中都经常会被用到,对大部分人来说类继承的方式用得频率会更高一些。

类继承的方式,相较于函数式组件,虽然写起来略繁琐,但是它拥有更多的特性:

  • 内部状态:state
  • 生命周期函数

函数式组件虽然没有 state 和生命周期函数等特性,但是它有更简洁的书写方式,另外还有更好的性能,不用处理一些复杂的特性,执行效率当然高了。

现在你可以无需关心 state 和生命周期函数的具体作用,下一篇文章我会详细讲解,等你看完下一篇文章之后,至于选择哪种方式的问题就很好解决了。在开发一个组件的时候,我是这样来做的:当我一开始就知道这个组件会用到 state 或者生命周期函数时,毫无疑问直接使用类继承的方式;如果一开始用不到这些特性也不确定未来会不会用到,那我就先用函数式组件,如果随着业务的演进,组件需要应用这些特性的时候,我会再把它重构成类继承的方式。这个重构非常简单,只需要将原来的函数变成组件类的 render 方法即可。

另外,还有一点需要注意,不管那种方式,组件的名称首字母必须为大写。严格来说,是 JSX 要求用户自定义的组件名首字母必须为大写,如果是小写字母开头,那么 React 会将其当成内置的组件直接将其渲染成一个 html 标签,从而不会正确渲染用户自定义的组件。

如果你非要将组件名称以小写字母开头,那你在以 JSX 语法使用之前也必须将其赋值为一个大写字母开头的变量,如下所示:

function helloMessage() {
return <div>Hello world.</div>
} const HelloMessage = helloMessage; ReactDOM.render(<HelloMessage />, mountNode);

但这有事何必呢,纯粹是没事儿找事儿,大家在实际项目开发时,直接将组件名以大写字母开头即可。

属性

上面说完了在 React 中两种定义组件的方式。在上面的例子中,我们定义的组件都是静态的,然而在实际的开发中,视图层组件往往会进行频繁更新,或者需要从后端 API 获取动态数据在组件中展示。这就需要组件拥有接收外部输入的能力。

属性是组件的输入

在第二篇文章 《新型前端开发方式》 中有说到 “视图是数据的映射”,那么其中说的数据指的就是属性。

如果把组件理解为一个函数,那么属性就是这个函数的参数,函数的返回值就是呈现到页面上的视图。而且通过上面部分的学习,在 React 中组件确实可以以函数的形式来定义,而且函数的参数就是一个包含当前组件接收到的所有属性的对象。

如下所示带有属性 name 的组件定义:

import React, {Component} from 'react';

class HelloMessage extends Component {
render() {
return <div>Hello {this.props.name}.</div>;
}
}

函数式:

import React from 'react';

function HelloMessage(props) {
return <div>Hello {props.name}.</div>;
} // 或者: const HelloMessage = props => <div>Hello {props.name}.</div>;

属性的传递也跟 HTML 一样(在本文的最后一部分会有各种类型属性的详细介绍),如下所示:

import React, {Component} from 'react';
import ReactDOM from 'react-dom'; class HelloMessageList extends Component {
render() {
return (
<div>
<HelloMessage name="Lucy" />
<HelloMessage name="Tom" />
<HelloMessage name="Jack" />
</div>
)
}
} ReactDOM.render(<HelloMessageList />, document.querySelector('#root'));

这样页面上会展示出:

Hello Lucy.
Hello Tom.
Hello Jack.

示例代码:https://codepen.io/Sarike/pen...

属性必须为只读的

属性必须为只读的,这一点非常重要,请严格遵守。对应到上面说到的,如果把组件理解为一个函数,那么这个函数必须为一个纯函数(Pure function),在纯函数中不能修改其参数,确定的输入必须有确定的输出。

虽然有些时候,你修改了组件的属性,貌似也能正常工作。没错,因为 JavaScript 语言特性的原因,没人能阻止你这么做。但是请先相信我,严格遵守这条规则不仅能让你少踩很多坑,而且能让你的应用稳定性更强、维护性更强。如果你直接修改组件的属性,React 并不会感知到此修改,从而不会重新渲染组件,就导致了当前组件的视图展示与数据不一致,但这个被修改的属性会随着下一次组件的渲染被生效到视图上,而且这次渲染的时机是不确定的,不难想象,如果一个规模较大的项目里充满了这种不确定性是多么痛苦的一件事情。总之,如果你随意修改组件的属性,会很容易让你的应用充满许多难以排查的 BUG。

默认属性

通常情况下,我们需要为组件的属性设为默认值。就像 HTML 标签的属性也有默认值一样,例如 form 标签的 method 属性默认值是 GET,input 标签的 type 属性默认值是 text 一样。

还是上面 HelloMessage 组件,如果需求是当不传入 name 属性时,默认展示 Hello World.,也就是说 name 属性的默认值是 World。

一种很容易想到的做法:

<div>Hello {this.props.name || 'World'}.</div>

这样确实可以解决当前这个需求,但是属性可能还会是个 Object,也可能是个函数,你当然可以先判断下这个属性是否为 undefined 然后决定是否使用默认值,但是这样会让代码显得很不优雅,而且也会增加很多繁琐的判断逻辑。

因此,React 提供了相应的机制可以设置组件属性的默认值,如下所示,你需要通过组件的静态字段 defaultProps 来设置组件属性的默认值。如下所示:

import React, {Component} from 'react';

class HelloMessage extends Component {
render() {
return <div>Hello {this.props.name}.</div>;
}
}
HelloMessage.defaultProps = {
name: 'World'
}

这样就可以了,<HelloMessage /> 这样不为组件设置任何属性,那么它就会在页面上展示Hello World.

示例代码:https://codepen.io/Sarike/pen...

属性的类型及校验

在开发较复杂的前端应用时,我们经常会遇到许多因为类型检查导致的问题,例如上面的 HelloMessage 组件,我期望其 name 属性只能是字符串类型的,你要是给我一个 Object,我是没法正确展示的。为了在开发过程中尽快的发现这类问题,React 为组件添加了类型检查的机制,你需要给组件设置静态字段 propTypes 来设置组件各个属性的类型检查器。

import React, {Component} from 'react';
import PropTypes from 'prop-types'; class HelloMessage extends Component {
render() {
return <div>Hello {this.props.name}.</div>;
}
}
HelloMessage.defaultProps = {
name: 'World'
}
HelloMessage.propTypes = {
name: PropTypes.string
}

这样在开发过程中 React 就能校验组件接收到的属性值是否符合指定的类型,如果校验不通过,将会抛出警告。React 只会在开发模式下进行属性类型检查,当代码进行生产发布后,为了减少额外的性能开销,类型检查将会被略过。

其实,为每一个组件编写完善的属性类型是一个非常好的习惯,这不仅能及时发现问题,更重要的是配合几句简单额注释,这将成为该组件一份非常好的文档,一个完善的组件应该具有良好的封装性和易复用性,在一个协作开发的项目中,其他开发者需要引用你开发的组件时,只需要看一下组件的属性列表,大致就可以了解如何来使用这个组件,省去了很多不必要的沟通。

下面是 React 提供的可用的数据类型检查器。

  • PropTypes.array
  • PropTypes.bool
  • PropTypes.func
  • PropTypes.number
  • PropTypes.object
  • PropTypes.string
  • PropTypes.symbol
  • PropTypes.element 元素,其实就是 JSX 表达式,上一篇文章有说过 JSX 是 React.createElement 的语法糖,一个 JSX 表达式实际上会生成一个 JS 对象,在 React 中称之为元素(Element)。
  • PropTypes.node 所有可以被渲染的数据类型,包括:数值, 字符串, 元素或者这些类型的数组。
  • PropTypes.instanceOf(Message) 某个类的实例
  • PropTypes.oneOf(['News', 'Photos']) 枚举,属性值必须为其中的某一个值。
  • PropTypes.oneOfType([PropTypes.string, PropTypes.number]) 类型枚举,属性必须为其中某一个类型。
  • PropTypes.arrayOf(PropTypes.number) 属性为一个数组,且数组中的元素必须符合指定类型。
  • PropTypes.objectOf(PropTypes.number) 属性为一个对象,且对象中的各个字段的值必须符合指定类型。
  • PropTypes.any 任何类型

如果你想指定某些属性为必需属性,你可以链式调动其 isRequired 来标识某个属性对于当前组件来说是必需的。如果在使用组件时未指定则会抛出警告提醒。

另外你还可以通过一个函数自定义属性验证器,如果验证不通过你需要返回一个 Error 实例,如下所示:

function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
}
}

设置组件的属性值

上面咱们了解到组件的属性有很多种类型,下面说一下各种类型的属性是如何传递给组件的。其实很简单,属性的值可以用一对大括号 { } 来包围,其中可以指定任意的 JavaScript 表达式。如下所示:

return (
<User
name="Tom" // 字符串
age={18} // 数值
isActivated={true} // 布尔值
interests={['basketball', 'music']} // 数组
address={{ city: 'Beijing', road: 'BeiWuHuan' }} // 对象
/>
)

展开操作符

你也可以用展开操作符 ... 将一个对象的所有字段展开,依次作为属性传递给组件,上面的代码等价于:

const userInfo = {
name: 'Tom',
age: 18,
isActivated: true,
interests: ['basketball', 'music'],
address: { city: 'Beijing', road: 'BeiWuHuan' }
}
return <User {...userInfo} />

值为 true 的属性的简写

如果是属性类型为布尔值,且当前属性值为 true 可以只写属性名,如下所示:

<input
disabled // 禁用该输入框
type="text"
/>

children 属性

用户自定义的组件内可以通过 this.props.children 来获取一个特殊的属性。该属性与其它属性的区别就是传递方式不同。

children 属性的值是指一对闭合的 JSX 标签中间的内容,如下所示:

<UserList>
<User name="Tom" />
<User name="Lucy" />
</UserList>

那么在 UserList 内部可以通过 this.props.children 来获取下面这个 JSX 片段:

<User name="Tom" />
<User name="Lucy" />

该示例中,获取到的实际上是一个包含两个 User 元素对象的数组。

总结

本文主要介绍了在 React 中组件的定义方式,以及几个关键的注意事项。另外介绍了组件属性的作用、属性默认值、属性类型校验以及如何为组件传递属性。

希望内容对大家有用,如有任何问题和建议可以给我留言,谢谢。

玩转 React(四)- 创造一个新的 HTML 标签的更多相关文章

  1. react:如何创建一个新项目

    如何用react创建一个新的项目 我们打开react官网:https://reactjs.org/docs/create-a-new-react-app.html 看到以下命令 npx create- ...

  2. 玩转 React(五)- 组件的内部状态和生命周期

    文章标题总算是可以正常一点了-- 通过之前的文章我们已经知道:在 React 体系中所谓的 "在 JavaScript 中编写 HTML 代码" 指的是 React 扩展了 Jav ...

  3. 手写一个React-Redux,玩转React的Context API

    上一篇文章我们手写了一个Redux,但是单纯的Redux只是一个状态机,是没有UI呈现的,所以一般我们使用的时候都会配合一个UI库,比如在React中使用Redux就会用到React-Redux这个库 ...

  4. 使用React并做一个简单的to-do-list

    1. 前言 说到React,我从一年之前就开始试着了解并且看了相关的入门教程,而且还买过一本<React:引领未来的用户界面开发框架 >拜读.React的轻量组件化的思想及其virtual ...

  5. 从零开始玩转JMX(四)——Apache Commons Modeler & Dynamic MBean

    Apache Commons Modeler 前面的Model MBean的创建方式看上去特别复杂,一个简单功能的类ModelMBeanUtils 写了很多代码,那有木有简单点的方式呢,答案是肯定的, ...

  6. 探讨一个新的两个进程间的通信和编程模型 (Windows)

    本文探讨一个新的Windows上的两个UI进程间的通信和编程模型. 开门见山,下面是这个通信模型的梗概图: 这个模型的设计目标描述如下: (1)发送数据接口:RpcSend, RpcPost RpcS ...

  7. 关于新feature对应的增加一个新的测试单子(QA)和文档单子(Doucmentation)的步骤

    一,增加一个new feature的文档单子. 1.new feature 增加对应的文档单子(公司有一个组是专门写产品说明的)所以增加一个新的功能就要有这个新的功能对应的一个文档(Documenta ...

  8. linux采用模块方法,添加一个新的设备

    该文转载自:http://rangercyh.blog.51cto.com/1444712/521244 系统调用是操作系统内核和应用程序之间的接口,而设备驱动程序是操作系统内核和机器硬件之间的接口. ...

  9. 如何用 React Native 创建一个iOS APP?

    诚然,React Native 结合了 Web 应用和 Native 应用的优势,可以使用 JavaScript 来开发 iOS 和 Android 原生应用.在 JavaScript 中用 Reac ...

随机推荐

  1. PyTorch深度学习实践——处理多维特征的输入

    处理多维特征的输入 课程来源:PyTorch深度学习实践--河北工业大学 <PyTorch深度学习实践>完结合集_哔哩哔哩_bilibili 这一讲介绍输入为多维数据时的分类. 一个数据集 ...

  2. 解决 “Project ERROR: Unknown module(s) in QT: webengine”以及“Your MaintenanceTool appears to be older than 3.0.2. .” 的办法

    1.环境 Windows10,Qt5.8.0 2.问题描述 需要使用到WebEngineView组件,在工程.pro中增加webengine后,Qt Creator应用程序输出中打印了 Project ...

  3. C# 字符串、数组和List的截取和转换

    using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using S ...

  4. LeetCode-080-删除有序数组中的重复项 II

    删除有序数组中的重复项 II 题目描述:给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 最多出现两次 ,返回删除后数组的新长度. 不要使用额外的数组空间,你必须在 原地 修改 ...

  5. C#控制台输入密码星号显示

    在Program类中的Main方法里: 1 public class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.Write ...

  6. 矩池云 | 利用LSTM框架实时预测比特币价格

    温馨提示:本案例只作为学习研究用途,不构成投资建议. 比特币的价格数据是基于时间序列的,因此比特币的价格预测大多采用LSTM模型来实现. 长期短期记忆(LSTM)是一种特别适用于时间序列数据(或具有时 ...

  7. PhpStrom 常用的插件

    .env files support 可以在env函数使用是提示.env文件中所有的key值的自动完成功能 Markdown support 在编写.md文件时有预览的功能 PHP composer. ...

  8. 打靶笔记-01-vulnhub-moneybox

    打靶笔记-01-vulnhub-moneybox 本篇笔记根据苑老师视频进行学习记录 https://www.bilibili.com/video/BV1Lv411n7Lq/?spm_id_from= ...

  9. Django-Multitenant,分布式多租户数据库项目实战(Python/Django+Postgres+Citus)

    Python/Django 支持分布式多租户数据库,如 Postgres+Citus. 通过将租户上下文添加到您的查询来实现轻松横向扩展,使数据库(例如 Citus)能够有效地将查询路由到正确的数据库 ...

  10. Docker的4种网络模式详细介绍

    docker run创建Docker容器时,可以用–net选项指定容器的网络模式,Docker有以下4种网络模式: bridge模式:使用–net =bridge指定: host模式:使用–net = ...