React Native 入門之旅

作為一名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)資料,解決方案有兩種:
    1. 后臺(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.

  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類型为狸。

  • 組件生命周期
    當(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é),文末附上傳送門氧猬,是我參考過的一些文章背犯。

附錄傳送門

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市盅抚,隨后出現(xiàn)的幾起案子漠魏,更是在濱河造成了極大的恐慌,老刑警劉巖妄均,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柱锹,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡丰包,警方通過查閱死者的電腦和手機(jī)禁熏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來邑彪,“玉大人瞧毙,你說我怎么就攤上這事。” “怎么了宙彪?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵撑柔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我您访,道長(zhǎng)铅忿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任灵汪,我火速辦了婚禮檀训,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘享言。我一直安慰自己峻凫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布览露。 她就那樣靜靜地躺著荧琼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪差牛。 梳的紋絲不亂的頭發(fā)上命锄,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音偏化,去河邊找鬼脐恩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛侦讨,可吹牛的內(nèi)容都是我干的驶冒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼韵卤,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼骗污!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起沈条,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤需忿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后拍鲤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贴谎,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡汞扎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年季稳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片澈魄。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡景鼠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情铛漓,我是刑警寧澤溯香,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站浓恶,受9級(jí)特大地震影響玫坛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜包晰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一湿镀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧伐憾,春花似錦勉痴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至胸嘴,卻和暖如春雏掠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背劣像。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來泰國打工磁玉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人驾讲。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓蚊伞,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親吮铭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子时迫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容