React Native 入门之旅

作为一名Android开发,学习React Native其实是一个很陡峭的过程,本文主要记录自己从接触React Native,到能够实现一个比较完整的Demo的过程中,涉及到的一些知识点,以及踩过的一些坑。

一、React Native 简介

<p>

React Native lets you build mobile apps using only JavaScript. It uses the same design as React, letting you compose a rich mobile UI from declarative components.

<p>

With React Native, you don't build a “mobile web app”, an “HTML5 app”, or a “hybrid app”. You build a real mobile app that's indistinguishable from an app built using Objective-C or Java. React Native uses the same fundamental UI building blocks as regular iOS and Android apps. You just put those building blocks together using JavaScript and React.

简单总结一下:ReactNative是由Facebook推出的,可以让开发者使用 JavaScript 和 React 创建基于Web,iOS 和 Android 平台原生应用的一套框架。

二、React Native 案例


在RN的官网上能够看到一些开发案例,不过基本上都是国外的应用,国内有使用到RN开发的应用主要包括QQ空间、QQ音乐、全民K歌等等。

三、React Native 基本概念

RN最基本的概念我认为应该是组件、属性、状态。

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

class HelloWorldApp extends Component {
   render() {
     return (
     <Text>Hello world!</Text>
   );
 }
}

AppRegistry.registerComponent('HelloWorldApp', () => HelloWorldApp);

组件其实就是界面上可显示的元素,类似于Android里面的View,通过render函数进行渲染,一个组件可以包含很多个子组件。RN内置的组件可以直接通过import { xxx } from 'react-native' 进行导入,当然也可以自定义组件。每个组件都拥有自己的属性和状态。将上面的例子进行完善,加入属性和状态:

import React, { Component } from 'react';
import { AppRegistry, Text, Image } from 'react-native';

class HelloWorldApp extends Component {
   constructor(props) {
     super(props);
     this.state = {showText: true};
   }
   render() {
     let pic = {
       uri: 'https://upload.wikimedia.org/wikipedia/commons/d/de/Bananavarieties.jpg'
     };
     return (
       <Image source={pic} style={{width: 193, height: 110}}/>
     );
   }
}

AppRegistry.registerComponent('HelloWorldApp', () => HelloWorldApp);

在构造函数中,初始化了组件的状态showText为true,那就可以在其他地方通过this.state.showText访问到该状态的值,在render函数里,组件的source即是组件的一个属性,可以通过this.props来获取属性的值。

可以通过setState函数改变组件的状态,每次状态改变都会重新触发render函数。

四、React Native 技术细节

  • 数据存储
    在Android里面,我们有xml和sqlite两种保存数据的方式,切换到RN开发,首先想到的也是数据存储问题,其实,RN也是支持的,简单列举如下:

AsyncStorage :key-value存值方式,支持写入、读取、移除
react-native-sqlite : 支持iOS数据库
react-native-android-sqlite :支持Android数据库
Realm :跨平台,可同时支持iOS和Android(推荐)

  • 页面切换及参数传递
    在Android里面,需要把所有Activity进行注册,然后通过startActivity进行跳转,通过bundle进行传值,在RN里面,可以通过路由进行跳转,通过属性进行传值,这里我推荐使用的是Navigator,一个简单的模版如下:

    import Splash from './Splash';
    const defaultRoute = {
       component: Splash
    };
    
    class RNDemo extends Component {
      _renderScene(route, navigator) {
          let Component = route.component;
          return (
            <Component {...route.params} navigator={navigator} />
         );
     }
     render() {
        return (
         <Navigator
           initialRoute={defaultRoute}
           renderScene={this._renderScene}
        />
      );
     } 
    }
    

这里将navigator以及route params里面的所有字段通过属性进行传递,所以新打开的组件能够获得navigator以及相关参数。
openPage() {
this.props.navigator.push({
component: MainScreen,
params: {
phone: this.state.phone,
yzm: this.state.yzm,
}
})
}

  _back() {
    this.props.navigator.pop();
  }

关于回调,可以定义一个回调函数作为参数传递,网上例子很多不再赘述,这里需要强调的一点是,如果A组件把navigator传递给了B组件,而B组件的子组件也需要使用这个navigator,那么需要B组件在创建子组件的时候,手动把这个参数继续传递下去,比如我的主界面是5个Tab页,需要在点击Tab页内某些组件的时候跳转到新的页面,那么在创建Tab页面的时候就需要手动传递参数:
<Page1 {...route.params} navigator={this.props.navigator}></Page1>

