寫給前端的 react-native 入門指南

2022-09-01 06:03:22

前言

本文主要介紹 react-native(下稱 RN) 的入門, 和前端的異同點

文章不涉及功能的具體實現

選擇優勢

我們先說說, 為什麼很多人會選擇使用 RN 、他對應的特性和普通 Web 的區別

  1. 前端資源, 生態的互通

因為使用的語言是 JS 和 react, 對於前端來說可以無縫切換, 並且他還能使用前端的各類包

在 JS 端, 安卓和 iOS 是同一套程式碼

  1. 熱更新

很多選擇使用 RN 的原因就是有熱更新

簡單解釋下熱更新, 在執行 APP 時, js 層我們可以通過接受到的通知, 來進行實時替換, 替換完畢之後一般是要重啟 APP 的, 這個時候可以詢問使用者, 也可以在下次重啟時重新載入新的 JS 程式碼

這樣可以保證使用者使用的 js 環境, 可以是較新的, 如果是原生 APP 的更新則需要讓使用者去應用商店重新下載

  1. 支援原生

RN 通過橋接與原生進行互動, 頁面級別的融入原生 APP

他的許多元件, 方法都是呼叫了原生方法/元件, 相對 webview 來說效能更好

跨端框架橫向對比

RN 和 Flutter 的簡單對比

環境

無論是 RN 還是 Flutter ,都需要 Android 和 IOS 的開發環境,也就是 JDKAndroid SDKXcode 等環境設定,而不同點在於:

  • RN 需要 npm 、node 、react-native-cli 等設定 。
  • Flutter 需要 flutter sdkAndroid Studio / VSCode 上的 DartFlutter 外掛。

針對前端來說 RN 環境相對友好一點

實現原理

AndroidIOS 上,預設情況下 FlutterReact Native需要一個原生平臺的
Activity / ViewController 支援,且在原生層面屬於一個「單頁面應用」,
而它們之間最大的不同點其實在於 UI 構建 :

  • RN:

React Native 是一套 UI 框架,預設情況下 React Native 會在 Activity 下載入 JS 檔案,然後執行在 JavaScriptCore 中解析 Bundle 檔案佈局,最終堆疊出一系列的原生控制元件進行渲染。

簡單來說就是 通過寫 JS 程式碼設定頁面佈局,然後 React Native 最終會解析渲染成原生控制元件,如 <View> 標籤對應 ViewGroup/UIView<ScrollView> 標籤對應 ScrollView/UIScrollView<Image> 標籤對應 ImageView/UIImageView 等。

  • Flutter :

Flutter 中絕大部分的 Widget 都與平臺無關, 開發者基於 Framework 開發 App ,而 Framework 執行在 Engine 之上,由 Engine 進行適配和跨平臺支援。這個跨平臺的支援過程,其實就是將 Flutter UI 中的 Widget 「資料化」 ,然後通過 Engine 上的 Skia 直接繪製到螢幕上 。

類似於前端的 canvas 繪圖

此節來自於文章: https://www.jianshu.com/p/da80214720eb

缺點

  • RN:
    • 不能完全相容W3C的規範,比如W3C裡面,可以輕易設定圓角的大小,粗細,邊框是實現和虛線,但是在使用者端,這個實現起來都比較難。所以這類技術都只能有限的支援W3C的標準。
    • js執行效能瓶頸。
    • 資料通訊的效能瓶頸。
  • Flutter:
    • 無法動態更新。
    • 記憶體和包大小佔用。
    • 學習成本高,生態不足。

js 執行環境

在使用 RN 時, JS 程式碼將會執行在兩個不同的環境上:

  • 大多數情況下,RN 使用的是 JavaScriptCore ,也就是 Safari 所使用的 JavaScript 引擎。但是在 iOS 上 JavaScriptCore 並沒有使用即時編譯技術(JIT),因為在 iOS 中應用無權擁有可寫可執行的記憶體頁(因此無法動態生成程式碼)。
  • 在使用 Chrome 偵錯時,所有的 JavaScript 程式碼都執行在 Chrome 中,並且通過 WebSocket 與原生程式碼通訊。此時的執行環境是 V8 引擎

所以在我們開啟偵錯的時候和正式的執行環境會有一些不一樣

RN 內建了 Babel 轉換器。所以很多語法我們是不需要再設定 babel 的, 語法環境直接上手即用
這裡 可以看到具體的設定

定時器

在 RN 中有針對動畫的定時器: InteractionManager

原生應用感覺流暢的一個重要原因就是在互動和動畫的過程中避免繁重的操作。
在 RN 裡,則受到了限制,因為我們只有一個 JavaScript 執行執行緒。於是就有了 InteractionManager 來確保在執行繁重工作之前所有的互動和動畫都已經處理完畢。

InteractionManager.runAfterInteractions(() => {
  // ...需要長時間同步執行的任務...
});

