You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

214 lines
15 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 02Component搭建静态页面的正确思路是什么
你好,我是蒋宏伟。
上一讲我们说到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也被 Angularv2 及更高版本)这种基于组件的架构模式所代替了;现在 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 = <Text>苹果</Text> // JSX
const element2 = <Text>火龙果</Text> // 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 (
<View style={{flexDirection: 'row', marginTop: 5}}>
<Text style={{flex: 1}}>{product.name}</Text>
<Text style={{width: 50}}>{product.price}</Text>
</View>
);
}
```
这段代码,对于一些新手来说可能有点长,我分四步和你解释:
第一步,导出组件。还记得单一责任原则吗?一个组件的责任要单一,一个文件的责任也要单一。因此通常一个文件中只有一个组件,用`export default`就可以将它导出,让其他文件`import`引入使用。
第二步,定义函数。组件是一种特殊的函数。组件名字的首字母一定是大写的,示例中的`Product`是组件,因此它的 `P`是大写的(当然,还有类组件,但用得会越来越少,这里我们不探讨,你可以自己额外搜些资料)。
第三步,接收入参。组件能从其父组件中接参数,而且组件是函数,因此该参数就是函数的入参,通常命名为属性 `props`。`props` 是一个对象,因此也可以直接对它进行解构,直接获取对象中的值。
示例代码中用的就是用解构的方式来获取参数的,它直接获取了`product`参数,这里的`product` 是数据因此`p`是小写的。
第四步,返回 JSX。组件的返回值就是 JSX我们前面也提到过它是用来描述 UI 页面的JSX 最终生成的是视图元素、文字元素。这里我们初始化了一个`<View/>`元素,和两个`<Text/>`元素。
我们概括一下,自定义复合组件就是一个纯粹的 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 (
<SafeAreaView style={{marginHorizontal: 30}}>
<ProductTable products={PRODUCTS} />
</SafeAreaView>
);
}
// ProductTable.js
import Category from './Category';
import Product from './Product';
export default function ProductTable({products}){
// ...
<Category category={products[i].category}
// ...
<Product product={products[i]}
// ...
}
// Category.js
export default function Category({category}){}
// ProductTable.js
export default function Product({product}) {}
```
这里我定义了五个文件,每个文件中都最多有一个的组件。
* index.js 文件:它是根文件,在该文件中`registerComponent`方法,会调用根组件 App然后开始逐级调用渲染应用
* App 组件:在 App 组件中,用于表示商品信息的数据变量 `PRODUCTS`,在被调用时会通过 ProductTable 组件的 `products` 属性传递下去;
* ProductTable 组件:它被 App 组件调用后,它的调用入参就是 `products`。`products` 是一个数组,数组中的每一项就是 `Product`组件的入参`product`。每一项中的分类,就是`Category` 组件的入参 `category`。还是一样,组件首字母是大写的,属性、入参的首字母是小写的;
* Category 组件:它会被 ProductTable 组件调用两次,第一次调用接收的入参`category`是“水果”,第二次是“蔬菜”;
* Product 组件:它会被 ProductTable 组件调用 6 次,生成 6 个不同的商品元素,展示在手机屏幕上。
**简而言之,组件间的数据是单向流动的,是逐层往下传递的。**调用是从根组件开始的,根组件会调用其子组件,子组件会调用子子组件,以此类推。调用过程中,数据会被当做组件的属性,层层传递下去。
## 总结
前面我们说了React/React Native 之所以选择基于组件的方式来构建应用,原因就在于组件更能够满足现代应用重交互重展示的特点。
搭建 React Native 静态页面的核心就是搭建组件。它的整体思路是,从上往下拆出组件,从下往上把拆出来的组件进行逐一实现和拼装。
在这一讲中,我们搭建的静态页是一个无交互的、轻展示的应用,但 React/React Native 也表现得很好。只要我们遵循单一责任原则,对 UI 设计稿进行拆分,我们就能设计出一个可扩展的、可维护的应用。
即使后续这个应用有了复杂的交互、有了复杂的展示形式,它也能很好地扩展。我们只需把那些复杂的组件,那些不再符合单一责任原则的组件,进行拆分就可以了。
最后,请你牢牢记住,宿主组件是最基础的材料,所有我们自定义的复合组件都基于宿主组件搭建出来的,而复合组件又能搭建出更上层的复合组件,这样一步一步,我们才能把静态页面搭建完成。
## 补充材料
* 学习 React 最好的地方就是 [React 官网](https://beta.reactjs.org/)。我给的官网地址是新官方地址,目前还是 beta 版本,但不妨碍它是学习 React 最好的地方。这一讲中商品列表静态页的案例,也是参考的 React 新官网改编的;
* 这节课里完整的商品列表静态页代码,我放在了 [GitHub](https://github.com/jiangleo/react-native-classroom.git) 上;
* 关于 React 为什么选择基于组件的架构方式,而不是 MVC在 2013 年的[《我们为什么要构建 React?》](https://zh-hans.reactjs.org/blog/2013/06/05/why-react.html)这篇文章汇中React 团队给出了答案。
## 思考题
静态页面很难体现组件架构相对其他架构的优势。我再找了一个带交互的页面,这个页面可以搜索商品和过滤无库存的商品。请你思考一下,当我们按照搜索、过滤、列表、种类、商品五个维度,用 MVC 方式来架构页面时,它的数据流向是什么样的?它相对于组件架构的优点缺点又是什么?
![图片](https://static001.geekbang.org/resource/image/c1/7c/c10647a47d8b2ed5ff0a07cbacb40d7c.png?wh=730x1000)
欢迎你在评论区分享你的观点,我是蒋宏伟,咱们下节课见。