React Native架构讨论

0 阅读6分钟

React Native架构讨论

前言

首先说为什么大多数公司都喜欢使用React Native进行跨平台开发,大家有什么其他原因可以评论,如果合理,我会更新到文章当中。

  • 降低成本,不需要招聘很多客户端(androidiosharmonyos
  • 热更新
  • 招聘容易,前端程序员相对于客户端更好招聘
  • 横向扩展

这篇文章主要讨论React Native如何去支持频繁迭代的业务和海量的业务。

React Native架构优势

整体架构

image-20260411181910951.png

有一个需要注意的是随着业务增加,变更包含原生代码的npm包依赖,原生需要更新,余下所有rn module也需要更新,所以需要创建一个rn版本和原生app版本的一个集合关系。

image-20260411183356968.png

新建一个业务

新业务一般单独放在独立的git仓库当中,权限分明。

简单介绍一下新项目配置.

npx @react-native-community/cli init hello1

React Native使用的是0.85.0版本

开始一个新业务务必开启严格模式。

typescript严格模式

tsconfig.json

{
  "extends": "@react-native/typescript-config",
  "compilerOptions": {
    "strict": true,
    "types": ["jest"]
  },
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["**/node_modules", "**/Pods"]
}

React 严格模式 index.tsx

/**
 * @format
 */

import { StrictMode } from 'react';
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';

AppRegistry.registerComponent(appName, () => () => (
    <StrictMode>
        <App />
    </StrictMode>
));

业务1逻辑

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 */

import { StatusBar, StyleSheet, Text, useColorScheme } from 'react-native';
import {
    EdgeInsets,
    SafeAreaProvider,
    useSafeAreaInsets,
} from 'react-native-safe-area-context';

function App() {
    const isDarkMode = useColorScheme() === 'dark';
    return (
        <SafeAreaProvider>
            <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
            <AppContent />
        </SafeAreaProvider>
    );
}

function AppContent() {
    const safeAreaInsets: EdgeInsets = useSafeAreaInsets();
    console.log('Safe area insets:', safeAreaInsets);
    return (
        <>
            <Text>业务1 Hello World!</Text>
        </>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
});

export default App;

开启react-compiler

npm install -D babel-plugin-react-compiler@latest

babel.config.js

module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    'babel-plugin-react-compiler',
  ]
};

在App.tsx

function AppContent() {
    const [count, setCount] = useState(0);
    const safeAreaInsets: EdgeInsets = useSafeAreaInsets();
    console.log('Safe area insets:', safeAreaInsets);
    const handlePress: () => void = () => {
        setCount(count + 1);
    }
    return (
        <>
            <Text onPress={handlePress}>业务1 Hello World! count: {count}</Text>
            <Image source={require('./images/image1.png')} />
            <HelloScreen /> // 不需要mono包裹
        </>
    );
}

点击刷新,查看dev tools的✨,证明react-compiler生效

运行app

npm run android

查看结果。

业务1结束,其他业务依次类推

业务最终产物

React Native打包产物分为3种

  1. js
  2. hermes bytecode
  3. Static Hermes source

hermes字节码好理解(v8使用的是累加器,hermes使用的是寄存器),和Java字节码一样解释执行。第三种就是所谓的Hermesv1(目前的还是半成品),运行原理就是将js代码编译为C语言源码,由Hermes编译为二进制(LLVM)可执行文件,不再需要虚拟机.

hermes出现之后,babel loose模式就过时了.

第一种

使用metro打包即可(repack不建议使用(不如metro))

npx react-native bundle \
  --platform android \
  --verbose \
  --minify true \
  --dev false \
  --entry-file index.tsx \
  --bundle-output dist/bundle/index.android.bundle \
  --sourcemap-output dist/bundle/index.android.bundle.js.map \
  --sourcemap-sources-root  . \
  --assets-dest dist/assets
# powershell
npx react-native bundle `
  --platform android `
  --verbose `
  --minify true `
  --dev false `
  --entry-file index.tsx `
  --bundle-output dist/bundle/index.android.bundle `
  --sourcemap-output dist/bundle/index.android.bundle.js.map `
  --sourcemap-sources-root  . `
  --reset-cache `
  --assets-dest dist/assets
  
