@bm-fe/react-native-multi-bundle 1.0.0-beta.3 → 1.0.0-beta.5
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 +71 -6
- package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderModule.kt +65 -126
- package/ios/ModuleLoader/ModuleLoaderModule.h +12 -0
- package/ios/ModuleLoader/ModuleLoaderModule.mm +495 -0
- package/package.json +54 -35
- package/react-native-multi-bundle.podspec +29 -0
- package/react-native.config.js +4 -1
- package/scripts/sync-bundles-to-assets.js +3 -3
- package/src/multi-bundle/LocalBundleManager.ts +32 -38
- package/src/multi-bundle/ModuleRegistry.ts +16 -16
- package/templates/metro.config.js.template +377 -6
package/INTEGRATION.md
CHANGED
|
@@ -120,25 +120,90 @@ class MainApplication : Application(), ReactApplication {
|
|
|
120
120
|
|
|
121
121
|
</details>
|
|
122
122
|
|
|
123
|
-
### iOS
|
|
123
|
+
### iOS(自动链接,无需手动配置)
|
|
124
124
|
|
|
125
|
-
####
|
|
125
|
+
#### 自动链接工作原理
|
|
126
|
+
|
|
127
|
+
安装包后,React Native CLI 和 CocoaPods 会自动:
|
|
128
|
+
1. 读取包中的 `react-native-multi-bundle.podspec`
|
|
129
|
+
2. 将 `ModuleLoader` 原生模块添加到项目中
|
|
130
|
+
3. 在构建时自动集成
|
|
131
|
+
|
|
132
|
+
#### 安装依赖
|
|
133
|
+
|
|
134
|
+
安装 npm 包后,运行 pod install:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
cd ios && pod install
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### 验证自动链接
|
|
141
|
+
|
|
142
|
+
安装包后,可以通过以下命令验证自动链接是否生效:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npx react-native config
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
在输出中应该能看到 `@bm-fe/react-native-multi-bundle` 的 iOS 配置信息。
|
|
149
|
+
|
|
150
|
+
#### 准备 Bundles 目录
|
|
151
|
+
|
|
152
|
+
确保 iOS 项目中有 `Bundles` 目录用于存放 bundle 文件:
|
|
153
|
+
|
|
154
|
+
1. 在 Xcode 中,右键点击项目 → New Group,创建 `Bundles` 目录
|
|
155
|
+
2. 或者在终端中创建:
|
|
156
|
+
```bash
|
|
157
|
+
mkdir -p ios/YourProject/Bundles/modules
|
|
158
|
+
```
|
|
126
159
|
|
|
127
|
-
|
|
160
|
+
3. **重要**:确保 `Bundles` 目录已添加到 Xcode 项目的 Resources 中:
|
|
161
|
+
- 在 Xcode 中,右键点击 `Bundles` 目录 → Add Files to "YourProject"
|
|
162
|
+
- 确保 "Create folder references" 已选中(蓝色文件夹图标)
|
|
163
|
+
- 这样 bundle 文件才会被打包到 app 中
|
|
164
|
+
|
|
165
|
+
#### Bundle 文件结构
|
|
166
|
+
|
|
167
|
+
构建完成后,iOS bundle 文件应放置在:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
ios/YourProject/Bundles/
|
|
171
|
+
├── bundle-manifest.json
|
|
172
|
+
├── main.jsbundle
|
|
173
|
+
└── modules/
|
|
174
|
+
├── home.jsbundle
|
|
175
|
+
├── details.jsbundle
|
|
176
|
+
└── settings.jsbundle
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### 手动集成(可选,仅在自动链接不工作时使用)
|
|
180
|
+
|
|
181
|
+
<details>
|
|
182
|
+
<summary>点击展开手动集成步骤</summary>
|
|
183
|
+
|
|
184
|
+
##### 1. 复制 Native 模块文件
|
|
185
|
+
|
|
186
|
+
将以下文件从 `node_modules/@bm-fe/react-native-multi-bundle/ios/ModuleLoader/` 复制到你的 iOS 项目:
|
|
128
187
|
|
|
129
188
|
- `ModuleLoaderModule.h`
|
|
130
|
-
- `ModuleLoaderModule.
|
|
189
|
+
- `ModuleLoaderModule.mm`
|
|
131
190
|
|
|
132
191
|
在 Xcode 中:
|
|
133
192
|
1. 右键点击项目 → Add Files to "YourProject"
|
|
134
193
|
2. 选择这两个文件
|
|
135
194
|
3. 确保 "Copy items if needed" 已勾选
|
|
136
195
|
|
|
137
|
-
|
|
196
|
+
##### 2. 验证编译
|
|
138
197
|
|
|
139
198
|
iOS 端的模块会自动注册(通过 `RCT_EXPORT_MODULE`),无需额外配置。
|
|
140
199
|
|
|
141
|
-
|
|
200
|
+
重新构建项目:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
npx react-native run-ios
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
</details>
|
|
142
207
|
|
|
143
208
|
## 初始化多 Bundle 系统
|
|
144
209
|
|
package/android/moduleloader/src/main/java/com/bitmart/exchange/module/loader/ModuleLoaderModule.kt
CHANGED
|
@@ -23,8 +23,7 @@ import java.io.InputStreamReader
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* 简单的 Bundle 加载模块
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
*
|
|
28
27
|
* 新架构下通过反射获取 ReactInstance,然后调用 loadJSBundle 方法
|
|
29
28
|
* 注意:ReactInstance 是 internal 类,必须完全通过反射操作
|
|
30
29
|
*/
|
|
@@ -34,11 +33,11 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
34
33
|
companion object {
|
|
35
34
|
const val NAME = "ModuleLoader"
|
|
36
35
|
private const val TAG = "ModuleLoaderModule"
|
|
37
|
-
|
|
36
|
+
|
|
38
37
|
// 缓存反射获取的类和方法,避免重复反射
|
|
39
38
|
private var reactHostImplClass: Class<*>? = null
|
|
40
39
|
private var reactInstanceClass: Class<*>? = null
|
|
41
|
-
|
|
40
|
+
|
|
42
41
|
init {
|
|
43
42
|
try {
|
|
44
43
|
reactHostImplClass = Class.forName("com.facebook.react.runtime.ReactHostImpl")
|
|
@@ -49,9 +48,6 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
// 记录已加载的 bundle,避免重复加载
|
|
53
|
-
private val loadedBundles = mutableSetOf<String>()
|
|
54
|
-
|
|
55
51
|
// 协程作用域,用于异步操作
|
|
56
52
|
private val moduleScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
57
53
|
|
|
@@ -59,71 +55,77 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
59
55
|
|
|
60
56
|
/**
|
|
61
57
|
* 加载子 bundle
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
58
|
+
*
|
|
59
|
+
* 加载策略(按优先级):
|
|
60
|
+
* 1. 旧架构:CatalystInstance(当前 RN 0.79.5)
|
|
61
|
+
* 2. 新架构:ReactHostImpl(完全 Bridgeless)
|
|
66
62
|
*/
|
|
67
63
|
@ReactMethod
|
|
68
64
|
fun loadBusinessBundle(bundleId: String, bundlePath: String, promise: Promise) {
|
|
69
65
|
Log.d(TAG, "loadBusinessBundle: bundleId=$bundleId, bundlePath=$bundlePath")
|
|
70
66
|
|
|
71
|
-
//
|
|
72
|
-
if (
|
|
73
|
-
Log.d(TAG, "Bundle already loaded: $bundleId")
|
|
74
|
-
val result = Arguments.createMap().apply {
|
|
75
|
-
putBoolean("success", true)
|
|
76
|
-
}
|
|
77
|
-
promise.resolve(result)
|
|
67
|
+
// 1️⃣ 优先:旧架构 CatalystInstance
|
|
68
|
+
if (tryLoadWithCatalystInstance(bundleId, bundlePath, promise)) {
|
|
78
69
|
return
|
|
79
70
|
}
|
|
80
71
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
}
|
|
72
|
+
// 2️⃣ 其次:新架构 ReactHostImpl
|
|
73
|
+
if (tryLoadWithReactHost(bundleId, bundlePath, promise)) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
95
76
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
}
|
|
77
|
+
// 都失败
|
|
78
|
+
Log.e(TAG, "No suitable runtime found")
|
|
79
|
+
promise.resolve(Arguments.createMap().apply {
|
|
80
|
+
putBoolean("success", false)
|
|
81
|
+
putString("errorMessage", "No CatalystInstance or ReactHostImpl available")
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 尝试使用旧架构 CatalystInstance 加载
|
|
87
|
+
* @return true 如果成功处理(包括成功加载或失败),false 如果 CatalystInstance 不可用
|
|
88
|
+
*/
|
|
89
|
+
@Suppress("DEPRECATION")
|
|
90
|
+
private fun tryLoadWithCatalystInstance(bundleId: String, bundlePath: String, promise: Promise): Boolean {
|
|
91
|
+
val catalystInstance = reactContext.catalystInstance ?: return false
|
|
118
92
|
|
|
93
|
+
Log.d(TAG, "Using CatalystInstance (same JS Runtime guaranteed)")
|
|
94
|
+
try {
|
|
95
|
+
val assetPath = "assets://$bundlePath"
|
|
96
|
+
catalystInstance.loadScriptFromAssets(reactContext.assets, assetPath, false)
|
|
97
|
+
Log.d(TAG, "Bundle loaded successfully: $bundleId")
|
|
98
|
+
promise.resolve(Arguments.createMap().apply { putBoolean("success", true) })
|
|
119
99
|
} catch (e: Exception) {
|
|
120
100
|
Log.e(TAG, "Failed to load bundle: $bundleId", e)
|
|
121
|
-
|
|
101
|
+
promise.resolve(Arguments.createMap().apply {
|
|
122
102
|
putBoolean("success", false)
|
|
123
103
|
putString("errorMessage", e.message ?: "Unknown error")
|
|
124
|
-
}
|
|
125
|
-
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 尝试使用新架构 ReactHostImpl 加载
|
|
111
|
+
* @return true 如果成功处理,false 如果 ReactHostImpl 不可用
|
|
112
|
+
*/
|
|
113
|
+
private fun tryLoadWithReactHost(bundleId: String, bundlePath: String, promise: Promise): Boolean {
|
|
114
|
+
val application = reactContext.applicationContext as? android.app.Application
|
|
115
|
+
val reactApplication = application as? com.facebook.react.ReactApplication ?: return false
|
|
116
|
+
|
|
117
|
+
val reactHost = reactApplication.reactHost
|
|
118
|
+
val hostImplClass = reactHostImplClass
|
|
119
|
+
|
|
120
|
+
if (reactHost == null || hostImplClass == null || !hostImplClass.isInstance(reactHost)) {
|
|
121
|
+
return false
|
|
126
122
|
}
|
|
123
|
+
|
|
124
|
+
Log.d(TAG, "Using ReactHostImpl (new architecture)")
|
|
125
|
+
val assetUrl = "assets://$bundlePath"
|
|
126
|
+
val bundleLoader = JSBundleLoader.createAssetLoader(reactContext, assetUrl, false)
|
|
127
|
+
loadBundleViaReflection(reactHost, bundleLoader, bundleId, promise)
|
|
128
|
+
return true
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
/**
|
|
@@ -138,12 +140,12 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
138
140
|
try {
|
|
139
141
|
val hostImplClass = reactHostImplClass ?: throw Exception("ReactHostImpl class not found")
|
|
140
142
|
val instanceClass = reactInstanceClass ?: throw Exception("ReactInstance class not found")
|
|
141
|
-
|
|
143
|
+
|
|
142
144
|
// 通过反射获取 private 的 reactInstance 字段
|
|
143
145
|
val reactInstanceField = hostImplClass.getDeclaredField("reactInstance")
|
|
144
146
|
reactInstanceField.isAccessible = true
|
|
145
147
|
val reactInstance = reactInstanceField.get(reactHost)
|
|
146
|
-
|
|
148
|
+
|
|
147
149
|
if (reactInstance == null) {
|
|
148
150
|
Log.e(TAG, "ReactInstance is null")
|
|
149
151
|
val result = Arguments.createMap().apply {
|
|
@@ -157,59 +159,16 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
157
159
|
// 通过反射调用 ReactInstance.loadJSBundle 方法
|
|
158
160
|
val loadJSBundleMethod = instanceClass.getMethod("loadJSBundle", JSBundleLoader::class.java)
|
|
159
161
|
loadJSBundleMethod.invoke(reactInstance, bundleLoader)
|
|
160
|
-
|
|
161
|
-
// 标记为已加载
|
|
162
|
-
loadedBundles.add(bundleId)
|
|
162
|
+
|
|
163
163
|
Log.d(TAG, "Bundle loaded successfully via ReactInstance: $bundleId")
|
|
164
164
|
|
|
165
165
|
val result = Arguments.createMap().apply {
|
|
166
166
|
putBoolean("success", true)
|
|
167
167
|
}
|
|
168
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
169
|
|
|
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
170
|
} catch (e: Exception) {
|
|
212
|
-
Log.e(TAG, "Failed to load bundle via
|
|
171
|
+
Log.e(TAG, "Failed to load bundle via ReactInstance: $bundleId", e)
|
|
213
172
|
val result = Arguments.createMap().apply {
|
|
214
173
|
putBoolean("success", false)
|
|
215
174
|
putString("errorMessage", e.message ?: "Unknown error")
|
|
@@ -218,24 +177,6 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
218
177
|
}
|
|
219
178
|
}
|
|
220
179
|
|
|
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
180
|
// ==================== Bundle Manifest Support ====================
|
|
240
181
|
|
|
241
182
|
/**
|
|
@@ -253,7 +194,7 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
253
194
|
val codePushDir = "${reactContext.filesDir.absolutePath}/CodePush"
|
|
254
195
|
val assetsBaseManifestPath = "$codePushDir/assets-base/bundle-manifest.json"
|
|
255
196
|
val assetsBaseManifestFile = File(assetsBaseManifestPath)
|
|
256
|
-
|
|
197
|
+
|
|
257
198
|
if (assetsBaseManifestFile.exists()) {
|
|
258
199
|
promise.resolve(assetsBaseManifestPath)
|
|
259
200
|
return
|
|
@@ -283,7 +224,7 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
283
224
|
val codePushDir = "${reactContext.filesDir.absolutePath}/CodePush"
|
|
284
225
|
val assetsBaseManifestPath = "$codePushDir/assets-base/bundle-manifest.json"
|
|
285
226
|
val assetsBaseManifestFile = File(assetsBaseManifestPath)
|
|
286
|
-
|
|
227
|
+
|
|
287
228
|
if (assetsBaseManifestFile.exists()) {
|
|
288
229
|
val content = readFileContent(assetsBaseManifestFile)
|
|
289
230
|
if (content != null) {
|
|
@@ -445,5 +386,3 @@ class ModuleLoaderModule(private val reactContext: ReactApplicationContext) : Re
|
|
|
445
386
|
}
|
|
446
387
|
}
|
|
447
388
|
}
|
|
448
|
-
|
|
449
|
-
|