React快速入门:初识组件

Web 网页结构一般都比较复杂,但复杂的结构中又经常包含一些重复模式。我们可以对网页结构进行划分,从中抽象出一些组件,以此简化代码逻辑。

本节以一个简单的分类书单应用为例,讲解应用结构划分和 React 组件设计的基本思路。

HTML结构

我们要开发的分类书单应用按类别罗列书单,每本书只展示封面,点击即可跳至京东购买。就这么简单的应用,HTML 结构也层层嵌套,相当复杂了:

See the Pen for-var by fasionchan (@fasionchan) on CodePen.

如果是直接写 HTML 结构,想想都要发狂——因为有一堆重复的结构:

  • 分类是一个 section 标签,因此每种类别都需要重复写一遍;
  • 书是一个 li 标签,因此每本书都需要重复写一遍;
  • 如果类别很多,每个类别下的书也多,那得写到怀疑人生;

数据驱动

我们在上节已经掌握了通过 React 虚拟 DOM 元素来组织网页的技巧,可以用 JS 根据数据生成虚拟 DOM 元素,事情得到极大简化:

See the Pen for-var by fasionchan (@fasionchan) on CodePen.

数据是一个书单分类数组,一般由后端接口返回。数组元素是一个代表图书分类对象,name 字段是分类名,books 字段是该类别下的图书列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[
  {
    "name": "编程语言/JavaScript",
    "books": [
      // ...
    ]
  },
  {
    "name": "Web前端/React",
    "books": [
      // ...
    ]
  }
];

图书列表也是一个数组,每个元素元素对象表示一本书,name 字段表示书名,image_url 字段表示图书封面图片 URLjingdong_url 表示京东购买链接:

1
2
3
4
5
6
7
8
[
  {
    "name": "JavaScript权威指南",
    "image_url": "https://cdn.fasionchan.com/p/e0756b1825f86a90994ce4bb2500624c4fdf1929.jpg",
    "jingdong_url": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAMUJK1olXDYAVVhcD0oXCl9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFkkWBW4PGlscQl9HCANtEhBAaDQBElh3XlpKCyAuEgoVQ2sPXVcZbQcyVF9cCUoWAmkBHGslXQEyHzBcOEonA2gME1MRWw8KV1dVCHsQA2Y4TAtXBVhdBgcNVyVLM184GGsSXQ8WUiwcWl8RcV84G2sWbURsVFhZDUMUCzwJT1pAWFVVXQ1aDxkXBmkJHwgcCgcFUV5tCkoWB2Y4Kw"
  },
  // ...
]

利用数组 map 方法,我们可以根据数据生产任意多的虚拟 DOM 元素,以生成图书类别为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
sections.map(({name, books}) => React.createElement(
  'section',
  {
    key: name,
  },
  React.createElement(
    'h1',
    null,
    name,
  ),
  React.createElement(
    'ul',
    // ...
  ),
);

这段代码 map 方法会为 sections 数组每个元素生成一个 section 虚拟元素,section 包含一个 h1 虚拟元素来展示书名,还包含一个 ul 来展示图书列表。

注意到,我们传了一个 key 参数给 section 元素,这个参数在 React 中非常重要。React 渲染在列表元素时,依赖参数 key 对比新旧节点的差异,看哪些节点需要重新渲染。

渲染图书列表跟渲染类别类似,也利用了数组的 map 方法,为每本书生成 li 虚拟元素,li 元素内又嵌套着 aimg 元素。完整代码请查看 CODEPEN 中的 JS 部分。

不知您注意到没?这种代码设计方式虽然从一定程度上实现了代码复用,但还不够优雅:

  • 代码嵌套过深,不利于阅读;
  • 代码仍无法复用到其他地方,比如其他页面;

简而言之,这个版本的代码仍是简单堆砌在一起。我们可以将其中的部分逻辑抽象出来,组织成独立的函数。这样既能简化代码逻辑,又能提升可复用性。

结构分析

开始进行逻辑抽象之前,我们先来分析当前代码的结构。它负责生成分类书单的虚拟 DOM 元素,结构跟网页的结构高度相关,因此我们可以从分析网页结构入手:

如上图是分类书单的整体结构,红色方框代表一个类别,由 section 标签组成,包含类别名称和图书列表;蓝色方框为图书列表,由 ul 标签组成,内部包含一组 li 标签,每个代表一本图书;绿色方框代表一本书,由 li 标签组成,内部嵌套 a 标签实现点击跳转,又嵌套 img 标签展示图书封面。

函数是组织代码的最基本单位,因此我们可以将每种方框的生成逻辑,组织成一个函数。函数则根据参数提供的数据,创建并返回虚拟 DOM 元素。

这样一来,只要调用函数即可创建对应的虚拟 DOM 结构,逻辑更为清晰,也更好复用。

React组件

像函数这样根据参数完成虚拟 DOM 元素创建的代码实体,在 React 中称为 组件component )。在 React 中,有两种不同的组件,一种是 函数组件 ,一种是 类组件