npx react-native bundle `
  --platform android `
  --verbose `
  --minify true `
  --dev false `
  --entry-file index.tsx `
  --bundle-output dist/bundle/index.android.bundle `
  --sourcemap-output dist/bundle/index.android.bundle.js.map `
  --sourcemap-sources-root  . `
  --assets-dest dist/assets

# 将 JS Bundle 转换为 Hermes 字节码
./hermesc -emit-binary -target=HBC -out index.android.bundle.hbc index.android.bundle

# 将 JS 转换为 Static Hermes 产物
./hermesc -emit-binary -target=SH -out index.android.bundle.sh index.android.bundle

# 查看release错误信息
npx metro-symbolicate dist/index.android.bundle.js.map < trace.txt

# 查看bundle包体分析
npx source-map-explorer ./dist/bundle/index.android.bundle ./dist/bundle/index.android.bundle.js.map --html result.html --no-border-checks

# 查看bundle包体分析
npx react-native-bundle-visualizer --platform android --entry-file index.tsx 

第二种

需要由第一种jsbundle生成字节码

node_modules/hermes-compiler/hermesc/win64-bin/hermesc.exe `
	-O4 `
	-g0 `
	-basic-block-profiling `
	-emit-binary `
	-strict `
	-output-source-map `
	-source-map dist/bundle/index.android.bundle.hbc.map `
	-freorder-registers `
	-fstatic-require `
	-fstrip-function-names `
	-fauto-detect-static-builtins `
	-finline `
	-target HBC `
	-out dist/bundle/index.android.bundle.hbc `
	./dist/bundle/index.android.bundle

source map 错误栈追踪使用,可以参考 Hermes | Sentry for React Native

第三种(实验阶段)

需要做一些修改

node_modules/hermes-compiler/hermesc/win64-bin/hermesc.exe `
	-O4 `
	-g0 `
	-emit-binary `
	-strict `
	-freorder-registers `
	-fstatic-require `
	-fstrip-function-names `
	-fauto-detect-static-builtins `
	-finline `
	-target SH `
	-out dist/bundle/index.android.bundle.sh `
	./dist/bundle/index.android.bundle

Static Hermes Typed Mode

对ts语法限制比较高。还不太成熟。

可以使用-typed开启Typed模式。

例如:

math.ts

function add(a: number, b: number ): number {
    return a + b;
}

编译

