@bm-fe/react-native-multi-bundle 1.0.0-beta.12 → 1.0.0-beta.13
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/README.md +27 -0
- package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderModule.kt +131 -192
- package/ios/ModuleLoader/ModuleLoaderModule.mm +240 -71
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/multi-bundle/README.md +48 -0
- package/src/multi-bundle/createModuleLoader.tsx +13 -3
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ React Native 多 Bundle 系统 - 支持模块按需加载和独立更新
|
|
|
8
8
|
- ✅ **模块依赖管理**:自动处理模块间的依赖关系
|
|
9
9
|
- ✅ **模块状态管理**:完整的生命周期管理(idle/loading/loaded/failed)
|
|
10
10
|
- ✅ **路由加载器**:`createModuleRouteLoader`,完美支持 React Navigation `getComponent` API
|
|
11
|
+
- ✅ **错误处理与重试**:自动错误处理,支持重试机制和自定义错误组件
|
|
11
12
|
- ✅ **预加载支持**:`preloadModule` 支持关键模块预加载
|
|
12
13
|
- ✅ **开发环境 Mock**:开发环境无需 Native 模块即可运行
|
|
13
14
|
- ✅ **TypeScript 支持**:完整的类型定义
|
|
@@ -79,6 +80,32 @@ export const createHomeScreen = createModuleRouteLoader('home', 'Home');
|
|
|
79
80
|
<Stack.Screen name="Home" getComponent={createHomeScreen} />
|
|
80
81
|
```
|
|
81
82
|
|
|
83
|
+
### 3. 自定义错误处理(可选)
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { createModuleLoader, type ErrorFallbackProps } from '@bm-fe/react-native-multi-bundle';
|
|
87
|
+
|
|
88
|
+
// 自定义错误组件
|
|
89
|
+
function MyErrorFallback({ moduleId, error, onRetry }: ErrorFallbackProps) {
|
|
90
|
+
return (
|
|
91
|
+
<View>
|
|
92
|
+
<Text>模块 {moduleId} 加载失败</Text>
|
|
93
|
+
<Button title="重试" onPress={onRetry} />
|
|
94
|
+
</View>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 使用自定义错误组件
|
|
99
|
+
const createHomeScreen = createModuleLoader(
|
|
100
|
+
'home',
|
|
101
|
+
(exports) => exports.routes.Home,
|
|
102
|
+
{
|
|
103
|
+
ErrorFallback: MyErrorFallback,
|
|
104
|
+
onError: (error) => console.error('加载失败:', error),
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
82
109
|
## 完整集成指南
|
|
83
110
|
|
|
84
111
|
详细的集成步骤请参考:[INTEGRATION.md](./INTEGRATION.md)
|
package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderModule.kt
CHANGED
|
@@ -16,10 +16,8 @@ import kotlinx.coroutines.withContext
|
|
|
16
16
|
import org.json.JSONObject
|
|
17
17
|
import java.io.BufferedReader
|
|
18
18
|
import java.io.File
|
|
19
|
-
import java.io.FileOutputStream
|
|
20
19
|
import java.io.FileReader
|
|
21
20
|
import java.io.IOException
|
|
22
|
-
import java.io.InputStream
|
|
23
21
|
import java.io.InputStreamReader
|
|
24
22
|
|
|
25
23
|
/**
|
|
@@ -27,10 +25,11 @@ import java.io.InputStreamReader
|
|
|
27
25
|
*
|
|
28
26
|
* 新架构下通过反射获取 ReactInstance,然后调用 loadJSBundle 方法
|
|
29
27
|
* 注意:ReactInstance 是 internal 类,必须完全通过反射操作
|
|
30
|
-
*
|
|
31
|
-
* Bundle
|
|
32
|
-
* 1. CodePush
|
|
33
|
-
* 2.
|
|
28
|
+
*
|
|
29
|
+
* Bundle 加载优先级(解决 CodePush 增量更新只包含变化 bundle 的问题):
|
|
30
|
+
* 1. CodePush 当前 package hash 目录(最新热更新的 bundle)
|
|
31
|
+
* 2. CodePush 历史 package hash 目录(按时间从新到旧遍历)
|
|
32
|
+
* 3. Assets(APK 内置的 bundle,最终 fallback)
|
|
34
33
|
*/
|
|
35
34
|
@ReactModule(name = ModuleLoaderModule.NAME)
|
|
36
35
|
class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
@@ -38,7 +37,7 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
38
37
|
companion object {
|
|
39
38
|
const val NAME = "ModuleLoader"
|
|
40
39
|
private const val TAG = "ModuleLoaderModule"
|
|
41
|
-
|
|
40
|
+
|
|
42
41
|
// CodePush 相关常量
|
|
43
42
|
private const val CODE_PUSH_FOLDER_PREFIX = "CodePush"
|
|
44
43
|
private const val STATUS_FILE = "codepush.json"
|
|
@@ -102,7 +101,7 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
102
101
|
|
|
103
102
|
Log.d(TAG, "Using CatalystInstance (same JS Runtime guaranteed)")
|
|
104
103
|
try {
|
|
105
|
-
// 1️⃣ 优先从 CodePush
|
|
104
|
+
// 1️⃣ 优先从 CodePush 目录加载(当前包 → 历史包)
|
|
106
105
|
val codePushBundlePath = getCodePushBundlePath(bundlePath)
|
|
107
106
|
if (codePushBundlePath != null) {
|
|
108
107
|
Log.d(TAG, "Loading bundle from CodePush: $codePushBundlePath")
|
|
@@ -111,8 +110,8 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
111
110
|
promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
|
|
112
111
|
return true
|
|
113
112
|
}
|
|
114
|
-
|
|
115
|
-
// 2️⃣ 回退到 assets
|
|
113
|
+
|
|
114
|
+
// 2️⃣ 回退到 assets 加载(APK 内置)
|
|
116
115
|
val assetPath = "assets://$bundlePath"
|
|
117
116
|
Log.d(TAG, "Loading bundle from assets: $assetPath")
|
|
118
117
|
catalystInstance.loadScriptFromAssets(reactContext.assets, assetPath, false)
|
|
@@ -144,19 +143,19 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
144
143
|
}
|
|
145
144
|
|
|
146
145
|
Log.d(TAG, "Using ReactHostImpl (new architecture)")
|
|
147
|
-
|
|
148
|
-
// 1️⃣ 优先从 CodePush
|
|
146
|
+
|
|
147
|
+
// 1️⃣ 优先从 CodePush 目录加载(当前包 → 历史包)
|
|
149
148
|
val codePushBundlePath = getCodePushBundlePath(bundlePath)
|
|
150
149
|
val bundleLoader = if (codePushBundlePath != null) {
|
|
151
150
|
Log.d(TAG, "Loading bundle from CodePush: $codePushBundlePath")
|
|
152
151
|
JSBundleLoader.createFileLoader(codePushBundlePath)
|
|
153
152
|
} else {
|
|
154
|
-
// 2️⃣ 回退到 assets
|
|
153
|
+
// 2️⃣ 回退到 assets 加载(APK 内置)
|
|
155
154
|
val assetUrl = "assets://$bundlePath"
|
|
156
155
|
Log.d(TAG, "Loading bundle from assets: $assetUrl")
|
|
157
156
|
JSBundleLoader.createAssetLoader(reactContext, assetUrl, false)
|
|
158
157
|
}
|
|
159
|
-
|
|
158
|
+
|
|
160
159
|
loadBundleViaReflection(reactHost, bundleLoader, bundleId, promise)
|
|
161
160
|
return true
|
|
162
161
|
}
|
|
@@ -228,21 +227,21 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
228
227
|
return try {
|
|
229
228
|
val statusFilePath = "${getCodePushPath()}/$STATUS_FILE"
|
|
230
229
|
val statusFile = File(statusFilePath)
|
|
231
|
-
|
|
230
|
+
|
|
232
231
|
if (!statusFile.exists()) {
|
|
233
232
|
Log.d(TAG, "CodePush status file not found: $statusFilePath")
|
|
234
233
|
return null
|
|
235
234
|
}
|
|
236
|
-
|
|
235
|
+
|
|
237
236
|
val content = statusFile.readText()
|
|
238
237
|
val json = JSONObject(content)
|
|
239
238
|
val packageHash = json.optString(CURRENT_PACKAGE_KEY, null)
|
|
240
|
-
|
|
239
|
+
|
|
241
240
|
if (packageHash.isNullOrEmpty()) {
|
|
242
241
|
Log.d(TAG, "No current package hash in CodePush status")
|
|
243
242
|
return null
|
|
244
243
|
}
|
|
245
|
-
|
|
244
|
+
|
|
246
245
|
Log.d(TAG, "Current CodePush package hash: $packageHash")
|
|
247
246
|
packageHash
|
|
248
247
|
} catch (e: Exception) {
|
|
@@ -261,70 +260,125 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
261
260
|
|
|
262
261
|
/**
|
|
263
262
|
* 从 CodePush 目录查找子 bundle 文件
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* 1.
|
|
267
|
-
* 2.
|
|
268
|
-
* 3.
|
|
269
|
-
*
|
|
263
|
+
*
|
|
264
|
+
* 查找优先级(解决增量更新只包含变化 bundle 的问题):
|
|
265
|
+
* 1. 当前 package hash 目录
|
|
266
|
+
* 2. 按时间从新到旧遍历其他 package hash 目录
|
|
267
|
+
* 3. 如果都找不到,返回 null,由调用方从 assets:// 加载
|
|
268
|
+
*
|
|
269
|
+
* 每个目录内的查找顺序:
|
|
270
|
+
* 1. <folder>/modules/<bundleFileName>
|
|
271
|
+
* 2. <folder>/<bundlePath>
|
|
272
|
+
* 3. <folder>/<bundleFileName>
|
|
273
|
+
*
|
|
270
274
|
* @param bundlePath 原始 bundle 路径,如 "modules/home.jsbundle"
|
|
271
|
-
* @return 完整的文件路径,如果不存在返回 null
|
|
275
|
+
* @return 完整的文件路径,如果不存在返回 null(调用方会从 assets:// 加载)
|
|
272
276
|
*/
|
|
273
277
|
private fun getCodePushBundlePath(bundlePath: String): String? {
|
|
274
|
-
val
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (!packageDir.exists()) {
|
|
280
|
-
Log.d(TAG, "CodePush package folder not found: $packageFolder")
|
|
278
|
+
val codePushDir = File(getCodePushPath())
|
|
279
|
+
|
|
280
|
+
// CodePush 目录不存在,直接返回 null(调用方会从 assets:// 加载)
|
|
281
|
+
if (!codePushDir.exists()) {
|
|
282
|
+
Log.d(TAG, "CodePush directory not found, will load from assets")
|
|
281
283
|
return null
|
|
282
284
|
}
|
|
283
|
-
|
|
284
|
-
// 提取文件名
|
|
285
|
+
|
|
285
286
|
val bundleFileName = File(bundlePath).name
|
|
286
|
-
|
|
287
|
-
|
|
287
|
+
val currentPackageHash = getCurrentPackageHash()
|
|
288
|
+
|
|
289
|
+
// 1️⃣ 优先从当前 package hash 目录查找
|
|
290
|
+
if (currentPackageHash != null) {
|
|
291
|
+
val currentPackageFolder = getPackageFolderPath(currentPackageHash)
|
|
292
|
+
val foundPath = findBundleInFolder(currentPackageFolder, bundlePath, bundleFileName)
|
|
293
|
+
if (foundPath != null) {
|
|
294
|
+
Log.d(TAG, "Found bundle in current package: $foundPath")
|
|
295
|
+
return foundPath
|
|
296
|
+
}
|
|
297
|
+
Log.d(TAG, "Bundle not found in current package ($currentPackageHash), searching other packages...")
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 2️⃣ 按时间从新到旧遍历其他 package hash 目录
|
|
301
|
+
val otherPackageFolders = getPackageFoldersSortedByTime(codePushDir, currentPackageHash)
|
|
302
|
+
for (packageFolder in otherPackageFolders) {
|
|
303
|
+
val foundPath = findBundleInFolder(packageFolder.absolutePath, bundlePath, bundleFileName)
|
|
304
|
+
if (foundPath != null) {
|
|
305
|
+
Log.d(TAG, "Found bundle in previous package (${packageFolder.name}): $foundPath")
|
|
306
|
+
return foundPath
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
Log.d(TAG, "Bundle not found in CodePush directories, will load from assets: $bundlePath")
|
|
311
|
+
return null
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 在指定文件夹中查找 bundle 文件
|
|
316
|
+
*
|
|
317
|
+
* 查找顺序:
|
|
318
|
+
* 1. <folder>/modules/<bundleFileName>
|
|
319
|
+
* 2. <folder>/<bundlePath>
|
|
320
|
+
* 3. <folder>/<bundleFileName>
|
|
321
|
+
*/
|
|
322
|
+
private fun findBundleInFolder(folderPath: String, bundlePath: String, bundleFileName: String): String? {
|
|
323
|
+
val folder = File(folderPath)
|
|
324
|
+
if (!folder.exists() || !folder.isDirectory) {
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
|
|
288
328
|
val searchPaths = listOf(
|
|
289
|
-
|
|
290
|
-
"$
|
|
291
|
-
|
|
292
|
-
"$packageFolder/$bundlePath",
|
|
293
|
-
// 3. <packageFolder>/<bundleFileName> (仅文件名)
|
|
294
|
-
"$packageFolder/$bundleFileName"
|
|
329
|
+
"$folderPath/modules/$bundleFileName",
|
|
330
|
+
"$folderPath/$bundlePath",
|
|
331
|
+
"$folderPath/$bundleFileName"
|
|
295
332
|
)
|
|
296
|
-
|
|
333
|
+
|
|
297
334
|
for (path in searchPaths) {
|
|
298
335
|
val file = File(path)
|
|
299
336
|
if (file.exists() && file.isFile) {
|
|
300
|
-
Log.d(TAG, "Found bundle in CodePush: $path")
|
|
301
337
|
return path
|
|
302
338
|
}
|
|
303
339
|
}
|
|
304
|
-
|
|
305
|
-
Log.d(TAG, "Bundle not found in CodePush package, searched: $searchPaths")
|
|
340
|
+
|
|
306
341
|
return null
|
|
307
342
|
}
|
|
308
343
|
|
|
344
|
+
/**
|
|
345
|
+
* 获取所有 package 目录,按修改时间从新到旧排序
|
|
346
|
+
* 排除当前 package、assets-base 和 codepush.json 等非 package 目录/文件
|
|
347
|
+
*/
|
|
348
|
+
private fun getPackageFoldersSortedByTime(codePushDir: File, currentPackageHash: String?): List<File> {
|
|
349
|
+
val excludedNames = setOf(STATUS_FILE, "download", "unzipped")
|
|
350
|
+
|
|
351
|
+
return codePushDir.listFiles()
|
|
352
|
+
?.filter { file ->
|
|
353
|
+
file.isDirectory &&
|
|
354
|
+
file.name != currentPackageHash &&
|
|
355
|
+
!excludedNames.contains(file.name)
|
|
356
|
+
}
|
|
357
|
+
?.sortedByDescending { it.lastModified() }
|
|
358
|
+
?: emptyList()
|
|
359
|
+
}
|
|
360
|
+
|
|
309
361
|
// ==================== Bundle Manifest Support ====================
|
|
310
362
|
|
|
311
363
|
/**
|
|
312
364
|
* 获取当前 bundle manifest 文件路径
|
|
313
|
-
*
|
|
365
|
+
*
|
|
314
366
|
* 查找优先级:
|
|
315
367
|
* 1. CodePush 当前包目录(热更新后的 manifest)
|
|
316
|
-
* 2.
|
|
368
|
+
* 2. 返回 null(调用方应使用 getCurrentBundleManifestContent 获取内容)
|
|
369
|
+
*
|
|
370
|
+
* 注意:如果需要从 assets 读取,请使用 getCurrentBundleManifestContent
|
|
317
371
|
*/
|
|
318
372
|
@ReactMethod
|
|
319
373
|
fun getCurrentBundleManifest(promise: Promise) {
|
|
320
374
|
try {
|
|
321
|
-
//
|
|
375
|
+
// 优先从 CodePush 当前包目录获取
|
|
322
376
|
val packageHash = getCurrentPackageHash()
|
|
323
377
|
if (packageHash != null) {
|
|
324
378
|
val packageFolder = getPackageFolderPath(packageHash)
|
|
325
379
|
val codePushManifestPath = "$packageFolder/bundle-manifest.json"
|
|
326
380
|
val codePushManifestFile = File(codePushManifestPath)
|
|
327
|
-
|
|
381
|
+
|
|
328
382
|
if (codePushManifestFile.exists()) {
|
|
329
383
|
Log.d(TAG, "Found manifest in CodePush package: $codePushManifestPath")
|
|
330
384
|
promise.resolve(codePushManifestPath)
|
|
@@ -332,22 +386,8 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
332
386
|
}
|
|
333
387
|
}
|
|
334
388
|
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// 从 assets-base 目录获取 manifest
|
|
339
|
-
val codePushDir = getCodePushPath()
|
|
340
|
-
val assetsBaseManifestPath = "$codePushDir/assets-base/bundle-manifest.json"
|
|
341
|
-
val assetsBaseManifestFile = File(assetsBaseManifestPath)
|
|
342
|
-
|
|
343
|
-
if (assetsBaseManifestFile.exists()) {
|
|
344
|
-
Log.d(TAG, "Found manifest in assets-base: $assetsBaseManifestPath")
|
|
345
|
-
promise.resolve(assetsBaseManifestPath)
|
|
346
|
-
return
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// 如果都不存在,返回 null
|
|
350
|
-
Log.w(TAG, "Bundle manifest not found in any location")
|
|
389
|
+
// CodePush 中找不到,返回 null(调用方应使用 getCurrentBundleManifestContent)
|
|
390
|
+
Log.d(TAG, "Manifest not in CodePush, use getCurrentBundleManifestContent to read from assets")
|
|
351
391
|
promise.resolve(null)
|
|
352
392
|
} catch (e: Exception) {
|
|
353
393
|
Log.e(TAG, "Failed to get current bundle manifest: ${e.message}")
|
|
@@ -357,10 +397,10 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
357
397
|
|
|
358
398
|
/**
|
|
359
399
|
* 获取当前 bundle manifest 文件内容
|
|
360
|
-
*
|
|
400
|
+
*
|
|
361
401
|
* 查找优先级:
|
|
362
402
|
* 1. CodePush 当前包目录(热更新后的 manifest)
|
|
363
|
-
* 2. assets
|
|
403
|
+
* 2. APK 内置 assets(最终 fallback)
|
|
364
404
|
*/
|
|
365
405
|
@ReactMethod
|
|
366
406
|
fun getCurrentBundleManifestContent(promise: Promise) {
|
|
@@ -373,7 +413,7 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
373
413
|
val packageFolder = getPackageFolderPath(packageHash)
|
|
374
414
|
val codePushManifestPath = "$packageFolder/bundle-manifest.json"
|
|
375
415
|
val codePushManifestFile = File(codePushManifestPath)
|
|
376
|
-
|
|
416
|
+
|
|
377
417
|
if (codePushManifestFile.exists()) {
|
|
378
418
|
val content = readFileContent(codePushManifestFile)
|
|
379
419
|
if (content != null) {
|
|
@@ -382,25 +422,15 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
382
422
|
}
|
|
383
423
|
}
|
|
384
424
|
}
|
|
385
|
-
|
|
386
|
-
// 2️⃣
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
val assetsBaseManifestPath = "$codePushDir/assets-base/bundle-manifest.json"
|
|
392
|
-
val assetsBaseManifestFile = File(assetsBaseManifestPath)
|
|
393
|
-
|
|
394
|
-
if (assetsBaseManifestFile.exists()) {
|
|
395
|
-
val content = readFileContent(assetsBaseManifestFile)
|
|
396
|
-
if (content != null) {
|
|
397
|
-
Log.d(TAG, "✓ Read bundle manifest from assets-base directory")
|
|
398
|
-
return@withContext content
|
|
399
|
-
}
|
|
425
|
+
|
|
426
|
+
// 2️⃣ 从 APK 内置 assets 读取
|
|
427
|
+
val assetsContent = readAssetContent("bundle-manifest.json")
|
|
428
|
+
if (assetsContent != null) {
|
|
429
|
+
Log.d(TAG, "✓ Read bundle manifest from assets")
|
|
430
|
+
return@withContext assetsContent
|
|
400
431
|
}
|
|
401
432
|
|
|
402
|
-
|
|
403
|
-
Log.w(TAG, "Bundle manifest content not found in any location")
|
|
433
|
+
Log.w(TAG, "Bundle manifest not found in any location")
|
|
404
434
|
null
|
|
405
435
|
}
|
|
406
436
|
promise.resolve(manifestContent)
|
|
@@ -412,116 +442,25 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
412
442
|
}
|
|
413
443
|
|
|
414
444
|
/**
|
|
415
|
-
*
|
|
445
|
+
* 从 assets 读取文件内容
|
|
416
446
|
*/
|
|
417
|
-
private fun
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
val
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
// 如果任一不存在,从 assets 复制
|
|
427
|
-
if (!manifestFile.exists() || !modulesDir.exists()) {
|
|
428
|
-
Log.d(TAG, "Initializing CodePush directory from assets...")
|
|
429
|
-
copyAssetsToCodePushDirectory(codePushDir)
|
|
430
|
-
}
|
|
431
|
-
} catch (e: Exception) {
|
|
432
|
-
Log.e(TAG, "Failed to initialize CodePush directory: ${e.message}")
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* 从 assets 复制 bundle-manifest.json 和 modules 目录到 CodePush 目录
|
|
438
|
-
*/
|
|
439
|
-
private fun copyAssetsToCodePushDirectory(codePushDir: String) {
|
|
440
|
-
try {
|
|
441
|
-
// 创建 assets-base 目录
|
|
442
|
-
val defaultPackageDir = "$codePushDir/assets-base"
|
|
443
|
-
val defaultPackageDirFile = File(defaultPackageDir)
|
|
444
|
-
if (!defaultPackageDirFile.exists()) {
|
|
445
|
-
defaultPackageDirFile.mkdirs()
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// 1. 复制 bundle-manifest.json
|
|
449
|
-
try {
|
|
450
|
-
val manifestAssetPath = "bundle-manifest.json"
|
|
451
|
-
val manifestInput = reactContext.assets.open(manifestAssetPath)
|
|
452
|
-
val manifestOutput = File("$defaultPackageDir/bundle-manifest.json")
|
|
453
|
-
copyInputStreamToFile(manifestInput, manifestOutput)
|
|
454
|
-
Log.d(TAG, "✓ Copied bundle-manifest.json to CodePush directory")
|
|
455
|
-
} catch (e: IOException) {
|
|
456
|
-
Log.w(TAG, "Warning: Could not copy bundle-manifest.json: ${e.message}")
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// 2. 复制 modules 目录
|
|
460
|
-
try {
|
|
461
|
-
val modulesAssetPath = "modules"
|
|
462
|
-
val modulesOutputDir = File("$defaultPackageDir/modules")
|
|
463
|
-
if (!modulesOutputDir.exists()) {
|
|
464
|
-
modulesOutputDir.mkdirs()
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
copyAssetFolder(modulesAssetPath, modulesOutputDir.absolutePath)
|
|
468
|
-
Log.d(TAG, "✓ Copied modules directory to CodePush directory")
|
|
469
|
-
} catch (e: IOException) {
|
|
470
|
-
Log.w(TAG, "Warning: Could not copy modules directory: ${e.message}")
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
} catch (e: Exception) {
|
|
474
|
-
Log.e(TAG, "Failed to copy assets to CodePush directory: ${e.message}")
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* 递归复制 asset 文件夹
|
|
480
|
-
*/
|
|
481
|
-
@Throws(IOException::class)
|
|
482
|
-
private fun copyAssetFolder(assetPath: String, targetPath: String) {
|
|
483
|
-
val assets = reactContext.assets.list(assetPath)
|
|
484
|
-
if (assets.isNullOrEmpty()) {
|
|
485
|
-
// 这是一个文件,不是目录
|
|
486
|
-
val input = reactContext.assets.open(assetPath)
|
|
487
|
-
val output = File(targetPath)
|
|
488
|
-
copyInputStreamToFile(input, output)
|
|
489
|
-
} else {
|
|
490
|
-
// 这是一个目录
|
|
491
|
-
val dir = File(targetPath)
|
|
492
|
-
if (!dir.exists()) {
|
|
493
|
-
dir.mkdirs()
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
for (asset in assets) {
|
|
497
|
-
val subAssetPath = "$assetPath/$asset"
|
|
498
|
-
val subTargetPath = "$targetPath/$asset"
|
|
499
|
-
copyAssetFolder(subAssetPath, subTargetPath)
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* 将 InputStream 复制到文件
|
|
506
|
-
*/
|
|
507
|
-
@Throws(IOException::class)
|
|
508
|
-
private fun copyInputStreamToFile(input: InputStream, output: File) {
|
|
509
|
-
var outputStream: FileOutputStream? = null
|
|
510
|
-
try {
|
|
511
|
-
outputStream = FileOutputStream(output)
|
|
512
|
-
val buffer = ByteArray(4096)
|
|
513
|
-
var bytesRead: Int
|
|
514
|
-
while (input.read(buffer).also { bytesRead = it } != -1) {
|
|
515
|
-
outputStream.write(buffer, 0, bytesRead)
|
|
447
|
+
private fun readAssetContent(assetPath: String): String? {
|
|
448
|
+
var reader: BufferedReader? = null
|
|
449
|
+
return try {
|
|
450
|
+
val inputStream = reactContext.assets.open(assetPath)
|
|
451
|
+
reader = BufferedReader(InputStreamReader(inputStream))
|
|
452
|
+
val content = StringBuilder()
|
|
453
|
+
var line: String?
|
|
454
|
+
while (reader.readLine().also { line = it } != null) {
|
|
455
|
+
content.append(line).append("\n")
|
|
516
456
|
}
|
|
457
|
+
content.toString()
|
|
458
|
+
} catch (e: IOException) {
|
|
459
|
+
Log.e(TAG, "Failed to read asset: $assetPath - ${e.message}")
|
|
460
|
+
null
|
|
517
461
|
} finally {
|
|
518
462
|
try {
|
|
519
|
-
|
|
520
|
-
} catch (e: IOException) {
|
|
521
|
-
// Ignore
|
|
522
|
-
}
|
|
523
|
-
try {
|
|
524
|
-
outputStream?.close()
|
|
463
|
+
reader?.close()
|
|
525
464
|
} catch (e: IOException) {
|
|
526
465
|
// Ignore
|
|
527
466
|
}
|
|
@@ -18,6 +18,11 @@ RCT_EXPORT_MODULE(ModuleLoader);
|
|
|
18
18
|
// Bundle manifest file name
|
|
19
19
|
static NSString *const BundleManifestFileName = @"bundle-manifest.json";
|
|
20
20
|
|
|
21
|
+
// CodePush 相关常量
|
|
22
|
+
static NSString *const CodePushFolderPrefix = @"CodePush";
|
|
23
|
+
static NSString *const StatusFileName = @"codepush.json";
|
|
24
|
+
static NSString *const CurrentPackageKey = @"currentPackage";
|
|
25
|
+
|
|
21
26
|
#pragma mark - Bundle Path Resolution
|
|
22
27
|
|
|
23
28
|
/**
|
|
@@ -84,7 +89,212 @@ static NSString *const BundleManifestFileName = @"bundle-manifest.json";
|
|
|
84
89
|
return nil;
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
#pragma mark - CodePush Bundle Path Resolution
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 获取 CodePush 基础目录
|
|
96
|
+
* 路径: <ApplicationSupport>/CodePush/ 或 <Documents>/CodePush/
|
|
97
|
+
*/
|
|
98
|
+
- (NSString *)getCodePushPath {
|
|
99
|
+
// 优先使用 Application Support 目录
|
|
100
|
+
NSString *appSupportPath = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) firstObject];
|
|
101
|
+
NSString *codePushPath = [appSupportPath stringByAppendingPathComponent:CodePushFolderPrefix];
|
|
102
|
+
|
|
103
|
+
if ([[NSFileManager defaultManager] fileExistsAtPath:codePushPath]) {
|
|
104
|
+
return codePushPath;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 回退到 Documents 目录
|
|
108
|
+
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
109
|
+
codePushPath = [documentsPath stringByAppendingPathComponent:CodePushFolderPrefix];
|
|
110
|
+
|
|
111
|
+
if ([[NSFileManager defaultManager] fileExistsAtPath:codePushPath]) {
|
|
112
|
+
return codePushPath;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return nil;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 获取当前 CodePush 包的 hash
|
|
120
|
+
* 从 <CodePushPath>/codepush.json 读取 currentPackage 字段
|
|
121
|
+
*/
|
|
122
|
+
- (NSString *)getCurrentPackageHash {
|
|
123
|
+
NSString *codePushPath = [self getCodePushPath];
|
|
124
|
+
if (!codePushPath) {
|
|
125
|
+
return nil;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
NSString *statusFilePath = [codePushPath stringByAppendingPathComponent:StatusFileName];
|
|
129
|
+
|
|
130
|
+
if (![[NSFileManager defaultManager] fileExistsAtPath:statusFilePath]) {
|
|
131
|
+
RCTLogInfo(@"[ModuleLoader] CodePush status file not found: %@", statusFilePath);
|
|
132
|
+
return nil;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@try {
|
|
136
|
+
NSData *data = [NSData dataWithContentsOfFile:statusFilePath];
|
|
137
|
+
if (!data) {
|
|
138
|
+
return nil;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
|
142
|
+
NSString *packageHash = json[CurrentPackageKey];
|
|
143
|
+
|
|
144
|
+
if (packageHash && packageHash.length > 0) {
|
|
145
|
+
RCTLogInfo(@"[ModuleLoader] Current CodePush package hash: %@", packageHash);
|
|
146
|
+
return packageHash;
|
|
147
|
+
}
|
|
148
|
+
} @catch (NSException *exception) {
|
|
149
|
+
RCTLogWarn(@"[ModuleLoader] Error reading CodePush status: %@", exception.reason);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return nil;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 获取 CodePush 包目录路径
|
|
157
|
+
* 路径: <CodePushPath>/<packageHash>/
|
|
158
|
+
*/
|
|
159
|
+
- (NSString *)getPackageFolderPath:(NSString *)packageHash {
|
|
160
|
+
NSString *codePushPath = [self getCodePushPath];
|
|
161
|
+
if (!codePushPath) {
|
|
162
|
+
return nil;
|
|
163
|
+
}
|
|
164
|
+
return [codePushPath stringByAppendingPathComponent:packageHash];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 在指定文件夹中查找 bundle 文件
|
|
169
|
+
*
|
|
170
|
+
* 查找顺序:
|
|
171
|
+
* 1. <folder>/modules/<bundleFileName>
|
|
172
|
+
* 2. <folder>/<bundlePath>
|
|
173
|
+
* 3. <folder>/<bundleFileName>
|
|
174
|
+
*/
|
|
175
|
+
- (NSString *)findBundleInFolder:(NSString *)folderPath
|
|
176
|
+
bundlePath:(NSString *)bundlePath
|
|
177
|
+
bundleFileName:(NSString *)bundleFileName {
|
|
178
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
179
|
+
|
|
180
|
+
BOOL isDirectory;
|
|
181
|
+
if (![fileManager fileExistsAtPath:folderPath isDirectory:&isDirectory] || !isDirectory) {
|
|
182
|
+
return nil;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
NSArray<NSString *> *searchPaths = @[
|
|
186
|
+
[folderPath stringByAppendingPathComponent:[@"modules/" stringByAppendingString:bundleFileName]],
|
|
187
|
+
[folderPath stringByAppendingPathComponent:bundlePath],
|
|
188
|
+
[folderPath stringByAppendingPathComponent:bundleFileName]
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
for (NSString *path in searchPaths) {
|
|
192
|
+
if ([fileManager fileExistsAtPath:path]) {
|
|
193
|
+
return path;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return nil;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 获取所有 package 目录,按修改时间从新到旧排序
|
|
202
|
+
* 排除当前 package 和非 package 目录/文件
|
|
203
|
+
*/
|
|
204
|
+
- (NSArray<NSString *> *)getPackageFoldersSortedByTime:(NSString *)codePushDir
|
|
205
|
+
currentPackageHash:(NSString *)currentPackageHash {
|
|
206
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
207
|
+
NSSet *excludedNames = [NSSet setWithArray:@[StatusFileName, @"download", @"unzipped"]];
|
|
208
|
+
|
|
209
|
+
NSError *error;
|
|
210
|
+
NSArray<NSString *> *contents = [fileManager contentsOfDirectoryAtPath:codePushDir error:&error];
|
|
211
|
+
if (error || !contents) {
|
|
212
|
+
return @[];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
NSMutableArray<NSDictionary *> *packageFolders = [NSMutableArray array];
|
|
216
|
+
|
|
217
|
+
for (NSString *name in contents) {
|
|
218
|
+
// 排除非 package 目录
|
|
219
|
+
if ([excludedNames containsObject:name]) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (currentPackageHash && [name isEqualToString:currentPackageHash]) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
NSString *fullPath = [codePushDir stringByAppendingPathComponent:name];
|
|
227
|
+
BOOL isDirectory;
|
|
228
|
+
if ([fileManager fileExistsAtPath:fullPath isDirectory:&isDirectory] && isDirectory) {
|
|
229
|
+
NSDictionary *attrs = [fileManager attributesOfItemAtPath:fullPath error:nil];
|
|
230
|
+
NSDate *modDate = attrs[NSFileModificationDate] ?: [NSDate distantPast];
|
|
231
|
+
[packageFolders addObject:@{@"path": fullPath, @"date": modDate}];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 按修改时间从新到旧排序
|
|
236
|
+
[packageFolders sortUsingComparator:^NSComparisonResult(NSDictionary *a, NSDictionary *b) {
|
|
237
|
+
return [b[@"date"] compare:a[@"date"]];
|
|
238
|
+
}];
|
|
239
|
+
|
|
240
|
+
NSMutableArray<NSString *> *sortedPaths = [NSMutableArray array];
|
|
241
|
+
for (NSDictionary *folder in packageFolders) {
|
|
242
|
+
[sortedPaths addObject:folder[@"path"]];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return sortedPaths;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 从 CodePush 目录查找子 bundle 文件
|
|
250
|
+
*
|
|
251
|
+
* 查找优先级(解决增量更新只包含变化 bundle 的问题):
|
|
252
|
+
* 1. 当前 package hash 目录
|
|
253
|
+
* 2. 按时间从新到旧遍历其他 package hash 目录
|
|
254
|
+
* 3. 如果都找不到,返回 nil,由调用方从内置目录加载
|
|
255
|
+
*
|
|
256
|
+
* @param bundlePath 原始 bundle 路径,如 "modules/home.jsbundle"
|
|
257
|
+
* @return 完整的文件路径,如果不存在返回 nil
|
|
258
|
+
*/
|
|
87
259
|
- (NSString *)getCodePushBundlePath:(NSString *)bundlePath {
|
|
260
|
+
NSString *codePushDir = [self getCodePushPath];
|
|
261
|
+
|
|
262
|
+
// CodePush 目录不存在,直接返回 nil
|
|
263
|
+
if (!codePushDir) {
|
|
264
|
+
RCTLogInfo(@"[ModuleLoader] CodePush directory not found, will load from MainBundle");
|
|
265
|
+
return nil;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
NSString *bundleFileName = [bundlePath lastPathComponent];
|
|
269
|
+
NSString *currentPackageHash = [self getCurrentPackageHash];
|
|
270
|
+
|
|
271
|
+
// 1️⃣ 优先从当前 package hash 目录查找
|
|
272
|
+
if (currentPackageHash) {
|
|
273
|
+
NSString *currentPackageFolder = [self getPackageFolderPath:currentPackageHash];
|
|
274
|
+
NSString *foundPath = [self findBundleInFolder:currentPackageFolder
|
|
275
|
+
bundlePath:bundlePath
|
|
276
|
+
bundleFileName:bundleFileName];
|
|
277
|
+
if (foundPath) {
|
|
278
|
+
RCTLogInfo(@"[ModuleLoader] Found bundle in current package: %@", foundPath);
|
|
279
|
+
return foundPath;
|
|
280
|
+
}
|
|
281
|
+
RCTLogInfo(@"[ModuleLoader] Bundle not found in current package (%@), searching other packages...", currentPackageHash);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 2️⃣ 按时间从新到旧遍历其他 package hash 目录
|
|
285
|
+
NSArray<NSString *> *otherPackageFolders = [self getPackageFoldersSortedByTime:codePushDir
|
|
286
|
+
currentPackageHash:currentPackageHash];
|
|
287
|
+
for (NSString *packageFolder in otherPackageFolders) {
|
|
288
|
+
NSString *foundPath = [self findBundleInFolder:packageFolder
|
|
289
|
+
bundlePath:bundlePath
|
|
290
|
+
bundleFileName:bundleFileName];
|
|
291
|
+
if (foundPath) {
|
|
292
|
+
RCTLogInfo(@"[ModuleLoader] Found bundle in previous package (%@): %@", [packageFolder lastPathComponent], foundPath);
|
|
293
|
+
return foundPath;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
RCTLogInfo(@"[ModuleLoader] Bundle not found in CodePush directories, will load from MainBundle: %@", bundlePath);
|
|
88
298
|
return nil;
|
|
89
299
|
}
|
|
90
300
|
|
|
@@ -330,14 +540,6 @@ RCT_EXPORT_METHOD(loadBusinessBundle:(NSString *)bundleId
|
|
|
330
540
|
|
|
331
541
|
#pragma mark - Bundle Manifest Methods
|
|
332
542
|
|
|
333
|
-
+ (NSString *)getApplicationSupportDirectory {
|
|
334
|
-
return [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0];
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
+ (NSString *)bundleAssetsPath {
|
|
338
|
-
return [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"assets"];
|
|
339
|
-
}
|
|
340
|
-
|
|
341
543
|
- (NSString *)readFileContent:(NSString *)filePath {
|
|
342
544
|
@try {
|
|
343
545
|
NSError *error;
|
|
@@ -359,7 +561,19 @@ RCT_EXPORT_METHOD(getCurrentBundleManifest:(RCTPromiseResolveBlock)resolve
|
|
|
359
561
|
@try {
|
|
360
562
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
361
563
|
|
|
362
|
-
// 1
|
|
564
|
+
// 1️⃣ 优先从 CodePush 当前包目录获取
|
|
565
|
+
NSString *packageHash = [self getCurrentPackageHash];
|
|
566
|
+
if (packageHash) {
|
|
567
|
+
NSString *packageFolder = [self getPackageFolderPath:packageHash];
|
|
568
|
+
NSString *codePushManifestPath = [packageFolder stringByAppendingPathComponent:BundleManifestFileName];
|
|
569
|
+
if ([fileManager fileExistsAtPath:codePushManifestPath]) {
|
|
570
|
+
RCTLogInfo(@"[ModuleLoader] Found manifest in CodePush package: %@", codePushManifestPath);
|
|
571
|
+
resolve(codePushManifestPath);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// 2️⃣ MainBundle/Bundles 目录(内置)
|
|
363
577
|
NSString *bundlesPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Bundles"];
|
|
364
578
|
NSString *bundlesManifestPath = [bundlesPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
365
579
|
if ([fileManager fileExistsAtPath:bundlesManifestPath]) {
|
|
@@ -368,33 +582,7 @@ RCT_EXPORT_METHOD(getCurrentBundleManifest:(RCTPromiseResolveBlock)resolve
|
|
|
368
582
|
return;
|
|
369
583
|
}
|
|
370
584
|
|
|
371
|
-
//
|
|
372
|
-
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
373
|
-
if (documentsPath) {
|
|
374
|
-
NSString *documentsManifestPath = [documentsPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
375
|
-
if ([fileManager fileExistsAtPath:documentsManifestPath]) {
|
|
376
|
-
resolve(documentsManifestPath);
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// 3. Application Support 目录
|
|
382
|
-
NSString *appSupportPath = [[self class] getApplicationSupportDirectory];
|
|
383
|
-
NSString *appSupportManifestPath = [appSupportPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
384
|
-
if ([fileManager fileExistsAtPath:appSupportManifestPath]) {
|
|
385
|
-
resolve(appSupportManifestPath);
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// 4. MainBundle assets 目录
|
|
390
|
-
NSString *assetsPath = [[self class] bundleAssetsPath];
|
|
391
|
-
NSString *assetsManifestPath = [assetsPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
392
|
-
if ([fileManager fileExistsAtPath:assetsManifestPath]) {
|
|
393
|
-
resolve(assetsManifestPath);
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// 5. MainBundle 根目录
|
|
585
|
+
// 3️⃣ MainBundle 根目录
|
|
398
586
|
NSString *mainBundleManifestPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:BundleManifestFileName];
|
|
399
587
|
if ([fileManager fileExistsAtPath:mainBundleManifestPath]) {
|
|
400
588
|
resolve(mainBundleManifestPath);
|
|
@@ -417,58 +605,39 @@ RCT_EXPORT_METHOD(getCurrentBundleManifestContent:(RCTPromiseResolveBlock)resolv
|
|
|
417
605
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
418
606
|
NSString *manifestContent = nil;
|
|
419
607
|
|
|
420
|
-
// 1
|
|
421
|
-
NSString *
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// 2. Documents 目录
|
|
433
|
-
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
|
|
434
|
-
if (documentsPath) {
|
|
435
|
-
NSString *documentsManifestPath = [documentsPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
436
|
-
if ([fileManager fileExistsAtPath:documentsManifestPath]) {
|
|
437
|
-
manifestContent = [self readFileContent:documentsManifestPath];
|
|
608
|
+
// 1️⃣ 优先从 CodePush 当前包目录读取
|
|
609
|
+
NSString *packageHash = [self getCurrentPackageHash];
|
|
610
|
+
if (packageHash) {
|
|
611
|
+
NSString *packageFolder = [self getPackageFolderPath:packageHash];
|
|
612
|
+
NSString *codePushManifestPath = [packageFolder stringByAppendingPathComponent:BundleManifestFileName];
|
|
613
|
+
if ([fileManager fileExistsAtPath:codePushManifestPath]) {
|
|
614
|
+
manifestContent = [self readFileContent:codePushManifestPath];
|
|
438
615
|
if (manifestContent) {
|
|
616
|
+
RCTLogInfo(@"[ModuleLoader] ✓ Read manifest from CodePush package");
|
|
439
617
|
dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
|
|
440
618
|
return;
|
|
441
619
|
}
|
|
442
620
|
}
|
|
443
621
|
}
|
|
444
622
|
|
|
445
|
-
//
|
|
446
|
-
NSString *
|
|
447
|
-
NSString *
|
|
448
|
-
if ([fileManager fileExistsAtPath:
|
|
449
|
-
manifestContent = [self readFileContent:
|
|
450
|
-
if (manifestContent) {
|
|
451
|
-
dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// 4. MainBundle assets 目录
|
|
457
|
-
NSString *assetsPath = [[self class] bundleAssetsPath];
|
|
458
|
-
NSString *assetsManifestPath = [assetsPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
459
|
-
if ([fileManager fileExistsAtPath:assetsManifestPath]) {
|
|
460
|
-
manifestContent = [self readFileContent:assetsManifestPath];
|
|
623
|
+
// 2️⃣ MainBundle/Bundles 目录(内置)
|
|
624
|
+
NSString *bundlesPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Bundles"];
|
|
625
|
+
NSString *bundlesManifestPath = [bundlesPath stringByAppendingPathComponent:BundleManifestFileName];
|
|
626
|
+
if ([fileManager fileExistsAtPath:bundlesManifestPath]) {
|
|
627
|
+
manifestContent = [self readFileContent:bundlesManifestPath];
|
|
461
628
|
if (manifestContent) {
|
|
629
|
+
RCTLogInfo(@"[ModuleLoader] ✓ Read manifest from Bundles");
|
|
462
630
|
dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
|
|
463
631
|
return;
|
|
464
632
|
}
|
|
465
633
|
}
|
|
466
634
|
|
|
467
|
-
//
|
|
635
|
+
// 3️⃣ MainBundle 根目录
|
|
468
636
|
NSString *mainBundleManifestPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:BundleManifestFileName];
|
|
469
637
|
if ([fileManager fileExistsAtPath:mainBundleManifestPath]) {
|
|
470
638
|
manifestContent = [self readFileContent:mainBundleManifestPath];
|
|
471
639
|
if (manifestContent) {
|
|
640
|
+
RCTLogInfo(@"[ModuleLoader] ✓ Read manifest from MainBundle");
|
|
472
641
|
dispatch_async(dispatch_get_main_queue(), ^{ resolve(manifestContent); });
|
|
473
642
|
return;
|
|
474
643
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ export {
|
|
|
18
18
|
} from './multi-bundle/config';
|
|
19
19
|
|
|
20
20
|
// 模块加载器
|
|
21
|
-
export { createModuleLoader, type CreateModuleLoaderOptions } from './multi-bundle/createModuleLoader';
|
|
21
|
+
export { createModuleLoader, type CreateModuleLoaderOptions, type ErrorFallbackProps } from './multi-bundle/createModuleLoader';
|
|
22
22
|
export { createModuleRouteLoader } from './multi-bundle/createModuleRouteLoader';
|
|
23
23
|
|
|
24
24
|
// 模块预加载
|
|
@@ -251,6 +251,8 @@ const createHomeScreen = createModuleRouteLoader('home', 'Home');
|
|
|
251
251
|
<Stack.Screen name="Home" getComponent={createHomeScreen} />
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
+
**注意**:`createModuleRouteLoader` 内部使用 `createModuleLoader`,因此也支持错误重试功能。如果模块加载失败,用户可以通过错误页面上的重试按钮重新加载模块。
|
|
255
|
+
|
|
254
256
|
### createModuleLoader
|
|
255
257
|
|
|
256
258
|
创建通用的模块加载器函数(用于自定义组件提取逻辑)。
|
|
@@ -263,14 +265,60 @@ function createModuleLoader(
|
|
|
263
265
|
): () => React.ComponentType<any>
|
|
264
266
|
```
|
|
265
267
|
|
|
268
|
+
**CreateModuleLoaderOptions**:
|
|
269
|
+
```typescript
|
|
270
|
+
interface CreateModuleLoaderOptions {
|
|
271
|
+
onError?: (error: Error) => void; // 错误处理回调
|
|
272
|
+
ErrorFallback?: React.ComponentType<ErrorFallbackProps>; // 自定义错误组件
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**ErrorFallbackProps**:
|
|
277
|
+
```typescript
|
|
278
|
+
interface ErrorFallbackProps {
|
|
279
|
+
moduleId: string; // 模块 ID
|
|
280
|
+
error: Error | null; // 错误对象
|
|
281
|
+
onRetry: () => void; // 重试回调(点击重试按钮会触发重新加载)
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
266
285
|
**使用示例**:
|
|
267
286
|
```typescript
|
|
287
|
+
// 基础用法
|
|
268
288
|
const createCustomScreen = createModuleLoader(
|
|
269
289
|
'my-module',
|
|
270
290
|
(exports) => exports.components.MyComponent
|
|
271
291
|
);
|
|
292
|
+
|
|
293
|
+
// 使用自定义错误组件
|
|
294
|
+
import { ErrorFallbackProps } from '@bm-fe/react-native-multi-bundle';
|
|
295
|
+
|
|
296
|
+
function MyCustomErrorFallback({ moduleId, error, onRetry }: ErrorFallbackProps) {
|
|
297
|
+
return (
|
|
298
|
+
<View>
|
|
299
|
+
<Text>模块 {moduleId} 加载失败</Text>
|
|
300
|
+
<Button title="重试" onPress={onRetry} />
|
|
301
|
+
</View>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const createCustomScreen = createModuleLoader(
|
|
306
|
+
'my-module',
|
|
307
|
+
(exports) => exports.components.MyComponent,
|
|
308
|
+
{
|
|
309
|
+
ErrorFallback: MyCustomErrorFallback,
|
|
310
|
+
onError: (error) => {
|
|
311
|
+
console.error('模块加载失败:', error);
|
|
312
|
+
},
|
|
313
|
+
}
|
|
314
|
+
);
|
|
272
315
|
```
|
|
273
316
|
|
|
317
|
+
**特性**:
|
|
318
|
+
- ✅ 自动重试:点击重试按钮会触发模块重新加载
|
|
319
|
+
- ✅ 自定义错误 UI:支持传入自定义错误组件
|
|
320
|
+
- ✅ 错误回调:可通过 `onError` 监听加载失败事件
|
|
321
|
+
|
|
274
322
|
|
|
275
323
|
### registerModuleRoute
|
|
276
324
|
|
|
@@ -11,8 +11,15 @@ import { ModuleErrorFallback } from './ModuleErrorFallback';
|
|
|
11
11
|
|
|
12
12
|
type ModuleState = 'idle' | 'loading' | 'success' | 'error';
|
|
13
13
|
|
|
14
|
-
interface
|
|
14
|
+
export interface ErrorFallbackProps {
|
|
15
|
+
moduleId: string;
|
|
16
|
+
error: Error | null;
|
|
17
|
+
onRetry: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CreateModuleLoaderOptions {
|
|
15
21
|
onError?: (error: Error) => void;
|
|
22
|
+
ErrorFallback?: React.ComponentType<ErrorFallbackProps>;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
/**
|
|
@@ -33,6 +40,7 @@ export function createModuleLoader(
|
|
|
33
40
|
const [InnerComponent, setInnerComponent] =
|
|
34
41
|
useState<React.ComponentType<any> | null>(null);
|
|
35
42
|
const [error, setError] = useState<Error | null>(null);
|
|
43
|
+
const [retryCount, setRetryCount] = useState(0);
|
|
36
44
|
|
|
37
45
|
useEffect(() => {
|
|
38
46
|
let isMounted = true;
|
|
@@ -63,16 +71,18 @@ export function createModuleLoader(
|
|
|
63
71
|
return () => {
|
|
64
72
|
isMounted = false;
|
|
65
73
|
};
|
|
66
|
-
}, [moduleId]);
|
|
74
|
+
}, [moduleId, retryCount]);
|
|
67
75
|
|
|
68
76
|
if (state === 'error') {
|
|
77
|
+
const FallbackComponent = options.ErrorFallback || ModuleErrorFallback;
|
|
69
78
|
return (
|
|
70
|
-
<
|
|
79
|
+
<FallbackComponent
|
|
71
80
|
moduleId={moduleId}
|
|
72
81
|
error={error}
|
|
73
82
|
onRetry={() => {
|
|
74
83
|
setState('idle');
|
|
75
84
|
setError(null);
|
|
85
|
+
setRetryCount((c) => c + 1);
|
|
76
86
|
}}
|
|
77
87
|
/>
|
|
78
88
|
);
|