我们先来看函数组件,它本质上只是一个满足约定接口的常规函数而已:

1
(props) => React.Node;

组件函数接收一个参数 props ,代表传给组件的参数对象;然后返回创建好的 React 虚拟 DOM 节点。我们采用自底向上的思路,先编写展示单本书的组件练练手:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function Book(props) {
  const { book, imgStyle={} } = props;
  return React.createElement(
    "a",
    {
      href: book.jingdong_url,
    },
    React.createElement(
      "img",
      {
        src: book.image_url,
        style: {
          width: 100,
          border: "#dddddd 1px solid",
          ...imgStyle,
        },
      },
    ),
  );
}

参数对象 props 向组件传递两个参数:book 是一个对象,传递待展示的书本信息;imgStyle 也是一个对象,传递封面图片的自定义样式。

书本只展示封面图片,需要创建 img 节点。为实现点击跳转,还需要在上面套一个 a 节点。因此,组件函数创建并返回 a 节点,参数指定 href 属性跳转到京东。a 节点嵌套 img 节点,参数 src 指定封面图片地址,参数 style 指定图片样式。

注意到,组件为 img 图片提供了默认样式,但用户可以传 imgStyle 参数对默认样式进行覆盖。这也是设计组件的最佳实践——既保证易用性,又不失灵活度。

React.createElement 创建 React 虚拟 DOM 元素时,还支持使用 React 组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
React.createElement(
  Book,
  {
  	book: {
      "name": "JavaScript权威指南",
      "image_url": "https://cdn.fasionchan.com/p/e0756b1825f86a90994ce4bb2500624c4fdf1929.jpg",
      "jingdong_url": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAMUJK1olXDYAVVhcD0oXCl9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFkkWBW4PGlscQl9HCANtEhBAaDQBElh3XlpKCyAuEgoVQ2sPXVcZbQcyVF9cCUoWAmkBHGslXQEyHzBcOEonA2gME1MRWw8KV1dVCHsQA2Y4TAtXBVhdBgcNVyVLM184GGsSXQ8WUiwcWl8RcV84G2sWbURsVFhZDUMUCzwJT1pAWFVVXQ1aDxkXBmkJHwgcCgcFUV5tCkoWB2Y4Kw"
    },
  },
);

这段代码创建虚拟 DOM 元素,渲染 Book 组件,最终展示图书《JavaScript权威指南》。注意到,书本信息通过 book 参数传给 Book 组件,imgStyle 不传则采用默认样式。

React 将虚拟 DOM 元素渲染成浏览器 DOM 元素后,结果大概是这样的:

1
2
3
<a href="https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAMUJK1olXDYAVVhcD0oXCl9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFkkWBW4PGlscQl9HCANtEhBAaDQBElh3XlpKCyAuEgoVQ2sPXVcZbQcyVF9cCUoWAmkBHGslXQEyHzBcOEonA2gME1MRWw8KV1dVCHsQA2Y4TAtXBVhdBgcNVyVLM184GGsSXQ8WUiwcWl8RcV84G2sWbURsVFhZDUMUCzwJT1pAWFVVXQ1aDxkXBmkJHwgcCgcFUV5tCkoWB2Y4Kw">
  <img src="https://cdn.fasionchan.com/p/e0756b1825f86a90994ce4bb2500624c4fdf1929.jpg" style="xxxx" />
</a>

接下来,我们可以在 Book 组件的基础上,编写用于展示图书列表的 BookList 组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function BookList(props) {
  const { books, style={} } = props;
  return React.createElement(
    "ul",
    {
      style: {
        listStyle: "none",
        display: "flex",
        flexWrap: 'wrap',
        padding: 0,
        ...style,
      },
    },
    books.map(book => React.createElement(
      "li",
      {
        key: book.name,
        style: {
          margin: "10px 5px",
        },
      },
      React.createElement(
        Book,
        {
          book,
        },
      ),
    )),
  );
}

BookList 组件接收两个参数,books 数组传递图书列表,style 对象传递自定义样式。图书列表组织成 ulli 结构,因此我们调用 books 数组 map 方法,为每本书生成 li 列表节点,li 节点则包含 Book 组件。

将简单组件稍加组合,即可构造更复杂的组件,像搭积木一般!

