第02课:React Native 基础知识

这一部分我们主要介绍 React Native 的大多数基础知识, 包括:

  • React Native 里面如何写样式和布局;
  • 显示内容;
  • 处理用户输入和单击;
  • 发起网络请求。

学完本部分内容,可以开发一个简单的 APP 了。

显示内容

Web 领域最基础的显示组件如 divph1ullispanimg 等在 React Native 里都有类似对应的显示组件,我们先来介绍几个用得比较多的。

View

类似于 div,主要拿来包其他的显示组件,如果不指定 widthheight 或者设置 flex,则显示大小由包的子组件决定,具体可参考这里

Text

类似于 span,主要用来包一段文字。 注意 React Native 里面不能像 Web 里一样直接写文字,必须把文字包含在 Text 组件里面。 还要注意,Text 的宽度会撑满父元素宽导致换行(有点类似块元素),这样说来其实 Text 更像是 p,具体可参考这里。下面的两个 Text 会显示在两行。

    <Text style={{backgroundColor: 'green'}}>Hello React Native!</Text>
    <Text style={{backgroundColor: 'yellow'}}>line 2!</Text>
Image

相当于 Web 里面的 img,具体可参考这里。 下面是常用的三种使用 Image 的方式,第一种是静态图片,也就是图片会打包的 APP 里一起发布,因此打包的时候是知道图片大小的,所以不需要指定图片宽和高。后面两种都是动态指定图片,所以需要指定宽和高,不然渲染不出来,一般用于 APP 发布之后从服务器下载图片资源,这样 APP 打包会小很多。

    <View>
      <Image
        source={require('./lzl.jpeg')}
      />
      <Image
        style={{ width: 250, height: 250 }}
        source={{ uri: 'http://e.hiphotos.baidu.com/baike/pic/item/ac6eddc451da81cb6de1cb4d5a66d0160924312e.jpg' }}
      />
      <Image
        style={{ width: 66, height: 58 }}
        source={{ uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAEXRFWHRTb2Z0d2FyZQBwbmdjcnVzaEB1SfMAAABQSURBVGje7dSxCQBACARB+2/ab8BEeQNhFi6WSYzYLYudDQYGBgYGBgYGBgYGBgYGBgZmcvDqYGBgmhivGQYGBgYGBgYGBgYGBgYGBgbmQw+P/eMrC5UTVAAAAABJRU5ErkJggg==' }}
      />
    </View>
列表

类似于 Web 里的 ulli,主要用来显示一列数据。

ScrollView 其参考资料如下:

包的子组件可以是不一样的, 并且支持横向滑动。如果 ScrollView 只有一个子组件, 可以通过 maximumZoomScaleminimumZoomScale 属性来支持双指缩放。

    <ScrollView>
      <Text style={{ backgroundColor: 'green' }}>Hello React Native!</Text>
      <Text style={{ backgroundColor: 'yellow' }}>line 2!</Text>
      <ScrollView maximumZoomScale={2} minimumZoomScale={0.5}>
        <Image
          style={{ width: 250, height: 250 }}
          source={{ uri: 'http://e.hiphotos.baidu.com/baike/pic/item/ac6eddc451da81cb6de1cb4d5a66d0160924312e.jpg' }}
        />
      </ScrollView>
      <Image
        style={{ width: 250, height: 250 }}
        source={{ uri: 'http://e.hiphotos.baidu.com/baike/pic/item/ac6eddc451da81cb6de1cb4d5a66d0160924312e.jpg' }}
      />
      <Image
        style={{ width: 250, height: 250 }}
        source={{ uri: 'http://e.hiphotos.baidu.com/baike/pic/item/ac6eddc451da81cb6de1cb4d5a66d0160924312e.jpg' }}
      />
      <Image
        style={{ width: 250, height: 250 }}
        source={{ uri: 'http://e.hiphotos.baidu.com/baike/pic/item/ac6eddc451da81cb6de1cb4d5a66d0160924312e.jpg' }}
      />
    </ScrollView>

要注意,ScrollView 适合于子组件数量不多的情况,因为即使子组件没有在屏幕里面,也会被渲染出来,所以数量太多的话会有内存问题。这种场景适合使用 FlatListSectionList

FlatList & SectionList

FlatList 适合于渲染结构比较类似的数据,并且可以支持很长很长的数据,因为它只会渲染当前显示在屏幕里面的那部分元素,而不像 ScrollView 那样把所有数据都渲染出来。FlatList 有两个props,一个是data用于传递数据,一个是 renderItem 用于将 data 里的每一个元素渲染成格式化的组件。

<FlatList
          data={[
            {key: 'Devin'},
            {key: 'Jackson'},
            {key: 'James'},
            {key: 'Joel'},
            {key: 'John'},
            {key: 'Jillian'},
            {key: 'Jimmy'},
            {key: 'Julie'},
          ]}
          renderItem={({item}) => <Text>{item.key}</Text>}
        />

另外 SectionList 用于渲染需要将数据做逻辑划分并且每个部分有个标题的列表,比如通讯录里面按照字母划分成不同的 section

<SectionList
          sections={[
            {title: 'D', data: ['Devin']},
            {title: 'J', data: ['Jackson', 'James', 'Jillian', 'Jimmy', 'Joel', 'John', 'Julie']},
          ]}
          renderItem={({item}) => <Text>{item}</Text>}
          renderSectionHeader={({section}) => <Text>{section.title}</Text>}
        />

参考资料:

ListView

老版本的 React Native 用 ListView 组件渲染列表,不过它会一次性渲染所有数据,所以有内存问题,现在已经 DEPRECATED 了,具体可参考这里

今年 三月份Facebook发布了新的组件,就是前面讲的 FlatListSectionList,直接用这两个就好了。

样式

在 React Native 里写样式不需要另外的语言或者语法,就只需要用 JavaScript。每个核心组件都接受一个 prop: stylestyle 的使用方式跟 Web 里面使用 CSS 基本一样,不同的是 RN 里面因为是用 JS 写的,需要用 camelCase,如 backgroundColor,具体可参考这里

style prop 可以是一个简单的 JS 对象,也可以是一个对象数组,后面的会把前面的样式覆盖掉,可以这样来继承样式。

import React, { Component } from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default class LotsOfStyles extends Component {
  render() {
    return (
      <View>
        <Text style={styles.red}>just red</Text>
        <Text style={styles.bigblue}>just bigblue</Text>
        <Text style={[styles.bigblue, styles.red]}>bigblue, then red</Text>
        <Text style={[styles.red, styles.bigblue]}>red, then bigblue</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  bigblue: {
    color: 'blue',
    fontWeight: 'bold',
    fontSize: 30,
  },
  red: {
    color: 'red',
  },
});
设置宽和高

通过给 style prop 设置固定的 widthheight 就可以实现。注意,RN 中不需要带单位(如 px),值代表的是 density-independent pixels,具体可参考这里

import React, { Component } from 'react';
import { View } from 'react-native';

export default class FixedDimensionsBasics extends Component {
  render() {
    return (
      <View>
        <View style={{width: 50, height: 50, backgroundColor: 'powderblue'}} />
        <View style={{width: 100, height: 100, backgroundColor: 'skyblue'}} />
        <View style={{width: 150, height: 150, backgroundColor: 'steelblue'}} />
      </View>
    );
  }
}

这种方式主要用来设置那些不管屏幕多大都是固定大小的组件。如果需要组件大小根据屏幕大小而变化的话,需要用 flex

Flex

如果只有一个组件设置为 flex,则它会占据所有可用的空间。如果有多个组件平级,可以通过 flex 值的大小,按权重来分配组件占据的屏幕大小,flex 值越大占据的空间(宽或者高)越大,具体可参考这里

注意:如果一个组件设置为 flex: 1,但是它的父组件既没有设置 flex: 1 又没有显示地设置 widthheight,则它占用空间为 0,即不会显示。

import React, { Component } from 'react';
import { View } from 'react-native';

export default class FlexDimensionsBasics extends Component {
  render() {
    return (
      <View style={{flex: 1}}>
        <View style={{flex: 1, backgroundColor: 'powderblue'}} />
        <View style={{flex: 2, backgroundColor: 'skyblue'}} />
        <View style={{flex: 3, backgroundColor: 'steelblue'}} />
      </View>
    );
  }
}

flex 其实是 Web 领域比较新的布局方式

不过要注意, RN里 flexDirection 默认是 column 而不是 row

输入

APP 用户主要通过打字或者手势(如点击)来进行输入, 我们来看看两个主要的输入组件。

TextInput

相当于 Web 里面的 <input type="text" />onChangeText 属性接受一个函数,在输入内容变化的时候会被调用。

      <TextInput
        style={{ width: 200, height: 40 }}
        placeholder="输入sth"
        onChangeText={text => Alert.alert('change: ' + text)}
      />

参考资料:

Button

相当于 Web里的 button。通过 color 属性指定颜色,通过 onPress 属性指定被点击后的回调函数。

<Button
  onPress={() => { Alert.alert('You tapped the button!')}}
  title="Press Me"
  color="red"
/>

参考资料:

网络请求

Fetch

RN 里面实现了 Web 里面的 Fetch API,可以很方面地发起网络请求。如果没有用过 fetch 的话,可以参考 MDN 的文档 Using Fetch

发起网络请求

很简单, 直接传递一个 url 就可以了。

fetch('https://mywebsite.com/mydata.json')

fetch 的第二个参数是一个可选的对象,可以在里面指定请求方法(如 POST)、请求头、body 等。

fetch('https://mywebsite.com/endpoint/', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'magicly',
    age: '25',
  })
})