对于Android而言,还存在一个问题是硬件返回,如果是原生的Android应用,点击返回键是返回到上一个界面,但RN构建的应用,点击返回直接退出了应用,所以这里,需要对硬件返回进行监听。所幸,在RN里面有BackAndroid可以监听返回键,示例如下:

  BackAndroid.addEventListener('hardwareBackPress',    
    function() {
      if (!this.onMainScreen()) {
      this.goBack();
      return true;
    }
   return false;
  });
  • 网络访问
    我们可以通过fetch函数进行网络访问,直接看官网的例子:
    fetch('https://mywebsite.com/endpoint/', {
    method: 'POST',
    headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    },
    body: JSON.stringify({
    firstParam: 'yourValue',
    secondParam: 'yourOtherValue',
    })
    })
    然而当我在项目里面依葫芦画瓢直接使用fetch函数的时候,问题出现了,功能实现不了,后台php无法获取到参数。去查看了相关资料,解决方案有两种:
    1. 后台php在解析RN请求的时候需要加上:

       $json = json_decode(file_get_contents('php://input'), true); 
      

The reason that you have to use file_get_contents('php://input') is because its not form data. It is passing in a raw body request there and so php doesn't know to parse that as JSON by default. You are passing in a JSON body and anytime you use something like that you will have to parse it that way.

  1. 修改RN代码
    toQueryString(obj) {
    return obj ? Object.keys(obj).sort().map(function (key) {
    var val = obj[key];
    if (Array.isArray(val)) {
    return val.sort().map(function (val2) {
    return encodeURIComponent(key) + '=' + encodeURIComponent(val2);
    }).join('&');
    }
    return encodeURIComponent(key) + '=' + encodeURIComponent(val);
    }).join('&') : '';
    }

     fetch('https://mywebsite.com/endpoint/', {
        method: 'POST',
        headers: {
         'Accept': 'application/json',
         'Content-Type': 'application/x-www-form-urlencoded',
       },
      body: toQueryString({
       'xxx': 'xxx',
       'xxx': 'xxx',
      })
     })
    

这里特别注意的是,Content-Type要改为application/x-www-form-urlencoded类型。

  • 组件生命周期
    当打开一个RN页面的时候,我会潜意识的去对应Android Activity的生命周期,onCreate, onResume, onPause, onDestroy等等。RN生命周期及相关调用如下:
生命周期 调用次数 能否使用setState
getDefaultProps 1(全局调用一次)
getInitialState 1
componentWillMount 1
render >=1
componentDidMount 1
componentWillReceiveProps >=0
shouldComponentUpdate >=0
componentWillUpdate >=0
componentDidUpdate >=0
componentWillUnmount 1

组件的生命周期,可以通过在组件里面打印log,观察具体调用。我遇到的一个问题是,需要在应用界面不可见的时候执行某些操作,对应Android相当于点击了Home键,那如何知道RN构建的应用当前是处于前台还是后台呢?所幸,RN里面有AppState这个API。

App States

  • active - The app is running in the foreground

  • background - The app is running in the background. The user is either in another app or on the home screen

  • inactive - This is a state that occurs when transitioning between foreground & background, and during periods of inactivity such as entering the Multitasking view or in the event of an incoming call

    componentDidMount() {
    AppState.addEventListener('change', this._handleAppStateChange.bind(this));
    };

    componentWillUnmount() {
    AppState.removeEventListener('change', this._handleAppStateChange.bind(this));
    };

    _handleAppStateChange(currentAppState) {
    console.log(currentAppState);
    }

五、React Native 签名打包

在开发过程中,无论是真机还是模拟器,都需要启动JS server,然后通过这个server下载相关的bundle文件加载运行,但在实际发布的时候,我们需要对应用进行签名,把相关的js文件和资源文件进行打包,当然,这里是针对Anroid的情况,以下是React Native生成正式包的步骤:

  • 通过Android Studio生成签名文件,并将该文件置于android/app文件目录下

  • 编辑 ~/.gradle/gradle.properties文件,添加以下内容

    MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
    MYAPP_RELEASE_KEY_ALIAS=my-key-alias
    MYAPP_RELEASE_STORE_PASSWORD=*****
    MYAPP_RELEASE_KEY_PASSWORD=*****
    
    备注:用android studio生成的签名文件后缀名为jks,比如我生成的测试签名信息为:
    
    MYAPP_RELEASE_STORE_FILE=keystore.jks
    MYAPP_RELEASE_KEY_ALIAS=stefanli
    MYAPP_RELEASE_STORE_PASSWORD=123456
    MYAPP_RELEASE_KEY_PASSWORD=123456
    
  • 编辑 android/app/build.gradle文件添加签名配置

      ...
      android {
          ...
          defaultConfig { ... }
          signingConfigs {
          release {
              storeFile file(MYAPP_RELEASE_STORE_FILE)
              storePassword MYAPP_RELEASE_STORE_PASSWORD
              keyAlias MYAPP_RELEASE_KEY_ALIAS
              keyPassword MYAPP_RELEASE_KEY_PASSWORD
          }
      }
      buildTypes {
          release {
              ...
              signingConfig signingConfigs.release
          }
      }
    }
    ...
    
  • 生成正式包

    $ cd android && ./gradlew assembleRelease
    