看到这里,相信大家对 React 组件应该有了初步认识,可以自己试着开发图书类别组件练练手(红框部分)。温馨提示,类别数据可通过 section 参数传递,接口如下:

1
2
3
4
5
6
7
8
function BookSection(props) {
  const { section } = props;
  return React.createElement(
    "section",
    null,
    // ...
  );
}

最后,创建一个 div 虚拟 DOM 节点,内部包含根据数据生成的多个 BookSection 节点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const element = React.createElement(
  "div",
  {
    style: {
      width: 500,
    },
  },
  sections.map(section => React.createElement(
    BookSection,
    {
      key: section.name,
      section,
    },
  )),
);

const container = document.querySelector('#root');
const root = ReactDOM.createRoot(container);
root.render(element);

虚拟 DOM 节点渲染到浏览器后,效果跟之前也是一样的,但代码要清晰很多:

See the Pen for-var by fasionchan (@fasionchan) on CodePen.

完整源码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
function Book(props) {
  const { book, imgStyle={} } = props;
  return React.createElement(
    "a",
    {
      href: book.jingdong_url,
    },
    React.createElement(
      "img",
      {
        src: book.image_url,
        style: {
          width: 100,
          border: "#dddddd 1px solid",
          ...imgStyle,
        },
      },
    ),
  );
}

function BookList(props) {
  const { books, style={} } = props;
  return React.createElement(
    "ul",
    {
      style: {
        listStyle: "none",
        display: "flex",
        flexWrap: 'wrap',
        padding: 0,
      },
    },
    books.map(book => React.createElement(
      "li",
      {
        key: book.name,
        style: {
          margin: "10px 5px",
        },
      },
      React.createElement(
        Book,
        {
          book,
        },
      ),
    )),
  );
}

function BookSection(props) {
  const { section } = props;
  return React.createElement(
    "section",
    null,
    React.createElement(
      "h1",
      null,
      section.name,
    ),
    React.createElement(
      BookList,
      {
        books: section.books,
      },
    ),
  );
}

