@flun/node-mobile-app 4.0.3 → 4.0.4

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/src/utils.js CHANGED
@@ -21,7 +21,7 @@ const ensureDir = dir => {
21
21
  return false;
22
22
  },
23
23
  /**
24
- * 递归复制,exclude 支持 'node_modules/'、'*.log'、'.git/'
24
+ * 递归复制,exclude 支持 'node_modules/'、'*.log'、'.git/'
25
25
  */
26
26
  copyDir = (src, dest, exclude = []) => {
27
27
  ensureDir(dest);
@@ -36,8 +36,8 @@ const ensureDir = dir => {
36
36
  readJson = file => JSON.parse(fs.readFileSync(file, 'utf-8')),
37
37
  writeJson = (file, obj) => fs.writeFileSync(file, JSON.stringify(obj, null, 2) + '\n', 'utf-8'),
38
38
  /**
39
- * 内容不同才写文件,避免不必要的 mtime 变化触发下游(Gradle/Metro)重编
40
- * 返回 true 表示写入了,false 表示内容相同跳过
39
+ * 内容不同才写文件,避免不必要的 mtime 变化触发下游(Gradle/Metro)重编
40
+ * 返回 true 表示写入了,false 表示内容相同跳过
41
41
  */
42
42
  writeIfChanged = (file, content) => {
43
43
  if (fs.existsSync(file)) {
@@ -49,7 +49,7 @@ const ensureDir = dir => {
49
49
  return true;
50
50
  },
51
51
  /**
52
- * 大小和 mtime 都一致才跳过复制,否则复制
52
+ * 大小和 mtime 都一致才跳过复制,否则复制
53
53
  * 用于用户项目 → nodejs-project 的增量复制
54
54
  */
55
55
  copyFileIfChanged = (src, dst) => {
@@ -70,9 +70,9 @@ const ensureDir = dir => {
70
70
  },
71
71
  /**
72
72
  * 统一的子进程启动:
73
- * - Windows:.cmd / .bat 需经 shell;把命令与参数拼成单字符串,
73
+ * - Windows:.cmd / .bat 需经 shell;把命令与参数拼成单字符串,
74
74
  * 避免 spawnSync(bin, args, { shell: true }) 触发 DEP0190。
75
- * - 其它平台:不用 shell,参数交由 Node 转义。
75
+ * - 其它平台:不用 shell,参数交由 Node 转义。
76
76
  */
77
77
  spawnCmd = (bin, args, baseOpts = {}) => {
78
78
  if (process.platform === 'win32') {
@@ -82,7 +82,7 @@ const ensureDir = dir => {
82
82
  return spawnSync(bin, args, { ...baseOpts, shell: false });
83
83
  },
84
84
  /**
85
- * 异步 spawn:参数同 spawnCmd,返回 ChildProcess
85
+ * 异步 spawn:参数同 spawnCmd,返回 ChildProcess
86
86
  * @param {string} bin 命令
87
87
  * @param {string[]} args 参数
88
88
  * @param {object} [baseOpts] spawn 选项
@@ -96,7 +96,7 @@ const ensureDir = dir => {
96
96
  return spawn(bin, args, { ...baseOpts, shell: false });
97
97
  },
98
98
  /**
99
- * 执行命令:默认实时输出(inherit);quiet=true 时捕获输出,成功静默、失败才打印
99
+ * 执行命令:默认实时输出(inherit);quiet=true 时捕获输出,成功静默、失败才打印
100
100
  */
101
101
  run = (bin, args, opts = {}) => {
102
102
  const { quiet, ...restOpts } = opts;
@@ -124,7 +124,7 @@ const ensureDir = dir => {
124
124
  return r;
125
125
  },
126
126
  /**
127
- * 执行命令:实时输出,仅首行加缩进前缀,其余行原样
127
+ * 执行命令:实时输出,仅首行加缩进前缀,其余行原样
128
128
  * @param {string} bin 命令
129
129
  * @param {string[]} args 参数
130
130
  * @param {object} [opts] spawn 选项 + prefix
@@ -155,7 +155,7 @@ const ensureDir = dir => {
155
155
  const configured = config?.android?.adbPath;
156
156
  if (configured) {
157
157
  if (fs.existsSync(configured)) return configured;
158
- console.warn(chalk.yellow('⚠️ config.android.adbPath 不存在,回退到自动查找: ' + configured));
158
+ console.warn(chalk.yellow('⚠️ config.android.adbPath 不存在,回退到自动查找: ' + configured));
159
159
  }
160
160
  // 2. 环境变量 ANDROID_HOME / ANDROID_SDK_ROOT
161
161
  for (const env of ['ANDROID_HOME', 'ANDROID_SDK_ROOT']) {
@@ -176,7 +176,7 @@ const ensureDir = dir => {
176
176
  return 'adb';
177
177
  },
178
178
  /**
179
- * 定位 hermesc(Hermes 编译器,用于把 JS bundle 编成字节码)
179
+ * 定位 hermesc(Hermes 编译器,用于把 JS bundle 编成字节码)
180
180
  */
181
181
  /**
182
182
  * 读取安装状态标志(记录上次由本工具安装的包与变体)
@@ -221,7 +221,7 @@ const ensureDir = dir => {
221
221
  ensureInstallable = (adb, appId, want, buildDir, serial) => {
222
222
  const installed = getInstalledVariant(adb, appId, serial);
223
223
  if (installed && installed !== want) {
224
- console.log(chalk.yellow(` 已安装 ${installed} 包与待装 ${want} 包签名不同,先卸载`));
224
+ console.log(chalk.yellow(` 已安装 ${installed} 包与待装 ${want} 包签名不同,先卸载`));
225
225
  spawnCmd(adb, [...(serial ? ['-s', serial] : []), 'uninstall', appId], { encoding: 'utf-8' });
226
226
  }
227
227
  },
@@ -274,4 +274,4 @@ import chalk from 'chalk';
274
274
  export {
275
275
  ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged, spawnCmdAsync, run, runQuiet, runStream,
276
276
  runSilent, findAdb, readInstallState, writeInstallState, getInstalledVariant, ensureInstallable, findHermesc, sanitizeFileName, buildEslintConfigSource
277
- };
277
+ };
package/template/App.tsx CHANGED
@@ -9,101 +9,97 @@ import runtime from './mobileApp.runtime';
9
9
  // 'system' → 跟随系统(内部用默认浅/深色)
10
10
  // { light: '#fff', dark: '#000' } → 按系统主题切换
11
11
  const resolveBgColor = (bg: string | { light: string; dark: string }, theme: string | null | undefined): string => {
12
- if (typeof bg === 'object' && bg !== null) {
12
+ if (typeof bg === 'object' && bg !== null)
13
13
  return (theme === 'dark' ? bg.dark : bg.light) || bg.light || '#ffffff';
14
- }
15
- if (bg === 'system') {
16
- return theme === 'dark' ? '#121212' : '#ffffff';
17
- }
14
+ if (bg === 'system') return theme === 'dark' ? '#121212' : '#ffffff';
18
15
  return bg || '#ffffff';
19
- };
16
+ },
17
+ App = () => {
18
+ const systemTheme = useColorScheme(),
19
+ [url, setUrl] = useState<string | null>(null), [failedReason, setFailedReason] = useState<string | null>(null),
20
+ [showDetail, setShowDetail] = useState(false),
21
+ styles = StyleSheet.create({
22
+ fill: { flex: 1 },
23
+ loading: { flex: 1, justifyContent: 'center', alignItems: 'center' },
24
+ errorContainer: {
25
+ flex: 1,
26
+ justifyContent: 'center',
27
+ alignItems: 'center',
28
+ padding: 24,
29
+ },
30
+ errorTitle: { fontSize: 20, fontWeight: '600', marginBottom: 16, color: '#c00' },
31
+ errorHint: { fontSize: 14, textAlign: 'center', color: '#888' },
32
+ detailToggle: { fontSize: 13, marginTop: 24, color: '#06c' },
33
+ detailText: {
34
+ fontSize: 12,
35
+ marginTop: 12,
36
+ paddingHorizontal: 16,
37
+ color: '#666',
38
+ fontFamily: 'monospace',
39
+ textAlign: 'center',
40
+ },
41
+ });
42
+ useEffect(() => {
43
+ NodeMobile.start('main.js');
20
44
 
21
- const App = () => {
22
- const systemTheme = useColorScheme(),
23
- [url, setUrl] = useState<string | null>(null), [failedReason, setFailedReason] = useState<string | null>(null),
24
- [showDetail, setShowDetail] = useState(false),
25
- styles = StyleSheet.create({
26
- fill: { flex: 1 },
27
- loading: { flex: 1, justifyContent: 'center', alignItems: 'center' },
28
- errorContainer: {
29
- flex: 1,
30
- justifyContent: 'center',
31
- alignItems: 'center',
32
- padding: 24,
33
- },
34
- errorTitle: { fontSize: 20, fontWeight: '600', marginBottom: 16, color: '#c00' },
35
- errorHint: { fontSize: 14, textAlign: 'center', color: '#888' },
36
- detailToggle: { fontSize: 13, marginTop: 24, color: '#06c' },
37
- detailText: {
38
- fontSize: 12,
39
- marginTop: 12,
40
- paddingHorizontal: 16,
41
- color: '#666',
42
- fontFamily: 'monospace',
43
- textAlign: 'center',
44
- },
45
- });
46
- useEffect(() => {
47
- NodeMobile.start('main.js');
45
+ const listener = (msg: unknown) => {
46
+ if (typeof msg !== 'string') return;
47
+ if (msg.startsWith('Node started on ')) {
48
+ const target = msg.slice('Node started on '.length).trim();
49
+ setFailedReason(null);
50
+ setUrl(target);
51
+ } else if (msg.startsWith('Node failed: ')) {
52
+ const reason = msg.slice('Node failed: '.length).trim();
53
+ setFailedReason(reason);
54
+ }
55
+ };
48
56
 
49
- const listener = (msg: unknown) => {
50
- if (typeof msg !== 'string') return;
51
- if (msg.startsWith('Node started on ')) {
52
- const target = msg.slice('Node started on '.length).trim();
53
- setFailedReason(null);
54
- setUrl(target);
55
- } else if (msg.startsWith('Node failed: ')) {
56
- const reason = msg.slice('Node failed: '.length).trim();
57
- setFailedReason(reason);
58
- }
59
- };
57
+ NodeMobile.channel.addListener('message', listener);
58
+ return () => NodeMobile.channel.removeListener('message', listener);
59
+ }, []);
60
60
 
61
- NodeMobile.channel.addListener('message', listener);
62
- return () => NodeMobile.channel.removeListener('message', listener);
63
- }, []);
61
+ // 失败:显示错误页(用户看提示,开发者点开看详情)
62
+ if (failedReason) {
63
+ return (
64
+ <View style={styles.errorContainer}>
65
+ <Text style={styles.errorTitle}>应用启动失败</Text>
66
+ <Text style={styles.errorHint}>请稍后重试,或联系应用发布者</Text>
67
+ <Text
68
+ style={styles.detailToggle}
69
+ onPress={() => setShowDetail((v) => !v)}
70
+ >
71
+ {showDetail ? '▼ 收起详细信息' : '▶ 查看详细信息'}
72
+ </Text>
73
+ {showDetail && (
74
+ <Text style={styles.detailText}>{failedReason}</Text>
75
+ )}
76
+ </View>
77
+ );
78
+ }
64
79
 
65
- // 失败:显示错误页(用户看提示,开发者点开看详情)
66
- if (failedReason) {
67
- return (
68
- <View style={styles.errorContainer}>
69
- <Text style={styles.errorTitle}>应用启动失败</Text>
70
- <Text style={styles.errorHint}>请稍后重试,或联系应用发布者</Text>
71
- <Text
72
- style={styles.detailToggle}
73
- onPress={() => setShowDetail((v) => !v)}
74
- >
75
- {showDetail ? '▼ 收起详细信息' : '▶ 查看详细信息'}
76
- </Text>
77
- {showDetail && (
78
- <Text style={styles.detailText}>{failedReason}</Text>
79
- )}
80
- </View>
81
- );
82
- }
80
+ // 尚未就绪:loading
81
+ if (!url) {
82
+ if (!runtime.showLoading) return <View style={styles.fill} />;
83
+ return (
84
+ <View style={styles.loading}>
85
+ <ActivityIndicator size="large" />
86
+ <Text>{runtime.loadingText}</Text>
87
+ </View>
88
+ );
89
+ }
83
90
 
84
- // 尚未就绪:loading
85
- if (!url) {
86
- if (!runtime.showLoading) return <View style={styles.fill} />;
87
91
  return (
88
- <View style={styles.loading}>
89
- <ActivityIndicator size="large" />
90
- <Text>{runtime.loadingText}</Text>
91
- </View>
92
+ <WebView
93
+ source={{ uri: url }}
94
+ style={[styles.fill, { backgroundColor: resolveBgColor(runtime.webview.backgroundColor, systemTheme) }]}
95
+ originWhitelist={['*']}
96
+ javaScriptEnabled={runtime.webview.enableJavaScript}
97
+ domStorageEnabled={runtime.webview.enableDomStorage}
98
+ allowFileAccess={runtime.webview.allowFileAccess}
99
+ allowUniversalAccessFromFileURLs={true}
100
+ mixedContentMode={runtime.webview.mixedContentMode}
101
+ />
92
102
  );
93
- }
94
-
95
- return (
96
- <WebView
97
- source={{ uri: url }}
98
- style={[styles.fill, { backgroundColor: resolveBgColor(runtime.webview.backgroundColor, systemTheme) }]}
99
- originWhitelist={['*']}
100
- javaScriptEnabled={runtime.webview.enableJavaScript}
101
- domStorageEnabled={runtime.webview.enableDomStorage}
102
- allowFileAccess={runtime.webview.allowFileAccess}
103
- allowUniversalAccessFromFileURLs={true}
104
- mixedContentMode={runtime.webview.mixedContentMode}
105
- />
106
- );
107
- };
103
+ };
108
104
 
109
105
  export { App };
package/template/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * React Native 应用入口
3
- * 挂载根组件 App(见 App.tsx)到 AppRegistry,
3
+ * 挂载根组件 App(见 App.tsx)到 AppRegistry,
4
4
  * 组件名取自 app.json 的 name(由 CLI 根据 appName 自动生成)
5
5
  *
6
6
  * 用户一般不需要修改这个文件
@@ -9,9 +9,7 @@ const exclusionList = exclusionListModule.default,
9
9
  watchFolders: [__dirname, resolve(workspaceRoot, 'node_modules')],
10
10
  resolver: {
11
11
  nodeModulesPaths: [resolve(__dirname, 'node_modules'), resolve(workspaceRoot, 'node_modules')],
12
- blockList: exclusionList([
13
- /[/\\]nodejs-assets[/\\].*/
14
- ])
12
+ blockList: exclusionList([/[/\\]nodejs-assets[/\\].*/])
15
13
  }
16
14
  };
17
15
 
@@ -2,32 +2,32 @@
2
2
 
3
3
  本文档讲两件事:
4
4
 
5
- 1. **用预编译产物**(推荐,90% 用户)—— 从仓库下载 `android-libnode.zip`,替换,跑 CLI,完事
5
+ 1. **用预编译产物**(推荐,90% 用户)—— 从仓库下载 `android-libnode.zip`,替换,跑 CLI,完事
6
6
  2. **自己编译**(进阶)—— 用 WSL + NDK 从源码编译带 full-icu 的 v22+
7
7
 
8
- **使用 `@flun/node-mobile-app` 时不需要看这份文档就能打包**——CLI 会自动处理 ABI 配置、缓存清理、原生模块编译。本文档只在**要换 libnode.so** 时才需要。
8
+ **使用 `@flun/node-mobile-app` 时不需要看这份文档就能打包**——CLI 会自动处理 ABI 配置、缓存清理、原生模块编译;本文档只在**要换 libnode.so** 时才需要;
9
9
 
10
10
  ---
11
11
 
12
12
  ## 一、自定义 libnode.so
13
13
 
14
- `@flun/node-mobile-app` 默认带的是官方 `nodejs-mobile` 的 **v18** 二进制。v18 的限制:
14
+ `@flun/node-mobile-app` 默认带的是官方 `nodejs-mobile` 的 **v18** 二进制;v18 的限制:
15
15
 
16
- | 限制 | 影响 |
17
- | ------------------------------ | -------------------------------------------------------- |
18
- | `--with-intl=none`(不带 ICU) | 不能用 `\p{...}` / `\P{...}` Unicode 正则 |
19
- | 无法用 Express 5 | `path-to-regexp@8` 内部用 `\p{ID_Start}`,v18 加载时崩溃 |
20
- | 停更 | 官方不再更新 |
16
+ | 限制 | 影响 |
17
+ | ------------------------------ | ------------------------------------------------------- |
18
+ | `--with-intl=none`(不带 ICU) | 不能用 `\p{...}` / `\P{...}` Unicode 正则 |
19
+ | 无法用 Express 5 | `path-to-regexp@8` 内部用 `\p{ID_Start}`,v18 加载时崩溃 |
20
+ | 停更 | 官方不再更新 |
21
21
 
22
- **要跑 Express 5 / 用新特性 / 用完整 Unicode 正则**,需替换为 **v22+ full-icu**。
22
+ **要跑 Express 5 / 用新特性 / 用完整 Unicode 正则**,需替换为 **v22+ full-icu**;
23
23
 
24
24
  ---
25
25
 
26
26
  ## 二、用预编译产物(推荐)
27
27
 
28
- > **注意**:从 `@flun/nodejs-mobile-react-native` 1.0.1 起,Android 的 `libnode.so` 由 postinstall 脚本从 Release 自动下载(默认 v18.20.4)。
29
- > **本节适用于**:要把自动下载的 v18 换成 **v22+ 自编译版本**(带 full-icu,能跑 Express 5)的场景。
30
- > 若只是用默认 v18,你**不需要**看本节——装包后 `libnode.so` 已就位。
28
+ > **注意**:从 `@flun/nodejs-mobile-react-native` 1.0.1 起,Android 的 `libnode.so` 由 postinstall 脚本从 Release 自动下载(默认 v18.20.4);
29
+ > **本节适用于**:要把自动下载的 v18 换成 **v22+ 自编译版本**(带 full-icu,能跑 Express 5)的场景;
30
+ > 若只是用默认 v18,你**不需要**看本节——装包后 `libnode.so` 已就位;
31
31
 
32
32
 
33
33
  ### 2.1 下载地址
@@ -39,7 +39,7 @@
39
39
  | GitHub | [OpenFlun/nodejs-mobile](https://github.com/OpenFlun/nodejs-mobile) | [Releases](https://github.com/OpenFlun/nodejs-mobile/releases) | [android-libnode.zip](https://github.com/OpenFlun/nodejs-mobile/releases/download/v22.23.2/android-libnode.zip) |
40
40
  | Gitee(国内) | [OpenFlun/nodejs-mobile](https://gitee.com/OpenFlun/nodejs-mobile) | [Releases](https://gitee.com/OpenFlun/nodejs-mobile/releases) | [android-libnode.zip](https://gitee.com/OpenFlun/nodejs-mobile/releases/download/v22.23.2/android-libnode.zip) |
41
41
 
42
- **文件名说明**:发布名为 `android-libnode.zip`(带 `android-` 前缀),为未来 iOS 等平台扩展预留接口(如 `ios-libnode.zip`)。**内容仍是标准的 `libnode/` 目录**(`bin/` + `include/`)。
42
+ **文件名说明**:发布名为 `android-libnode.zip`(带 `android-` 前缀),为未来 iOS 等平台扩展预留接口(如 `ios-libnode.zip`);**内容仍是标准的 `libnode/` 目录**(`bin/` + `include/`);
43
43
 
44
44
  ### 2.2 支持矩阵
45
45
 
@@ -51,9 +51,9 @@
51
51
 
52
52
  **当前 v22 下载入口**:
53
53
  - 双架构(推荐):`android-libnode.zip`(约 88 MB)
54
- - 单架构 zip 已废弃,不再发布
54
+ - 单架构 zip 已废弃,不再发布
55
55
 
56
- **armeabi-v7a 为什么放弃**:V8 v22 在 x64 主机上无法交叉编译 32 位 ARM 目标(详见 5.1)。
56
+ **armeabi-v7a 为什么放弃**:V8 v22 在 x64 主机上无法交叉编译 32 位 ARM 目标(详见 5.1);
57
57
 
58
58
  ### 2.3 下载与替换
59
59
 
@@ -74,7 +74,7 @@ libnode/
74
74
  # 进入插件的 android 目录
75
75
  cd <你的项目>/node_modules/@flun/nodejs-mobile-react-native/android
76
76
 
77
- # 备份官方 v18(可选,方便回退)
77
+ # 备份官方 v18(可选,方便回退)
78
78
  cp -r libnode libnode.bak-v18
79
79
 
80
80
  # 整体替换
@@ -99,7 +99,7 @@ Move-Item "$env:TEMP\libnode-new\libnode" "$plugin\libnode"
99
99
 
100
100
  ### 2.4 调整 ABI 配置
101
101
 
102
- **如果下载的 `android-libnode.zip` 只含一个架构**(比如只编了 arm64-v8a),在 `mobileAppConfig.js` 里同步:
102
+ **如果下载的 `android-libnode.zip` 只含一个架构**(比如只编了 arm64-v8a),在 `mobileAppConfig.js` 里同步:
103
103
 
104
104
  ```js
105
105
  android: {
@@ -107,7 +107,7 @@ android: {
107
107
  }
108
108
  ```
109
109
 
110
- **如果双架构**(arm64-v8a + x86_64,当前预编译产物即为双架构):
110
+ **如果双架构**(arm64-v8a + x86_64,当前预编译产物即为双架构):
111
111
 
112
112
  ```js
113
113
  android: {
@@ -132,17 +132,17 @@ npx node-mobile-app build # 打包 APK
132
132
  | Patch `app/build.gradle` 的 `ndk.abiFilters` | 用 `mobileAppConfig.js` 的值 |
133
133
  | Patch `android/gradle.properties` 的 `reactNativeArchitectures` | 同步 ABI 列表 |
134
134
  | Patch 插件 `build.gradle` 的 `abiFilters` | 让插件只编指定架构 |
135
- | 重编原生模块(自动检测 `binding.gyp`) | 见 2.6 |
135
+ | 重编原生模块(自动检测 `binding.gyp`) | 见 2.6 |
136
136
 
137
- **你不需要手动清任何缓存。**
137
+ **你不需要手动清任何缓存;**
138
138
 
139
139
  ### 2.6 重编原生模块(仅当项目含原生模块)
140
140
 
141
- Node v18 → v22 后,ABI 变了(`NODE_MODULE_VERSION` 从 108 → 127)。**如果项目里装了带 C++ 代码的 npm 包**(如 `bcrypt`、`sqlite3`、`sharp`),换 v22 的 `libnode.so` 后需要重编才能兼容。
141
+ Node v18 → v22 后,ABI 变了(`NODE_MODULE_VERSION` 从 108 → 127);**如果项目里装了带 C++ 代码的 npm 包**(如 `bcrypt`、`sqlite3`、`sharp`),换 v22 的 `libnode.so` 后需要重编才能兼容;
142
142
 
143
- **默认自动处理**:CLI 会扫描 `nodejs-project/node_modules` 下的 `binding.gyp` 判断是否有原生模块,自动决定是否重编,**无需配置**。
143
+ **默认自动处理**:CLI 会扫描 `nodejs-project/node_modules` 下的 `binding.gyp` 判断是否有原生模块,自动决定是否重编,**无需配置**;
144
144
 
145
- **手动强制重编**(特殊场景:换过 `libnode.so` 但依赖没变,检测机制未触发):
145
+ **手动强制重编**(特殊场景:换过 `libnode.so` 但依赖没变,检测机制未触发):
146
146
 
147
147
  ```bash
148
148
  # Linux / macOS
@@ -154,11 +154,11 @@ NODE_MOBILE_FORCE_REBUILD=1 npx node-mobile-app test
154
154
  $env:NODE_MOBILE_FORCE_REBUILD=1; npx node-mobile-app test
155
155
  ```
156
156
 
157
- **没有原生模块(纯 JS 依赖)**:无需关心,检测会自动跳过。
157
+ **没有原生模块(纯 JS 依赖)**:无需关心,检测会自动跳过;
158
158
 
159
- **切换架构(arm64 ↔ x86_64)**:不需要重编。JS 依赖跨架构,只有少数原生模块分架构。
159
+ **切换架构(arm64 ↔ x86_64)**:不需要重编;JS 依赖跨架构,只有少数原生模块分架构;
160
160
 
161
- **Windows 限制**:Windows 上 nodejs-mobile 无法编译原生模块(上游限制)。此功能仅 Linux / macOS 可用。
161
+ **Windows 限制**:Windows 上 nodejs-mobile 无法编译原生模块(上游限制);此功能仅 Linux / macOS 可用;
162
162
 
163
163
  ### 2.7 验证
164
164
 
@@ -202,13 +202,13 @@ Start-Sleep -Seconds 5
202
202
 
203
203
  预期:`I NODEJS-MOBILE: === ICU check: \p{ID_Start} OK`
204
204
 
205
- 若显示 `ICU check failed` → `libnode.so` 不带 full-icu → 见第三章自己编,或回退到 Express 4。
205
+ 若显示 `ICU check failed` → `libnode.so` 不带 full-icu → 见第三章自己编,或回退到 Express 4;
206
206
 
207
207
  ---
208
208
 
209
209
  ## 三、自己编译 libnode.so
210
210
 
211
- **环境要求**:WSL(或 Linux),因为构建脚本依赖 `make` / `sed` / NDK。
211
+ **环境要求**:WSL(或 Linux),因为构建脚本依赖 `make` / `sed` / NDK;
212
212
 
213
213
  ### 3.1 准备源码与 NDK
214
214
 
@@ -225,7 +225,7 @@ unzip android-ndk-r27-linux.zip
225
225
 
226
226
  ### 3.2 关键:启用 full-icu
227
227
 
228
- **默认 `--with-intl=none`,必须改成 `full-icu`**,否则 Express 5 跑不了。
228
+ **默认 `--with-intl=none`,必须改成 `full-icu`**,否则 Express 5 跑不了;
229
229
 
230
230
  ```bash
231
231
  cd /mnt/d/nodejs-mobile-build/nodejs-mobile
@@ -254,7 +254,7 @@ rm -rf out config.gypi config.mk config.status
254
254
  ./android-configure $NDK 30 arm64
255
255
  make -j4
256
256
 
257
- # 剥离符号(必须,否则 .so 会大很多)
257
+ # 剥离符号(必须,否则 .so 会大很多)
258
258
  cp out/Release/libnode.so out/Release/libnode.so.bak
259
259
  $STRIP out/Release/libnode.so
260
260
 
@@ -265,7 +265,7 @@ diff <(nm -D out/Release/libnode.so.bak | awk '{print $2, $3}' | sort) \
265
265
 
266
266
  # 验证 full-icu
267
267
  strings out/Release/libnode.so | grep -c "icudt78l"
268
- # 应输出几千(约 4300),否则没编进 ICU
268
+ # 应输出几千(约 4300),否则没编进 ICU
269
269
  ```
270
270
 
271
271
  ### 3.4 多架构编译(推荐用脚本)
@@ -291,7 +291,7 @@ echo "✅ full-icu 已启用"
291
291
 
292
292
  mkdir -p $TMP $OUT
293
293
 
294
- # 逐架构编译,暂存 .so
294
+ # 逐架构编译,暂存 .so
295
295
  for ARCH in arm64 x86_64; do
296
296
  echo "=== 编译 $ARCH ==="
297
297
  cd $SRC
@@ -330,10 +330,10 @@ chmod +x /mnt/d/nodejs-mobile-build/build-all-archs.sh
330
330
 
331
331
  ### 3.5 多架构编译注意事项
332
332
 
333
- 1. **每次切换架构必须清 `out`**:中间文件不能混用,否则奇怪的编译错误。脚本里已经 `rm -rf out`。
334
- 2. **切换架构会清空 `out`**:所以脚本编完一个架构立即把 `.so` 复制到 `libnode-tmp/<arch>/` 独立保存。
335
- 3. **每次切架构前确认 `--with-intl=full-icu` 还在**:`android_configure.py` 是源码,不会因切换架构被重置——但如果你从 Git 重新 clone 或覆盖过源码,修改会丢,务必再确认。
336
- 4. **验证两个架构都带 ICU**:`strings libnode.so | grep -c "icudt78l"` 应输出几千(约 4300),否则该架构没编进 ICU。
333
+ 1. **每次切换架构必须清 `out`**:中间文件不能混用,否则奇怪的编译错误;脚本里已经 `rm -rf out`;
334
+ 2. **切换架构会清空 `out`**:所以脚本编完一个架构立即把 `.so` 复制到 `libnode-tmp/<arch>/` 独立保存;
335
+ 3. **每次切架构前确认 `--with-intl=full-icu` 还在**:`android_configure.py` 是源码,不会因切换架构被重置——但如果你从 Git 重新 clone 或覆盖过源码,修改会丢,务必再确认;
336
+ 4. **验证两个架构都带 ICU**:`strings libnode.so | grep -c "icudt78l"` 应输出几千(约 4300),否则该架构没编进 ICU;
337
337
 
338
338
  ---
339
339
 
@@ -343,14 +343,14 @@ chmod +x /mnt/d/nodejs-mobile-build/build-all-archs.sh
343
343
 
344
344
  **官方数据(strip 后)**:
345
345
 
346
- | 版本 | arm64-v8a | x86_64 |
347
- | ----------------------- | ------------- | ------------- |
348
- | v18(官方,无 ICU) | 59.6 MB | 62.3 MB |
349
- | v22(自编译,full-icu) | **约 113 MB** | **约 118 MB** |
346
+ | 版本 | arm64-v8a | x86_64 |
347
+ | ---------------------- | ------------- | ------------- |
348
+ | v18(官方,无 ICU) | 59.6 MB | 62.3 MB |
349
+ | v22(自编译,full-icu) | **约 113 MB** | **约 118 MB** |
350
350
 
351
- **zip 后**:`android-libnode.zip`(含两架构 `.so` + headers)约 88 MB。
351
+ **zip 后**:`android-libnode.zip`(含两架构 `.so` + headers)约 88 MB;
352
352
 
353
- 体积差异约 50 MB,来自三部分:Node v22 核心引擎 + full-icu 数据(约 25–30 MB)+ V8 版本升级。**不是编译出错**。
353
+ 体积差异约 50 MB,来自三部分:Node v22 核心引擎 + full-icu 数据(约 25–30 MB)+ V8 版本升级;**不是编译出错**;
354
354
 
355
355
  ### 4.2 full-icu 与 Express 版本
356
356
 
@@ -374,7 +374,7 @@ chmod +x /mnt/d/nodejs-mobile-build/build-all-archs.sh
374
374
  | `res.json(obj, 200)` | `res.status(200).json(obj)` |
375
375
  | `res.send(200, body)` | `res.status(200).send(body)` |
376
376
 
377
- **不要用 `@alloc/path-to-regexp` 替换原版**:它的 `exports` 只有 `import` 条件,CJS 下 `require()` 报 `No "exports" main defined`。用原版 `path-to-regexp@8.4.2`。
377
+ **不要用 `@alloc/path-to-regexp` 替换原版**:它的 `exports` 只有 `import` 条件,CJS 下 `require()` 报 `No "exports" main defined`;用原版 `path-to-regexp@8.4.2`;
378
378
 
379
379
  ---
380
380
 
@@ -382,27 +382,27 @@ chmod +x /mnt/d/nodejs-mobile-build/build-all-archs.sh
382
382
 
383
383
  ### 5.1 armeabi-v7a 为什么放弃
384
384
 
385
- **结论**:v22+ **放弃 armeabi-v7a(32 位 ARM)**。
385
+ **结论**:v22+ **放弃 armeabi-v7a(32 位 ARM)**;
386
386
 
387
387
  **原因**:
388
388
 
389
- 1. **V8 官方限制**:x64 主机上交叉编译 32 位 arm 目标时,`deps/v8/include/v8config.h` 第 914 行硬性拒绝:
389
+ 1. **V8 官方限制**:x64 主机上交叉编译 32 位 arm 目标时,`deps/v8/include/v8config.h` 第 914 行硬性拒绝:
390
390
 
391
391
  ```
392
392
  #error Target architecture arm is only supported on arm and ia32 host
393
393
  ```
394
394
 
395
- 即使强行绕过,后续 Torque 工具仍报大量 8 字节对齐错误。
395
+ 即使强行绕过,后续 Torque 工具仍报大量 8 字节对齐错误;
396
396
 
397
- 2. **实际意义有限**:Android 从 2019 年起要求新应用提供 64 位版本,现代设备几乎全部 arm64-v8a。
397
+ 2. **实际意义有限**:Android 从 2019 年起要求新应用提供 64 位版本,现代设备几乎全部 arm64-v8a;
398
398
 
399
- **替代**:需 32 位 ARM 支持,用 Node.js v18 或 v20。
399
+ **替代**:需 32 位 ARM 支持,用 Node.js v18 或 v20;
400
400
 
401
401
  ### 5.2 x86_64 的说明
402
402
 
403
- x86_64 在 x64 主机上**无交叉编译问题**,与 arm64 流程一致。`android-libnode.zip` 已含 x86_64 产物。
403
+ x86_64 在 x64 主机上**无交叉编译问题**,与 arm64 流程一致;`android-libnode.zip` 已含 x86_64 产物;
404
404
 
405
- **用途**:Android Studio / 命令行的 x86_64 架构 AVD(模拟器)。
405
+ **用途**:Android Studio / 命令行的 x86_64 架构 AVD(模拟器);
406
406
 
407
407
  **用模拟器时**:
408
408
  - `mobileAppConfig.js` 的 `abiFilters` 含 `x86_64`
@@ -411,29 +411,29 @@ x86_64 在 x64 主机上**无交叉编译问题**,与 arm64 流程一致。`an
411
411
 
412
412
  ### 5.3 自编译相关坑
413
413
 
414
- | 问题 | 原因 | 解决 |
415
- | ------------------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
416
- | `number 116 is not a function` | `libnode.so` 无 full-icu | 重新用 `--with-intl=full-icu` 编译,或回退 Express 4 |
417
- | `\p{...}` 正则报错 | 同上 | 同上 |
418
- | `No "exports" main defined` | 用了 `@alloc/path-to-regexp` | 撤销 `overrides`,清 `node_modules` + `package-lock.json` 重装 |
419
- | `Cannot find module 'rn-bridge'` | Node 20+ 不再把链接绑定暴露给 `require()` | 需修改 Node.js `lib/internal/modules/cjs/loader.js`,从 `NODE_PATH` 按目录加载 `rn-bridge`(入口由该目录 `package.json` 的 `main` 决定) |
420
- | 启动即崩:`pthread_mutex_lock called on a destroyed mutex` + `Fatal signal 6 (SIGABRT)`;`require('rn-bridge')` 返回的对象无 `channel` 属性 | v22 loader 补丁按 `rn-bridge/index.js` 硬编码定位,而插件入口因 `type: module` 已改名(如 `index.cjs`),匹配失败 → Node 回退到 `process._linkedBinding('rn_bridge')` 拿裸 binding | 使用按**目录** `rn-bridge` 定位的 loader 补丁(入口由该目录 `package.json` 的 `main` 字段解析) |
421
- | JNI 编译报 `undefined symbol: v8::Exception::Error` | 插件旧头文件(v18 时代)与 v22 `libnode.so` ABI 不匹配 | **用 `android-libnode.zip` 整目录替换**,其 `include/node/` 是 v22 官方 headers |
422
- | `NODE_MODULE_VERSION` 不匹配 | 原生模块未重编 | `NODE_MOBILE_FORCE_REBUILD=1` 后重跑 CLI |
423
- | Windows 下 `Unsupported operating system for nodejs-mobile native builds` | 插件硬编码只支持 macOS/Linux | 官方 v22 产物已含 Windows 支持(`windows-x86_64` + `host_os=win32`) |
424
- | Gradle 9 报 `Could not find method exec()` | Gradle 9 移除 `exec()` | 官方 v22 产物已修复(改用 `providers.exec`) |
414
+ | 问题 | 原因 | 解决 |
415
+ | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
416
+ | `number 116 is not a function` | `libnode.so` 无 full-icu | 重新用 `--with-intl=full-icu` 编译,或回退 Express 4 |
417
+ | `\p{...}` 正则报错 | 同上 | 同上 |
418
+ | `No "exports" main defined` | 用了 `@alloc/path-to-regexp` | 撤销 `overrides`,清 `node_modules` + `package-lock.json` 重装 |
419
+ | `Cannot find module 'rn-bridge'` | Node 20+ 不再把链接绑定暴露给 `require()` | 需修改 Node.js `lib/internal/modules/cjs/loader.js`,从 `NODE_PATH` 按目录加载 `rn-bridge`(入口由该目录 `package.json` 的 `main` 决定) |
420
+ | 启动即崩:`pthread_mutex_lock called on a destroyed mutex` + `Fatal signal 6 (SIGABRT)`;`require('rn-bridge')` 返回的对象无 `channel` 属性 | v22 loader 补丁按 `rn-bridge/index.js` 硬编码定位,而插件入口因 `type: module` 已改名(如 `index.cjs`),匹配失败 → Node 回退到 `process._linkedBinding('rn_bridge')` 拿裸 binding | 使用按**目录** `rn-bridge` 定位的 loader 补丁(入口由该目录 `package.json` 的 `main` 字段解析) |
421
+ | JNI 编译报 `undefined symbol: v8::Exception::Error` | 插件旧头文件(v18 时代)与 v22 `libnode.so` ABI 不匹配 | **用 `android-libnode.zip` 整目录替换**,其 `include/node/` 是 v22 官方 headers |
422
+ | `NODE_MODULE_VERSION` 不匹配 | 原生模块未重编 | `NODE_MOBILE_FORCE_REBUILD=1` 后重跑 CLI |
423
+ | Windows 下 `Unsupported operating system for nodejs-mobile native builds` | 插件硬编码只支持 macOS/Linux | 官方 v22 产物已含 Windows 支持(`windows-x86_64` + `host_os=win32`) |
424
+ | Gradle 9 报 `Could not find method exec()` | Gradle 9 移除 `exec()` | 官方 v22 产物已修复(改用 `providers.exec`) |
425
425
 
426
426
  ---
427
427
 
428
428
  ## 六、注意事项
429
429
 
430
- 1. **插件本身可能不兼容**:`nodejs-mobile-react-native` 的 C++ 胶水代码为特定 Node.js 版本编写。升级到 v22+ 时可能需自行修改插件源码并重编。
431
- 2. **依赖库版本**:部分 npm 包限制 Node.js 版本。检查 `package.json` 的 `engines`。
432
- 3. **非官方产物**:自编译的 `libnode.so` 未经充分测试,生产环境前充分测试。
433
- 4. **多架构建议**:优先保证 `arm64-v8a`,按需补 `x86_64`。`armeabi-v7a` v22+ 已放弃。
434
- 5. **full-icu 是 Express 5 的硬前提**:不要图省事用 `--with-intl=none` 编译。
435
- 6. **`android-libnode.zip` 是唯一发布物**:与官方 nodejs-mobile 结构一致(`bin/` + `include/`),整目录替换,无需手动改头文件。
436
- 7. **CLI 会自动处理缓存**:替换 `libnode.so` 后跑 `test` / `build`,指纹机制检测到变化自动清 `.cxx` / `android/build` / `app/build`,全量重编。**无需手动清任何缓存**。
430
+ 1. **插件本身可能不兼容**:`nodejs-mobile-react-native` 的 C++ 胶水代码为特定 Node.js 版本编写;升级到 v22+ 时可能需自行修改插件源码并重编;
431
+ 2. **依赖库版本**:部分 npm 包限制 Node.js 版本;检查 `package.json` 的 `engines`;
432
+ 3. **非官方产物**:自编译的 `libnode.so` 未经充分测试,生产环境前充分测试;
433
+ 4. **多架构建议**:优先保证 `arm64-v8a`,按需补 `x86_64`;`armeabi-v7a` v22+ 已放弃;
434
+ 5. **full-icu 是 Express 5 的硬前提**:不要图省事用 `--with-intl=none` 编译;
435
+ 6. **`android-libnode.zip` 是唯一发布物**:与官方 nodejs-mobile 结构一致(`bin/` + `include/`),整目录替换,无需手动改头文件;
436
+ 7. **CLI 会自动处理缓存**:替换 `libnode.so` 后跑 `test` / `build`,指纹机制检测到变化自动清 `.cxx` / `android/build` / `app/build`,全量重编;**无需手动清任何缓存**;
437
437
 
438
438
  ---
439
439