React Native 跨平台之旅(二) —— 组件基础

187 阅读12分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

从现在开始,正式进入React Native学习旅程,开始探寻其语法以及对于原生逻辑的兼容和自身的性能优化,这将是一段漫长而有趣的旅程~~

1. 组件的概念

在React Native中,组件一词尤为重要,这也是近年来一些主流前端框架所共同强调的组件化

组件化是指解耦复杂系统时将多个功能模块拆分重组的过程,有多种属性、状态反映其内部特性

image.png

光看定义可能觉得迷糊,解决问题的办法往往源于生活,想象一个公司就是一个系统,然后对应上面的定义展开想象:
多个功能模块的拆分,emm...,似乎有点像部门划分,而部门组织架构就像树一样一级一级地可以继续细分,这不就是拆分吗?而小的部门可以组合成大的体系,部门内进行调整,人员重新分组,这不也是部门重组吗?所谓的多种属性其实就像是部门的人员、分工、责任区域之类,状态反映内部特性可以看做部门负责维护的迭代项目状态或者是人员的加班情况,像是某某部门连续加班赶进度,是不是反映了特点?

那么,从这里大致可以总结出组件的个体往往就是职责明确基于具体任务可重用代码模块,更进一步的理解往后通过实践慢慢总结吧

2. 核心组件与原生组件

React Native主要解决的是移动端跨平台的问题,而所对应的平台就是Android和iOS两大移动平台,因此对于组件的了解首先需要从这两个平台开始

2.1. 移动开发中的View

在Android开发当中,也存在各种控件,比如:TextViewImageViewListView,用来展示各种数据以及完成与用户交互的任务(iOS当然也有对应的控件),在Android当中,统称为View,并且它们也继承View这个类 这些组成移动端UI的形形色色的View其实恰好能与组件的概念相吻合

因此,无论是Android的View还是iOS的View在概念层次上都能统一到组件上,这便给了React Native对二者进行封装提供了基础

2.2. 原生组件

在Android中编写组件和在iOS中编写组件的方式显然是不同的,首先二者的语言都不同
但是,利用React Native可以统一使用JavaScript来编写组件,而不必关注某一平台的细节,在程序运行时,React Native再为对应的平台创建对应的View

似乎这样看来,React Native只是对于原生平台的组件进行了一层封装,隐藏了平台间的差异性,其他的实现依然依赖于各自平台的特点,这便是React Native的原生组件

2.3. 核心组件

核心组件是原生组件中比较常用、基础的一部分
具体的使用文档可以参考下方

核心组件和API · React Native 中文网

这些个原生组件与Android和iOS各自的组件之间都存在着对应关系(借来官网的表格) image.png

从之前的理解来说,就是React Native的原生组件会在程序运行的时候转成表格同一行对应的Android或是iOS的View

在模拟器或者是开发者模式下打开“显示布局边界”,运行之前的React Native代码,可以发现在其界面上有布局的划分,这说明布局中是原生的组件,如果是React之类的H5界面是不会显示的

image.png

这是关于React Native组件关系的图,React Native的组件属于React组件的一部分,这部分原生组件中含有核心组件、社区的组件以及自定义的组件等等,实际上也体现了其强大的社区以及组件本身的扩展性

3. React基础知识

把Native去掉,其实还是React,代码的语法还是要依照React来,因此React的基础知识还是需要掌握的,只需一些皮毛

3.1. 组件

在React当中,组件可以根据其定义的形式,分为函数式组件Class组件

3.1.1. 函数式组件

在开始之前,先将项目中App.js中的代码全部删光

import React from "react";   // 必须导入React,因为要用组件
import { Text } from "react-native";   // 采用原生组件

const App = () => {   // 箭头函数
  return (
    <Text>这是我的第一个App \(^o^)/~</Text>   // JSX语法,直接返回元素
  )
}

export default App;   // 导出组件给外部使用

直接上代码,这就是一个简单的函数式组件了

