@bm-fe/react-native-multi-bundle 1.0.0-beta.1 → 1.0.0-beta.2

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 CHANGED
@@ -55,93 +55,70 @@ npx react-native start
55
55
 
56
56
  ## Native 模块集成
57
57
 
58
- ### Android
58
+ ### Android(自动链接,无需手动配置)
59
59
 
60
- #### 1. 复制 Native 模块文件
60
+ #### 自动链接工作原理
61
61
 
62
- 将以下文件从 `templates/native/android/` 复制到你的 Android 项目:
62
+ 安装包后,React Native CLI 会自动:
63
+ 1. 读取包中的 `react-native.config.js` 配置
64
+ 2. 将 `ModuleLoaderPackage` 添加到 `PackageList` 中
65
+ 3. 在构建时自动集成原生模块
63
66
 
64
- - `ModuleLoaderModule.kt` → `android/app/src/main/java/com/yourpackage/ModuleLoaderModule.kt`
65
- - `ModuleLoaderPackage.kt` → `android/app/src/main/java/com/yourpackage/ModuleLoaderPackage.kt`
67
+ #### 验证自动链接
66
68
 
67
- **重要**:修改文件中的包名 `com.yourpackage` 为你的实际包名。
69
+ 安装包后,可以通过以下命令验证自动链接是否生效:
68
70
 
69
- #### 2. 注册 ModuleLoaderPackage
71
+ ```bash
72
+ npx react-native config
73
+ ```
70
74
 
71
- 根据你使用的 React Native 架构版本,选择对应的方式:
75
+ 在输出中应该能看到 `@bm-fe/react-native-multi-bundle` 的配置信息。
72
76
 
73
- ##### 方式一:新架构(React Native 0.82+,使用 ReactHost)
77
+ #### 确保 assets 目录存在
74
78
 
75
- 如果你的 `MainApplication.kt` 使用 `ReactHost`(新架构),在 `PackageList` 中添加:
79
+ 确保 `android/app/src/main/assets/` 目录存在,bundle 文件将放在这里:
76
80
 
77
- ```kotlin
78
- package com.yourpackage
81
+ ```bash
82
+ mkdir -p android/app/src/main/assets/modules
83
+ ```
79
84
 
80
- import android.app.Application
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
- class MainApplication : Application(), ReactApplication {
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
- override fun onCreate() {
101
- super.onCreate()
102
- loadReactNative(this)
103
- }
104
- }
105
- ```
89
+ <details>
90
+ <summary>点击展开手动集成步骤</summary>
106
91
 
107
- ##### 方式二:旧架构(使用 ReactNativeHost)
92
+ ##### 1. 复制 Native 模块文件
108
93
 
109
- 如果你的 `MainApplication.kt` 使用 `ReactNativeHost`(旧架构),在 `getPackages()` 方法中添加:
94
+ 将以下文件从 `node_modules/@bm-fe/react-native-multi-bundle/templates/native/android/` 复制到你的 Android 项目:
110
95
 
111
- ```kotlin
112
- package com.yourpackage
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
- import android.app.Application
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 reactNativeHost: ReactNativeHost =
124
- object : DefaultReactNativeHost(this) {
125
- override fun getPackages(): List<ReactPackage> =
126
- PackageList(this).packages.apply {
127
- // Packages that cannot be autolinked yet can be added manually here
128
- add(ModuleLoaderPackage())
129
- }
130
-
131
- override fun getJSMainModuleName(): String = "index"
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
- **注意**:React Native 0.82+ 默认使用新架构(ReactHost),推荐使用方式一。
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
+
@@ -0,0 +1,4 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.bitmart.exchange.module.loader">
3
+ </manifest>
4
+
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
3
+
@@ -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
+
@@ -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.1",
3
+ "version": "1.0.0-beta.2",
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",
@@ -46,6 +50,8 @@
46
50
  "dependencies": {
47
51
  "@react-navigation/native": "^7.1.21",
48
52
  "@react-navigation/native-stack": "^7.6.4",
53
+ "react": "19.1.1",
54
+ "react-native": "0.82.1",
49
55
  "react-native-fs": "^2.20.0",
50
56
  "react-native-safe-area-context": "^5.6.2",
51
57
  "react-native-screens": "^4.18.0"
@@ -59,6 +65,7 @@
59
65
  "@react-native-community/cli-platform-ios": "20.0.2",
60
66
  "@react-native/babel-preset": "0.82.1",
61
67
  "@react-native/eslint-config": "0.82.1",
68
+ "@react-native/gradle-plugin": "0.82.1",
62
69
  "@react-native/metro-config": "0.82.1",
63
70
  "@react-native/typescript-config": "0.82.1",
64
71
  "@rnx-kit/babel-preset-metro-react-native": "^3.0.0",
@@ -73,6 +80,7 @@
73
80
  "patch-package": "^8.0.1",
74
81
  "postinstall-postinstall": "^2.1.0",
75
82
  "prettier": "3.6.2",
83
+ "react-native": "^0.82.1",
76
84
  "react-test-renderer": "^19.1.1",
77
85
  "standard-version": "^9.5.0",
78
86
  "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
- if (!__DEV__) {
65
- try {
66
- const nativeManifest = await NativeModules.CodePush.getCurrentBundleManifestContent();
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();
@@ -341,3 +341,4 @@ function preloadModule(moduleId: string): Promise<void>
341
341
  - [使用指南](../docs/使用指南.md)
342
342
  - [多 Bundle 架构设计](../docs/React%20Native%20多%20bundle%20技术方案.md)
343
343
 
344
+