const sections = [
  {
    "name": "编程语言/JavaScript",
    "books": [
      {
        "name": "JavaScript权威指南",
        "image_url": "https://cdn.fasionchan.com/p/e0756b1825f86a90994ce4bb2500624c4fdf1929.jpg",
        "jingdong_url": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAMUJK1olXDYAVVhcD0oXCl9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFkkWBW4PGlscQl9HCANtEhBAaDQBElh3XlpKCyAuEgoVQ2sPXVcZbQcyVF9cCUoWAmkBHGslXQEyHzBcOEonA2gME1MRWw8KV1dVCHsQA2Y4TAtXBVhdBgcNVyVLM184GGsSXQ8WUiwcWl8RcV84G2sWbURsVFhZDUMUCzwJT1pAWFVVXQ1aDxkXBmkJHwgcCgcFUV5tCkoWB2Y4Kw"
      },
      {
        "name": "JavaScript高级程序设计",
        "image_url": "https://cdn.fasionchan.com/p/ea43461ed636d5f5eaec230fa0c01b458d8383eb.jpg",
        "jingdong_url": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAM4JK1olXDYCV1ZZAU8eAl9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFksUC2sBH1IUQl9HCANtXhlUZBJ0Ti52XmRlVQdccTJSfTxfa1cZbQcyVF9cCUoWAmkBHGslXQEyAjBdCUoWAm4NH1wSbQcyVFlZAEMTBWcLHl4RXzYFVFdtXxtVWzFXSQJFAmheZG5tC3sQA2YcHSlUDxIEJm5tCHsUMy1mSVMdDgcKBg5UDhhHBjgBGwsTCVVRAVteXRwfU2wPS1glXwcDUFdtOA"
      },
      {
        "name": "JavaScript语言精粹",
        "image_url": "https://cdn.fasionchan.com/p/6a3da198a07137d09d907eec4335a9cb7f4dcd83.jpg",
        "jingdong_url": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAM4JK1olXDYCV1dcAEMTBV9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFksUCm4AE18TQl9HCANtdz1LaB9NGQdwIlp6FSUqYCBfVQ9NTVcZbQcyVF9cCUoWAmkBHGslXQEyAjBdCUoWAm4NG14WbQcyVFlZAEMTCm4IHFwcVTYFVFdtXxtVWzFXSQJFAmheZG5tC3sQA2YcHSlUDxIEJm5tCHsUMy1mT1scVVUAXVhYC0kRBj0PGggcXFRQUVlaAUkWCmYME10lXwcDUFdtOA"
      },
      {
        "name": "JavaScript设计模式与开发实践",
        "image_url": "https://cdn.fasionchan.com/p/cae8752ad06201cdda82939da97c1e4b500d62b3.jpg",
        "jingdong_url": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAM4JK1olXDYCVl1aDEkVBl9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFksVAGgMGVkQQl9HCANtQ1V1d3VtUhN3AnxpFggkU0pnfjZ6a1cZbQcyVF9cCUoWAmkBHGslXQEyAjBdCUoWAm4NH1wSbQcyVFlaCUoWB2sAEl0cVDYFVFdtXxtVWzFXSQJFAmheZG5tC3sQA2YcHSlUDxIEJm5tCHsUMy1mSwsTVQ4KUAtcCRgRBmxbH1NGDwAHBllcXUlHUWcNHg4lXwcDUFdtOA"
      },
      {
        "name": "你不知道的JavaScript(上卷)",
        "image_url": "https://cdn.fasionchan.com/p/ee1d97ffe30a426a9ede54f0967cddde873c3690.jpg",
        "jingdong_url": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAM4JK1olXDYCV1dUAE4QBV9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFksUCmYAHlwTQl9HCANtchhTBCt_bEVwPmR4CS5DYw9NY2wLa1cZbQcyVF9cCUoWAmkBHGslXQEyAjBdCUoWAWgJGVoWbQcyVFlZAEMTBWcMGVMcXTYFVFdtXxtVWzFXSQJFAmheZG5tC3sQA2YcHSlUDxIEJm5tCHsUMy1mGlpBCAcFVVoPCEoRBm9aGlNAVQIGAwtUAUgQVGgLHFglXwcDUFdtOA"
      },
      {
        "name": "你不知道的JavaScript(中卷)",
        "image_url": "https://cdn.fasionchan.com/p/cb6362f73ffd3cc3653433a084a625184c218e2c.jpg",
        "jingdong_url": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAMUJK1olXDYAVVtdCUgTBV9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFkkWBm8JGF8TQl9HCANtAU0UdwxYGSF3OwEEJl0taT1kHRhRe1cZbQcyVF9cCUoWAmkBHGslXQEyHzBcOEonA2gME1MRVAUKXF1ZDHsQA2Y4TAtXBVhdBgcNVyVLM184GGsSXQ8WUiwcWl8RcV84G2sWbURsVgsJX09DUGgKHF4SWFFSVw5cXUkTBDgIHQxBXlIAV1dtCkoWB2Y4Kw"
      },
      {
        "name": "你不知道的JavaScript(下卷)",
        "image_url": "https://cdn.fasionchan.com/p/29d4a49e48512a9053b88b14247c26279dee594b.jpg",
        "jingdong_url": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAM4JK1olXDYCV11VCEIRBl9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFksUAGcIEl0QQl9HCANtb05TdidJZBtwIVZdFyU0fUtXQStgXVcZbQcyVF9cCUoWAmkBHGslXQEyAjBdCUoWAm4NH1wSbQcyVFlZAEMTBWcAG10cVDYFVFdtXxtVWzFXSQJFAmheZG5tC3sQA2YcHSlUDxIEJm5tCHsUMy1mS18SCgJVVwsKARsSBm4LTlIVWAELVQ5aWkhCA2YNEl0lXwcDUFdtOA"
      }
    ]
  },
  {
    "name": "Web前端/React",
    "books": [
      {
        "isbn": "9787519856540",
        "name": "React学习手册",
        "image_url": "https://cdn.fasionchan.com/p/157e6c7ff5fbe889405c922065280027930cc929.jpeg",
        "jingdong_url": "https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAM4JK1olXDYCV11UAUoWCl9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFksUAGYBGlocQl9HCANtakJ-AippYCdwBlZ_Ti0Db0MVVj1UXVcZbQcyVF9cCUoWAmkBHGslXQEyAjBdCUoWAm4MH14dbQcyVFlZAEoQAW4JH1ITWjYFVFdtXxtVWzFXSQJFAmheZG5tC3sQA2YcHSlUDxIEJm5tCHsUMy1mGgsQD1YCXV5bCxkRBmxYGlNFVQcFVAtbCE8UC24LTFwlXwcDUFdtOA"
      }
    ]
  }
];

const element = React.createElement(
  "div",
  {
    style: {
      width: 500,
    },
  },
  sections.map(section => React.createElement(
    BookSection,
    {
      key: section.name,
      section,
    },
  )),
);

const container = document.querySelector('#root');
const root = ReactDOM.createRoot(container);
root.render(element);

小菜学React】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学React】系列文章首发于公众号【小菜学编程】,敬请关注: