@bm-fe/react-native-multi-bundle 1.0.0-beta.1 → 1.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INTEGRATION.md +42 -65
- package/android/moduleloader/build.gradle +93 -0
- package/android/moduleloader/src/main/AndroidManifest.xml +4 -0
- package/android/moduleloader/src/main/AndroidManifestNew.xml +3 -0
- package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderModule.kt +449 -0
- package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderPackage.kt +24 -0
- package/package.json +9 -2
- package/react-native.config.js +23 -0
- package/src/multi-bundle/LocalBundleManager.ts +25 -13
- package/src/multi-bundle/README.md +1 -0
package/INTEGRATION.md
CHANGED
|
@@ -55,93 +55,70 @@ npx react-native start
|
|
|
55
55
|
|
|
56
56
|
## Native 模块集成
|
|
57
57
|
|
|
58
|
-
### Android
|
|
58
|
+
### Android(自动链接,无需手动配置)
|
|
59
59
|
|
|
60
|
-
####
|
|
60
|
+
#### 自动链接工作原理
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
安装包后,React Native CLI 会自动:
|
|
63
|
+
1. 读取包中的 `react-native.config.js` 配置
|
|
64
|
+
2. 将 `ModuleLoaderPackage` 添加到 `PackageList` 中
|
|
65
|
+
3. 在构建时自动集成原生模块
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
- `ModuleLoaderPackage.kt` → `android/app/src/main/java/com/yourpackage/ModuleLoaderPackage.kt`
|
|
67
|
+
#### 验证自动链接
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
安装包后,可以通过以下命令验证自动链接是否生效:
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
```bash
|
|
72
|
+
npx react-native config
|
|
73
|
+
```
|
|
70
74
|
|
|
71
|
-
|
|
75
|
+
在输出中应该能看到 `@bm-fe/react-native-multi-bundle` 的配置信息。
|
|
72
76
|
|
|
73
|
-
|
|
77
|
+
#### 确保 assets 目录存在
|
|
74
78
|
|
|
75
|
-
|
|
79
|
+
确保 `android/app/src/main/assets/` 目录存在,bundle 文件将放在这里:
|
|
76
80
|
|
|
77
|
-
```
|
|
78
|
-
|
|
81
|
+
```bash
|
|
82
|
+
mkdir -p android/app/src/main/assets/modules
|
|
83
|
+
```
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
import com.yourpackage.ModuleLoaderPackage
|
|
82
|
-
import com.facebook.react.PackageList
|
|
83
|
-
import com.facebook.react.ReactApplication
|
|
84
|
-
import com.facebook.react.ReactHost
|
|
85
|
-
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
|
|
86
|
-
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
|
85
|
+
#### 手动集成(可选,仅在自动链接不工作时使用)
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
override val reactHost: ReactHost by lazy {
|
|
90
|
-
getDefaultReactHost(
|
|
91
|
-
context = applicationContext,
|
|
92
|
-
packageList =
|
|
93
|
-
PackageList(this).packages.apply {
|
|
94
|
-
// Packages that cannot be autolinked yet can be added manually here
|
|
95
|
-
add(ModuleLoaderPackage())
|
|
96
|
-
},
|
|
97
|
-
)
|
|
98
|
-
}
|
|
87
|
+
如果自动链接不工作(极少数情况),可以手动集成:
|
|
99
88
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
loadReactNative(this)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
```
|
|
89
|
+
<details>
|
|
90
|
+
<summary>点击展开手动集成步骤</summary>
|
|
106
91
|
|
|
107
|
-
#####
|
|
92
|
+
##### 1. 复制 Native 模块文件
|
|
108
93
|
|
|
109
|
-
|
|
94
|
+
将以下文件从 `node_modules/@bm-fe/react-native-multi-bundle/templates/native/android/` 复制到你的 Android 项目:
|
|
110
95
|
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
- `ModuleLoaderModule.kt` → `android/app/src/main/java/com/yourpackage/ModuleLoaderModule.kt`
|
|
97
|
+
- `ModuleLoaderPackage.kt` → `android/app/src/main/java/com/yourpackage/ModuleLoaderPackage.kt`
|
|
113
98
|
|
|
114
|
-
|
|
99
|
+
**重要**:修改文件中的包名 `com.yourpackage` 为你的实际包名。
|
|
100
|
+
|
|
101
|
+
##### 2. 注册 ModuleLoaderPackage
|
|
102
|
+
|
|
103
|
+
在 `MainApplication.kt` 中添加:
|
|
104
|
+
|
|
105
|
+
```kotlin
|
|
115
106
|
import com.yourpackage.ModuleLoaderPackage
|
|
116
|
-
import com.facebook.react.ReactApplication
|
|
117
|
-
import com.facebook.react.ReactPackage
|
|
118
|
-
import com.facebook.react.PackageList
|
|
119
|
-
import com.facebook.react.ReactNativeHost
|
|
120
|
-
import com.facebook.react.defaults.DefaultReactNativeHost
|
|
121
107
|
|
|
122
108
|
class MainApplication : Application(), ReactApplication {
|
|
123
|
-
override val
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
|
133
|
-
// ... 其他配置
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ... 其他代码
|
|
109
|
+
override val reactHost: ReactHost by lazy {
|
|
110
|
+
getDefaultReactHost(
|
|
111
|
+
context = applicationContext,
|
|
112
|
+
packageList = PackageList(this).packages.apply {
|
|
113
|
+
add(ModuleLoaderPackage()) // 手动添加
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
// ...
|
|
137
118
|
}
|
|
138
119
|
```
|
|
139
120
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
#### 3. 确保 assets 目录存在
|
|
143
|
-
|
|
144
|
-
确保 `android/app/src/main/assets/` 目录存在,bundle 文件将放在这里。
|
|
121
|
+
</details>
|
|
145
122
|
|
|
146
123
|
### iOS
|
|
147
124
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
def isNewArchitectureEnabled() {
|
|
2
|
+
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
apply plugin: "com.android.library"
|
|
6
|
+
apply plugin: "org.jetbrains.kotlin.android"
|
|
7
|
+
|
|
8
|
+
if (isNewArchitectureEnabled()) {
|
|
9
|
+
apply plugin: "com.facebook.react"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def getExtOrDefault(name) {
|
|
13
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["ModuleLoader_" + name]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def getExtOrIntegerDefault(name) {
|
|
17
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ModuleLoader_" + name]).toInteger()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
def supportsNamespace() {
|
|
21
|
+
def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
|
|
22
|
+
def major = parsed[0].toInteger()
|
|
23
|
+
def minor = parsed[1].toInteger()
|
|
24
|
+
|
|
25
|
+
// Namespace support was added in 7.3.0
|
|
26
|
+
return (major == 7 && minor >= 3) || major >= 8
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
android {
|
|
30
|
+
if (supportsNamespace()) {
|
|
31
|
+
namespace "com.bitmart.exchange.module.loader"
|
|
32
|
+
|
|
33
|
+
sourceSets {
|
|
34
|
+
main {
|
|
35
|
+
manifest.srcFile "src/main/AndroidManifestNew.xml"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
41
|
+
|
|
42
|
+
defaultConfig {
|
|
43
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
44
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
45
|
+
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
buildFeatures {
|
|
49
|
+
buildConfig true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
buildTypes {
|
|
53
|
+
release {
|
|
54
|
+
minifyEnabled false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
lintOptions {
|
|
59
|
+
disable "GradleCompatible"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
compileOptions {
|
|
63
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
64
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
kotlinOptions {
|
|
68
|
+
jvmTarget = "17"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
repositories {
|
|
73
|
+
mavenCentral()
|
|
74
|
+
google()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def isInDevelopment = project.hasProperty('ModuleLoader_development') && project.property('ModuleLoader_development') == "true"
|
|
78
|
+
|
|
79
|
+
dependencies {
|
|
80
|
+
if (isInDevelopment) {
|
|
81
|
+
implementation("com.facebook.react:react-android:0.71.+")
|
|
82
|
+
} else {
|
|
83
|
+
// For < 0.71, this will be from the local maven repo
|
|
84
|
+
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
|
|
85
|
+
//noinspection GradleDynamicVersion
|
|
86
|
+
implementation "com.facebook.react:react-native:+"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Kotlin Coroutines
|
|
90
|
+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
|
91
|
+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
|
92
|
+
}
|
|
93
|
+
|
package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderModule.kt
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
package com.bitmart.exchange.module.loader
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
7
|
+
import com.facebook.react.bridge.ReactMethod
|
|
8
|
+
import com.facebook.react.bridge.Arguments
|
|
9
|
+
import com.facebook.react.bridge.JSBundleLoader
|
|
10
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
11
|
+
import kotlinx.coroutines.CoroutineScope
|
|
12
|
+
import kotlinx.coroutines.Dispatchers
|
|
13
|
+
import kotlinx.coroutines.SupervisorJob
|
|
14
|
+
import kotlinx.coroutines.launch
|
|
15
|
+
import kotlinx.coroutines.withContext
|
|
16
|
+
import java.io.BufferedReader
|
|
17
|
+
import java.io.File
|
|
18
|
+
import java.io.FileOutputStream
|
|
19
|
+
import java.io.FileReader
|
|
20
|
+
import java.io.IOException
|
|
21
|
+
import java.io.InputStream
|
|
22
|
+
import java.io.InputStreamReader
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 简单的 Bundle 加载模块
|
|
26
|
+
* 用于在同一个 JS 运行时中动态加载子 bundle
|
|
27
|
+
*
|
|
28
|
+
* 新架构下通过反射获取 ReactInstance,然后调用 loadJSBundle 方法
|
|
29
|
+
* 注意:ReactInstance 是 internal 类,必须完全通过反射操作
|
|
30
|
+
*/
|
|
31
|
+
@ReactModule(name = ModuleLoaderModule.NAME)
|
|
32
|
+
class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
33
|
+
|
|
34
|
+
companion object {
|
|
35
|
+
const val NAME = "ModuleLoader"
|
|
36
|
+
private const val TAG = "ModuleLoaderModule"
|
|
37
|
+
|
|
38
|
+
// 缓存反射获取的类和方法,避免重复反射
|
|
39
|
+
private var reactHostImplClass: Class<*>? = null
|
|
40
|
+
private var reactInstanceClass: Class<*>? = null
|
|
41
|
+
|
|
42
|
+
init {
|
|
43
|
+
try {
|
|
44
|
+
reactHostImplClass = Class.forName("com.facebook.react.runtime.ReactHostImpl")
|
|
45
|
+
reactInstanceClass = Class.forName("com.facebook.react.runtime.ReactInstance")
|
|
46
|
+
} catch (e: ClassNotFoundException) {
|
|
47
|
+
Log.w(TAG, "New architecture classes not found, will use old architecture")
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 记录已加载的 bundle,避免重复加载
|
|
53
|
+
private val loadedBundles = mutableSetOf<String>()
|
|
54
|
+
|
|
55
|
+
// 协程作用域,用于异步操作
|
|
56
|
+
private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
57
|
+
|
|
58
|
+
override fun getName(): String = NAME
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 加载子 bundle
|
|
62
|
+
*
|
|
63
|
+
* @param bundleId 模块 ID(如 "home", "details", "settings")
|
|
64
|
+
* @param bundlePath bundle 文件路径(如 "modules/home.bundle")
|
|
65
|
+
* @param promise 加载结果回调
|
|
66
|
+
*/
|
|
67
|
+
@ReactMethod
|
|
68
|
+
fun loadBusinessBundle(bundleId: String, bundlePath: String, promise: Promise) {
|
|
69
|
+
Log.d(TAG, "loadBusinessBundle: bundleId=$bundleId, bundlePath=$bundlePath")
|
|
70
|
+
|
|
71
|
+
// 已经加载过的 bundle,直接返回成功
|
|
72
|
+
if (loadedBundles.contains(bundleId)) {
|
|
73
|
+
Log.d(TAG, "Bundle already loaded: $bundleId")
|
|
74
|
+
val result = Arguments.createMap().apply {
|
|
75
|
+
putBoolean("success", true)
|
|
76
|
+
}
|
|
77
|
+
promise.resolve(result)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// 获取 ReactApplication
|
|
83
|
+
val application = reactContext.applicationContext as? android.app.Application
|
|
84
|
+
val reactApplication = application as? com.facebook.react.ReactApplication
|
|
85
|
+
|
|
86
|
+
if (reactApplication == null) {
|
|
87
|
+
Log.e(TAG, "Application is not a ReactApplication")
|
|
88
|
+
val result = Arguments.createMap().apply {
|
|
89
|
+
putBoolean("success", false)
|
|
90
|
+
putString("errorMessage", "Application is not a ReactApplication")
|
|
91
|
+
}
|
|
92
|
+
promise.resolve(result)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
val reactHost = reactApplication.reactHost
|
|
97
|
+
|
|
98
|
+
// 构建 assets 路径
|
|
99
|
+
val assetUrl = "assets://$bundlePath"
|
|
100
|
+
Log.d(TAG, "Loading bundle from: $assetUrl")
|
|
101
|
+
|
|
102
|
+
// 创建 JSBundleLoader
|
|
103
|
+
val bundleLoader = JSBundleLoader.createAssetLoader(
|
|
104
|
+
reactContext,
|
|
105
|
+
assetUrl,
|
|
106
|
+
false // 异步加载
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
// 检查是否是新架构(ReactHostImpl)
|
|
110
|
+
val hostImplClass = reactHostImplClass
|
|
111
|
+
if (reactHost != null && hostImplClass != null && hostImplClass.isInstance(reactHost)) {
|
|
112
|
+
loadBundleViaReflection(reactHost, bundleLoader, bundleId, promise)
|
|
113
|
+
} else {
|
|
114
|
+
// 旧架构回退方案:使用 CatalystInstance
|
|
115
|
+
Log.d(TAG, "Falling back to CatalystInstance (old architecture)")
|
|
116
|
+
loadBundleWithCatalystInstance(bundleId, bundlePath, promise)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
} catch (e: Exception) {
|
|
120
|
+
Log.e(TAG, "Failed to load bundle: $bundleId", e)
|
|
121
|
+
val result = Arguments.createMap().apply {
|
|
122
|
+
putBoolean("success", false)
|
|
123
|
+
putString("errorMessage", e.message ?: "Unknown error")
|
|
124
|
+
}
|
|
125
|
+
promise.resolve(result)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 新架构方案:完全通过反射获取 ReactInstance 并加载 bundle
|
|
131
|
+
*/
|
|
132
|
+
private fun loadBundleViaReflection(
|
|
133
|
+
reactHost: Any,
|
|
134
|
+
bundleLoader: JSBundleLoader,
|
|
135
|
+
bundleId: String,
|
|
136
|
+
promise: Promise
|
|
137
|
+
) {
|
|
138
|
+
try {
|
|
139
|
+
val hostImplClass = reactHostImplClass ?: throw Exception("ReactHostImpl class not found")
|
|
140
|
+
val instanceClass = reactInstanceClass ?: throw Exception("ReactInstance class not found")
|
|
141
|
+
|
|
142
|
+
// 通过反射获取 private 的 reactInstance 字段
|
|
143
|
+
val reactInstanceField = hostImplClass.getDeclaredField("reactInstance")
|
|
144
|
+
reactInstanceField.isAccessible = true
|
|
145
|
+
val reactInstance = reactInstanceField.get(reactHost)
|
|
146
|
+
|
|
147
|
+
if (reactInstance == null) {
|
|
148
|
+
Log.e(TAG, "ReactInstance is null")
|
|
149
|
+
val result = Arguments.createMap().apply {
|
|
150
|
+
putBoolean("success", false)
|
|
151
|
+
putString("errorMessage", "ReactInstance not available")
|
|
152
|
+
}
|
|
153
|
+
promise.resolve(result)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 通过反射调用 ReactInstance.loadJSBundle 方法
|
|
158
|
+
val loadJSBundleMethod = instanceClass.getMethod("loadJSBundle", JSBundleLoader::class.java)
|
|
159
|
+
loadJSBundleMethod.invoke(reactInstance, bundleLoader)
|
|
160
|
+
|
|
161
|
+
// 标记为已加载
|
|
162
|
+
loadedBundles.add(bundleId)
|
|
163
|
+
Log.d(TAG, "Bundle loaded successfully via ReactInstance: $bundleId")
|
|
164
|
+
|
|
165
|
+
val result = Arguments.createMap().apply {
|
|
166
|
+
putBoolean("success", true)
|
|
167
|
+
}
|
|
168
|
+
promise.resolve(result)
|
|
169
|
+
|
|
170
|
+
} catch (e: Exception) {
|
|
171
|
+
Log.e(TAG, "Failed to load bundle via ReactInstance: $bundleId", e)
|
|
172
|
+
val result = Arguments.createMap().apply {
|
|
173
|
+
putBoolean("success", false)
|
|
174
|
+
putString("errorMessage", e.message ?: "Unknown error")
|
|
175
|
+
}
|
|
176
|
+
promise.resolve(result)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 旧架构回退方案:使用 CatalystInstance 加载 bundle
|
|
182
|
+
*/
|
|
183
|
+
@Suppress("DEPRECATION")
|
|
184
|
+
private fun loadBundleWithCatalystInstance(bundleId: String, bundlePath: String, promise: Promise) {
|
|
185
|
+
try {
|
|
186
|
+
val catalystInstance = reactContext.catalystInstance
|
|
187
|
+
if (catalystInstance == null) {
|
|
188
|
+
Log.e(TAG, "CatalystInstance is null")
|
|
189
|
+
val result = Arguments.createMap().apply {
|
|
190
|
+
putBoolean("success", false)
|
|
191
|
+
putString("errorMessage", "CatalystInstance not available")
|
|
192
|
+
}
|
|
193
|
+
promise.resolve(result)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
val assetPath = "assets://$bundlePath"
|
|
198
|
+
catalystInstance.loadScriptFromAssets(
|
|
199
|
+
reactContext.assets,
|
|
200
|
+
assetPath,
|
|
201
|
+
false
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
loadedBundles.add(bundleId)
|
|
205
|
+
Log.d(TAG, "Bundle loaded successfully via CatalystInstance: $bundleId")
|
|
206
|
+
|
|
207
|
+
val result = Arguments.createMap().apply {
|
|
208
|
+
putBoolean("success", true)
|
|
209
|
+
}
|
|
210
|
+
promise.resolve(result)
|
|
211
|
+
} catch (e: Exception) {
|
|
212
|
+
Log.e(TAG, "Failed to load bundle via CatalystInstance: $bundleId", e)
|
|
213
|
+
val result = Arguments.createMap().apply {
|
|
214
|
+
putBoolean("success", false)
|
|
215
|
+
putString("errorMessage", e.message ?: "Unknown error")
|
|
216
|
+
}
|
|
217
|
+
promise.resolve(result)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 检查 bundle 是否已加载
|
|
223
|
+
*/
|
|
224
|
+
@ReactMethod
|
|
225
|
+
fun isBundleLoaded(bundleId: String, promise: Promise) {
|
|
226
|
+
promise.resolve(loadedBundles.contains(bundleId))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 获取已加载的 bundle 列表
|
|
231
|
+
*/
|
|
232
|
+
@ReactMethod
|
|
233
|
+
fun getLoadedBundles(promise: Promise) {
|
|
234
|
+
val array = Arguments.createArray()
|
|
235
|
+
loadedBundles.forEach { array.pushString(it) }
|
|
236
|
+
promise.resolve(array)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ==================== Bundle Manifest Support ====================
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 获取当前 bundle manifest 文件路径
|
|
243
|
+
* 优先从 assets-base 目录获取,如果不存在则返回 null
|
|
244
|
+
*/
|
|
245
|
+
@ReactMethod
|
|
246
|
+
fun getCurrentBundleManifest(promise: Promise) {
|
|
247
|
+
try {
|
|
248
|
+
|
|
249
|
+
// 确保 CodePush 目录已初始化
|
|
250
|
+
ensureCodePushDirectoryInitialized()
|
|
251
|
+
|
|
252
|
+
// 从 assets-base 目录获取 manifest
|
|
253
|
+
val codePushDir = "${reactContext.filesDir.absolutePath}/CodePush"
|
|
254
|
+
val assetsBaseManifestPath = "$codePushDir/assets-base/bundle-manifest.json"
|
|
255
|
+
val assetsBaseManifestFile = File(assetsBaseManifestPath)
|
|
256
|
+
|
|
257
|
+
if (assetsBaseManifestFile.exists()) {
|
|
258
|
+
promise.resolve(assetsBaseManifestPath)
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 如果都不存在,返回 null
|
|
263
|
+
promise.resolve(null)
|
|
264
|
+
} catch (e: Exception) {
|
|
265
|
+
Log.e(TAG, "Failed to get current bundle manifest: ${e.message}")
|
|
266
|
+
promise.reject("MANIFEST_ERROR", "Failed to get manifest path", e)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 获取当前 bundle manifest 文件内容
|
|
272
|
+
* 使用协程在 IO 线程读取文件内容
|
|
273
|
+
*/
|
|
274
|
+
@ReactMethod
|
|
275
|
+
fun getCurrentBundleManifestContent(promise: Promise) {
|
|
276
|
+
moduleScope.launch {
|
|
277
|
+
try {
|
|
278
|
+
val manifestContent = withContext(Dispatchers.IO) {
|
|
279
|
+
// 确保 CodePush 目录已初始化
|
|
280
|
+
ensureCodePushDirectoryInitialized()
|
|
281
|
+
|
|
282
|
+
// 从 assets-base 目录读取 manifest
|
|
283
|
+
val codePushDir = "${reactContext.filesDir.absolutePath}/CodePush"
|
|
284
|
+
val assetsBaseManifestPath = "$codePushDir/assets-base/bundle-manifest.json"
|
|
285
|
+
val assetsBaseManifestFile = File(assetsBaseManifestPath)
|
|
286
|
+
|
|
287
|
+
if (assetsBaseManifestFile.exists()) {
|
|
288
|
+
val content = readFileContent(assetsBaseManifestFile)
|
|
289
|
+
if (content != null) {
|
|
290
|
+
Log.d(TAG, "✓ Read bundle manifest from assets-base directory")
|
|
291
|
+
return@withContext content
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 如果都不存在,返回 null
|
|
296
|
+
null
|
|
297
|
+
}
|
|
298
|
+
promise.resolve(manifestContent)
|
|
299
|
+
} catch (e: Exception) {
|
|
300
|
+
Log.e(TAG, "Failed to get bundle manifest content: ${e.message}")
|
|
301
|
+
promise.reject("MANIFEST_CONTENT_ERROR", "Failed to read manifest content", e)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 确保 CodePush 目录已从 assets 初始化
|
|
308
|
+
*/
|
|
309
|
+
private fun ensureCodePushDirectoryInitialized() {
|
|
310
|
+
try {
|
|
311
|
+
val codePushDir = "${reactContext.filesDir.absolutePath}/CodePush"
|
|
312
|
+
val assetsBaseDir = "$codePushDir/assets-base"
|
|
313
|
+
|
|
314
|
+
// 检查 assets-base 目录是否有 bundle-manifest.json 和 modules
|
|
315
|
+
val manifestFile = File("$assetsBaseDir/bundle-manifest.json")
|
|
316
|
+
val modulesDir = File("$assetsBaseDir/modules")
|
|
317
|
+
|
|
318
|
+
// 如果任一不存在,从 assets 复制
|
|
319
|
+
if (!manifestFile.exists() || !modulesDir.exists()) {
|
|
320
|
+
Log.d(TAG, "Initializing CodePush directory from assets...")
|
|
321
|
+
copyAssetsToCodePushDirectory(codePushDir)
|
|
322
|
+
}
|
|
323
|
+
} catch (e: Exception) {
|
|
324
|
+
Log.e(TAG, "Failed to initialize CodePush directory: ${e.message}")
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* 从 assets 复制 bundle-manifest.json 和 modules 目录到 CodePush 目录
|
|
330
|
+
*/
|
|
331
|
+
private fun copyAssetsToCodePushDirectory(codePushDir: String) {
|
|
332
|
+
try {
|
|
333
|
+
// 创建 assets-base 目录
|
|
334
|
+
val defaultPackageDir = "$codePushDir/assets-base"
|
|
335
|
+
val defaultPackageDirFile = File(defaultPackageDir)
|
|
336
|
+
if (!defaultPackageDirFile.exists()) {
|
|
337
|
+
defaultPackageDirFile.mkdirs()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 1. 复制 bundle-manifest.json
|
|
341
|
+
try {
|
|
342
|
+
val manifestAssetPath = "bundle-manifest.json"
|
|
343
|
+
val manifestInput = reactContext.assets.open(manifestAssetPath)
|
|
344
|
+
val manifestOutput = File("$defaultPackageDir/bundle-manifest.json")
|
|
345
|
+
copyInputStreamToFile(manifestInput, manifestOutput)
|
|
346
|
+
Log.d(TAG, "✓ Copied bundle-manifest.json to CodePush directory")
|
|
347
|
+
} catch (e: IOException) {
|
|
348
|
+
Log.w(TAG, "Warning: Could not copy bundle-manifest.json: ${e.message}")
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 2. 复制 modules 目录
|
|
352
|
+
try {
|
|
353
|
+
val modulesAssetPath = "modules"
|
|
354
|
+
val modulesOutputDir = File("$defaultPackageDir/modules")
|
|
355
|
+
if (!modulesOutputDir.exists()) {
|
|
356
|
+
modulesOutputDir.mkdirs()
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
copyAssetFolder(modulesAssetPath, modulesOutputDir.absolutePath)
|
|
360
|
+
Log.d(TAG, "✓ Copied modules directory to CodePush directory")
|
|
361
|
+
} catch (e: IOException) {
|
|
362
|
+
Log.w(TAG, "Warning: Could not copy modules directory: ${e.message}")
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
} catch (e: Exception) {
|
|
366
|
+
Log.e(TAG, "Failed to copy assets to CodePush directory: ${e.message}")
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 递归复制 asset 文件夹
|
|
372
|
+
*/
|
|
373
|
+
@Throws(IOException::class)
|
|
374
|
+
private fun copyAssetFolder(assetPath: String, targetPath: String) {
|
|
375
|
+
val assets = reactContext.assets.list(assetPath)
|
|
376
|
+
if (assets.isNullOrEmpty()) {
|
|
377
|
+
// 这是一个文件,不是目录
|
|
378
|
+
val input = reactContext.assets.open(assetPath)
|
|
379
|
+
val output = File(targetPath)
|
|
380
|
+
copyInputStreamToFile(input, output)
|
|
381
|
+
} else {
|
|
382
|
+
// 这是一个目录
|
|
383
|
+
val dir = File(targetPath)
|
|
384
|
+
if (!dir.exists()) {
|
|
385
|
+
dir.mkdirs()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (asset in assets) {
|
|
389
|
+
val subAssetPath = "$assetPath/$asset"
|
|
390
|
+
val subTargetPath = "$targetPath/$asset"
|
|
391
|
+
copyAssetFolder(subAssetPath, subTargetPath)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 将 InputStream 复制到文件
|
|
398
|
+
*/
|
|
399
|
+
@Throws(IOException::class)
|
|
400
|
+
private fun copyInputStreamToFile(input: InputStream, output: File) {
|
|
401
|
+
var outputStream: FileOutputStream? = null
|
|
402
|
+
try {
|
|
403
|
+
outputStream = FileOutputStream(output)
|
|
404
|
+
val buffer = ByteArray(4096)
|
|
405
|
+
var bytesRead: Int
|
|
406
|
+
while (input.read(buffer).also { bytesRead = it } != -1) {
|
|
407
|
+
outputStream.write(buffer, 0, bytesRead)
|
|
408
|
+
}
|
|
409
|
+
} finally {
|
|
410
|
+
try {
|
|
411
|
+
input.close()
|
|
412
|
+
} catch (e: IOException) {
|
|
413
|
+
// Ignore
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
outputStream?.close()
|
|
417
|
+
} catch (e: IOException) {
|
|
418
|
+
// Ignore
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* 读取文件内容
|
|
425
|
+
*/
|
|
426
|
+
private fun readFileContent(file: File): String? {
|
|
427
|
+
var reader: BufferedReader? = null
|
|
428
|
+
return try {
|
|
429
|
+
reader = BufferedReader(FileReader(file))
|
|
430
|
+
val content = StringBuilder()
|
|
431
|
+
var line: String?
|
|
432
|
+
while (reader.readLine().also { line = it } != null) {
|
|
433
|
+
content.append(line).append("\n")
|
|
434
|
+
}
|
|
435
|
+
content.toString()
|
|
436
|
+
} catch (e: IOException) {
|
|
437
|
+
Log.e(TAG, "Failed to read file: ${file.absolutePath} - ${e.message}")
|
|
438
|
+
null
|
|
439
|
+
} finally {
|
|
440
|
+
try {
|
|
441
|
+
reader?.close()
|
|
442
|
+
} catch (e: IOException) {
|
|
443
|
+
// Ignore
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
|
package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderPackage.kt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package com.bitmart.exchange.module.loader
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ModuleLoader ReactPackage
|
|
10
|
+
* 注册 ModuleLoaderModule 到 React Native
|
|
11
|
+
*/
|
|
12
|
+
class ModuleLoaderPackage : ReactPackage {
|
|
13
|
+
@Suppress("DEPRECATION")
|
|
14
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
15
|
+
return listOf(ModuleLoaderModule(reactContext))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Suppress("DEPRECATION")
|
|
19
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
20
|
+
return emptyList()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bm-fe/react-native-multi-bundle",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
4
|
"description": "React Native 多 Bundle 系统 - 支持模块按需加载和独立更新",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"src",
|
|
21
21
|
"scripts",
|
|
22
22
|
"templates",
|
|
23
|
+
"android/moduleloader",
|
|
24
|
+
"react-native.config.js",
|
|
23
25
|
"README.md",
|
|
24
26
|
"INTEGRATION.md",
|
|
25
27
|
"LICENSE"
|
|
@@ -35,9 +37,11 @@
|
|
|
35
37
|
"version:minor": "standard-version --release-as minor",
|
|
36
38
|
"version:major": "standard-version --release-as major",
|
|
37
39
|
"version:beta": "npm version prerelease --preid=beta --no-git-tag-version --no-scripts",
|
|
40
|
+
"prepublishOnly": "rm -rf android/moduleloader/build",
|
|
38
41
|
"publish:beta": "npm publish --tag beta",
|
|
39
42
|
"release:beta": "npm run version:beta && npm run publish:beta",
|
|
40
|
-
"release": "npm run version && npm publish"
|
|
43
|
+
"release": "npm run version && npm publish",
|
|
44
|
+
"android": "react-native run-android"
|
|
41
45
|
},
|
|
42
46
|
"peerDependencies": {
|
|
43
47
|
"react": ">=18.0.0",
|
|
@@ -59,6 +63,7 @@
|
|
|
59
63
|
"@react-native-community/cli-platform-ios": "20.0.2",
|
|
60
64
|
"@react-native/babel-preset": "0.82.1",
|
|
61
65
|
"@react-native/eslint-config": "0.82.1",
|
|
66
|
+
"@react-native/gradle-plugin": "0.82.1",
|
|
62
67
|
"@react-native/metro-config": "0.82.1",
|
|
63
68
|
"@react-native/typescript-config": "0.82.1",
|
|
64
69
|
"@rnx-kit/babel-preset-metro-react-native": "^3.0.0",
|
|
@@ -73,6 +78,8 @@
|
|
|
73
78
|
"patch-package": "^8.0.1",
|
|
74
79
|
"postinstall-postinstall": "^2.1.0",
|
|
75
80
|
"prettier": "3.6.2",
|
|
81
|
+
"react-native": "^0.82.1",
|
|
82
|
+
"react": "19.1.1",
|
|
76
83
|
"react-test-renderer": "^19.1.1",
|
|
77
84
|
"standard-version": "^9.5.0",
|
|
78
85
|
"typescript": "5.9.3"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native 配置文件
|
|
3
|
+
* 用于配置原生模块的自动链接
|
|
4
|
+
*
|
|
5
|
+
* 当此包被安装到其他 RN 项目时,React Native CLI 会自动读取此配置
|
|
6
|
+
* 并将 ModuleLoaderPackage 自动注册到 PackageList 中
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
// 配置原生模块自动链接
|
|
11
|
+
dependency: {
|
|
12
|
+
platforms: {
|
|
13
|
+
android: {
|
|
14
|
+
sourceDir: './android/moduleloader',
|
|
15
|
+
packageImportPath: 'import com.bitmart.exchange.module.loader.ModuleLoaderPackage;',
|
|
16
|
+
packageInstance: 'new ModuleLoaderPackage()',
|
|
17
|
+
buildTypes: ['debug', 'release'],
|
|
18
|
+
},
|
|
19
|
+
ios: null, // iOS 暂未实现
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
@@ -61,9 +61,31 @@ function convertNativeManifestToBundleManifest(nativeManifest: any): BundleManif
|
|
|
61
61
|
*/
|
|
62
62
|
async function getCurrentBundleManifest(): Promise<BundleManifest> {
|
|
63
63
|
// 生产环境:从 Native 模块获取 manifest
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// if (!__DEV__) {
|
|
65
|
+
// try {
|
|
66
|
+
// const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
|
|
67
|
+
// if (nativeManifest) {
|
|
68
|
+
// return convertNativeManifestToBundleManifest(nativeManifest);
|
|
69
|
+
// } else {
|
|
70
|
+
// // Native 模块返回 null 或 undefined
|
|
71
|
+
// throw new Error(
|
|
72
|
+
// '[LocalBundleManager] Native module returned null manifest. ' +
|
|
73
|
+
// 'Please ensure bundle-manifest.json exists in the app bundle.'
|
|
74
|
+
// );
|
|
75
|
+
// }
|
|
76
|
+
// } catch (error) {
|
|
77
|
+
// console.error(
|
|
78
|
+
// `[LocalBundleManager] Failed to get manifest from Native: ${error}`
|
|
79
|
+
// );
|
|
80
|
+
// // 生产环境无法获取 manifest 时抛出异常
|
|
81
|
+
// throw new Error(
|
|
82
|
+
// `[LocalBundleManager] Failed to get manifest from Native module: ${error}`
|
|
83
|
+
// );
|
|
84
|
+
// }
|
|
85
|
+
// }
|
|
86
|
+
console.log("ReactNativeJS","nativeManifest getCurrentBundleManifestContent")
|
|
87
|
+
const nativeManifest = await NativeModules.ModuleLoader.getCurrentBundleManifestContent();
|
|
88
|
+
console.log("ReactNativeJS","nativeManifest "+nativeManifest)
|
|
67
89
|
if (nativeManifest) {
|
|
68
90
|
return convertNativeManifestToBundleManifest(nativeManifest);
|
|
69
91
|
} else {
|
|
@@ -73,16 +95,6 @@ async function getCurrentBundleManifest(): Promise<BundleManifest> {
|
|
|
73
95
|
'Please ensure bundle-manifest.json exists in the app bundle.'
|
|
74
96
|
);
|
|
75
97
|
}
|
|
76
|
-
} catch (error) {
|
|
77
|
-
console.error(
|
|
78
|
-
`[LocalBundleManager] Failed to get manifest from Native: ${error}`
|
|
79
|
-
);
|
|
80
|
-
// 生产环境无法获取 manifest 时抛出异常
|
|
81
|
-
throw new Error(
|
|
82
|
-
`[LocalBundleManager] Failed to get manifest from Native module: ${error}`
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
98
|
|
|
87
99
|
// 开发环境:从开发服务器获取 manifest
|
|
88
100
|
const config = getGlobalConfig();
|