作为一名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无法获取到参数。去查看了相关资料,解决方案有两种:-
后台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.
-
修改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入门的一些东西,还有很多模块的内容,我一边学习再一边总结,文末附上传送门,是我参考过的一些文章。
附录传送门