其中,签名apk路径:android/app/build/outputs/apk
bundle文件路径:android/app/build/intermediates/assets/release/

如果要导出bundle文件和资源文件,还可以执行以下两个命令:

  • 切换到项目主目录,生成assets文件夹

      mkdir -p android/app/src/main/assets
    
  • 输出对应的bundle文件和资源文件

    react-native bundle --platform android --dev false 
    --entry-file index.android.js \ 
    --bundle-output android/app/src/main/assets/index.android.bundle \
    --assets-dest android/app/src/main/res/
    

    其中,platform是应用运行平台,dev表示是否为开发模式,entry-file是入口js文件,bundle-output为输出的bundle文件路径,assets-dest是输出的资源图片路径。

六、React Native 动态更新

最开始选择学习RN,其中很大一个原因是RN的动态更新能力,虽然目前有很多hotfix框架,但或多或少都存在一些兼容性问题,并且能力有限,只能小范围的修改源代码,而不能替换资源文件。

我们知道RN运行的时候,是去读取assets目录下的bundle文件,所以只要能够动态的替换这个文件,那么就能够做到动态更新,但显然,assets是不允许写入文件的,不过所幸的是,ReactActivity是允许重新指定bundle加载路径的:

/** 
* Returns a custom path of the bundle file. 
* This is used in cases the bundle should be loaded from a custom path. 
* By default it is loaded from Android assets, from a path specified by 
* {@link getBundleAssetName} e.g. 
* "file://sdcard/myapp_cache/index.android.bundle"
*/

protected @Nullable String getJSBundleFile() { return null;}

我们可以在MainActivity重写这个方法,指定bundle文件路径,如果该路径下的文件存在,则返回指定文件路径,如果不存在,就返回null,默认到assets路径下加载bundle文件,所以当需要更新的时候,只需要下载相关的bundle文件到指定目录就可以了。

@Nullable
@Override
protected String getJSBundleFile() {
   String jsBundleFile = getFilesDir().getAbsolutePath() + "/index.android.bundle";
   File file = new File(jsBundleFile);    
   return file != null && file.exists() ? jsBundleFile : null;
}

当然,仅仅替换bundle文件并没有解决所有问题,我们知道,bundle文件只包含了js代码,那资源文件又该如何处理呢?在更新的时候,如果只是下载了bundle文件,会导致原有项目中所有图片都不可见,这又是为什么呢?打开node_modules/react-native/Libraries/Image/Image.android.js文件,查看Image的render函数,然后逐层追踪源码,会在resolveAssetSource.js文件中看到一个很重要的函数:

function getBundleSourcePath(): ?string {
  if (_bundleSourcePath === undefined) {
    const scriptURL = SourceCode.scriptURL;
    if (!scriptURL) {
      // scriptURL is falsy, we have nothing to go on here
      _bundleSourcePath = null;
      return _bundleSourcePath;
    }
    if (scriptURL.startsWith('assets://')) {
      // running from within assets, no offline path to use
      _bundleSourcePath = null;
      return _bundleSourcePath;
    }
    if (scriptURL.startsWith('file://')) {
      // cut off the protocol
      _bundleSourcePath = scriptURL.substring(7, scriptURL.lastIndexOf('/') + 1);
    } else {
      _bundleSourcePath = scriptURL.substring(0, scriptURL.lastIndexOf('/') + 1);
    }
  }
  return _bundleSourcePath;
}

在AssetSourceResolver.js中看到两个比较重要的函数:

defaultAsset(): ResolvedAssetSource {
  if (this.isLoadedFromServer()) {
    return this.assetServerURL();
  }

 if (Platform.OS === 'android') {
    return this.isLoadedFromFileSystem() ?
    this.drawableFolderInBundle() :
    this.resourceIdentifierWithoutScale();
  } else {
    return this.scaledAssetPathInBundle();
  }
}

drawableFolderInBundle(): ResolvedAssetSource {
  const path = this.bundlePath || '';
  return this.fromSource(
    'file://' + path + getAssetPathInDrawableFolder(this.asset)
  );
}

源码不再具体分析,简单总结一下,如果我们指定了bundle文件的加载路径,那我们的图片资源也会在该路径下去加载,比如:

/data/data/com.rndemo/files/index.android.bundle
/data/data/com.rndemo/files/drawable-mdpi/image_splash_img.png

那么问题又来了,是不是每次更新,都需要去下载所有的资源图片,其实并不需要,因为我们有RN的源码,所以直接在源码里面修改加载策略就可以了,具体不再赘述。

这篇文章主要简单介绍了RN入门的一些东西,还有很多模块的内容,我一边学习再一边总结,文末附上传送门,是我参考过的一些文章。

附录传送门

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容