相比較另外的幾個定時器:

  • requestAnimationFrame(): 用來執行在一段時間內控制檢視動畫的程式碼
  • setImmediate/setTimeout/setInterval(): 在稍後執行程式碼。注意這有可能會延遲當前正在進行的動畫。
  • runAfterInteractions(): 在稍後執行程式碼,不會延遲當前進行的動畫。

Hermes 引擎

Hermes 是專門針對 RN 應用而優化的全新開源 JavaScript 引擎。對於很多應用來說,啟用 Hermes 引擎可以優化啟動時間,減少記憶體佔用以及空間佔用。

Hermes 的特色

  • 預編譯位元組碼(引擎載入二進位制程式碼效率高於執行 JS 指令碼)
  • 無 JIT 編譯器(減小了引擎大小,優化記憶體佔用,但直接執行 JS 指令碼的效能差於 V8 和 JSC)
  • 針對行動端的垃圾回收策略

優化原理

傳統 JavaScript 引擎通常是以上圖的模式完成程式碼執行的,編譯階段只完成 babel 跳脫和 minify 壓縮,產物還是 JavaScript 指令碼,解釋與執行的任務都需要在執行時完成(如 V8 引擎,還會在執行時將 JavaScript 編譯為本地機器碼)很明顯缺點就是在執行時需要邊解釋邊執行,甚至需要佔用系統資源執行編譯任務。

Hermes 引擎使用了 aot 編譯的方式,將解釋和編譯過程前置到編譯階段,執行時只完成機器碼的執行,大大提高了執行效率。

原生 ui 元件

在 RN 中的一個優勢就是可以插入原生元件, 提高 APP 的效能
假如我們在 js 中要使用 ImageView, 那就需要這幾步:

  1. 建立一個 ViewManager 的子類。
  2. 實現createViewInstance方法。
  3. 匯出檢視的屬性設定器:使用@ReactProp(或@ReactPropGroup)註解。
  4. 把這個檢視管理類註冊到應用程式包的createViewManagers裡。
  5. 實現 JavaScript 模組。

上述是安卓的新增, 相對來說 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 庫的 installlink

在我們使用三方原生庫的時候, 就需要做一個 link 的功能

我們隨著 RN 釋出的所有庫都在倉庫中的Libraries資料夾下。其中有一些是純 Javascript 程式碼,你只需要去import它們就可以使用了。另外有一些庫基於一些原生程式碼實現,你必須把這些檔案新增到你的應用,否則應用會在你使用這些庫的時候產生報錯。

而 link 它就是手動連結專案中的依賴項的替代方法。

而手動連結是一個很麻煩的事情, 安卓和 iOS 的方案還是不相同的, 具體可檢視

現狀

很幸運的是, 但是如果我們使用的 RN 庫是在 0.60 以上的, 就可以不需要使用 link指令了

在安卓中他會自動連結, 而在 iOS 中, 則可以使用 cocoapods 來下載原生包

cocoapods

簡單介紹一下, CocoaPods 是一個用於 SwiftObjective-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-screensreact-native-reanimated v2 等庫的優化, 最終形成最終接近原生的體驗

至於為什麼大部分放在 js 端, 他有什麼好處, 我會放在下面熱更新部分講解

而後者使用原生容器來作為路由介面, 如 <ScreenContainer> 或者 <Screen>, 他帶來了原生的效能、特性和體驗, 但在我們使用此庫或者要整合另外的庫時會帶來一些麻煩

和前端有什麼異同

在 APP 中的路由會出現一個概念 堆疊(stack), 這就和 web 中最大的一點不同了

這裡用一張圖來介紹下:

當我們到一個新頁面時, 上一個頁面是不會銷燬的(大多數情況), 他是將新頁面新增到棧中, 所以在 APP 中, 要經常小心記憶體的洩漏問題

熱更新

這是一個在 RN 中最常用到的以及最大的一個優勢功能--熱更新

熱更新方案

一般來說有三種方案:

關於熱更新的注意點:

  • 蘋果App允許使用熱更新Apple's developer agreement, 為了不影響使用者體驗,規定必須使用靜默更新。 Google Play不能使用靜默更新,必須彈框告知使用者App有更新。中國的android市場必須採用靜默更新(如果彈框提示,App會被「請上傳最新版本的二進位制應用包」原因駁回)。
  • react-native-code-push只更新資原始檔,不會更新java和Objective C,所以npm升級依賴包版本的時候,如果依賴包使用的在地化實現, 這時候必須更改應用版本號, 然後重新編譯app釋出到應用商店。

一般來說手機熱更新的流程:

其中檢測、下載、重啟等等, 都是 npm 包 react-native-code-push 提供的 API

關於熱更新還有進一步優化的空間, 如: 一次打包出來的 bundle 過大, 對其進行分包, 本文就不在深入解析了

APP 更新

