我們都知道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,因為它已經幫我們打包好了。 - 因為在
React
的project.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…