# 02|Component:搭建静态页面的正确思路是什么? 你好,我是蒋宏伟。 上一讲我们说到,React/React Native 开启了“基于组件”构建应用的潮流。 在工作中,特别是业务类的开发需求,绝大多数都是写页面。写页面分为两步,第一步是搭建静态页面,第二步是给静态页面添加交互让它动起来。这第一步至关重要,它决定了 UI 设计稿要拆分成哪些组件,这些组件又是如何组织起来的,这些都会影响程序的可扩展性和可维护性,甚至还有团队的合作方法。 我们这一讲的目的,就是让你有一个正确的基于组件搭建静态页面的思路,不让第一步走偏。要知道,如果后面再去纠正,要花费的成本就大了去了。 ## 组件:可组合、可复用的“拆稿”方式 在开始使用组件这种方式构建静态页面之前,请你先思考一个问题,为什么 React/React Native 选择了基于组件的架构方式呢? 理论上,除了组件这种方式外,常见的构建应用方式还有:类似 HTML/CSS/JavaScript 这种的分层架构、基于 MVC 的分层架构。那为什么 React/React Native 没有选择这两种架构方式呢? **这是因为,基于组件的架构模式,或许是现在重展示、重交互应用的最好选择。** 记得我 2015 年刚入门的时候,还有一种岗位叫做网页重构工程师,我还面过这种岗位。那个时候,架构模式就是把 UI 设计稿拆成 3 层:HTML、CSS、JavaScript。网页重构工程师负责 HTML、CSS 部分,前端工程师负责 JavaScript 部分。但是后来我发现网页重构工程师这种岗位越来越少了,也庆幸自己没有上错车。 现在,相信你也看到了,把 UI 设计稿拆成完全独立的 HTML/CSS/JavaScript三个部分的这种架构已经不是主流了;2010 年开源的、代表 MVC 架构模式的 AngularJS也被 Angular(v2 及更高版本)这种基于组件的架构模式所代替了;现在 iOS、Android 应用也有很多是基于组件开发的。 为什么会有这种现象呢?我先给你看一张架构对比图,你先可以体会一下它们之间的区别,找找原因: ![图片](https://static001.geekbang.org/resource/image/00/b6/00e902a0949ecfa5a8748ef66df420b6.jpg?wh=1920x524) 现代应用都很复杂,而且非常重交互、重展示。如果 React Native 选择的是类似 HTML/CSS/JavaScript 的模板、样式、逻辑分离的分层架构,那可想而知,我们的三层代码都会非常臃肿。 如果 React Native 选择的是 MVC 架构,把逻辑控制、数据模型和视图进行分层,对程序横向分层纵向打通,这样代码颗粒度是会变小。但在重交互的前提下,层和层之间、列和列之间的数据流向却更复杂了。流动的方向不止是 MVC 架构图中画 “3+3” 的 6 个方向,而是层和层之间的 “3_3_2” 个方向,列和列之间的 “3_3_2” 个方向,非常复杂。 React/React Native 选择的是基于组件的架构模式,它有三个好处: * 第一,组件是内聚的,组件内既有逻辑,又有状态,还有视图,一个组件可以独立完成一件事情,这也使得 UI 模块复用变得简单; * 第二,组件之间是可以组合的,一个页面可以拆分成若干个大组件,大组件也可以拆分成小组件,当某个组件变大变臃肿时也可以进一步地拆分; * 第三,组件和组件之间的数据流向永远是确定,永远是从上往下流动的,简单明了。 **组件可组合、可复用的特性,和组件之间单向数据流的模式**,在现代应用重交互重展示的情况下,显然更吃香,这也是 React/React Native 选择基于组件来构建应用的原因。 ## 单一责任原则 现在我们回到第一步,基于组件搭建静态页面。 我们直接来看一个具体的例子。这里我放了一个简易商品列表页的 UI 设计稿,你可以先停下来思考一下,想一想你会把它拆成那些组件?你这么拆的原因又是什么? ![图片](https://static001.geekbang.org/resource/image/d4/23/d4264b371fee1038da912e7737afce23.png?wh=1000x802) 我们直接来揭晓答案,拆组件要准守一个原则,**单一责任原则**。 这也是 React 官方倡导的原则,这个原则的意思是**每个组件都应该只有一个单一的功能,并且这个组件和其他组件没有相互依赖**。当然,完全没有相互依赖是不可能的,但这种思路具有很高的指导价值,一个组件的依赖越少,设计得越好。 给你举个例子,一个组件你引用的依赖越多,这些依赖就像陌生的英语单词,你得去其他文件中去查词典,才能知道这些依赖的意思。依赖越多,越难读懂,也越难维护。 因此,为了可读性、可维护性、可测试性,就要减少组件的外部依赖,这就是单一责任原则的指导价值。 这样说来,在拆分简易商品列表页的 UI 设计稿时,我们就要尽可能地拆的更细一些,保证每个组件的责任单一,因为涉及到 UI 稿建议你打开文稿查看一下,那我们拆分结果如图所示: ![图片](https://static001.geekbang.org/resource/image/e2/94/e22a8ff50c7bbdb637ed6eb42892dd94.png?wh=1000x594) 你可以看到,这个简易商品列表已经被拆分了 3 个组件,具体如下: 1. ProductTable(紫色):它是商品列表组件,显示商品列表和表头; 2. Category(青色):它是类别组件,显示一类商品的种类; 3. Product(黄色):它是商品组件,显示某个具体的商品名称和价格。 ## 宿主组件:生产基础视图的工厂 当你有了怎么把 UI 设计稿拆分成组件的思路后,接下来就要构建静态页面了。 要构建静态页面,就要有基础的视图材料。在 React Native 中那些最基础、不可再拆的视图材料,大都是由 React Native 框架提供的**宿主视图**。 比如,UI 设计稿中的水果名称:“苹果”、“火龙果”,价格:“¥1”、“¥2”,还有最顶部的搜索框,这些都是宿主视图。 而生产宿主视图的工厂,就是宿主组件(Host Components)。这些**宿主组件通常是 React Native 框架提供的组件,它们和你用 JavaScript 自定义的组件不同,宿主组件是直接由 iOS/Android 原生平台实现的。** 除了 React Native 框架提供的宿主组件外,一些社区库也提供了宿主组件,甚至你自己也可以创建宿主组件。 它们共同的特点是,这些宿主组件上层是 JavaScript 部分,底层是 Native 部分,这两部分是通过 React Native 框架联系起来的。也就是说,你调用宿主组件时,底层直接渲染的是 Native 视图。 那么,我们这个简易商品列表页的 UI 设计稿中,用到了那些宿主组件呢?其实有三种: * 容器组件 View:顾名思义它就是一个容器,可以用来包裹其他的组件,类似于 Web 中用于嵌套的 div; * 文字组件 Text:设计稿中的文字,比如水果名字“苹果”、“梨子”,价格“1元”、“3元”等等,这些类似于 Web 中装载文字的 span。 * 安全区域组件 SafeAreaView:它是最外层的容器组件,用于适配 iPhoneX等的刘海儿屏。 宿主组件就是一个生产基础视图的工厂,你可以用 Text 组件实例化不同的文字视图。比如,我们可以实例化一个“苹果”文字,也可以再实例化另一个“火龙果”文字,代码如下: ```plain import {Text} from 'react-native'; const element1 = 苹果 // JSX const element2 = 火龙果 // JSX ``` 你看啊,在这段用 JavaScript 书写的代码中,使用了**类似 HTML 的声明式语法,JSX**。我们先从 react-native 框架中引入了 Text 组件,然后通过 JSX 语法,用一对单闭合标签将 Text 组件进行实例化,生成 Text 元素 element1。当 element1 这个元素渲染到手机屏幕上,就是文字“苹果”了,element2 就是文字“火龙果”。 ## 复合组件:纯 JavaScript 函数 现在,你已经有了构建静态页面的宿主组件了,接下来你需要用这些宿主组件,搭建你自己事先拆好的自定义组件了,包括: * ProductTable 商品列表组件 * Category 类别组件 * Product 商品组件 要创建自定义的宿主组件,你必须写 Native 代码。但上面 3 个自定义组件,**你可以直接用 JavaScript 创建,不用写 Native 代码,这类组件也叫复合组件(Composite Components)**。这些复合组件是基于宿主组件或其他复合组件搭建而成的。 现在我们来创建第一个自定义的复合组件:Product 商品组件,它的示例代码如下: ```plain export default function Product({product = {name: '苹果', price: '1元'} }) { return ( {product.name} {product.price} ); } ``` 这段代码,对于一些新手来说可能有点长,我分四步和你解释: 第一步,导出组件。还记得单一责任原则吗?一个组件的责任要单一,一个文件的责任也要单一。因此通常一个文件中只有一个组件,用`export default`就可以将它导出,让其他文件`import`引入使用。 第二步,定义函数。组件是一种特殊的函数。组件名字的首字母一定是大写的,示例中的`Product`是组件,因此它的 `P`是大写的(当然,还有类组件,但用得会越来越少,这里我们不探讨,你可以自己额外搜些资料)。 第三步,接收入参。组件能从其父组件中接参数,而且组件是函数,因此该参数就是函数的入参,通常命名为属性 `props`。`props` 是一个对象,因此也可以直接对它进行解构,直接获取对象中的值。 示例代码中用的就是用解构的方式来获取参数的,它直接获取了`product`参数,这里的`product` 是数据因此`p`是小写的。 第四步,返回 JSX。组件的返回值就是 JSX,我们前面也提到过,它是用来描述 UI 页面的,JSX 最终生成的是视图元素、文字元素。这里我们初始化了一个``元素,和两个``元素。 我们概括一下,自定义复合组件就是一个纯粹的 JavaScript 函数,谁调用它,谁就可以给它传入参数,同样它调用谁,它就可以给谁传入参数,而 JSX 闭合标签就是调用函数的语法糖。 ## 静态页面的最终实现 现在你知道了 Product 商品组件如何定义,那么 Category 类别组件、ProductTable 商品列表组件对你来说,也就很容易了。 最后我们来看下,静态页的最终实现,完整代码有点长,我就不都贴出来了,你可以看看文末补充材料中的链接,现在我们只看下它整体长什么样子: ```plain // index.js AppRegistry.registerComponent('appName', () => App); // App.js const PRODUCTS = [ {category: '水果', price: '¥1', name: 'PingGuo'}, ]; export default function App() { return ( ); } // ProductTable.js import Category from './Category'; import Product from './Product'; export default function ProductTable({products}){ // ...