作為一名Android開發(fā),學(xué)習(xí)React Native其實(shí)是一個(gè)很陡峭的過程,本文主要記錄自己從接觸React Native遣总,到能夠?qū)崿F(xiàn)一個(gè)比較完整的Demo的過程中,涉及到的一些知識(shí)點(diǎn)轨功,以及踩過的一些坑旭斥。
一、React Native 簡(jiǎn)介
<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.
簡(jiǎn)單總結(jié)一下:ReactNative是由Facebook推出的古涧,可以讓開發(fā)者使用 JavaScript 和 React 創(chuàng)建基于Web垂券,iOS 和 Android 平臺(tái)原生應(yīng)用的一套框架。
二羡滑、React Native 案例
在RN的官網(wǎng)上能夠看到一些開發(fā)案例菇爪,不過基本上都是國外的應(yīng)用,國內(nèi)有使用到RN開發(fā)的應(yīng)用主要包括QQ空間柒昏、QQ音樂娄帖、全民K歌等等。
三昙楚、React Native 基本概念
RN最基本的概念我認(rèn)為應(yīng)該是組件近速、屬性、狀態(tài)堪旧。
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);
組件其實(shí)就是界面上可顯示的元素削葱,類似于Android里面的View,通過render函數(shù)進(jìn)行渲染淳梦,一個(gè)組件可以包含很多個(gè)子組件析砸。RN內(nèi)置的組件可以直接通過import { xxx } from 'react-native' 進(jìn)行導(dǎo)入,當(dāng)然也可以自定義組件爆袍。每個(gè)組件都擁有自己的屬性和狀態(tài)首繁。將上面的例子進(jìn)行完善作郭,加入屬性和狀態(tài):
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);
在構(gòu)造函數(shù)中,初始化了組件的狀態(tài)showText為true弦疮,那就可以在其他地方通過this.state.showText訪問到該狀態(tài)的值夹攒,在render函數(shù)里,組件的source即是組件的一個(gè)屬性胁塞,可以通過this.props來獲取屬性的值咏尝。
可以通過setState函數(shù)改變組件的狀態(tài),每次狀態(tài)改變都會(huì)重新觸發(fā)render函數(shù)啸罢。
四编检、React Native 技術(shù)細(xì)節(jié)
- 數(shù)據(jù)存儲(chǔ)
在Android里面,我們有xml和sqlite兩種保存數(shù)據(jù)的方式扰才,切換到RN開發(fā)允懂,首先想到的也是數(shù)據(jù)存儲(chǔ)問題,其實(shí)衩匣,RN也是支持的蕾总,簡(jiǎn)單列舉如下:
AsyncStorage :key-value存值方式,支持寫入舵揭、讀取谤专、移除
react-native-sqlite : 支持iOS數(shù)據(jù)庫
react-native-android-sqlite :支持Android數(shù)據(jù)庫
Realm :跨平臺(tái)躁锡,可同時(shí)支持iOS和Android(推薦)
-
頁面切換及參數(shù)傳遞
在Android里面午绳,需要把所有Activity進(jìn)行注冊(cè),然后通過startActivity進(jìn)行跳轉(zhuǎn)映之,通過bundle進(jìn)行傳值拦焚,在RN里面,可以通過路由進(jìn)行跳轉(zhuǎn)杠输,通過屬性進(jìn)行傳值赎败,這里我推薦使用的是Navigator,一個(gè)簡(jiǎn)單的模版如下: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里面的所有字段通過屬性進(jìn)行傳遞蠢甲,所以新打開的組件能夠獲得navigator以及相關(guān)參數(shù)僵刮。
openPage() {
this.props.navigator.push({
component: MainScreen,
params: {
phone: this.state.phone,
yzm: this.state.yzm,
}
})
}
_back() {
this.props.navigator.pop();
}
關(guān)于回調(diào),可以定義一個(gè)回調(diào)函數(shù)作為參數(shù)傳遞鹦牛,網(wǎng)上例子很多不再贅述搞糕,這里需要強(qiáng)調(diào)的一點(diǎn)是,如果A組件把navigator傳遞給了B組件曼追,而B組件的子組件也需要使用這個(gè)navigator窍仰,那么需要B組件在創(chuàng)建子組件的時(shí)候,手動(dòng)把這個(gè)參數(shù)繼續(xù)傳遞下去礼殊,比如我的主界面是5個(gè)Tab頁驹吮,需要在點(diǎn)擊Tab頁內(nèi)某些組件的時(shí)候跳轉(zhuǎn)到新的頁面针史,那么在創(chuàng)建Tab頁面的時(shí)候就需要手動(dòng)傳遞參數(shù):
<Page1 {...route.params} navigator={this.props.navigator}></Page1>
對(duì)于Android而言,還存在一個(gè)問題是硬件返回碟狞,如果是原生的Android應(yīng)用啄枕,點(diǎn)擊返回鍵是返回到上一個(gè)界面,但RN構(gòu)建的應(yīng)用篷就,點(diǎn)擊返回直接退出了應(yīng)用射亏,所以這里,需要對(duì)硬件返回進(jìn)行監(jiān)聽竭业。所幸智润,在RN里面有BackAndroid可以監(jiān)聽返回鍵,示例如下:
BackAndroid.addEventListener('hardwareBackPress',
function() {
if (!this.onMainScreen()) {
this.goBack();
return true;
}
return false;
});
- 網(wǎng)絡(luò)訪問
我們可以通過fetch函數(shù)進(jìn)行網(wǎng)絡(luò)訪問未辆,直接看官網(wǎng)的例子:
fetch('https://mywebsite.com/endpoint/', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
firstParam: 'yourValue',
secondParam: 'yourOtherValue',
})
})
然而當(dāng)我在項(xiàng)目里面依葫蘆畫瓢直接使用fetch函數(shù)的時(shí)候窟绷,問題出現(xiàn)了,功能實(shí)現(xiàn)不了咐柜,后臺(tái)php無法獲取到參數(shù)兼蜈。去查看了相關(guān)資料,解決方案有兩種:-
后臺(tái)php在解析RN請(qǐng)求的時(shí)候需要加上:
$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類型为狸。
- 組件生命周期
當(dāng)打開一個(gè)RN頁面的時(shí)候,我會(huì)潛意識(shí)的去對(duì)應(yīng)Android Activity的生命周期遗契,onCreate, onResume, onPause, onDestroy等等辐棒。RN生命周期及相關(guān)調(diào)用如下:
生命周期 | 調(diào)用次數(shù) | 能否使用setState |
---|---|---|
getDefaultProps | 1(全局調(diào)用一次) | 否 |
getInitialState | 1 | 否 |
componentWillMount | 1 | 是 |
render | >=1 | 否 |
componentDidMount | 1 | 是 |
componentWillReceiveProps | >=0 | 是 |
shouldComponentUpdate | >=0 | 否 |
componentWillUpdate | >=0 | 否 |
componentDidUpdate | >=0 | 否 |
componentWillUnmount | 1 | 否 |
組件的生命周期,可以通過在組件里面打印log牍蜂,觀察具體調(diào)用漾根。我遇到的一個(gè)問題是,需要在應(yīng)用界面不可見的時(shí)候執(zhí)行某些操作鲫竞,對(duì)應(yīng)Android相當(dāng)于點(diǎn)擊了Home鍵辐怕,那如何知道RN構(gòu)建的應(yīng)用當(dāng)前是處于前臺(tái)還是后臺(tái)呢?所幸从绘,RN里面有AppState這個(gè)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 簽名打包
在開發(fā)過程中僵井,無論是真機(jī)還是模擬器陕截,都需要啟動(dòng)JS server,然后通過這個(gè)server下載相關(guān)的bundle文件加載運(yùn)行驹沿,但在實(shí)際發(fā)布的時(shí)候艘策,我們需要對(duì)應(yīng)用進(jìn)行簽名,把相關(guān)的js文件和資源文件進(jìn)行打包渊季,當(dāng)然朋蔫,這里是針對(duì)Anroid的情況罚渐,以下是React Native生成正式包的步驟:
通過Android Studio生成簽名文件,并將該文件置于android/app文件目錄下
-
編輯 ~/.gradle/gradle.properties文件驯妄,添加以下內(nèi)容
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,比如我生成的測(cè)試簽名信息為: 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/
如果要導(dǎo)出bundle文件和資源文件,還可以執(zhí)行以下兩個(gè)命令:
-
切換到項(xiàng)目主目錄青扔,生成assets文件夾
mkdir -p android/app/src/main/assets
-
輸出對(duì)應(yīng)的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是應(yīng)用運(yùn)行平臺(tái),dev表示是否為開發(fā)模式微猖,entry-file是入口js文件谈息,bundle-output為輸出的bundle文件路徑,assets-dest是輸出的資源圖片路徑凛剥。
六侠仇、React Native 動(dòng)態(tài)更新
最開始選擇學(xué)習(xí)RN,其中很大一個(gè)原因是RN的動(dòng)態(tài)更新能力犁珠,雖然目前有很多hotfix框架逻炊,但或多或少都存在一些兼容性問題,并且能力有限犁享,只能小范圍的修改源代碼余素,而不能替換資源文件。
我們知道RN運(yùn)行的時(shí)候炊昆,是去讀取assets目錄下的bundle文件桨吊,所以只要能夠動(dòng)態(tài)的替換這個(gè)文件,那么就能夠做到動(dòng)態(tài)更新窑眯,但顯然屏积,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重寫這個(gè)方法,指定bundle文件路徑姥卢,如果該路徑下的文件存在卷要,則返回指定文件路徑,如果不存在独榴,就返回null僧叉,默認(rèn)到assets路徑下加載bundle文件,所以當(dāng)需要更新的時(shí)候棺榔,只需要下載相關(guān)的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;
}
當(dāng)然,僅僅替換bundle文件并沒有解決所有問題症歇,我們知道郎笆,bundle文件只包含了js代碼谭梗,那資源文件又該如何處理呢?在更新的時(shí)候宛蚓,如果只是下載了bundle文件激捏,會(huì)導(dǎo)致原有項(xiàng)目中所有圖片都不可見,這又是為什么呢凄吏?打開node_modules/react-native/Libraries/Image/Image.android.js文件远舅,查看Image的render函數(shù),然后逐層追蹤源碼痕钢,會(huì)在resolveAssetSource.js文件中看到一個(gè)很重要的函數(shù):
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中看到兩個(gè)比較重要的函數(shù):
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)
);
}
源碼不再具體分析图柏,簡(jiǎn)單總結(jié)一下,如果我們指定了bundle文件的加載路徑任连,那我們的圖片資源也會(huì)在該路徑下去加載爆办,比如:
/data/data/com.rndemo/files/index.android.bundle
/data/data/com.rndemo/files/drawable-mdpi/image_splash_img.png
那么問題又來了,是不是每次更新课梳,都需要去下載所有的資源圖片距辆,其實(shí)并不需要,因?yàn)槲覀冇蠷N的源碼暮刃,所以直接在源碼里面修改加載策略就可以了跨算,具體不再贅述。
這篇文章主要簡(jiǎn)單介紹了RN入門的一些東西椭懊,還有很多模塊的內(nèi)容诸蚕,我一邊學(xué)習(xí)再一邊總結(jié),文末附上傳送門氧猬,是我參考過的一些文章背犯。
附錄傳送門