React Native架构讨论
前言
首先说为什么大多数公司都喜欢使用React Native进行跨平台开发,大家有什么其他原因可以评论,如果合理,我会更新到文章当中。
- 降低成本,不需要招聘很多客户端(
android、ios、harmonyos) - 热更新
- 招聘容易,前端程序员相对于客户端更好招聘
- 横向扩展
这篇文章主要讨论React Native如何去支持频繁迭代的业务和海量的业务。
React Native架构优势
整体架构
有一个需要注意的是随着业务增加,变更包含原生代码的npm包依赖,原生需要更新,余下所有rn module也需要更新,所以需要创建一个rn版本和原生app版本的一个集合关系。
新建一个业务
新业务一般单独放在独立的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种
- 纯
js hermes bytecodeStatic 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;
}
SHUnit为Static Hermes Unit,math.ts就是一个SHUnit.
加载顺序: Static Hermes > HBC > js
多rn module加载
通常将rn的bundle代码和资源图片放在一起类似如下
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的知识星球, 互相学习,互相交流。没啥经验,做的不好各位大佬轻点喷,欢迎大家加入!
![]()