node_modules/hermes-compiler/hermesc/win64-bin/hermesc.exe `
	-O4 `
	-g0 `
	-emit-binary `
	-strict `
	-freorder-registers `
	-fstatic-require `
	-fstrip-function-names `
	-fauto-detect-static-builtins `
	-finline `
	-target SH `
	-typed `
	-out dist/bundle/math.c `
	./dist/src/math.ts

产物

// facebook/hermes/include/hermes/VM/static_h.h
#include "hermes/VM/static_h.h"

#include <stdlib.h>


static uint32_t unit_index;
static inline SHSymbolID* get_symbols(SHUnit *);
static inline SHWritePropertyCacheEntry* get_write_prop_cache(SHUnit *);
static inline SHReadPropertyCacheEntry* get_read_prop_cache(SHUnit *);
static inline SHPrivateNameCacheEntry* get_private_name_cache(SHUnit *);
static const SHSrcLoc s_source_locations[];
static SHNativeFuncInfo s_function_info_table[];
static SHLegacyValue _0_global(SHRuntime *shr);
// ./dist/src/math.ts:2:1
static SHLegacyValue _0_global(SHRuntime *shr) {
  _SH_MODEL();
  struct {
    SHLocals head;
  } locals;
  _sh_check_native_stack_overflow(shr);
  SHLegacyValue *frame = _sh_enter(shr, &locals.head, 1);
  locals.head.count =0;
  SHUnit *shUnit = shr->units[unit_index];
  locals.head.unit = shUnit;
  locals.head.src_location_idx = 0;
  SHLegacyValue np0 = _sh_ljs_undefined();
  frame[-4] = _sh_ljs_native_pointer(&locals.head);

L0:
  ;
  np0 = _sh_ljs_undefined();
  _sh_leave(shr, &locals.head, frame);
  return np0;
}
static unsigned char s_literal_val_buffer[0] = {};
static unsigned char s_obj_key_buffer[0] = {};
static const SHShapeTableEntry s_obj_shape_table[] = {
};

static const SHSrcLoc s_source_locations[] = {
  { .filename_idx = 0, .line = 0, .column = 0 },
};

static SHNativeFuncInfo s_function_info_table[] = {
  { .name_index = 1, .arg_count = 0, .prohibit_invoke = 2, .kind = 0 },
};
static const char s_ascii_pool[] = {
  '\0',
  'g', 'l', 'o', 'b', 'a', 'l', '\0',
};
static const char16_t s_u16_pool[] = {
};
static const uint32_t s_strings[] = {0,0,0,1,6,615793799,};
#define CREATE_THIS_UNIT sh_export_this_unit
struct UnitData {
  SHUnit unit;
  SHSymbolID symbol_data[2];
  SHWritePropertyCacheEntry write_prop_cache_data[0];
  SHReadPropertyCacheEntry read_prop_cache_data[0];
  SHPrivateNameCacheEntry private_name_cache_data[0];
  SHCompressedPointer object_literal_class_cache[0];
};
SHUnit *CREATE_THIS_UNIT(void) {
  struct UnitData *unit_data = calloc(sizeof(struct UnitData), 1);
  *unit_data = (struct UnitData){.unit = {.index = &unit_index,.num_symbols =2, .num_write_prop_cache_entries = 0, .num_read_prop_cache_entries = 0, .ascii_pool = s_ascii_pool, .u16_pool = s_u16_pool,.strings = s_strings, .symbols = unit_data->symbol_data,.write_prop_cache = unit_data->write_prop_cache_data,.read_prop_cache = unit_data->read_prop_cache_data, .private_name_cache = unit_data->private_name_cache_data, .obj_key_buffer = s_obj_key_buffer, .obj_key_buffer_size = 0, .literal_val_buffer = s_literal_val_buffer, .literal_val_buffer_size = 0, .obj_shape_table = s_obj_shape_table, .obj_shape_table_count = 0, .object_literal_class_cache = unit_data->object_literal_class_cache, .source_locations = s_source_locations, .source_locations_size = 1, .unit_main = _0_global, .unit_main_info = &s_function_info_table[0], .unit_name = "sh_compiled" }};
  return (SHUnit *)unit_data;
}

SHSymbolID *get_symbols(SHUnit *unit) {
  return ((struct UnitData *)unit)->symbol_data;
}

SHWritePropertyCacheEntry *get_write_prop_cache(SHUnit *unit) {
  return ((struct UnitData *)unit)->write_prop_cache_data;
}
SHReadPropertyCacheEntry *get_read_prop_cache(SHUnit *unit) {
  return ((struct UnitData *)unit)->read_prop_cache_data;
}
SHPrivateNameCacheEntry *get_private_name_cache(SHUnit *unit) {
  return ((struct UnitData *)unit)->private_name_cache_data;
}

typedef struct SHConsoleContext SHConsoleContext;

SHConsoleContext *init_console_bindings(SHRuntime *shr);

void free_console_context(SHConsoleContext *consoleContext);

bool run_event_loop(
    SHRuntime *shr,
    SHConsoleContext *consoleContext);

int main(int argc, char **argv) {
  // 初始化 Static hermes 运行时
  SHRuntime *shr = _sh_init(argc, argv);
  // 绑定console
  SHConsoleContext *consoleContext = init_console_bindings(shr);
  // 初始化 math.ts 并且开启事件循环
  bool success =
    _sh_initialize_units(shr, 1, CREATE_THIS_UNIT) &&
    run_event_loop(shr, consoleContext);
  // C++ 回收 consoleContext facebook/hermes/tools/shermes/ConsoleBindings.cpp
  free_console_context(consoleContext);
  // Destroy a runtime instance
  _sh_done(shr);
  return success ? 0 : 1;
}

SHUnitStatic Hermes Unitmath.ts就是一个SHUnit.

加载顺序: Static Hermes > HBC > js

rn module加载

通常将rnbundle代码和资源图片放在一起类似如下

image-20260411224703341.png

Android的话最好放在

/data/data/<packageName>/files/rn_bundles
# 完整路径 code是自定义的,方便多个需求并行开发、提测
/data/data/<packageName>/files/rn_bundles/rn_module1_${code}/index.android.bundle

下。

MainApplication.kt

class MainApplication : Application(), ReactApplication {

    private val jsBundleFilePath: String
        get() {
            val rnModuleName: String = getSharedPreferences("rn_config", MODE_PRIVATE).getString(
                "js_module_name",
                "rn_module1"
            ) ?: "rn_module1"
            return File(filesDir, "rn_modules/$rnModuleName/index.android.bundle").absolutePath
        }

    override val reactHost: ReactHost by lazy {
        getDefaultReactHost(
            context = applicationContext,
            packageList =
                PackageList(this).packages.apply {
                    // Packages that cannot be autolinked yet can be added manually here, for example:
                    // add(MyReactNativePackage())
                },
            useDevSupport = false,
            jsBundleFilePath = jsBundleFilePath,
        )
    }

    override fun onCreate() {
        super.onCreate()
        loadReactNative(this)
    }
}

加载其他bundle只需要进入native加载bundle页面(跳转时需要传递对应的rn_module[i]参数)修改js_module_name的值,然后下载bundle,放入对应目录,然后进入rn的页面即可。

我想搞一个学习react native的知识星球, 互相学习,互相交流。没啥经验,做的不好各位大佬轻点喷,欢迎大家加入!