在上面我們講到了, 原生元件更新之後, 就需要重新下載 APP 了, 那麼怎麼方便地更新呢?

這裡就用到了我之前寫的一篇文章, 原理如下圖:

原文點選這裡

上述的熱更新和 APP 更新, 在 electron 上也有對應的實現, 對於 web 端的同學來說, 這是一個值得參考的資訊

其他不同點

在 APP 中還有很多細節與 Web 端不同, 這裡列出幾點

debug 方案

開發過 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

  • 0.59-0.60 的升級

在這兩個版本直接有很多的 breaking change

其中 iOS 端最大的改動就是, 包變成了 CocoaPods(上面已經講過)
這讓我們的 package 依賴也需要對應的升級(預計會有 50%以上的包升級), 所以影響範圍基本就是整個專案

而安卓方面, 則是 link 的方式變化了, 另外就是 build.gradlesettings.gradleAndroidX 的設定的修改

這裡要介紹一下官方的升級工具: https://react-native-community.github.io/upgrade-helper/
他能比較對應的版本, 把其中的 changes 顯示出來

  • 0.68 的升級

另一個就是 0.67 到 0.68 的升級, 在這個版本變更中, RN 進行了四點調整:

  1. JavaScript Interface(JSI) - 通訊的更新
  2. Fabric - 新的渲染系統
  3. Turbo Modules - Native 模組的增強
  4. CodeGen - 靜態型別檢查器

因為這是一個很底層的修改, 可能會導致現有的所有元件發生變化, 影響範圍幾乎覆蓋全域性

新的架構

這裡我們就來講一下 0.68 中的更新具體是什麼

JavaScript Interface(JSI)

原有的架構我在上文中已經講過了, 他存在的一些問題:
目前 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 中的主程序和渲染程序的通訊一樣

Fabric

在老架構中,RN 佈局是非同步的,這導致在宿主檢視中渲染巢狀的 RN 檢視,會有佈局「抖動」的問題。

而新的架構中和 JSI 一樣, 採用的是跨平臺的解決方案,共用了核心的 C++ 實現。

簡單的解釋就是 JSI 的 UI 版本.

當然還有一些其他的優點:

  • 藉助多優先順序和同步事件的能力,渲染器可以提高使用者互動的優先順序,來確保他們的操作得到及時的處理。
  • React Suspense 的整合,允許你在 React 中更符合直覺地寫請求資料程式碼。
  • 允許你在 RN 使用 React Concurrent 可中斷渲染功能。
  • 更容易實現 RN 的伺服器端渲染。

Turbo Modules

在之前的架構中 JS 使用的所有 Native Modules(例如藍芽、地理位置、檔案儲存等)都必須在應用程式開啟之前進行初始化,這意味著即使使用者不需要某些模組,但是它仍然必須在啟動時進行初始化。

Turbo Modules 基本上是對這些舊的 Native 模組的增強,正如在前面介紹的那樣,現在 JS 將能夠持有這些模組的參照,所以 JS 程式碼可以僅在需要時才載入對應模組,這樣可以將顯著縮短 RN 應用的啟動時間

CodeGen

Codegen 主要是用於保證 JS 程式碼和 C++ 的 JSI 可以正常通訊的靜態型別檢查器,通過使用型別化的 JS 作為參考來源,CodeGen 將定義可以被 Turbo 模組和 Fabric 使用的介面,另外 Codegen 會在構建時生成 Native 程式碼,減少執行時的開支。

skia

現在 RN 也學習了 Flutterskia 渲染, 但是目前它還處於 alpha release 的階段

這是一個值得期待的方向, 目前該庫支援 ImageTextShaderEffectsShapesAnimations 等操作

缺點

目前來說 RN 存在的缺點:

  1. 舊庫的問題
    目前 RN 的生態環境確實還算可以, 但是也有很多舊倉庫, 不止是對於 RN 版本的相容, 對於各類的業務需求也需要客製化
    並且缺少維護, 有了 issue 也不能及時修理
  2. 效能方面
    RN 的效能, 確實比 webview 好很多, 但也比原生差, 在很複雜的場景中就需要使用原生頁面/元件了
  3. 控制元件
    RN 目前使用最多的還是 antd 版本的元件庫, 他能支援很多場景, 但也是缺少人員維護
  4. 相容
    上面講了很多 RN 的升級問題, 其實到現在 RN 還沒到正式版本 1.0.0, 所以他的很多 API 都會有 break change

總結

文章還有其他的一些問題沒有在文章裡詳細說明, 比如安卓的打包、簽名, iOS 的上架, 常用的程式碼、圖片優化手段, 字型解決方案,
啟動屏, 長列表的問題等等, 不過這些也都是細枝末節, 主體的比較基本上都講到了

總體來說, RN 目前的情況還是很不錯的, 未來和 flutter 的競爭也不虛, 作為一個前端來拓展行動端方向時 RN 是最好的一個選擇

參照