@flun/node-mobile-app 2.0.4 → 2.0.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/CHANGELOG.md +5 -1
- package/README.md +97 -97
- package/package.json +2 -2
- package/scripts/copy-files.js +22 -23
- package/src/generate.js +3 -3
- package/src/install.js +6 -17
- package/src/patcher.js +2 -2
- package/src/platform-ios.js +1 -1
- package/src/utils.js +1 -1
- package/template/metro.config.js +4 -4
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @flun/node-mobile-app
|
|
2
2
|
|
|
3
|
-
将 Node.js 项目一键打包为 Android / iOS
|
|
3
|
+
将 Node.js 项目一键打包为 Android / iOS 移动应用;运行时由 @flun/nodejs-mobile-react-native 提供,无需修改你的 Node 代码;
|
|
4
4
|
|
|
5
|
-
**核心理念**:你的的 Node
|
|
5
|
+
**核心理念**:你的的 Node 项目保持不变,CLI 负责搬运、编译、打包;所有定制集中在 `mobileAppConfig.js` 一个文件;
|
|
6
6
|
|
|
7
7
|
- GitHub:https://github.com/OpenFlun/node-mobile-app
|
|
8
8
|
- Gitee(中国):https://gitee.com/OpenFlun/node-mobile-app
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
|
|
12
12
|
## 特性
|
|
13
13
|
|
|
14
|
-
- **零侵入**:Node
|
|
15
|
-
- **自动识别服务地址**:拦截 `server.js` 里的 `listen()`
|
|
16
|
-
- **失败可见**:5
|
|
17
|
-
-
|
|
14
|
+
- **零侵入**:Node 项目源码、依赖、配置都不动,CLI 在独立目录里完成构建
|
|
15
|
+
- **自动识别服务地址**:拦截 `server.js` 里的 `listen()` 调用,动态获取真实协议和端口
|
|
16
|
+
- **失败可见**:5 秒内未启动、加载异常,App 显示错误页并支持展开详情(开发者可查)
|
|
17
|
+
- **增量构建**:文件指纹判断缓存,无变化时 10 秒级重跑
|
|
18
18
|
- **跨平台**:Android(Windows / macOS / Linux)+ iOS(仅 macOS)
|
|
19
|
-
- **离线可用**:`build` 打出的 APK / IPA 内置 JS
|
|
19
|
+
- **离线可用**:`build` 打出的 APK / IPA 内置 JS,无需连电脑也能跑
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
| ------------ | ------------------------------------------------------------------- |
|
|
27
27
|
| Node.js | 需满足 `package.json` 的 `engines.node` 字段 |
|
|
28
28
|
| Android 构建 | JDK **17**、Android SDK(minSdk 29 / targetSdk 36 / compileSdk 37) |
|
|
29
|
-
| iOS 构建 | macOS + Xcode + CocoaPods
|
|
29
|
+
| iOS 构建 | macOS + 完整 Xcode + CocoaPods |
|
|
30
30
|
| 可选 | watchman(提升 Metro 文件监听性能) |
|
|
31
|
-
| Windows 额外 | Visual Studio + Git for Windows
|
|
31
|
+
| Windows 额外 | Visual Studio + Git for Windows(仅编译原生模块时需要,见下方说明) |
|
|
32
32
|
|
|
33
|
-
**JDK 必须是 17
|
|
33
|
+
**JDK 必须是 17**,Android Gradle Plugin 目前不支持更高版本;
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
@@ -85,13 +85,13 @@
|
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
**关键约定**:
|
|
88
|
-
- `template/`
|
|
89
|
-
- 你的持久化的自定义项应写进 `mobileAppConfig.js
|
|
90
|
-
- `template/.watchmanconfig` 会在 `copyTemplate` 时复制到 `node-mobile-app-build/.watchmanconfig
|
|
88
|
+
- `template/` 是**只读模板**;CLI 每次 `test`/`build` 会把它复制到你的项目的 `node-mobile-app-build/`,你的如手改模板内容会被覆盖
|
|
89
|
+
- 你的持久化的自定义项应写进 `mobileAppConfig.js`,或通过 `keepFiles` 保留特定模板文件
|
|
90
|
+
- `template/.watchmanconfig` 会在 `copyTemplate` 时复制到 `node-mobile-app-build/.watchmanconfig`,供 Metro 的 watchman 使用
|
|
91
91
|
|
|
92
92
|
### .watchmanconfig 说明
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
位于包根目录,配置 watchman 的文件监控行为:
|
|
95
95
|
|
|
96
96
|
```json
|
|
97
97
|
{
|
|
@@ -106,8 +106,8 @@
|
|
|
106
106
|
}
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
- **`ignore_dirs`**:排除的目录(构建产物、缓存、IDE
|
|
110
|
-
- **`settle
|
|
109
|
+
- **`ignore_dirs`**:排除的目录(构建产物、缓存、IDE 配置);**不要排除 `node_modules`**,Metro 需要监控它来解析依赖
|
|
110
|
+
- **`settle`**:防抖延迟(毫秒),短时间内多次文件变更只触发一次重编
|
|
111
111
|
- 改完后需 `watchman watch-del-all && watchman shutdown-server` 才生效
|
|
112
112
|
|
|
113
113
|
---
|
|
@@ -135,16 +135,16 @@
|
|
|
135
135
|
npm i -D @flun/node-mobile-app
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
安装完成后,会在你的项目根目录自动生成 `mobileAppConfig.js`(配置文件模板);
|
|
139
139
|
---
|
|
140
140
|
|
|
141
141
|
## 快速开始
|
|
142
142
|
|
|
143
143
|
```bash
|
|
144
144
|
# 1. 编辑配置
|
|
145
|
-
# 打开 mobileAppConfig.js
|
|
145
|
+
# 打开 mobileAppConfig.js,改 appId / appName / serverPath 等
|
|
146
146
|
|
|
147
|
-
# 2. 开发调试(需连 USB
|
|
147
|
+
# 2. 开发调试(需连 USB,走 Metro 热更新,如需用模拟器请在 mobileAppConfig.js文件中显式配置)
|
|
148
148
|
npx node-mobile-app test
|
|
149
149
|
|
|
150
150
|
# 3. 打包发布
|
|
@@ -155,27 +155,27 @@ npx node-mobile-app build --release
|
|
|
155
155
|
|
|
156
156
|
## 命令说明
|
|
157
157
|
|
|
158
|
-
| 命令 | 用途
|
|
159
|
-
| ------------------------------------- |
|
|
160
|
-
| `npx node-mobile-app test` | 编译并安装到设备(**需连 USB
|
|
161
|
-
| `npx node-mobile-app test --ios` | iOS 版(仅 macOS)
|
|
162
|
-
| `npx node-mobile-app build` | 打 debug APK(**离线可用**)
|
|
163
|
-
| `npx node-mobile-app build --release` | 打 release APK(默认用 debug keystore
|
|
164
|
-
| `npx node-mobile-app build --ios` | 打 IPA(仅 macOS)
|
|
165
|
-
| `npx node-mobile-app clean` | 清理构建缓存
|
|
158
|
+
| 命令 | 用途 |
|
|
159
|
+
| ------------------------------------- | --------------------------------------------------------------------------------- |
|
|
160
|
+
| `npx node-mobile-app test` | 编译并安装到设备(**需连 USB**,走 Metro 热更新) |
|
|
161
|
+
| `npx node-mobile-app test --ios` | iOS 版(仅 macOS) |
|
|
162
|
+
| `npx node-mobile-app build` | 打 debug APK(**离线可用**) |
|
|
163
|
+
| `npx node-mobile-app build --release` | 打 release APK(默认用 debug keystore 签名,可安装;上架需配置 `android.signing`) |
|
|
164
|
+
| `npx node-mobile-app build --ios` | 打 IPA(仅 macOS) |
|
|
165
|
+
| `npx node-mobile-app clean` | 清理构建缓存 |
|
|
166
166
|
|
|
167
167
|
### 常用选项
|
|
168
168
|
|
|
169
|
-
| 选项 | 用途
|
|
170
|
-
| --------------- |
|
|
171
|
-
| `--ios` | 目标平台为 iOS(默认 Android)
|
|
172
|
-
| `--release` | 打 release 包(仅 `build`)
|
|
173
|
-
| `--device <id>` | 指定设备(`test`)
|
|
174
|
-
| `--arch <abi>` | 指定 ABI
|
|
169
|
+
| 选项 | 用途 |
|
|
170
|
+
| --------------- | --------------------------------------- |
|
|
171
|
+
| `--ios` | 目标平台为 iOS(默认 Android) |
|
|
172
|
+
| `--release` | 打 release 包(仅 `build`) |
|
|
173
|
+
| `--device <id>` | 指定设备(`test`) |
|
|
174
|
+
| `--arch <abi>` | 指定 ABI,逗号分隔(`build`,仅 Android) |
|
|
175
175
|
|
|
176
176
|
### 安装 APK 到手机
|
|
177
177
|
|
|
178
|
-
`build` 打出的 APK 在 `outputDir`(默认 `./out
|
|
178
|
+
`build` 打出的 APK 在 `outputDir`(默认 `./out/`)里;手动装到手机:
|
|
179
179
|
|
|
180
180
|
```powershell
|
|
181
181
|
# Windows PowerShell
|
|
@@ -192,13 +192,13 @@ adb install -r ./out/<appName>-<versionName>-debug.apk
|
|
|
192
192
|
- `-r` = 覆盖安装(保留应用数据)
|
|
193
193
|
- 不知道 APK 的确切文件名?看 `build` 命令的末尾输出(会打印完整路径)
|
|
194
194
|
- `adb` 找不到?在 `mobileAppConfig.js` 里配 `android.adbPath`
|
|
195
|
-
- `test`
|
|
195
|
+
- `test` 命令已自动安装,无需手动
|
|
196
196
|
|
|
197
197
|
---
|
|
198
198
|
|
|
199
199
|
## 配置文件 `mobileAppConfig.js`
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
所有定制集中在这一个文件;**不配置也能跑**(用默认值);
|
|
202
202
|
|
|
203
203
|
### 必填
|
|
204
204
|
|
|
@@ -212,41 +212,41 @@ adb install -r ./out/<appName>-<versionName>-debug.apk
|
|
|
212
212
|
| ------------- | ---------------- | -------------------------------------------------------------------------- |
|
|
213
213
|
| `appName` | `string \| null` | 应用显示名称(默认从 `package.json` 的 `name` 读) |
|
|
214
214
|
| `outputDir` | `string \| null` | APK / IPA 输出目录(默认 `./out`) |
|
|
215
|
-
| `appId` | `string` |
|
|
215
|
+
| `appId` | `string` | 应用唯一标识,反向域名格式(Android `applicationId` / iOS Bundle ID) |
|
|
216
216
|
| `versionCode` | `int` | 内部版本号(Android `versionCode` / iOS `CFBundleVersion`) |
|
|
217
217
|
| `versionName` | `string` | 你的可见版本名(Android `versionName` / iOS `CFBundleShortVersionString`) |
|
|
218
218
|
|
|
219
219
|
### WebView
|
|
220
220
|
|
|
221
|
-
| 字段 | 类型 | 默认 | 说明
|
|
222
|
-
| --------------------- | --------- | ------------------- |
|
|
223
|
-
| `showLoading` | `boolean` | `true` | 是否显示加载指示器
|
|
224
|
-
| `loadingText` | `string` | `'正在启动服务...'` | 加载提示文字
|
|
225
|
-
| `enableJavaScript` | `boolean` | `true` | 是否启用 JavaScript
|
|
226
|
-
| `enableDomStorage` | `boolean` | `true` | 是否启用 DOM Storage
|
|
227
|
-
| `allowFileAccess` | `boolean` | `true` | 是否允许访问本地文件
|
|
228
|
-
| `mixedContentMode` | `string` | `'always'` | 混合内容模式:`never` / `always` / `compatibility`
|
|
229
|
-
| `backgroundColor` | 见下 | `'#ffffff'` | WebView
|
|
230
|
-
| `allowSelfSignedCert` | `boolean` | `false` | 见下文「HTTPS 自签证书」
|
|
221
|
+
| 字段 | 类型 | 默认 | 说明 |
|
|
222
|
+
| --------------------- | --------- | ------------------- | ------------------------------------------------------------------------------------------ |
|
|
223
|
+
| `showLoading` | `boolean` | `true` | 是否显示加载指示器 |
|
|
224
|
+
| `loadingText` | `string` | `'正在启动服务...'` | 加载提示文字 |
|
|
225
|
+
| `enableJavaScript` | `boolean` | `true` | 是否启用 JavaScript |
|
|
226
|
+
| `enableDomStorage` | `boolean` | `true` | 是否启用 DOM Storage |
|
|
227
|
+
| `allowFileAccess` | `boolean` | `true` | 是否允许访问本地文件 |
|
|
228
|
+
| `mixedContentMode` | `string` | `'always'` | 混合内容模式:`never` / `always` / `compatibility` |
|
|
229
|
+
| `backgroundColor` | 见下 | `'#ffffff'` | WebView 背景色;支持:固定色 `'#ffffff'` / 跟随系统 `'system'` / 深浅两值 `{ light, dark }` |
|
|
230
|
+
| `allowSelfSignedCert` | `boolean` | `false` | 见下文「HTTPS 自签证书」 |
|
|
231
231
|
|
|
232
232
|
### Android
|
|
233
233
|
|
|
234
|
-
| 字段 | 类型 | 默认 | 说明
|
|
235
|
-
| ----------------------- | ---------------- | -------------------- |
|
|
236
|
-
| `sdkPath` | `string \| null` | `null` | Android SDK 路径(默认自动查找)
|
|
237
|
-
| `adbPath` | `string \| null` | `null` | adb 路径(默认自动查找)
|
|
238
|
-
| `minSdkVersion` | `int` | `29` | 最低 SDK(29=Android 10.0)
|
|
239
|
-
| `targetSdkVersion` | `int` | `36` | 目标 SDK
|
|
240
|
-
| `compileSdkVersion` | `int` | `37` | 编译 SDK
|
|
241
|
-
| `buildToolsVersion` | `string` | `'37.0.0'` | 构建工具版本
|
|
242
|
-
| `abiFilters` | `string[]` | `['arm64-v8a']` | 打包架构(真机 `arm64-v8a
|
|
243
|
-
| `icon` | `string` | `'./build/icon.png'` | 应用图标(建议 512×512 PNG)
|
|
244
|
-
| `usesCleartextTraffic` | `boolean` | `true` | 是否允许明文 HTTP
|
|
245
|
-
| `permissions` | `string[]` | `[]` | 额外权限(如 `'android.permission.CAMERA'`)
|
|
246
|
-
| `signing.keystore` | `string` | — | 签名 keystore
|
|
247
|
-
| `signing.storePassword` | `string` | — | keystore 密码
|
|
248
|
-
| `signing.keyAlias` | `string` | — | key 别名
|
|
249
|
-
| `signing.keyPassword` | `string` | — | key 密码
|
|
234
|
+
| 字段 | 类型 | 默认 | 说明 |
|
|
235
|
+
| ----------------------- | ---------------- | -------------------- | ---------------------------------------------------------------------- |
|
|
236
|
+
| `sdkPath` | `string \| null` | `null` | Android SDK 路径(默认自动查找) |
|
|
237
|
+
| `adbPath` | `string \| null` | `null` | adb 路径(默认自动查找) |
|
|
238
|
+
| `minSdkVersion` | `int` | `29` | 最低 SDK(29=Android 10.0) |
|
|
239
|
+
| `targetSdkVersion` | `int` | `36` | 目标 SDK |
|
|
240
|
+
| `compileSdkVersion` | `int` | `37` | 编译 SDK |
|
|
241
|
+
| `buildToolsVersion` | `string` | `'37.0.0'` | 构建工具版本 |
|
|
242
|
+
| `abiFilters` | `string[]` | `['arm64-v8a']` | 打包架构(真机 `arm64-v8a`,模拟器加 `x86_64`) |
|
|
243
|
+
| `icon` | `string` | `'./build/icon.png'` | 应用图标(建议 512×512 PNG) |
|
|
244
|
+
| `usesCleartextTraffic` | `boolean` | `true` | 是否允许明文 HTTP |
|
|
245
|
+
| `permissions` | `string[]` | `[]` | 额外权限(如 `'android.permission.CAMERA'`) |
|
|
246
|
+
| `signing.keystore` | `string` | — | 签名 keystore 路径(相对你的项目根);不填则用模板自带的 debug keystore |
|
|
247
|
+
| `signing.storePassword` | `string` | — | keystore 密码 |
|
|
248
|
+
| `signing.keyAlias` | `string` | — | key 别名 |
|
|
249
|
+
| `signing.keyPassword` | `string` | — | key 密码 |
|
|
250
250
|
|
|
251
251
|
### iOS
|
|
252
252
|
|
|
@@ -284,15 +284,15 @@ adb install -r ./out/<appName>-<versionName>-debug.apk
|
|
|
284
284
|
|
|
285
285
|
## 服务地址说明
|
|
286
286
|
|
|
287
|
-
CLI **自动识别 `server.js` 实际监听的协议和端口**——拦截 `net.Server.prototype.listen
|
|
287
|
+
CLI **自动识别 `server.js` 实际监听的协议和端口**——拦截 `net.Server.prototype.listen`,从 `server.address()` 取真实端口,从 `Server` 类型判断 `http` / `https`;
|
|
288
288
|
|
|
289
|
-
因此 `mobileAppConfig.js`
|
|
289
|
+
因此 `mobileAppConfig.js` **不需要**配置端口/协议;动态端口(`listen(0)`)会拿到系统分配的真实端口;
|
|
290
290
|
|
|
291
291
|
**失败场景**:
|
|
292
292
|
- `server.js` 加载抛异常 → App 显示"应用启动失败"
|
|
293
293
|
- 5 秒内未调用 `listen()` → App 显示"应用启动失败"
|
|
294
294
|
|
|
295
|
-
|
|
295
|
+
你的看到简要提示,开发者点开"▶ 查看详细信息"能看到具体原因;
|
|
296
296
|
|
|
297
297
|
---
|
|
298
298
|
|
|
@@ -308,37 +308,37 @@ CLI **自动识别 `server.js` 实际监听的协议和端口**——拦截 `net
|
|
|
308
308
|
| HTTPS + 自签证书 + 未装设备(本地测试最常见) | **必须设为 `true`** |
|
|
309
309
|
|
|
310
310
|
**实现方式**:
|
|
311
|
-
- Android:patch `RNCWebViewClient.java
|
|
312
|
-
- iOS:patch `RNCWebViewImpl.m
|
|
311
|
+
- Android:patch `RNCWebViewClient.java`,仅对 `127.0.0.1` / `localhost` 放行
|
|
312
|
+
- iOS:patch `RNCWebViewImpl.m`,同上
|
|
313
313
|
|
|
314
|
-
|
|
314
|
+
其他域名仍严格校验;生产环境建议关闭;
|
|
315
315
|
|
|
316
316
|
---
|
|
317
317
|
|
|
318
318
|
## Android / iOS 差异
|
|
319
319
|
|
|
320
|
-
| 项 | Android
|
|
321
|
-
| ------------ |
|
|
322
|
-
| 构建环境 | Windows / macOS / Linux
|
|
323
|
-
| 运行时版本 |
|
|
324
|
-
| Express 版本 | 产物带 full-icu
|
|
325
|
-
| full-icu | 自 v22.23.2 产物起支持
|
|
326
|
-
| `test` 离线 | ✓(内置 bundle)
|
|
327
|
-
| 图标尺寸 | 5 个密度(48~192)
|
|
320
|
+
| 项 | Android | iOS |
|
|
321
|
+
| ------------ | --------------------------------------------------------- | ---------------------------------------- |
|
|
322
|
+
| 构建环境 | Windows / macOS / Linux | **仅 macOS** |
|
|
323
|
+
| 运行时版本 | 官方提供多个版本,可用 `NODE_MOBILE_PREBUILT_VERSION` 切换 | **当前为 v18.20.4**,更高版本后续逐步适配 |
|
|
324
|
+
| Express 版本 | 产物带 full-icu,4.x / 5.x 均可用 | 当前产物不带 full-icu,**必须用 4.x** |
|
|
325
|
+
| full-icu | 自 v22.23.2 产物起支持 | 暂无 |
|
|
326
|
+
| `test` 离线 | ✓(内置 bundle) | ✓(需 `FORCE_BUNDLING=1`) |
|
|
327
|
+
| 图标尺寸 | 5 个密度(48~192) | 9 个尺寸(40~1024) |
|
|
328
328
|
|
|
329
|
-
**iOS
|
|
329
|
+
**iOS 运行时版本**:iOS 附件的实际运行时版本随适配进度变化,**当前对应 v18.20.4**;如需更高版本,可克隆 [nodejs-mobile 仓库](https://github.com/OpenFlun/nodejs-mobile) 自行编译 `NodeMobile.xcframework`,流程与 Android 自编译 `libnode.so` 类似(见 `自定义libnode指南.md`);
|
|
330
330
|
|
|
331
331
|
---
|
|
332
332
|
|
|
333
333
|
## 你的项目结构
|
|
334
334
|
|
|
335
|
-
运行 CLI
|
|
335
|
+
运行 CLI 后,你的项目根目录的样子:
|
|
336
336
|
|
|
337
337
|
```
|
|
338
338
|
你的项目/
|
|
339
339
|
├── package.json # 你自己的
|
|
340
340
|
├── mobileAppConfig.js # CLI 生成的配置(可编辑)
|
|
341
|
-
├── server.js # Node
|
|
341
|
+
├── server.js # Node 启动脚本(你写的,或 CLI 生成的示例)
|
|
342
342
|
├── node_modules/ # 你的依赖
|
|
343
343
|
├── build/ # 你的资源(图标、keystore 等)
|
|
344
344
|
│ ├── icon.png
|
|
@@ -346,11 +346,11 @@ CLI **自动识别 `server.js` 实际监听的协议和端口**——拦截 `net
|
|
|
346
346
|
│── 你的其它文件/目录...
|
|
347
347
|
└── node-mobile-app-build/ # CLI 生成的 RN 工程(可整体删除重建)
|
|
348
348
|
├── android/ ios/ App.tsx index.js ...
|
|
349
|
-
├── mobileApp.runtime.ts # CLI
|
|
349
|
+
├── mobileApp.runtime.ts # CLI 生成,App.tsx 读
|
|
350
350
|
└── nodejs-assets/
|
|
351
351
|
└── nodejs-project/ # 你的 Node 项目副本
|
|
352
352
|
├── server.js # 你的源码
|
|
353
|
-
├── main.js # CLI
|
|
353
|
+
├── main.js # CLI 生成,桥接代码
|
|
354
354
|
│── 你允许的其它生产文件/目录...
|
|
355
355
|
├── package.json # 只保留 dependencies
|
|
356
356
|
└── node_modules/ # 你的生产依赖
|
|
@@ -362,18 +362,18 @@ CLI **自动识别 `server.js` 实际监听的协议和端口**——拦截 `net
|
|
|
362
362
|
|
|
363
363
|
### Q:`test` 拔掉 USB 后 App 加载慢?
|
|
364
364
|
|
|
365
|
-
`test` 打的是 debug
|
|
365
|
+
`test` 打的是 debug 版,启动时**先尝试连 Metro**,失败后才 fallback 到内置 bundle;这个"等超时"是 RN debug 版的固有行为;
|
|
366
366
|
|
|
367
367
|
- 需要**秒开**:用 `npx node-mobile-app build`
|
|
368
368
|
- 需要**热更新**:`test` 保持 USB 连接
|
|
369
369
|
|
|
370
370
|
### Q:`test` 装到手机后拔 USB 就崩?
|
|
371
371
|
|
|
372
|
-
前提是 APK 里有内置 bundle
|
|
372
|
+
前提是 APK 里有内置 bundle;`test` 每次都会先 `react-native bundle` 打进 APK;**如果崩,检查 CLI 输出里 `[6/9] 生成 JS bundle` 是否成功**;
|
|
373
373
|
|
|
374
374
|
### Q:Node 进程启动失败怎么排查?
|
|
375
375
|
|
|
376
|
-
App 显示"应用启动失败"页 → 点"▶ 查看详细信息" → 看到具体原因(如 `加载 ./server.js 失败 --
|
|
376
|
+
App 显示"应用启动失败"页 → 点"▶ 查看详细信息" → 看到具体原因(如 `加载 ./server.js 失败 -- ...`);
|
|
377
377
|
|
|
378
378
|
进一步用 `adb logcat` 看 Node 日志:
|
|
379
379
|
|
|
@@ -393,11 +393,11 @@ adb logcat | grep -E "NODEJS-MOBILE|ReactNativeJS"
|
|
|
393
393
|
- **Android**:内置完整 ICU(full-icu)→ 可以用 Express 5
|
|
394
394
|
- **iOS**:暂无 ICU → 必须用 Express 4
|
|
395
395
|
|
|
396
|
-
Express 5 的路由语法与 4 不同(如 `app.get('*')` 要改成 `app.get('/{*splat}')
|
|
396
|
+
Express 5 的路由语法与 4 不同(如 `app.get('*')` 要改成 `app.get('/{*splat}')`),迁移时留意;
|
|
397
397
|
|
|
398
398
|
### Q:`libnode.so` 和 `NodeMobile.xcframework` 是怎么来的?
|
|
399
399
|
|
|
400
|
-
从 `@flun/nodejs-mobile-react-native` 1.0.1
|
|
400
|
+
从 `@flun/nodejs-mobile-react-native` 1.0.1 起,两套预编译二进制**不在 npm 包里**,改为 postinstall 阶段自动从 Gitee / GitHub Release 下载:
|
|
401
401
|
|
|
402
402
|
| 资源 | 目标位置 |
|
|
403
403
|
| -------------------------------- | --------------------------------------------------------------------------- |
|
|
@@ -405,33 +405,33 @@ Express 5 的路由语法与 4 不同(如 `app.get('*')` 要改成 `app.get('/
|
|
|
405
405
|
| `ios-nodemobile.zip`(仅 macOS) | `node_modules/@flun/nodejs-mobile-react-native/ios/NodeMobile.xcframework/` |
|
|
406
406
|
|
|
407
407
|
- 默认下载版本以 `@flun/nodejs-mobile-react-native` 的 `package.json` 中 `prebuiltAssets.version` 字段为准
|
|
408
|
-
- Gitee
|
|
409
|
-
-
|
|
410
|
-
-
|
|
408
|
+
- Gitee 优先,失败自动回退 GitHub
|
|
409
|
+
- 已装版本匹配则跳过,不重复下
|
|
410
|
+
- 全部源失败时打印下载链接,**不中断 npm 安装**
|
|
411
411
|
|
|
412
412
|
环境变量:
|
|
413
413
|
|
|
414
414
|
```bash
|
|
415
415
|
NODE_MOBILE_PREBUILT_VERSION=<版本号> # 覆盖默认版本(须为该包 Release 中实际存在的 tag)
|
|
416
|
-
NODE_MOBILE_PREBUILT_SKIP=1
|
|
417
|
-
NODE_MOBILE_PREBUILT_IOS_SKIP=1
|
|
416
|
+
NODE_MOBILE_PREBUILT_SKIP=1 # 跳过全部下载
|
|
417
|
+
NODE_MOBILE_PREBUILT_IOS_SKIP=1 # 只跳过 iOS
|
|
418
418
|
```
|
|
419
419
|
|
|
420
420
|
### Q:如何替换成自定义编译的 `libnode.so`?
|
|
421
421
|
|
|
422
|
-
如果你自编译了 `libnode.so`(如需要特定 Node 版本或自定义 ICU
|
|
422
|
+
如果你自编译了 `libnode.so`(如需要特定 Node 版本或自定义 ICU),可手动覆盖自动下载的产物:
|
|
423
423
|
|
|
424
424
|
1. 把编译好的 `libnode.so` 覆盖到 `android/libnode/bin/<架构>/`(**只放你实际编译的架构**)
|
|
425
|
-
2. 如果 `mobileAppConfig.js` 的 `android.abiFilters` 与 libnode
|
|
425
|
+
2. 如果 `mobileAppConfig.js` 的 `android.abiFilters` 与 libnode 支持的架构不一致,同步调整
|
|
426
426
|
3. 跑 `npx node-mobile-app build` 或 `npx node-mobile-app test`
|
|
427
427
|
|
|
428
|
-
**CLI 会自动处理缓存**:指纹机制检测到 `libnode.so` 或 `abiFilters`
|
|
428
|
+
**CLI 会自动处理缓存**:指纹机制检测到 `libnode.so` 或 `abiFilters` 变化,自动清理 `.cxx` / `android/build` / `app/build`,全量重编;**你的无需手动清任何缓存**;
|
|
429
429
|
|
|
430
|
-
详细流程见 `自定义libnode指南.md
|
|
430
|
+
详细流程见 `自定义libnode指南.md`;
|
|
431
431
|
|
|
432
432
|
### Q:iOS 提示 `fallbackSourceURL does not override`?
|
|
433
433
|
|
|
434
|
-
RN 0.87 的 API
|
|
434
|
+
RN 0.87 的 API 若变动,`AppDelegate.swift` 里的 fallback 可能不生效——不影响"连 Mac + Metro"正常开发,只是拔 Mac 后无法 fallback 到内置 bundle;届时反馈,我们更新适配;
|
|
435
435
|
|
|
436
436
|
---
|
|
437
437
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flun/node-mobile-app",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "将 Node.js 项目一键打包为 Android / iOS 移动应用,运行时由 @flun/nodejs-mobile-react-native 提供,无需修改你的 Node 代码;",
|
|
5
5
|
"author": "flun <open@flun.top>",
|
|
6
6
|
"keywords": [
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"eslint-plugin-react": "^7.37.5",
|
|
63
63
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
64
64
|
"eslint-plugin-react-native": "^5.0.0",
|
|
65
|
-
"@flun/nodejs-mobile-react-native": "^2.0.
|
|
65
|
+
"@flun/nodejs-mobile-react-native": "^2.0.3"
|
|
66
66
|
},
|
|
67
67
|
"allowScripts": {
|
|
68
68
|
"node": true,
|
package/scripts/copy-files.js
CHANGED
|
@@ -6,31 +6,30 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__filename),
|
|
7
7
|
initCwd = process.env.INIT_CWD, fileName = 'mobileAppConfig.js', targetDir = initCwd ? path.resolve(initCwd) : '',
|
|
8
8
|
// 要拷贝的文件和目标文件路径
|
|
9
|
-
sourceFile = path.join(__dirname, '..', fileName), targetFile = path.join(targetDir, fileName)
|
|
9
|
+
sourceFile = path.join(__dirname, '..', fileName), targetFile = path.join(targetDir, fileName),
|
|
10
|
+
/**
|
|
11
|
+
* 复制文件到项目根目录
|
|
12
|
+
* >查看定义:@see {@link copyFile}
|
|
13
|
+
* @returns {boolean} - 复制是否成功
|
|
14
|
+
*/
|
|
15
|
+
copyFile = () => {
|
|
16
|
+
console.log(`🔍 检查 ${fileName} 文件...`), console.log(`📁 项目根目录:${targetDir}`);
|
|
17
|
+
try {
|
|
18
|
+
// 无 INIT_CWD(非用户项目安装场景)|| 目标目录不是 Node 项目 || 包自身的 npm install(开发调试)→ 跳过
|
|
19
|
+
if (!targetDir) return true;
|
|
20
|
+
if (!fs.existsSync(path.join(targetDir, 'package.json'))) return true;
|
|
21
|
+
if (targetDir === path.resolve(__dirname, '..')) return true;
|
|
22
|
+
if (fs.existsSync(targetFile)) return true; // 目标文件已存在 → 直接返回
|
|
23
|
+
console.log(`⚠️ 在项目根目录未找到 ${fileName} 文件,正在创建...`);
|
|
10
24
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
// 无 INIT_CWD(非用户项目安装场景)|| 目标目录不是 Node 项目 || 包自身的 npm install(开发调试)→ 跳过
|
|
20
|
-
if (!targetDir) return true;
|
|
21
|
-
if (!fs.existsSync(path.join(targetDir, 'package.json'))) return true;
|
|
22
|
-
if (targetDir === path.resolve(__dirname, '..')) return true;
|
|
23
|
-
if (fs.existsSync(targetFile)) return true; // 目标文件已存在 → 直接返回
|
|
24
|
-
console.log(`⚠️ 在项目根目录未找到 ${fileName} 文件,正在创建...`);
|
|
25
|
-
|
|
26
|
-
fs.copyFileSync(sourceFile, targetFile); // 复制源文件到项目根目录
|
|
27
|
-
console.log(`✓ 已创建 ${fileName} 示例文件:${targetFile}`);
|
|
28
|
-
return true;
|
|
29
|
-
} catch (error) {
|
|
30
|
-
console.error(`✗ 创建 ${fileName} 文件失败:`, error.message);
|
|
31
|
-
return false;
|
|
25
|
+
fs.copyFileSync(sourceFile, targetFile); // 复制源文件到项目根目录
|
|
26
|
+
console.log(`✓ 已创建 ${fileName} 示例文件:${targetFile}`);
|
|
27
|
+
return true;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(`✗ 创建 ${fileName} 文件失败:`, error.message);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
32
|
}
|
|
33
|
-
}
|
|
34
33
|
|
|
35
34
|
// 执行脚本并导出函数
|
|
36
35
|
if (process.argv[1] === __filename) copyFile();
|
package/src/generate.js
CHANGED
|
@@ -94,11 +94,11 @@ const generateMainJs = (targetDir, config, moduleType) => {
|
|
|
94
94
|
enableDomStorage: config.webview.enableDomStorage,
|
|
95
95
|
allowFileAccess: config.webview.allowFileAccess,
|
|
96
96
|
mixedContentMode: config.webview.mixedContentMode,
|
|
97
|
-
backgroundColor: config.webview.backgroundColor
|
|
97
|
+
backgroundColor: config.webview.backgroundColor
|
|
98
98
|
},
|
|
99
99
|
}, content = `// 自动生成,请勿手改\nexport default ${JSON.stringify(data, null, 2)} as const;\n`,
|
|
100
100
|
changed = writeIfChanged(path.join(buildDir, 'mobileApp.runtime.ts'), content);
|
|
101
101
|
console.log(changed ? ' ✓ 已生成 mobileApp.runtime.ts' : ' ✓ mobileApp.runtime.ts 无变化');
|
|
102
|
-
}
|
|
102
|
+
};
|
|
103
103
|
|
|
104
|
-
export { generateMainJs, generateRuntime }
|
|
104
|
+
export { generateMainJs, generateRuntime }
|
package/src/install.js
CHANGED
|
@@ -89,31 +89,20 @@ const hashDeps = (pkgPath, hasNative, forceRebuild) => {
|
|
|
89
89
|
*/
|
|
90
90
|
installUserDeps = (targetDir, userProjectDir, config) => {
|
|
91
91
|
const assetsDir = path.dirname(targetDir), bnmFile = path.join(assetsDir, 'BUILD_NATIVE_MODULES.txt'),
|
|
92
|
-
hashFile = path.join(targetDir, '.deps-hash'),
|
|
93
|
-
|
|
94
|
-
forceRebuild = process.env.NODE_MOBILE_FORCE_REBUILD === '1';
|
|
95
|
-
|
|
96
|
-
// 自动检测是否有原生模块(binding.gyp)
|
|
97
|
-
const hasNative = hasNativeModules(userProjectDir);
|
|
98
|
-
fs.writeFileSync(bnmFile, hasNative ? '1' : '0');
|
|
92
|
+
hashFile = path.join(targetDir, '.deps-hash'), nodeModulesDir = path.join(targetDir, 'node_modules'),
|
|
93
|
+
forceRebuild = process.env.NODE_MOBILE_FORCE_REBUILD === '1', hasNative = hasNativeModules(userProjectDir);
|
|
99
94
|
|
|
95
|
+
fs.writeFileSync(bnmFile, hasNative ? '1' : '0'); // 自动检测是否有原生模块(binding.gyp)
|
|
100
96
|
// 计算当前依赖 hash(含 native + force 状态)
|
|
101
|
-
const pkgPath = path.join(targetDir, 'package.json'),
|
|
102
|
-
currentHash = hashDeps(pkgPath, hasNative, forceRebuild),
|
|
97
|
+
const pkgPath = path.join(targetDir, 'package.json'), currentHash = hashDeps(pkgPath, hasNative, forceRebuild),
|
|
103
98
|
hasModules = fs.existsSync(nodeModulesDir);
|
|
104
99
|
let oldHash = null;
|
|
105
100
|
if (fs.existsSync(hashFile)) try { oldHash = fs.readFileSync(hashFile, 'utf-8').trim(); } catch { };
|
|
106
|
-
|
|
107
|
-
if (hasModules && oldHash === currentHash && !forceRebuild) {
|
|
108
|
-
return console.log(' ✓ 依赖无变化,跳过安装');
|
|
109
|
-
}
|
|
101
|
+
if (hasModules && oldHash === currentHash && !forceRebuild) return console.log(' ✓ 依赖无变化,跳过安装');
|
|
110
102
|
if (forceRebuild) console.log(' ℹ️ NODE_MOBILE_FORCE_REBUILD=1,强制重装依赖');
|
|
111
103
|
|
|
112
104
|
const env = { ...process.env };
|
|
113
|
-
if (hasNative)
|
|
114
|
-
env.NODEJS_MOBILE_BUILD_NATIVE_MODULES = '1';
|
|
115
|
-
console.log(' ℹ️ 检测到原生模块(binding.gyp),将编译原生代码');
|
|
116
|
-
}
|
|
105
|
+
if (hasNative) env.NODEJS_MOBILE_BUILD_NATIVE_MODULES = '1', console.log(' ℹ️ 检测到原生模块(binding.gyp),将编译原生代码');
|
|
117
106
|
|
|
118
107
|
console.log(' 安装用户生产依赖(nodejs-assets/nodejs-project)...');
|
|
119
108
|
run('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], { cwd: targetDir, env });
|
package/src/patcher.js
CHANGED
|
@@ -53,7 +53,7 @@ const resolveAppName = (userProjectDir, config) => {
|
|
|
53
53
|
stringsFile = path.join(buildDir, 'android', 'app', 'src', 'main', 'res', 'values', 'strings.xml'),
|
|
54
54
|
ktFiles = [
|
|
55
55
|
path.join(buildDir, 'android', 'app', 'src', 'main', 'java', 'com', 'myapp', 'MainActivity.kt'),
|
|
56
|
-
path.join(buildDir, 'android', 'app', 'src', 'main', 'java', 'com', 'myapp', 'MainApplication.kt')
|
|
56
|
+
path.join(buildDir, 'android', 'app', 'src', 'main', 'java', 'com', 'myapp', 'MainApplication.kt')
|
|
57
57
|
];
|
|
58
58
|
// 1. build.gradle
|
|
59
59
|
if (fs.existsSync(gradleFile)) {
|
|
@@ -370,5 +370,5 @@ const resolveAppName = (userProjectDir, config) => {
|
|
|
370
370
|
// ===== 导出 =====
|
|
371
371
|
export {
|
|
372
372
|
resolveAppName, escapeKotlinString, escapeSwiftString, patchAppConfig, patchAndroidConfig, patchSigning,
|
|
373
|
-
patchIcon, patchIosConfig, patchIosIcon
|
|
373
|
+
patchIcon, patchIosConfig, patchIosIcon
|
|
374
374
|
};
|
package/src/platform-ios.js
CHANGED
package/src/utils.js
CHANGED
|
@@ -99,7 +99,7 @@ const ensureDir = dir => {
|
|
|
99
99
|
candidates = [
|
|
100
100
|
path.join(localAppData, 'Android', 'Sdk', 'platform-tools', 'adb.exe'),
|
|
101
101
|
path.join(home, 'Library', 'Android', 'sdk', 'platform-tools', 'adb'),
|
|
102
|
-
path.join(home, 'Android', 'Sdk', 'platform-tools', 'adb')
|
|
102
|
+
path.join(home, 'Android', 'Sdk', 'platform-tools', 'adb')
|
|
103
103
|
];
|
|
104
104
|
for (const c of candidates) if (c && fs.existsSync(c)) return c;
|
|
105
105
|
return 'adb';
|
package/template/metro.config.js
CHANGED
|
@@ -3,15 +3,15 @@ import exclusionListModule from 'metro-config/private/defaults/exclusionList';
|
|
|
3
3
|
import { dirname, resolve } from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
|
|
6
|
-
const exclusionList = exclusionListModule.default
|
|
7
|
-
|
|
6
|
+
const exclusionList = exclusionListModule.default,
|
|
7
|
+
__filename = fileURLToPath(import.meta.url), __dirname = dirname(__filename), workspaceRoot = resolve(__dirname, '..'),
|
|
8
8
|
config = {
|
|
9
9
|
watchFolders: [__dirname, resolve(workspaceRoot, 'node_modules')],
|
|
10
10
|
resolver: {
|
|
11
11
|
nodeModulesPaths: [resolve(__dirname, 'node_modules'), resolve(workspaceRoot, 'node_modules')],
|
|
12
12
|
blockList: exclusionList([
|
|
13
|
-
/[/\\]nodejs-assets[/\\]
|
|
14
|
-
])
|
|
13
|
+
/[/\\]nodejs-assets[/\\].*/
|
|
14
|
+
])
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
17
|
|