完整的参数列表可以参考 Fetch Request文档

处理响应

网络请求是一个典型的异步操作,所以 fetch 返回的是一个 Promise,比写回调方便很多。

function getMoviesFromApiAsync() {
    return fetch('https://facebook.github.io/react-native/movies.json')
      .then((response) => response.json())
      .then((responseJson) => {
        return responseJson.movies;
      })
      .catch((error) => {
        console.error(error);
      });
  }

也可以直接使用 ES2017 里的 async/await 语法:

async function getMoviesFromApi() {
    try {
      let response = await fetch('https://facebook.github.io/react-native/movies.json');
      let responseJson = await response.json();
      return responseJson.movies;
    } catch(error) {
      console.error(error);
    }
  }

注意,现在 iOS 默认会阻止没有用 SSL 加密的请求,所以如果访问的接口是 HTTP 的话,需要添加 App Transport Security 例外。如果事先知道要访问哪些域名的接口,那只添加这些域名为例外会更安全,但是如果不能提前知道,那可能需要完全禁用 ATS。从 2017 年 1 月开始,禁用 ATS提交审核的时候需要专门说明理由,更多内容可以参考 苹果的文档

参考资料:https://facebook.github.io/react-native/docs/network.html

使用其他网络库

RN 里面其实也实现了 XMLHttpRequest接口 的。这意味着你可以使用第三方库比如 frisbee 或者 axios,或者直接使用 XMLHttpRequest 接口。

const request = new XMLHttpRequest();
request.onreadystatechange = (e) => {
  if (request.readyState !== 4) {
    return;
  }

  if (request.status === 200) {
    console.log('success', request.responseText);
  } else {
    console.warn('error');
  }
};

request.open('GET', 'https://mywebsite.com/endpoint/');
request.send();

注意,不管是 fetch 还是 XMLHttpRequest,都没有跨域一说,因为这是原生应用而不是 web,所以根本就没有 CORS 一说。

WebSocket

RN 也是支持 WebSocket 的,拿来做实时应用(如 IM 或者游戏)很方便。如果真的要用 WS 开发实时应用的话, 强烈推荐一下 socket.io

const ws = new WebSocket('ws://host.com/path');

ws.onopen = () => {
  // connection opened
  ws.send('something'); // send a message
};

ws.onmessage = (e) => {
  // a message was received
  console.log(e.data);
};

ws.onerror = (e) => {
  // an error occurred
  console.log(e.message);
};

ws.onclose = (e) => {
  // connection closed
  console.log(e.code, e.reason);
};

第一个 APP

有了前面的知识准备,我们现在来做一个简单的 APP:一个简单的图片“搜索引擎”。当然搜索引擎可不是在一个 APP 里能做出来的,这让我想起以前在网上论坛看到有人问:

我会 PHP,请问几天能做出一个百度那样的东西来呢?

下面有个牛人回复:

根本不用 PHP,5 分钟就可以做出来。写一个 HTML 页面,然后用 iframe 嵌入 baidu.com 就可以了。

我们今天就用类似的方式去做,不要觉得这很 low,互联网发展早期有一类工具叫“元搜索引擎”,就是调用多家搜索引擎的数据,然后整合返回给用户的。

获取数据

首先选择一家图片搜索引擎,国内当然是 百度图片 啦,不然用 Google 的话还得让用户想着怎么翻Q。

我们调选一张风景图,例如:

enter image description here

然后鼠标点击右键选择显示网页源码

enter image description here

然后对照着 DOM 结构分析,找到了数据部分:

enter image description here

