探索 react-native run-ios(android)

lyxia_iOS發表於2019-03-04

我們都知道React Navite在開發的時候,需要在React Native根目錄下執行react-native run-ios(或run-android),或者在Xcode中執行原生iOS專案(對於Android則是在Android Studio中執行原生Android專案),然後在對應的React Native根目錄下執行npm start(開啟nodejs服務,開啟JS Server)。

寫這篇文章的目的

1、梳理react-native run-ios(android)的完整流程,並識別ios和android的區別。
2、理解debug下ios的詭異現象,在沒開JS Server時,有時載入條為什麼會變成load pre bundle並能正常執行,有時為什麼閃崩。
3、幫助不懂原生的朋友快速進入code狀態,不要每次都等待react-native run-ios(android)
4、最後的黑魔法,在團隊協作下,寫js的可以根本不需要iOS和Android環境(當然,這需要原生開發夥伴的幫助),原生也不需要裝nodejs。

探索“react-native run-ios”到底做了什麼


react-native run-ios:
在控制檯可以看到輸出:

> $ react-native run-ios
Found Xcode project LayoutDemo.xcodeproj
We couldn`t boot your defined simulator due to an already booted simulator. We are limited to one simulator lau
nched at a time.
Launching iPhone 6 (iOS 10.3)...
Building using "xcodebuild -project LayoutDemo.xcodeproj -configuration Debug -scheme LayoutDemo -destination i
d=BB4E36F2-D6B3-447F-91E9-8D1F5B56022E -derivedDataPath build"複製程式碼

1、使用xcodebuild來編譯專案
在輸出中可以看到React Native首先是尋找Xcode project(尋找方式後面會說明),然後使用 xcodebuild來編譯專案。你可以想象就是用xcode開啟專案,然後按執行,一樣的效果。
2、定義了打bundle包的指令碼
在編譯引數中設定了React Native的編譯指令碼(可以在project.pbxproj中檢視到react-native-xcode.sh指令碼的設定),目錄是在:/node_modules/react-native/packager/react-native-xcode.sh。
react-native-xcode.sh

case "$CONFIGURATION" in
  Debug)  //在Debug模式下
    # Speed up build times by skipping the creation of the offline package for debug
    # builds on the simulator since the packager is supposed to be running anyways.
    if [[ "$PLATFORM_NAME" == *simulator ]]; then
      echo "Skipping bundling for Simulator platform"  //使用模擬器跑時不打bundle包,退出sh指令碼
      exit 0;
    fi

    DEV=true //設定DEV為true
    ;;
  "")
    echo "$0 must be invoked by Xcode" 
    exit 1
    ;;
  *)
    DEV=false //設定DEV為false
    ;;
esac
...
# Xcode project file for React Native apps is located in ios/ subfolder
cd ${REACT_NATIVE_DIR}/../.. //進入React Native根目錄
...
if [[ "$CONFIGURATION" = "Debug" && ! "$PLATFORM_NAME" == *simulator ]]; then
  ...
  //如果在Debug環境在,並且不是由模擬器執行則對localhost和ip.txt中的內容設定ATS為允許http(因為node.js服務是開在http://localhost:8081上的)
 NSAppTransportSecurity:NSExceptionDomains:localhost:NSTemporaryExceptionAllowsInsecureHTTPLoads bool true" "$PLIST"
  $PLISTBUDDY -c "Add NSAppTransportSecurity:NSExceptionDomains:$IP.xip.io:NSTemporaryExceptionAllowsInsecureHTTPLoads bool true" "$PLIST"
  echo "$IP.xip.io" > "$DEST/ip.txt"
fi
...
BUNDLE_FILE="$DEST/main.jsbundle"  //設定輸出main.jsbundle的目錄
//執行react-native下的bundle命令,相當於自己執行`react-native bundle`
$NODE_BINARY "$REACT_NATIVE_DIR/local-cli/cli.js" bundle 
  --entry-file "$ENTRY_FILE" 
  --platform ios 
  --dev $DEV 
  --reset-cache 
  --bundle-output "$BUNDLE_FILE" 
  --assets-dest "$DEST"複製程式碼

從指令碼能看出在Debug模式下,不會為模擬器打bundle包,但是會為真機打bundle包,這也就是為什麼我們在真機除錯後,然後斷開nodejs服務(不能開啟remote debug js模式,不然會閃崩),重新進入應用時也會正確的載入bundle,螢幕上方會出現`load pre bundle`的字樣,並且是黑色的背景條,如果是載入nodejs伺服器的則會是綠色的背景條並出現載入進度。但是當我們在模擬器上做同樣的操作時,比如先正常開啟nodejs伺服器載入除錯,然後在重新開啟應用,它載入不到bundle會崩潰。
當我們設定Rlease模式時,必須用xcode編譯才會打bundle包,此時無論是在模擬器還是在真機執行,都會打bundle包。
輸出如下:

/Users/lyxia/Documents/ios/React_Native/LayoutDemo/ios/build/Build/Intermediates/LayoutDemo.build/Debug-iphoneos/LayoutDemo.build/Script-00DD1BFF1BD5951E006B06BC.sh
+DEST=/Users/lyxia/Documents/ios/React_Native/LayoutDemo/ios/build/Build/Products/Debug-iphoneos/LayoutDemo.app
+ [[ Debug = Deug ]]
+ [[ ! iphoneos == *simulator ]]
+ PLISTBUDDY=/usr/libexec/PlistBuddy

+PLIST=/Users/lyxia/Documents/ios/React_Native/LayoutDemo/ios/build/Build/Products/Debug-iphoneos/LayoutDemo.app/Info.plist
++ ipconfig getifaddr en0
+ IP=192.168.16.111
+ `[` -z 192.168.16.111 `]`
+ /usr/libexec/PlistBuddy -c `Add NSAppTransportSecurity:NSExceptionDomains:localhost:NSTemporaryExceptionAllow
sInsecureHTTPLoads bool true` /Users/lyxia/Documents/ios/React_Native/LayoutDemo/ios/build/Build/Products/Debug
-iphoneos/LayoutDemo.app/Info.plist
+ /usr/libexec/PlistBuddy -c `Add NSAppTransportSecurity:NSExceptionDomains:192.168.16.111.xip.io:NSTemporaryEx
ceptionAllowsInsecureHTTPLoads bool true` /Users/lyxia/Documents/ios/React_Native/LayoutDemo/ios/build/Build/Pr
oducts/Debug-iphoneos/LayoutDemo.app/Info.plist
+ echo 192.168.16.111.xip.io
+BUNDLE_FILE=/Users/lyxia/Documents/ios/React_Native/LayoutDemo/ios/build/Build/Products/Debug-iphoneos/LayoutDemo.app/main.jsbundle

+ node /Users/lyxia/Documents/ios/React_Native/LayoutDemo/node_modules/react-native/local-cli/cli.js bundle --entry-file index.ios.js --platform ios --dev true --reset-cache --bundle-output/Users/lyxia/Documents/ios/React_Native/LayoutDemo/ios/build/Build/Products/Debug-iphoneos/LayoutDemo.app/main.jsbundle --assets-dest /Users/ly
xia/Documents/ios/React_Native/LayoutDemo/ios/build/Build/Products/Debug-iphoneos/LayoutDemo.app

[2017-04-01 11:26:20] <START> Initializing Packager
[2017-04-01 11:26:21] <START> Building Haste Map
[2017-04-01 11:26:21] <END>   Building Haste Map (575ms)
[2017-04-01 11:26:21] <END>   Initializing Packager (1644ms)
[2017-04-01 11:26:21] <START> Transforming files

Warning: The transform cache was reset.

[2017-04-01 11:26:40] <END>   Transforming files (18750ms)
bundle: start
bundle: finish
bundle: Writing bundle output to: /Users/lyxia/Documents/ios/React_Native/LayoutDemo/ios/build/Build/Products/D
ebug-iphoneos/LayoutDemo.app/main.jsbundle
bundle: Copying 5 asset files
bundle: Done writing bundle output
bundle: Done copying assets
+ [[ ! -n true ]]複製程式碼

3、自動彈出一個框來啟動nodejs服務
../node_modules/react-native/React/React.xcodeproj/project.pbxproj檔案中我們可以看到PBXShellScriptBuildPhase section中定義了Start Packager的shell執行,最終會執行到../node_modules/react-native/packager/launchPackager.command。一路追溯,可以看到最後執行到node "$THIS_DIR/../local-cli/cli.js" start "$@"也就是我們常用的npm start

總結:所以現在我們整理一下這整個流程:

  • 使用xcodebuild編譯專案
  • 因為在使用react-native init <專案名>生成的專案中,project.pbxproj裡面會新增生成bundle的Bundle React Native code and images編譯引數,所以我們在真機完成除錯後,nodejs關了,也能去載入本地的bundle,因為它已經幫我們打包好了。
  • 因為在Reactproject.pbxproj中新增了Start Packager的編譯引數,所以它會判斷是否已經開啟nodejs服務,如果沒有則會幫我們開啟。

組合:按需求組合使用
這個流程使我們只要執行react-native run-ios就可以編譯專案,開啟nodejs服務。
因此我們可以換個方向來想,如果我們是在原生的iOS專案中接入RN,那應該怎麼做呢?
只需用Xcode執行原生專案,然後npm start即可。當然我們也可以在編譯引數中加入Bundle React Native code and images讓它每次執行為我們自動打包最新的bundle,這樣當我們除錯好後,就算關了nodejs服務,也能繼續執行。

問題:如何尋找.xcodeproj檔案
../node_modules/react-native/local-cli/runIOS/runIOS.js裡面可以找到react-native run-ios的實現,並且有各種引數的舉例說明:

{
    desc: `Pass a non-standard location of iOS directory`,
    cmd: `react-native run-ios --project-path "./app/ios"`,
}
...
{
    command: `--project-path [string]`,
    description: `Path relative to project root where the Xcode project `
      + `(.xcodeproj) lives. The default is `ios`.`,
    default: `ios`,
  }複製程式碼

可以看到預設是在ios.目錄下尋找以.xcworkspace或者.xcodeproj結尾的檔案,不過可以使用--project-path [string]來指定檔案目錄。

探索“react-native run-android”到底做了什麼


同樣我們在控制檯執行react-native run-android,看輸出:

> $ react-native run-android
Starting JS server...
Running /Users/lyxia/Documents/Android/adt-bundle-mac-x86_64-20140702/sdk//platform-tools/adb -s C
oolpad5890-a1778fd9 reverse tcp:8081 tcp:8081
Building and installing the app on the device (cd android && ./gradlew installDebug)...
Starting a new Gradle Daemon for this build (subsequent builds will be faster).
> Configuring > 1/2 projects > :app > Compiling script into cache^C
:app:preBuild UP-TO-DATE
:app:preDebugBuild UP-TO-DATE
:app:checkDebugManifest
:app:preReleaseBuild UP-TO-DATE
:app:prepareComAndroidSupportAppcompatV72301Library
:app:prepareComAndroidSupportRecyclerviewV72301Library
:app:prepareComAndroidSupportSupportV42321Library複製程式碼

是的,Android和我們的iOS寶寶是不一樣的,他一開始就去檢視JS Server是否開啟,如果沒開他就會去開啟,然後使用gradle來編譯專案並安裝。
可以在../node_modules/react-native/local-cli/runAndroid/runAndroid.js中檢視react-native run-android的實現,這裡不貼出來了。

注意點
1、在Release下,會打包bundle:

if (args.configuration.toUpperCase() === `RELEASE`) {
      console.log(chalk.bold(
        `Generating the bundle for the release build...`
      ));
      child_process.execSync(
        `react-native bundle ` +
        `--platform android ` +
        `--dev false ` +
        `--entry-file index.android.js ` +
        `--bundle-output ${androidProjectDir}/app/src/main/assets/index.android.bundle ` +
        `--assets-dest ${androidProjectDir}/app/src/main/res/`,
        {
          stdio: [process.stdin, process.stdout, process.stderr],
        }
      );複製程式碼

2、如何驗證android專案是否存在:
在React Native根目錄下尋找android/gradlew,是的,你沒看錯,全程不可配,乖乖放好路徑吧,不然就使用Android Studio執行,然後執行npm start

總結:整理react-native run-android的完整流程

  • 檢查是否需要開啟JS Server
  • 檢查是否存在android/gradlew
  • 檢查是否有可用的Android裝置已連線
  • 檢查是否在release環境下,如果是則打包bundle
  • 使用gradle編譯android專案並安裝

條理比iOS清晰許多,因為都寫在一個檔案中。

對比“react-native run-ios(android)的相同與不同”


相同點:

  • 都是要去尋找原生專案是否存在,iOS是存在/ios/*.xcworkspace或者/ios/.xcodeproj結尾的檔案,Android是存在android/gradlew檔案。
  • 都會為我們開啟JS Server。
  • 都會在Release環境下為我們打包Bundle。
  • 都可以真機執行。

不同點:

  • iOS的專案路徑可配,當我們在原生的iOS中接入React Native時,專案路徑很可能不符合它預設的路徑,此時可以通過--project-path修改。而Android不可以指定專案路徑,如果/android/gradlew不存在,則不可以使用react-native run-android
  • Debug環境下,iOS在真機上執行時(react-native run-ios --device),也會我們打包bundle,但是Android不會。
  • iOS使用xcodebuild來編譯專案,Android使用gradle來編譯。

使用以上知識來解決常見問題


問題1:
我需不需要每天開機都react-native run-ios(android)
回答:
在沒有更改原生程式碼的情況下,是不需要的,只要裝置或者是模擬器上安裝了應用,只需要npm start開啟JS Server即可,讓應用能載入到nodejs服務上的js bundle即可。

問題2:
如果我電腦上沒有Android和iOS環境,能不能除錯RN專案。
回答:
可以跑,只需要手機上已經裝好RN的Debug版的專案,並且電腦開啟了JS Server。Android機需要在開發者選項裡把ip和埠改成開了JS Server的ip和埠即可。iOS就要麻煩些了,需要在原生新增ip.txt檔案,然後指定ip為開了JS Server的ip,重編,即可(這裡就需要有Xcode了)。

問題3:
可不可以使用1個JS Server同時除錯iOS和Android專案。
回答:
可以的。


歡迎在評論區提出問題和錯誤。

簡書同步更新地址:www.jianshu.com/u/b92ab7b3a…

相關文章