本文主要介紹 react-native(下稱 RN) 的入門, 和前端的異同點
文章不涉及功能的具體實現
我們先說說, 為什麼很多人會選擇使用 RN 、他對應的特性和普通 Web 的區別
因為使用的語言是 JS 和 react, 對於前端來說可以無縫切換, 並且他還能使用前端的各類包
在 JS 端, 安卓和 iOS 是同一套程式碼
很多選擇使用 RN 的原因就是有熱更新
簡單解釋下熱更新, 在執行 APP 時, js 層我們可以通過接受到的通知, 來進行實時替換, 替換完畢之後一般是要重啟 APP 的, 這個時候可以詢問使用者, 也可以在下次重啟時重新載入新的 JS 程式碼
這樣可以保證使用者使用的 js 環境, 可以是較新的, 如果是原生 APP 的更新則需要讓使用者去應用商店重新下載
RN 通過橋接與原生進行互動, 頁面級別的融入原生 APP
他的許多元件, 方法都是呼叫了原生方法/元件, 相對 webview 來說效能更好
RN 和 Flutter 的簡單對比
無論是 RN 還是 Flutter
,都需要 Android 和 IOS 的開發環境,也就是 JDK
、Android SDK
、Xcode
等環境設定,而不同點在於:
react-native-cli
等設定 。Flutter
需要 flutter sdk
和 Android Studio
/ VSCode
上的 Dart
與 Flutter
外掛。針對前端來說 RN 環境相對友好一點
在 Android 和 IOS 上,預設情況下 Flutter 和 React Native 都需要一個原生平臺的
Activity
/ ViewController
支援,且在原生層面屬於一個「單頁面應用」, 而它們之間最大的不同點其實在於 UI 構建 :
React Native 是一套 UI 框架,預設情況下 React Native 會在 Activity
下載入 JS 檔案,然後執行在 JavaScriptCore
中解析 Bundle 檔案佈局,最終堆疊出一系列的原生控制元件進行渲染。
簡單來說就是 通過寫 JS 程式碼設定頁面佈局,然後 React Native 最終會解析渲染成原生控制元件,如 <View>
標籤對應 ViewGroup/UIView
,<ScrollView>
標籤對應 ScrollView/UIScrollView
,<Image>
標籤對應 ImageView/UIImageView
等。
Flutter 中絕大部分的 Widget
都與平臺無關, 開發者基於 Framework
開發 App ,而 Framework
執行在 Engine
之上,由 Engine
進行適配和跨平臺支援。這個跨平臺的支援過程,其實就是將 Flutter UI 中的 Widget
「資料化」 ,然後通過 Engine
上的 Skia
直接繪製到螢幕上 。
類似於前端的 canvas 繪圖
此節來自於文章: https://www.jianshu.com/p/da80214720eb
在使用 RN 時, JS 程式碼將會執行在兩個不同的環境上:
所以在我們開啟偵錯的時候和正式的執行環境會有一些不一樣
RN 內建了 Babel
轉換器。所以很多語法我們是不需要再設定 babel
的, 語法環境直接上手即用
在 這裡 可以看到具體的設定
在 RN 中有針對動畫的定時器: InteractionManager
原生應用感覺流暢的一個重要原因就是在互動和動畫的過程中避免繁重的操作。
在 RN 裡,則受到了限制,因為我們只有一個 JavaScript 執行執行緒。於是就有了 InteractionManager
來確保在執行繁重工作之前所有的互動和動畫都已經處理完畢。
InteractionManager.runAfterInteractions(() => {
// ...需要長時間同步執行的任務...
});
相比較另外的幾個定時器:
requestAnimationFrame()
: 用來執行在一段時間內控制檢視動畫的程式碼setImmediate/setTimeout/setInterval()
: 在稍後執行程式碼。注意這有可能會延遲當前正在進行的動畫。runAfterInteractions()
: 在稍後執行程式碼,不會延遲當前進行的動畫。Hermes 是專門針對 RN 應用而優化的全新開源 JavaScript 引擎。對於很多應用來說,啟用 Hermes 引擎可以優化啟動時間,減少記憶體佔用以及空間佔用。
傳統 JavaScript 引擎通常是以上圖的模式完成程式碼執行的,編譯階段只完成 babel 跳脫和 minify 壓縮,產物還是 JavaScript 指令碼,解釋與執行的任務都需要在執行時完成(如 V8 引擎,還會在執行時將 JavaScript 編譯為本地機器碼)很明顯缺點就是在執行時需要邊解釋邊執行,甚至需要佔用系統資源執行編譯任務。
Hermes 引擎使用了 aot 編譯的方式,將解釋和編譯過程前置到編譯階段,執行時只完成機器碼的執行,大大提高了執行效率。
在 RN 中的一個優勢就是可以插入原生元件, 提高 APP 的效能
假如我們在 js
中要使用 ImageView
, 那就需要這幾步:
ViewManager
的子類。createViewInstance
方法。@ReactProp
(或@ReactPropGroup
)註解。createViewManagers
裡。上述是安卓的新增, 相對來說 iOS 會簡單一點:
RCTViewManager
的子類。RCT_EXPORT_MODULE()
宏標記。-(UIView *)view
方法。// RNTMapManager.m
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>
@interface RNTMapManager : RCTViewManager
@end
@implementation RNTMapManager
RCT_EXPORT_MODULE(RNTMap)
- (UIView *)view
{
return [[MKMapView alloc] init];
}
@end
在 JS 中使用:
// MapView.js
import { requireNativeComponent } from 'react-native';
// requireNativeComponent 自動把'RNTMap'解析為'RNTMapManager'
export default requireNativeComponent('RNTMap');
// MyApp.js
import MapView from './MapView.js';
...
render() {
return <MapView style={{ flex: 1 }} />;
}
這是簡單的展示, 關於傳值的話就是多一些屬性的判斷
除了原生元件之外, js 還能傳值給行動端, 新增監聽事件(包括 promise 回撥), 對應的行動端也都可以
這樣就組成了兩端完整的通訊體系
上圖就是 react-native-splash-screen
庫的 install
和 link
在我們使用三方原生庫的時候, 就需要做一個 link
的功能
我們隨著 RN 釋出的所有庫都在倉庫中的Libraries資料夾下。其中有一些是純 Javascript 程式碼,你只需要去import它們就可以使用了。另外有一些庫基於一些原生程式碼實現,你必須把這些檔案新增到你的應用,否則應用會在你使用這些庫的時候產生報錯。
而 link 它就是手動連結專案中的依賴項的替代方法。
而手動連結是一個很麻煩的事情, 安卓和 iOS 的方案還是不相同的, 具體可檢視
很幸運的是, 但是如果我們使用的 RN 庫是在 0.60
以上的, 就可以不需要使用 link
指令了
在安卓中他會自動連結, 而在 iOS 中, 則可以使用 cocoapods
來下載原生包
簡單介紹一下,
CocoaPods
是一個用於Swift
和Objective-C
Cocoa專案的依賴管理器.
類比的話, 看成npm
即可.
使用 cocoapods 時, 會需要檔案 Podfile
, 可以類比成 package.json
之後通過指令 pod install
(類比 npm install
) 下載, 下載完畢之後會存於 Pods
檔案下, 同時也存在 lock
檔案:
Podfile.lock
, Manifest.lock
兩份
悄悄說一句, 如果沒有FQ工具 pod 的下載會變得很麻煩, 經常會卡住
在 RN 中常用的路由管理有兩個: 一是 React Navigation
, 另一個是 react-native-navigation
這兩的區別在於, 前者是通過 JS 程式碼, 通過 monorepo
的組合, 並且通過 react-native-screens
和 react-native-reanimated v2
等庫的優化, 最終形成最終接近原生的體驗
至於為什麼大部分放在 js 端, 他有什麼好處, 我會放在下面熱更新部分講解
而後者使用原生容器來作為路由介面, 如 <ScreenContainer>
或者 <Screen>
, 他帶來了原生的效能、特性和體驗, 但在我們使用此庫或者要整合另外的庫時會帶來一些麻煩
在 APP 中的路由會出現一個概念 堆疊(stack), 這就和 web 中最大的一點不同了
這裡用一張圖來介紹下:
當我們到一個新頁面時, 上一個頁面是不會銷燬的(大多數情況), 他是將新頁面新增到棧中, 所以在 APP 中, 要經常小心記憶體的洩漏問題
這是一個在 RN 中最常用到的以及最大的一個優勢功能--熱更新
一般來說有三種方案:
關於熱更新的注意點:
一般來說手機熱更新的流程:
其中檢測、下載、重啟等等, 都是 npm 包 react-native-code-push 提供的 API
關於熱更新還有進一步優化的空間, 如: 一次打包出來的 bundle 過大, 對其進行分包, 本文就不在深入解析了
在上面我們講到了, 原生元件更新之後, 就需要重新下載 APP 了, 那麼怎麼方便地更新呢?
這裡就用到了我之前寫的一篇文章, 原理如下圖:
上述的熱更新和 APP 更新, 在
electron
上也有對應的實現, 對於 web 端的同學來說, 這是一個值得參考的資訊
在 APP 中還有很多細節與 Web 端不同, 這裡列出幾點
開發過 H5 的人應該對於 vconsole
很熟悉, 在 RN 中也有一個 vconsole
的元件, 用來 debug、列印console、檢視請求、顯示各類資訊等等
之前我也封裝過一個 RN 的 vconsole
外掛: react-native-vconsole, 結合了多個外掛的優點
在安卓機上特有的一種功能, 他就是物理鍵
使用者可以直接點選物理鍵來進行後退, 當退到最首頁的時候, 就需要顯示提示 再按一次退出
這就需要對其進行特殊適配:
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress)
handleBackPress = () => {
if (//如果是第一個頁面) {
const timestamp = new Date().valueOf()
if (timestamp - firstClick > 2000) {
firstClick = timestamp
ToastAndroid.show('再按一次退出', ToastAndroid.SHORT)
return true // 返回 true,意思是阻止預設操作
}
}
return false
}
在手機上會有狀態列這麼一個場景, 這是一個很影響視覺的功能
可以看到上圖中, 在顯示訊號和電池那一塊的變化, 這一部分就是狀態列
在 RN 中時候我們通過此 API 來控制:
<StatusBar barStyle="dark-content" backgroundColor="#ecf0f1" />
當然, 為了適配多種情況(在 APP 中要在頁面的進入, 離開, 其他小功能的變化時修改狀態列), 有時候一些頁面是需要透明狀態列, 也需要一些特殊設定
很多時候這個元件並不是直接就用的, 需要包裝來適配大多數的頁面
在 RN 中有幾個版本是有很大的 breaking change
在這兩個版本直接有很多的 breaking change
其中 iOS 端最大的改動就是, 包變成了 CocoaPods(上面已經講過)
這讓我們的 package 依賴也需要對應的升級(預計會有 50%以上的包升級), 所以影響範圍基本就是整個專案
而安卓方面, 則是 link 的方式變化了, 另外就是 build.gradle
、settings.gradle
、 AndroidX
的設定的修改
這裡要介紹一下官方的升級工具: https://react-native-community.github.io/upgrade-helper/
他能比較對應的版本, 把其中的 changes 顯示出來
另一個就是 0.67 到 0.68 的升級, 在這個版本變更中, RN 進行了四點調整:
因為這是一個很底層的修改, 可能會導致現有的所有元件發生變化, 影響範圍幾乎覆蓋全域性
這裡我們就來講一下 0.68 中的更新具體是什麼
原有的架構我在上文中已經講過了, 他存在的一些問題:
目前 RN 使用 Bridge Module
通訊(其中還需要資料轉換和解碼), 傳送的訊息本質上是非同步的, 也就是說如果是即時性比較高的操作, 比如拖拽, 就會出現失幀的情況
而在全新架構中,Bridge 將被一個名為 JavaScript Interface 的模組所代替,它是一個輕量級的通用層,用 C++ 編寫,JavaScript Engine 可以使用它直接執行或者呼叫 native。
在 JSI 裡 Native 方法會通過 C++ Host Objects 暴露給 JS, 而 JS 可以持有對這些物件的參照,並且使用這些參照直接呼叫對應的方法。
舉個簡單的例子就是
這就類似於 Web 裡 JS 程式碼可以儲存對任何 DOM 元素的參照,並在它上面呼叫方法:
const container = document.createElement(‘div’);
如果你有 electron
的經驗, 原有的通訊模式就和 electron
中的主程序和渲染程序的通訊一樣
在老架構中,RN 佈局是非同步的,這導致在宿主檢視中渲染巢狀的 RN 檢視,會有佈局「抖動」的問題。
而新的架構中和 JSI 一樣, 採用的是跨平臺的解決方案,共用了核心的 C++ 實現。
簡單的解釋就是 JSI 的 UI 版本.
當然還有一些其他的優點:
在之前的架構中 JS 使用的所有 Native Modules(例如藍芽、地理位置、檔案儲存等)都必須在應用程式開啟之前進行初始化,這意味著即使使用者不需要某些模組,但是它仍然必須在啟動時進行初始化。
Turbo Modules 基本上是對這些舊的 Native 模組的增強,正如在前面介紹的那樣,現在 JS 將能夠持有這些模組的參照,所以 JS 程式碼可以僅在需要時才載入對應模組,這樣可以將顯著縮短 RN 應用的啟動時間。
Codegen 主要是用於保證 JS 程式碼和 C++ 的 JSI 可以正常通訊的靜態型別檢查器,通過使用型別化的 JS 作為參考來源,CodeGen 將定義可以被 Turbo 模組和 Fabric 使用的介面,另外 Codegen 會在構建時生成 Native 程式碼,減少執行時的開支。
現在 RN 也學習了 Flutter
的 skia
渲染, 但是目前它還處於 alpha release
的階段
這是一個值得期待的方向, 目前該庫支援 Image
、Text
、Shader
、Effects
、Shapes
、Animations
等操作
目前來說 RN 存在的缺點:
1.0.0
, 所以他的很多 API 都會有 break change文章還有其他的一些問題沒有在文章裡詳細說明, 比如安卓的打包、簽名, iOS 的上架, 常用的程式碼、圖片優化手段, 字型解決方案,
啟動屏, 長列表的問題等等, 不過這些也都是細枝末節, 主體的比較基本上都講到了
總體來說, RN 目前的情況還是很不錯的, 未來和 flutter
的競爭也不虛, 作為一個前端來拓展行動端方向時 RN
是最好的一個選擇