应该怎么理解呢,上面我给出了注释:
首先,React是一定要从'react'中导入的,因为需要在内存中生成组件节点,但这现在还不是真正的组件,利用这一特点来生成组件本身需要用到React,因此这一行代码基本是固定的
下一行代表我们从'react-native'包中引入了原生组件Text,最后这个组件在Android平台应该会转成TextView
下面一整个用{}括起来的代码块内容就是一个箭头函数,前面是对于这个函数的引用,也就是用一个变量来表示这个函数,App代表的就是这个函数;函数的内容是返回组件Text,内容文本就是开始与结束标签中间的部分,相当于TextView的text属性的值是中间的文本,将组件作为返回值进行返回,因此App其实指代的是这个Text组件
最后一句表示将App组件导出,因为只有导出后其他js文件才可以引用到这个组件

image.png

实际给到了index.js,不过具体先不探讨,只要知道此时App组件,也就是这个文本被放到了页面布局当中就足够,它现在是我们自己编写的函数式组件

image.png

因为该组件是以函数的形式声明的,因而就是函数式组件

3.1.2. Class组件

Class组件相较于先前的函数式组件更加复杂一些,还是先给出代码

import React, { Component } from "react";  // 必须引入Component
import { Text } from "react-native";

class App extends Component {   // 继承Component
  render() {   // 需要返回组件
    return (
      <Text>这是我的第一个App \(^o^)/~</Text>
    )
  }
}

export default App;

这里class组件可以与函数式组件对比着看
首先,需要额外从react包中导入Component,并且这种带有{}的导入称为解构,大致就是导入右边整体中的一个部分
在进行组件声明的时候可以看到extends,这代表的是继承,和Java语言的写法很是接近,继承Component代表当前声明的class组件需要Component的某些标准特性,render()需要我们自己来提供,就像Java里面对于原有方法的覆写,这里需要返回组件,因为按照标准需要是React的节点对象,之后这部分内容会用于页面的渲染

image.png

运行一下,结果与之前并没有什么不同,但是既然一样为什么需要这种更复杂的?这是因为他能做的是更多
这就和为什么用VS Code而不是用记事本来写代码是一个道理

3.2. JSX

JSX语法其实实质上还是JavaScript,只是写的时候可以把组件直接写在里面,它只是一种封装而成的语法糖

之前所写的两种组件的代码都是使用的JSX,从简单使用上来说,需要了解这样简单的代码模板其实已经可以应付简单的组件了,各个带有标签的组件其实是JSX.Element类型,这是最终需要导出的React组件节点,这些节点最终会组织成一棵组件树,这也是与Android的View的层级很相似的

通常JSX中JS的部分用来描述逻辑,而<组件>的格式描述的就是一个个节点,之前所写的两个组件都可以使用<App>来表示,需要注意的一点是作为组件返回的标签只能有一个,这表示返回时,必须将组件节点包裹成一个整体

image.png

对应Android就像是需要使用某种ViewGroup包裹一样

image.png

另外JSX支持在标签内使用JS语法,只是需要{}来保护一下

class App extends Component {

  render() {
    let a = 2;    
    return (
      <View>
        <Text>这是我的第一个App \(^o^)/~{a == 1 ? '很哇塞' : '一般般'}</Text>  // 嵌入JS表达式
      </View>
    )
  }
}
image.png

深入 JSX – React (reactjs.org)

3.3. 组件自定义

先前的两个组件内容比较简单,现在需要理解下组件中两个重要的概念PropsState,这将帮助你构建更加灵活丰富的组件

3.3.1. 属性(Props)

属性其实非常容易理解

image.png

这是VS Code的首选项页面,这其实就可以看做是一个组件,像字体大小就是一个属性名(Key),而14就是属性值(Value),这就是一种偏好设置,方便定制,而且往往以K,V对的形式出现

<LinearLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="match_parent"
      android:layout_height="match_parent">
    <TextView
        android:id="@+id/text_view_id"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="@string/hello" />
 </LinearLayout>

在Android中也是一样,这些属性也是使用键值对表示,为的是描述清楚组件的特征,通过暴露给外界修改的属性来对于组件进行更细致的描述,以便满足需求

再添加一个JS,定义一个函数式组件

import React from "react";
import { Text, View, Image } from "react-native";

const Girl = (props) => {   // 属性
    return (
      <View>
        <Image source={{uri: "https://pic1.zhimg.com/v2-181cfa90f04a0c7efa0f3c8c77b420b4_r.jpg"}}
        style={{width: 200, height: 200}}/>
        <Text>{props.name}</Text>   // 属性名为name的属性
      </View>
    )
  }
  
  export default Girl;

