@bm-fe/react-native-multi-bundle 1.0.0-beta.0 → 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 +101 -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 +11 -3
- package/react-native.config.js +23 -0
- package/scripts/build-multi-bundle.js +29 -5
- package/src/multi-bundle/LocalBundleManager.ts +25 -13
- package/src/multi-bundle/ModuleLoaderMock.ts +105 -17
- package/src/multi-bundle/ModuleRegistry.ts +0 -2
- package/src/multi-bundle/README.md +1 -0
- package/src/multi-bundle/init.ts +94 -51
- package/templates/metro.config.js.template +157 -5
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.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"
|
|
@@ -34,10 +36,12 @@
|
|
|
34
36
|
"version:patch": "standard-version --release-as patch",
|
|
35
37
|
"version:minor": "standard-version --release-as minor",
|
|
36
38
|
"version:major": "standard-version --release-as major",
|
|
37
|
-
"version:beta": "npm version prerelease --preid=beta --no-git-tag-version",
|
|
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
|
+
|
|
@@ -13,17 +13,41 @@
|
|
|
13
13
|
* - --env: 'development' | 'staging' | 'production' (默认: 'production')
|
|
14
14
|
*
|
|
15
15
|
* 环境变量:
|
|
16
|
-
* - PROJECT_ROOT:
|
|
16
|
+
* - PROJECT_ROOT: 项目根目录(可选,默认从当前工作目录向上查找包含 multi-bundle.config.json 的目录)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const path = require('path');
|
|
21
21
|
const { execSync } = require('child_process');
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
/**
|
|
24
|
+
* 查找项目根目录
|
|
25
|
+
* 从当前工作目录向上查找,直到找到包含 multi-bundle.config.json 的目录
|
|
26
|
+
* 如果找不到,则使用当前工作目录(通常打包命令在项目根目录执行)
|
|
27
|
+
*/
|
|
28
|
+
function findProjectRoot() {
|
|
29
|
+
// 如果通过环境变量指定,直接使用
|
|
30
|
+
if (process.env.PROJECT_ROOT) {
|
|
31
|
+
return process.env.PROJECT_ROOT;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 从当前工作目录开始向上查找
|
|
35
|
+
let currentDir = process.cwd();
|
|
36
|
+
const root = path.parse(currentDir).root;
|
|
37
|
+
|
|
38
|
+
while (currentDir !== root) {
|
|
39
|
+
const configFile = path.join(currentDir, 'multi-bundle.config.json');
|
|
40
|
+
if (fs.existsSync(configFile)) {
|
|
41
|
+
return currentDir;
|
|
42
|
+
}
|
|
43
|
+
currentDir = path.dirname(currentDir);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 如果找不到配置文件,使用当前工作目录(通常打包命令在项目根目录执行)
|
|
47
|
+
return process.cwd();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PROJECT_ROOT = findProjectRoot();
|
|
27
51
|
const CONFIG_FILE = path.join(PROJECT_ROOT, 'multi-bundle.config.json');
|
|
28
52
|
const OUTPUT_DIR = path.join(PROJECT_ROOT, 'build/bundles');
|
|
29
53
|
|
|
@@ -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();
|