这时候发现,图片数据的信息(包括 URL、宽和高等)就在 imgData 这个对象里,我们只需要用字符串截取出这段,然后用 JSON.parse 解析成 Javascript Object 就可以访问 thumbURLwidthheight 等字段了。

      const start = "app.setData('imgData', ";
      const end = ']}';
      const startIndex = html.indexOf(start);
      const endIndex = html.indexOf(end, startIndex);
      const dataStr = html.slice(startIndex + start.length, endIndex + end.length);
      const data = JSON.parse(dataStr.replace(/'/g, '"')); // 这里是因为 data:image/jpeg 那部分百度用的是单引号',不是合法的 json

显示图片

接下来就只需要把前面获取到的图片显示出来就可以了。显示图片用 Image 组件:

          <Image
            style={{ width: 200, height: 200 }}
            source={{ uri: 'http://e.hiphotos.baidu.com/baike/pic/item/ac6eddc451da81cb6de1cb4d5a66d0160924312e.jpg' }}
          />

这里要注意两点,一个是 source 里面传递的是一个 object,里面是 uri 不是 url 哦。 第二个是一定要记得设置 widthheight,否则是显示不出来的。

因为我们获取到的是很多张图片,所以用列表方式显示。由前面的介绍得知可以用 ScrollViewFlatList

        <ScrollView>
          {
            this.state.isLoading ? <ActivityIndicator />
              :
              <FlatList
                data={this.state.imgs}
                renderItem={({ item }) => {
                  const img = item;
                  if (!img.thumbURL) {
                    return null;
                  }
                  return <Image
                    style={{ width: this.width, height: this.width * img.height / img.width }}
                    source={{ uri: img.thumbURL }}
                  />
                }}
                keyExtractor={(item, index) => {
                  return item.thumbURL;
                }}
              />
          }
        </ScrollView>

获取用户输入

如果我们只能显示默认的风景图照片,那怎么能叫搜索引擎呢。所以用 TextInput 和接收用户输入,用 Button 来触发搜索。

            <View style={{ flex: 1, flexDirection: 'row', }}>
              <TextInput
                style={{ width: this.width - 100, height: 40, borderColor: 'black', borderWidth: 1, }}
                placeholder="汉源湖"
                onChangeText={text => this.setState({ text })}
              />
              <Button
                onPress={this.buttonHandler}
                title="搜索"
                color="#841584"
              />
            </View>

完整结果

下图就是我们的成果啦。

搜索汉源湖图片
搜索轿顶山图片

App.js 完整代码如下:

import React from 'react';
import { StyleSheet, ActivityIndicator, Dimensions, SectionList, FlatList, ScrollView, Alert, Button, Text, View, Image, TextInput } from 'react-native';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      text: '',
      isLoading: true,
      imgs: [],
    }
    this.width = Dimensions.get('window').width;
    this.height = Dimensions.get('window').height;
  }
  componentDidMount() {
    this.searchHandler();
  }
  async getImgsFromBaidu(query) {
    try {
      let response = await fetch(`https://image.baidu.com/search/index?tn=baiduimage&ipn=r&ct=201326592&cl=2&lm=-1&st=-1&fm=index&fr=&hs=0&xthttps=111111&sf=1&fmq=&pv=&ic=0&nc=1&z=&se=1&showtab=0&fb=0&width=&height=&face=0&istype=2&ie=utf-8&word=${query}&oq=${query}&rsp=-1`);
      let html = await response.text();
      const start = "app.setData('imgData', ";
      const end = ']}';
      const startIndex = html.indexOf(start);
      const endIndex = html.indexOf(end, startIndex);
      const dataStr = html.slice(startIndex + start.length, endIndex + end.length);
      const data = JSON.parse(dataStr.replace(/'/g, '"'));
      return data
    } catch (error) {
      Alert.alert('error: ' + error)
      console.error(error);
    }
  }
  async searchHandler(query = '汉源湖') {
    query = query || '汉源湖';// default parameter只处理undefined的情况
    this.setState({
      isLoading: true,
    });
    const data = await this.getImgsFromBaidu(query);
    const imgs = data.data.map(e => {
      delete e.base64;
      return e;
    }).filter(e => !!e.thumbURL);
    this.setState({
      isLoading: false,
      imgs,
    })
  }
  buttonHandler = () => {
    this.searchHandler(this.state.text);
  }
  render() {
    return (
      <View >
        <ScrollView>
          <View style={{ height: 20 }} />
          <View>
            <View style={{ flex: 1, flexDirection: 'row', }}>
              <TextInput style={{
                width: 300,
                height: 40,
                borderColor: 'black',
                borderWidth: 1,
              }}
                placeholder="汉源湖"
                onChangeText={text => this.setState({ text })}
              />
              <Button
                onPress={this.buttonHandler}
                title="搜索"
                color="#841584"
              />
            </View>
          </View>
          {
            this.state.isLoading ? <ActivityIndicator />
              :
              <FlatList
                data={this.state.imgs}
                renderItem={({ item }) => {
                  const img = item;
                  if (!img.thumbURL) {
                    return null;
                  }
                  return <Image
                    style={{ width: this.width, height: this.width * img.height / img.width }}
                    source={{ uri: img.thumbURL }}
                  />
                }}
                keyExtractor={(item, index) => {
                  return item.thumbURL;
                }}
              />
          }
        </ScrollView>
      </View>
    );
  }
}


const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'white',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

export default App;

RN 开发 APP 还是蛮快的嘛,而且这段代码支持 Android 和 iOS 哦。

单击这里查看完整代码

接下来,我们详细看看 RN 里面支持哪些 CSS 的样式和布局。

微信扫描登录