在App.js中使用,这里添加的Image组件其本身也有自带的属性,uri给的是图片路径,style是样式(也可以使用外部样式表),这里用来设置图片的大小

import React, { Component } from "react";
import { Text, View } from "react-native";
import Girl from "./Girl";   // 将组件引入

class App extends Component {

  render() {
    let a = 2;
    return (
      <View>
        <Girl name='佩内洛普 克鲁兹'/>   // 自定义的属性赋值
      </View>
    )
  }
}

export default App;
image.png

3.3.2. 状态(State)

组件免不了需要与用户发生交互,而交互就会带来变化,然而属性显得不灵活,这时候就出现了状态的概念

状态在这里又该怎么理解呢?同样举简单的例子,比如进度条,它初始化后就是一个长条的形状,但是随着数据的加载,它中间填充的色块的长度会变化,但是整体的外观并没有变,外观其实就对应属性,而色块的变化就对应状态的变化,专属于当前的组件

按照React开发的惯例,首次渲染时对于属性进行初始化,而后组件的交互通常使用状态记录

那么,直接来看一看具体在代码中应当如何使用

import React, { useState } from "react";    
import { Text, View, Image, Button } from "react-native";

const Girl = (props) => {
    const [likes, setLikes] = useState(0)   // 状态初始化
    return (
      <View>
        <Image source={{uri: "https://pic1.zhimg.com/v2-181cfa90f04a0c7efa0f3c8c77b420b4_r.jpg"}}
        style={{width: 200, height: 200}}/>
        <Text>{props.name}</Text>
        <Button onPress={() => setLikes(likes + 1)} title={'喜欢 ' + likes}/>  <!--添加Button-->
      </View>
    )
  }
  
  export default Girl;

这里用的是函数式组件,用到的是useState,首先需要从react中进行导入,利用useState在函数式组件内部操作状态

其中const [likes, setLikes] = useState(0)这句声明是核心,可以理解为这是useState的一种初始化模板,[<状态值>,<set状态的函数>] = useState(<状态的初值>),对应于上面的代码,使用到的状态就是likes变量,setLikes就是用来给likes设置状态值的方法,并且likes初始的值是0

在声明完状态和设置状态的函数后,我们添加一个Button组件来体现状态的变更过程
可以看到,在<Button>当中有2个属性onPresstitle,其中onPress定义点击事件,当点击这个按钮组件时会触发对应的逻辑,将likes的值加1,而title则是用来显示按钮上显示的文本,里面的内容包含了likes状态的值,这类似于一种点赞操作

state.gif

点击按钮,按钮的内容便+1,这就是状态的变化,这种变化是用户的操作导致的

刚刚的使用useStateHook方式仅可以在函数式组件中使用,如果使用class组件,需要使用另外一种方式

import React, { Component } from "react";
import { Text, View, Image, Button } from "react-native";

class Girl extends Component {

  state = { likes: 0 }    // 状态初始化

  constructor(props) {   // 构造器
    super(props);     // 属性初始化
  }

  render() {
    return(
      <View>
        <Image source={{uri: "https://pic1.zhimg.com/v2-181cfa90f04a0c7efa0f3c8c77b420b4_r.jpg"}}
        style={{width: 200, height: 200}}/>
        <Text>{this.props.name}</Text>
        <Button onPress={() => {this.setState({ likes: this.state.likes + 1 })} } title={'喜欢 ' + this.state.likes}/>
      </View>
    )
  }
}
  
export default Girl;

把上面的函数式组件转换为了一样的class组件,这个内容就要多出不少内容,状态state的初始化使用的是键值对,可以使用多组
属性利用constructor构造器传入props,里面调用super()会将值付给this,与Java里的初始化属性一样 state的内容是JS里的对象,可以直接.获取成员,如this.state.likes,设置属性则是使用setState()方法,同样是键值对对应

其中涉及到部分组件生命周期相关的知识,目前仅做简单了解,之后再从React本身的写法具体深入
最终的实现效果是和前一个一样的

如果仅仅是简单的组件,通过上面的例子,外加查些文档应该不难,然而很多东西用出来却不明白心里很不是滋味,因此React语法的基础之后还是需要补上才行~~